Route app async work through AppRuntime

This commit is contained in:
2026-06-17 18:56:28 +02:00
parent 1a64118b2c
commit d632efb10f
9 changed files with 103 additions and 270 deletions

View File

@@ -18,6 +18,28 @@ agent or engineer to remove them without reconstructing context from chat.
## Reductions ## Reductions
- 2026-06-17: `DEBT-0039` was narrowed again. `App::dialog_browse()` in
`src/app_dialogs_workflow.cpp` now delegates retained browse-dialog button
wiring and selected-path open execution through
`src/legacy_document_open_services.*`, so the app dialog shell no longer
owns that document-open handoff inline while retained dialog creation,
unsaved-project prompting, project-open execution, and title/layer refresh
still remain.
- 2026-06-17: `DEBT-0038` was narrowed again. The retained cloud upload and
download background execution in `src/legacy_cloud_services.cpp` now routes
through `AppRuntime::canvas_async_task` instead of a file-static worker
singleton, while retained prompt/progress lifetime, OpenGL context guards,
thumbnail loading, and transfer execution still remain in the cloud bridge.
- 2026-06-17: `DEBT-0048` was narrowed again. The retained ABR/PPBR import path
in `src/legacy_brush_package_import_services.cpp` now uses
`AppRuntime::canvas_async_task` instead of a file-static worker singleton,
while the legacy preset panel remains the importer/storage owner and broader
brush asset/UI ownership transfer still remains open.
- 2026-06-17: `DEBT-0044` was narrowed again. The retained asynchronous
timelapse export path in `src/legacy_document_export_services.cpp` now routes
through `AppRuntime::canvas_async_task` instead of a file-static worker
singleton, while retained `App::rec_export`, animation MP4 execution, and
mobile/Web completion paths still remain in the video-export bridge.
- 2026-06-17: `DEBT-0036` was narrowed again. `src/canvas_layer.cpp` now - 2026-06-17: `DEBT-0036` was narrowed again. `src/canvas_layer.cpp` now
routes retained `Layer` / `LayerFrame` render queueing through routes retained `Layer` / `LayerFrame` render queueing through
`src/renderer_gl/render_runtime_dispatch.h` instead of direct `src/renderer_gl/render_runtime_dispatch.h` instead of direct

View File

@@ -79,6 +79,10 @@ Current conclusion:
- `AppRuntime` now owns synchronized running flags and explicit - `AppRuntime` now owns synchronized running flags and explicit
same-thread/post-reject queue behavior, but broader app/runtime singleton same-thread/post-reject queue behavior, but broader app/runtime singleton
reach and retained shell ownership still remain. reach and retained shell ownership still remain.
- The document-browse dialog handoff now lives in
`src/legacy_document_open_services.*`, and retained cloud upload/download,
brush-package import, and timelapse-export async paths now use
`AppRuntime::canvas_async_task` instead of file-static worker singletons.
- Platform extraction improved substantially and the root app source group no - Platform extraction improved substantially and the root app source group no
longer compiles Web platform sources directly, but broader CMake and longer compiles Web platform sources directly, but broader CMake and
entrypoint cleanup are not complete. entrypoint cleanup are not complete.

View File

@@ -59,6 +59,12 @@ Key facts:
- `AppRuntime` now owns synchronized running flags plus explicit post/reject, - `AppRuntime` now owns synchronized running flags plus explicit post/reject,
same-thread execution, and queue-drain behavior, but broader singleton reach same-thread execution, and queue-drain behavior, but broader singleton reach
and app-shell ownership remain. and app-shell ownership remain.
- Retained cloud upload/download, brush-package import, and timelapse-export
async paths now route through `AppRuntime::canvas_async_task`, but dialog and
execution ownership still remains in retained app/document/cloud bridges.
- `App::dialog_browse()` no longer owns browse-dialog button wiring inline; the
retained document-open bridge now owns that handoff in
`src/legacy_document_open_services.*`.
## Parallel Assignment Rules ## Parallel Assignment Rules

