Own grid workers and thin Apple platform bridge

This commit is contained in:
2026-06-16 07:02:49 +02:00
parent 953fa11744
commit 75f57213ca
8 changed files with 292 additions and 57 deletions

View File

@@ -18,6 +18,27 @@ agent or engineer to remove them without reconstructing context from chat.
## Reductions ## Reductions
- 2026-06-16: `DEBT-0051`/`DEBT-0052`/`DEBT-0055` were narrowed again.
`src/platform_apple/apple_platform_services.*` no longer reaches `App::I`
for clipboard, display/share, cursor-visibility, or save-ui-state behavior;
those calls now flow through narrow injected Apple bridge callbacks from
`src/platform_legacy/legacy_platform_services.cpp`, while retained Apple
bridge construction and broader platform singleton reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` density-resolve
display execution now routes through
`legacy_canvas_draw_merge_services.h` instead of living inline in
`NodeCanvas::draw()`; the cache-to-screen composite block and broader canvas
draw orchestration remain retained.
- 2026-06-16: `DEBT-0053` was narrowed again. `App::pick_file_save(...)` no
longer launches a detached worker for background prepared-file writes; it
now uses a service-owned `std::jthread` queue and posts prepared-file save
completion back to the UI thread, while retained platform save/download
handoff execution remains.
- 2026-06-16: `DEBT-0036` was narrowed again. The retained grid lightmap
launch in `src/legacy_grid_ui_services.cpp` no longer uses a detached
worker thread; it now uses a service-owned `std::jthread` queue with
UI-thread state handoff, while retained bake execution and grid rendering
ownership remain.
- 2026-06-16: `DEBT-0048` was narrowed again. The retained ABR/PPBR import - 2026-06-16: `DEBT-0048` was narrowed again. The retained ABR/PPBR import
bridge in `src/legacy_brush_package_import_services.cpp` no longer launches bridge in `src/legacy_brush_package_import_services.cpp` no longer launches
detached worker threads; it now uses a service-owned `std::jthread` queue, detached worker threads; it now uses a service-owned `std::jthread` queue,

View File

@@ -94,10 +94,10 @@ Current architecture mismatches that must be treated as real blockers:
- `pp_platform_api` still compiles Apple implementation files instead of only - `pp_platform_api` still compiles Apple implementation files instead of only
platform-neutral policy and interface code. platform-neutral policy and interface code.
- `src/platform_apple/apple_platform_services.cpp` and - `src/platform_apple/apple_platform_services.cpp` no longer reaches `App::I`
parts of the concrete platform layer still reach `App::I`; Linux FPS title directly, and Linux FPS title reporting now uses an injected callback, but
reporting now uses an injected callback, but Apple singleton reach and other retained Apple bridging in `platform_legacy` and other platform/app coupling
platform/app coupling remain. remain.
- `src/platform_legacy/legacy_platform_services.*` is still part of the live - `src/platform_legacy/legacy_platform_services.*` is still part of the live
app shell. app shell.
- `pp_panopainter_ui` still depends on `pp_legacy_app`. - `pp_panopainter_ui` still depends on `pp_legacy_app`.
@@ -107,7 +107,8 @@ Current architecture mismatches that must be treated as real blockers:
rather than thin composition/binding surfaces. rather than thin composition/binding surfaces.
- `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use - `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use
global singleton reach, raw observer pointers, detached `std::thread` global singleton reach, raw observer pointers, detached `std::thread`
launches, and ad hoc mutex/condition-variable ownership. launches in several canvas/export/preview paths, and ad hoc
mutex/condition-variable ownership.
- Modern C++23 usage exists in extracted components, especially `std::span`, - Modern C++23 usage exists in extracted components, especially `std::span`,
explicit result/status objects, and a few concepts, but the live app still explicit result/status objects, and a few concepts, but the live app still
does not consistently express ownership, thread affinity, or renderer does not consistently express ownership, thread affinity, or renderer

View File

@@ -42,13 +42,15 @@ Completed, blocked, and superseded task history moved to
`src/node_canvas.cpp`, `src/app.cpp`, and `src/app_dialogs.cpp`. `src/node_canvas.cpp`, `src/app.cpp`, and `src/app_dialogs.cpp`.
- The platform boundary is not finished: - The platform boundary is not finished:
- `pp_platform_api` still compiles Apple implementation files - `pp_platform_api` still compiles Apple implementation files
- Apple platform services still reach `App::I` - `platform_apple` no longer reaches `App::I` directly, and Linux FPS title
- Linux FPS title reporting now uses an injected callback, but broader reporting now uses an injected callback, but retained Apple bridging and
platform-to-app singleton reach is still open broader platform-to-app singleton reach are still open in
`platform_legacy`
- `platform_legacy` is still part of the live app shell - `platform_legacy` is still part of the live app shell
- The app runtime boundary is not finished: - The app runtime boundary is not finished:
- render/UI queues are static `App` state - render/UI queues are static `App` state
- detached workers still launch from canvas, grid, preview, and event code - detached workers still launch from canvas, preview, document export, and
recording code
- thread-affinity rules are enforced by convention and asserts instead of - thread-affinity rules are enforced by convention and asserts instead of
explicit runtime contracts explicit runtime contracts
- The UI ownership boundary is not finished: - The UI ownership boundary is not finished:
@@ -125,6 +127,9 @@ Current slice:
- `NodeStrokePreview` final composite plus preview-texture copy now route - `NodeStrokePreview` final composite plus preview-texture copy now route
through `legacy_node_stroke_preview_execution_services.h`, but the preview through `legacy_node_stroke_preview_execution_services.h`, but the preview
node still owns most live-pass and retained GL resource execution. node still owns most live-pass and retained GL resource execution.
- `NodeCanvas` display resolve for the `m_density != 1.f` path now routes
through `legacy_canvas_draw_merge_services.h`, but the cache-to-screen
composite block and broader canvas draw orchestration are still inline.
Write scope: Write scope:
- `src/node_stroke_preview.cpp` - `src/node_stroke_preview.cpp`
@@ -348,7 +353,10 @@ Current slice:
moving behind owned runtime/service objects moving behind owned runtime/service objects
- brush package import/export now use service-owned `std::jthread` workers and - brush package import/export now use service-owned `std::jthread` workers and
UI-thread completion handoff UI-thread completion handoff
- canvas, grid, preview, and event-side detached work is still open - prepared-file save work and grid lightmap launch now also use service-owned
workers with explicit UI-thread handoff
- canvas, preview, document export, and recording-side detached work are still
open
Write scope: Write scope:
- `src/canvas.cpp` - `src/canvas.cpp`
@@ -547,8 +555,10 @@ which means the platform layer is not a platform layer yet.
Current slice: Current slice:
- Linux FPS title updates now route through an injected callback installed from - Linux FPS title updates now route through an injected callback installed from
`App::set_platform_services()` `App::set_platform_services()`
- Apple singleton reach and the remaining platform callback surface are still - `platform_apple` clipboard, display/share, cursor, and save-ui-state calls
open now route through injected Apple bridge callbacks instead of `App::I`
- retained Apple callback injection and broader `platform_legacy` singleton
reach are still open
Write scope: Write scope:
- `src/platform_apple/*` - `src/platform_apple/*`

View File

@@ -5,6 +5,14 @@
#include "app_core/document_platform_io.h" #include "app_core/document_platform_io.h"
#include "app_core/document_sharing.h" #include "app_core/document_sharing.h"
#include "platform_api/platform_services.h" #include "platform_api/platform_services.h"
#include <condition_variable>
#include <deque>
#include <functional>
#include <mutex>
#include <stop_token>
#include <thread>
#ifdef __LINUX__ #ifdef __LINUX__
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
#include "platform_linux/linux_platform_services.h" #include "platform_linux/linux_platform_services.h"
@@ -14,6 +22,79 @@
namespace { namespace {
class LegacyPreparedFileWorker final {
public:
LegacyPreparedFileWorker()
: worker_([this](std::stop_token stop_token) {
run(stop_token);
})
{
}
~LegacyPreparedFileWorker()
{
shutdown();
}
void post(std::function<void()> task)
{
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopping_)
return;
tasks_.push_back(std::move(task));
}
cv_.notify_one();
}
private:
void shutdown()
{
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
cv_.notify_all();
}
void run(std::stop_token stop_token)
{
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [&] {
return stopping_ || stop_token.stop_requested() || !tasks_.empty();
});
if ((stopping_ || stop_token.stop_requested()) && tasks_.empty())
break;
task = std::move(tasks_.front());
tasks_.pop_front();
}
if (task) {
try {
task();
} catch (...) {
LOG("prepared file worker task failed");
}
}
}
}
std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::function<void()>> tasks_;
bool stopping_ = false;
std::jthread worker_;
};
LegacyPreparedFileWorker& prepared_file_worker()
{
static LegacyPreparedFileWorker worker;
return worker;
}
[[nodiscard]] GLint rgba8_internal_format() noexcept [[nodiscard]] GLint rgba8_internal_format() noexcept
{ {
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format()); return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
@@ -201,15 +282,31 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam
} }
LOG("App::pick_file_save %s", target.path.c_str()); LOG("App::pick_file_save %s", target.path.c_str());
auto write_and_save = [=] { if (target.write_on_background_thread) {
writer(target.path); auto* app = this;
save_prepared_file(target.path, target.suggested_name, callback); prepared_file_worker().post([
}; app,
writer = std::move(writer),
callback = std::move(callback),
path = target.path,
suggested_name = target.suggested_name
]() mutable {
writer(path);
app->ui_task([app,
path = std::move(path),
suggested_name = std::move(suggested_name),
callback = std::move(callback)]() mutable {
app->save_prepared_file(
std::move(path),
std::move(suggested_name),
std::move(callback));
});
});
return;
}
if (target.write_on_background_thread) writer(target.path);
std::thread(write_and_save).detach(); save_prepared_file(target.path, target.suggested_name, std::move(callback));
else
write_and_save();
} }
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback) void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)

View File

@@ -7,9 +7,89 @@
#include "image.h" #include "image.h"
#include "node_panel_grid.h" #include "node_panel_grid.h"
#include <condition_variable>
#include <deque>
#include <functional>
#include <mutex>
#include <stop_token>
#include <thread>
namespace pp::panopainter { namespace pp::panopainter {
namespace { namespace {
class LegacyGridWorker final {
public:
LegacyGridWorker()
: worker_([this](std::stop_token stop_token) {
run(stop_token);
})
{
}
~LegacyGridWorker()
{
shutdown();
}
void post(std::function<void()> task)
{
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopping_)
return;
tasks_.push_back(std::move(task));
}
cv_.notify_one();
}
private:
void shutdown()
{
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
cv_.notify_all();
}
void run(std::stop_token stop_token)
{
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [&] {
return stopping_ || stop_token.stop_requested() || !tasks_.empty();
});
if ((stopping_ || stop_token.stop_requested()) && tasks_.empty())
break;
task = std::move(tasks_.front());
tasks_.pop_front();
}
if (task) {
try {
task();
} catch (...) {
LOG("grid worker task failed");
}
}
}
}
std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::function<void()>> tasks_;
bool stopping_ = false;
std::jthread worker_;
};
LegacyGridWorker& grid_worker()
{
static LegacyGridWorker worker;
return worker;
}
class LegacyGridUiServices final : public pp::app::GridUiServices { class LegacyGridUiServices final : public pp::app::GridUiServices {
public: public:
explicit LegacyGridUiServices(NodePanelGrid& panel) noexcept explicit LegacyGridUiServices(NodePanelGrid& panel) noexcept
@@ -66,13 +146,17 @@ public:
if (!renders_lightmap) if (!renders_lightmap)
return; return;
auto* panel = &panel_; auto panel = std::static_pointer_cast<NodePanelGrid>(panel_.shared_from_this());
std::thread([panel] { grid_worker().post([panel] {
BT_SetTerminate(); BT_SetTerminate();
panel->bake_uvs(); panel->bake_uvs();
if (App::I) {
App::I->ui_task([panel] {
panel->m_hm_shading->set_index(3); panel->m_hm_shading->set_index(3);
panel->m_shade_mode = NodePanelGrid::ShadeMode::Textured; panel->m_shade_mode = NodePanelGrid::ShadeMode::Textured;
}).detach(); });
}
});
} }
void commit_heightmap(bool updates_ground_opacity) override void commit_heightmap(bool updates_ground_opacity) override

View File

@@ -6,11 +6,6 @@
#include <array> #include <array>
#include <utility> #include <utility>
#if defined(__IOS__) || defined(__OSX__)
#include "app_core/app.h"
#include <dispatch/dispatch.h>
#endif
namespace pp::platform::apple { namespace pp::platform::apple {
namespace { namespace {
@@ -54,26 +49,16 @@ std::vector<std::string> AppleDocumentPlatformServices::document_browse_roots(
std::string AppleDocumentPlatformServices::clipboard_text() const std::string AppleDocumentPlatformServices::clipboard_text() const
{ {
#if defined(__IOS__) if (bridge_.clipboard_text)
return [App::I->ios_view clipboard_get_string]; return bridge_.clipboard_text();
#elif defined(__OSX__)
return [App::I->osx_view clipboard_get_string];
#else
return {}; return {};
#endif
} }
bool AppleDocumentPlatformServices::set_clipboard_text(std::string_view text) const bool AppleDocumentPlatformServices::set_clipboard_text(std::string_view text) const
{ {
const std::string value(text); if (bridge_.set_clipboard_text)
#if defined(__IOS__) return bridge_.set_clipboard_text(text);
return [App::I->ios_view clipboard_set_string:value];
#elif defined(__OSX__)
return [App::I->osx_view clipboard_set_string:value];
#else
(void)value;
return false; return false;
#endif
} }
void AppleDocumentPlatformServices::pick_image(PickedPathCallback callback) const void AppleDocumentPlatformServices::pick_image(PickedPathCallback callback) const
@@ -158,9 +143,8 @@ void AppleDocumentPlatformServices::display_file(std::string_view path) const
{ {
const std::string value(path); const std::string value(path);
#if defined(__IOS__) #if defined(__IOS__)
dispatch_async(dispatch_get_main_queue(), ^{ if (bridge_.display_file)
[App::I->ios_view display_file:value]; bridge_.display_file(value);
});
#elif defined(__OSX__) #elif defined(__OSX__)
[[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:value.c_str()]]; [[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:value.c_str()]];
#else #else
@@ -171,14 +155,9 @@ void AppleDocumentPlatformServices::display_file(std::string_view path) const
void AppleDocumentPlatformServices::share_file(std::string_view path) const void AppleDocumentPlatformServices::share_file(std::string_view path) const
{ {
const std::string value(path); const std::string value(path);
#if defined(__IOS__) #if defined(__IOS__) || defined(__OSX__)
dispatch_async(dispatch_get_main_queue(), ^{ if (bridge_.share_file)
[App::I->ios_view share_file:[NSString stringWithUTF8String:value.c_str()]]; bridge_.share_file(value);
});
#elif defined(__OSX__)
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->osx_view share_file:[NSString stringWithUTF8String:value.c_str()]];
});
#else #else
(void)value; (void)value;
#endif #endif
@@ -187,7 +166,8 @@ void AppleDocumentPlatformServices::share_file(std::string_view path) const
void AppleDocumentPlatformServices::set_cursor_visible(bool visible) const void AppleDocumentPlatformServices::set_cursor_visible(bool visible) const
{ {
#if defined(__OSX__) #if defined(__OSX__)
[App::I->osx_view show_cursor:visible]; if (bridge_.set_cursor_visible)
bridge_.set_cursor_visible(visible);
#else #else
(void)visible; (void)visible;
#endif #endif
@@ -196,7 +176,8 @@ void AppleDocumentPlatformServices::set_cursor_visible(bool visible) const
void AppleDocumentPlatformServices::save_ui_state() const void AppleDocumentPlatformServices::save_ui_state() const
{ {
#if defined(__OSX__) #if defined(__OSX__)
[App::I->osx_app save_ui_state]; if (bridge_.save_ui_state)
bridge_.save_ui_state();
#endif #endif
} }

View File

@@ -16,6 +16,12 @@ struct AppleDocumentPickerBridge {
std::function<void(std::vector<std::string> file_types, PickedPathCallback callback)> pick_save_file; std::function<void(std::vector<std::string> file_types, PickedPathCallback callback)> pick_save_file;
std::function<void(PickedPathCallback callback)> pick_directory; std::function<void(PickedPathCallback callback)> pick_directory;
std::function<std::string(std::string_view path)> format_working_directory_path; std::function<std::string(std::string_view path)> format_working_directory_path;
std::function<std::string()> clipboard_text;
std::function<bool(std::string_view text)> set_clipboard_text;
std::function<void(std::string path)> display_file;
std::function<void(std::string path)> share_file;
std::function<void(bool visible)> set_cursor_visible;
std::function<void()> save_ui_state;
}; };
class AppleDocumentPlatformServices { class AppleDocumentPlatformServices {

View File

@@ -112,6 +112,23 @@ public:
pp::platform::PlatformFamily::ios, pp::platform::PlatformFamily::ios,
[] { [] {
pp::platform::apple::AppleDocumentPickerBridge bridge; pp::platform::apple::AppleDocumentPickerBridge bridge;
bridge.clipboard_text = [] {
return [App::I->ios_view clipboard_get_string];
};
bridge.set_clipboard_text = [](std::string_view text) {
const std::string value(text);
return [App::I->ios_view clipboard_set_string:value];
};
bridge.display_file = [](std::string path) {
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->ios_view display_file:path];
});
};
bridge.share_file = [](std::string path) {
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->ios_view share_file:[NSString stringWithUTF8String:path.c_str()]];
});
};
bridge.pick_image = [](pp::platform::PickedPathCallback callback) { bridge.pick_image = [](pp::platform::PickedPathCallback callback) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[App::I->ios_view pick_photo:callback]; [App::I->ios_view pick_photo:callback];
@@ -132,6 +149,24 @@ public:
pp::platform::PlatformFamily::macos, pp::platform::PlatformFamily::macos,
[] { [] {
pp::platform::apple::AppleDocumentPickerBridge bridge; pp::platform::apple::AppleDocumentPickerBridge bridge;
bridge.clipboard_text = [] {
return [App::I->osx_view clipboard_get_string];
};
bridge.set_clipboard_text = [](std::string_view text) {
const std::string value(text);
return [App::I->osx_view clipboard_set_string:value];
};
bridge.share_file = [](std::string path) {
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->osx_view share_file:[NSString stringWithUTF8String:path.c_str()]];
});
};
bridge.set_cursor_visible = [](bool visible) {
[App::I->osx_view show_cursor:visible];
};
bridge.save_ui_state = [] {
[App::I->osx_app save_ui_state];
};
bridge.pick_file = []( bridge.pick_file = [](
std::vector<std::string> file_types, std::vector<std::string> file_types,
pp::platform::PickedPathCallback callback) { pp::platform::PickedPathCallback callback) {