View File

@@ -1,6 +1,7 @@
#include "pch.h" #include "pch.h"
#include "app.h" #include "app.h"
#include "app_core/document_resize.h" #include "app_core/document_resize.h"
#include "legacy_document_open_services.h"
#include "legacy_document_canvas_services.h" #include "legacy_document_canvas_services.h"
#include "legacy_document_session_services.h" #include "legacy_document_session_services.h"
#include "legacy_ui_overlay_services.h" #include "legacy_ui_overlay_services.h"
@@ -10,32 +11,6 @@
namespace { namespace {
void wire_document_browse_dialog_actions(
App& app,
const std::shared_ptr<NodeDialogBrowse>& dialog,
Node& overlay_anchor,
pp::ui::NodeHandle overlay_handle)
{
const auto close_dialog = [&overlay_anchor, overlay_handle]() {
const auto close_status =
pp::panopainter::close_legacy_overlay_node(overlay_anchor, overlay_handle);
(void)close_status;
};
dialog->btn_ok->on_click = [&app, dialog, close_dialog](Node*)
{
if (dialog->is_selected())
{
app.open_document(dialog->selected_path);
close_dialog();
}
};
dialog->btn_cancel->on_click = [close_dialog](Node*)
{
close_dialog();
};
}
void wire_document_save_dialog_buttons( void wire_document_save_dialog_buttons(
App& app, App& app,
const std::shared_ptr<NodeDialogSave>& dialog, const std::shared_ptr<NodeDialogSave>& dialog,
@@ -201,7 +176,11 @@ void App::dialog_browse()
return; return;
} }
const auto overlay_handle = overlay.value(); const auto overlay_handle = overlay.value();
wire_document_browse_dialog_actions(*this, dialog, *overlay_anchor, overlay_handle); pp::panopainter::wire_legacy_document_browse_dialog_actions(
*this,
dialog,
*overlay_anchor,
overlay_handle);
}; };
continue_document_workflow_after_optional_save(show_dialog); continue_document_workflow_after_optional_save(show_dialog);

View File

@@ -5,88 +5,32 @@
#include "app.h" #include "app.h"
#include "node_panel_brush.h" #include "node_panel_brush.h"
#include <condition_variable>
#include <deque>
#include <functional> #include <functional>
#include <string> #include <string>
#include <mutex>
#include <stop_token>
#include <thread>
namespace pp::panopainter { namespace pp::panopainter {
namespace { namespace {
class LegacyBrushPackageWorker final { void queue_legacy_brush_package_import_job(
public: App& app,
LegacyBrushPackageWorker() std::shared_ptr<NodePanelBrushPreset> presets,
: worker_([this](std::stop_token stop_token) { pp::app::BrushPackageImportKind kind,
run(stop_token); std::string path_string)
})
{
}
~LegacyBrushPackageWorker()
{
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("brush package import worker task failed");
}
}
}
}
std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::function<void()>> tasks_;
bool stopping_ = false;
std::jthread worker_;
};
LegacyBrushPackageWorker& brush_package_worker()
{ {
static LegacyBrushPackageWorker worker; app.runtime().canvas_async_task([presets = std::move(presets),
return worker; kind,
path_string = std::move(path_string)]() mutable {
BT_SetTerminate();
if (!presets) {
return;
}
if (kind == pp::app::BrushPackageImportKind::abr) {
presets->import_abr(path_string);
return;
}
presets->import_ppbr(path_string);
});
} }
class LegacyBrushPackageImportServices final : public pp::app::BrushPackageImportServices { class LegacyBrushPackageImportServices final : public pp::app::BrushPackageImportServices {
@@ -98,20 +42,8 @@ public:
void import_brush_package(pp::app::BrushPackageImportKind kind, std::string_view path) override void import_brush_package(pp::app::BrushPackageImportKind kind, std::string_view path) override
{ {
auto presets = app_.presets;
const auto path_string = std::string(path); const auto path_string = std::string(path);
brush_package_worker().post([presets, kind, path_string] { queue_legacy_brush_package_import_job(app_, app_.presets, kind, std::move(path_string));
BT_SetTerminate();
if (!presets) {
return;
}
if (kind == pp::app::BrushPackageImportKind::abr) {
presets->import_abr(path_string);
return;
}
presets->import_ppbr(path_string);
});
} }
private: private:

View File

@@ -21,79 +21,6 @@ pp::foundation::Status execute_legacy_cloud_download_selection_action(
namespace { namespace {
class LegacyCloudWorker final {
public:
LegacyCloudWorker()
: worker_([this](std::stop_token stop_token) {
run(stop_token);
})
{
}
~LegacyCloudWorker()
{
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("cloud worker task failed");
}
}
}
}
std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::function<void()>> tasks_;
bool stopping_ = false;
std::jthread worker_;
};
LegacyCloudWorker& cloud_worker()
{
static LegacyCloudWorker worker;
return worker;
}
#if WITH_CURL #if WITH_CURL
int progress_callback_download(void* clientp, curl_off_t dltotal, int progress_callback_download(void* clientp, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
@@ -330,7 +257,7 @@ void setup_cloud_publish_prompt(App& app, bool save_before_upload)
const auto prompt_plan = pp::app::plan_cloud_publish_prompt(); const auto prompt_plan = pp::app::plan_cloud_publish_prompt();
auto dialog = app.message_box(prompt_plan.title, prompt_plan.message, prompt_plan.show_cancel); auto dialog = app.message_box(prompt_plan.title, prompt_plan.message, prompt_plan.show_cancel);
wire_cloud_publish_prompt_buttons(dialog, [&app, save_before_upload] { wire_cloud_publish_prompt_buttons(dialog, [&app, save_before_upload] {
queue_legacy_cloud_worker_task([app = &app, save_before_upload] { app.runtime().canvas_async_task([app = &app, save_before_upload] {
execute_cloud_publish_worker(*app, save_before_upload); execute_cloud_publish_worker(*app, save_before_upload);
}); });
}); });
@@ -438,7 +365,7 @@ public:
void start_download(const pp::app::CloudDownloadRequest& request) override void start_download(const pp::app::CloudDownloadRequest& request) override
{ {
queue_legacy_cloud_worker_task([app = &app_, request] { app_.runtime().canvas_async_task([app = &app_, request] {
execute_cloud_download_thread(*app, request); execute_cloud_download_thread(*app, request);
}); });
} }
@@ -458,7 +385,11 @@ void show_cloud_save_required_warning(App& app)
void queue_legacy_cloud_worker_task(std::function<void()> task) void queue_legacy_cloud_worker_task(std::function<void()> task)
{ {
cloud_worker().post(std::move(task)); if (App::I == nullptr) {
LOG("cloud worker task skipped: app unavailable");
return;
}
App::I->runtime().canvas_async_task(std::move(task));
} }
pp::foundation::Status execute_legacy_cloud_upload_plan( pp::foundation::Status execute_legacy_cloud_upload_plan(

View File

@@ -8,16 +8,11 @@
#include "paint_renderer/compositor.h" #include "paint_renderer/compositor.h"
#include <array> #include <array>
#include <condition_variable>
#include <fstream> #include <fstream>
#include <deque>
#include <functional> #include <functional>
#include <mutex>
#include <limits> #include <limits>
#include <span> #include <span>
#include <string> #include <string>
#include <stop_token>
#include <thread>
namespace pp::panopainter { namespace pp::panopainter {
namespace { namespace {
@@ -36,79 +31,6 @@ struct LegacyDocumentExportSnapshotReports {
pp::paint_renderer::DocumentFrameFacePngExportResult face_pngs; pp::paint_renderer::DocumentFrameFacePngExportResult face_pngs;
}; };
class LegacyDocumentVideoExportWorker final {
public:
LegacyDocumentVideoExportWorker()
: worker_([this](std::stop_token stop_token) {
run(stop_token);
})
{
}
~LegacyDocumentVideoExportWorker()
{
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("document video export worker task failed");
}
}
}
}
std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::function<void()>> tasks_;
bool stopping_ = false;
std::jthread worker_;
};
LegacyDocumentVideoExportWorker& document_video_export_worker()
{
static LegacyDocumentVideoExportWorker worker;
return worker;
}
pp::foundation::Status write_export_binary_file(std::string_view path, std::span<const std::byte> bytes) pp::foundation::Status write_export_binary_file(std::string_view path, std::span<const std::byte> bytes)
{ {
if (path.empty()) { if (path.empty()) {
@@ -846,7 +768,7 @@ public:
auto* app = &app_; auto* app = &app_;
auto path_string = std::string(path); auto path_string = std::string(path);
if (asynchronous_) { if (asynchronous_) {
document_video_export_worker().post([app, path_string = std::move(path_string)]() mutable { app_.runtime().canvas_async_task([app, path_string = std::move(path_string)]() mutable {
BT_SetTerminate(); BT_SetTerminate();
app->rec_export(path_string); app->rec_export(path_string);
app->ui_task([app, path_string = std::move(path_string)]() mutable { app->ui_task([app, path_string = std::move(path_string)]() mutable {

View File

@@ -9,6 +9,7 @@
#include "legacy_history_services.h" #include "legacy_history_services.h"
#include "legacy_ui_overlay_services.h" #include "legacy_ui_overlay_services.h"
#include "log.h" #include "log.h"
#include "node_dialog_browse.h"
#include "node_panel_brush.h" #include "node_panel_brush.h"
#include "node_panel_layer.h" #include "node_panel_layer.h"
@@ -154,6 +155,32 @@ private:
} // namespace } // namespace
void wire_legacy_document_browse_dialog_actions(
App& app,
const std::shared_ptr<NodeDialogBrowse>& dialog,
Node& overlay_anchor,
pp::ui::NodeHandle overlay_handle)
{
const auto close_dialog = [&overlay_anchor, overlay_handle]() {
const auto close_status =
pp::panopainter::close_legacy_overlay_node(overlay_anchor, overlay_handle);
(void)close_status;
};
dialog->btn_ok->on_click = [&app, dialog, close_dialog](Node*)
{
if (dialog->is_selected())
{
app.open_document(dialog->selected_path);
close_dialog();
}
};
dialog->btn_cancel->on_click = [close_dialog](Node*)
{
close_dialog();
};
}
pp::foundation::Status execute_legacy_document_open_plan( pp::foundation::Status execute_legacy_document_open_plan(
App& app, App& app,
pp::app::DocumentOpenPlanAction action, pp::app::DocumentOpenPlanAction action,

View File

@@ -2,11 +2,15 @@
#include "app_core/document_session.h" #include "app_core/document_session.h"
#include "foundation/result.h" #include "foundation/result.h"
#include "ui_core/overlay_lifetime.h"
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <memory>
class App; class App;
class Node;
class NodeDialogBrowse;
namespace pp::panopainter { namespace pp::panopainter {
@@ -19,6 +23,12 @@ namespace pp::panopainter {
pp::app::DocumentOpenPlanAction action, pp::app::DocumentOpenPlanAction action,
const pp::app::DocumentOpenRoute& route); const pp::app::DocumentOpenRoute& route);
void wire_legacy_document_browse_dialog_actions(
App& app,
const std::shared_ptr<NodeDialogBrowse>& dialog,
Node& overlay_anchor,
pp::ui::NodeHandle overlay_handle);
void execute_legacy_downloaded_project_open( void execute_legacy_downloaded_project_open(
App& app, App& app,
std::string_view path, std::string_view path,