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
- 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
routes retained `Layer` / `LayerFrame` render queueing through
`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
same-thread/post-reject queue behavior, but broader app/runtime singleton
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
longer compiles Web platform sources directly, but broader CMake and
entrypoint cleanup are not complete.

View File

@@ -59,6 +59,12 @@ Key facts:
- `AppRuntime` now owns synchronized running flags plus explicit post/reject,
same-thread execution, and queue-drain behavior, but broader singleton reach
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

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "app.h"
#include "app_core/document_resize.h"
#include "legacy_document_open_services.h"
#include "legacy_document_canvas_services.h"
#include "legacy_document_session_services.h"
#include "legacy_ui_overlay_services.h"
@@ -10,32 +11,6 @@
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(
App& app,
const std::shared_ptr<NodeDialogSave>& dialog,
@@ -201,7 +176,11 @@ void App::dialog_browse()
return;
}
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);

View File

@@ -5,102 +5,21 @@
#include "app.h"
#include "node_panel_brush.h"
#include <condition_variable>
#include <deque>
#include <functional>
#include <string>
#include <mutex>
#include <stop_token>
#include <thread>
namespace pp::panopainter {
namespace {
class LegacyBrushPackageWorker final {
public:
LegacyBrushPackageWorker()
: worker_([this](std::stop_token stop_token) {
run(stop_token);
})
void queue_legacy_brush_package_import_job(
App& app,
std::shared_ptr<NodePanelBrushPreset> presets,
pp::app::BrushPackageImportKind kind,
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;
return worker;
}
class LegacyBrushPackageImportServices final : public pp::app::BrushPackageImportServices {
public:
explicit LegacyBrushPackageImportServices(App& app) noexcept
: app_(app)
{
}
void import_brush_package(pp::app::BrushPackageImportKind kind, std::string_view path) override
{
auto presets = app_.presets;
const auto path_string = std::string(path);
brush_package_worker().post([presets, kind, path_string] {
app.runtime().canvas_async_task([presets = std::move(presets),
kind,
path_string = std::move(path_string)]() mutable {
BT_SetTerminate();
if (!presets) {
return;
@@ -114,6 +33,19 @@ public:
});
}
class LegacyBrushPackageImportServices final : public pp::app::BrushPackageImportServices {
public:
explicit LegacyBrushPackageImportServices(App& app) noexcept
: app_(app)
{
}
void import_brush_package(pp::app::BrushPackageImportKind kind, std::string_view path) override
{
const auto path_string = std::string(path);
queue_legacy_brush_package_import_job(app_, app_.presets, kind, std::move(path_string));
}
private:
App& app_;
};

View File

@@ -21,79 +21,6 @@ pp::foundation::Status execute_legacy_cloud_download_selection_action(
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
int progress_callback_download(void* clientp, curl_off_t dltotal,
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();
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] {
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);
});
});
@@ -438,7 +365,7 @@ public:
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);
});
}
@@ -458,7 +385,11 @@ void show_cloud_save_required_warning(App& app)
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(

View File

@@ -8,16 +8,11 @@
#include "paint_renderer/compositor.h"
#include <array>
#include <condition_variable>
#include <fstream>
#include <deque>
#include <functional>
#include <mutex>
#include <limits>
#include <span>
#include <string>
#include <stop_token>
#include <thread>
namespace pp::panopainter {
namespace {
@@ -36,79 +31,6 @@ struct LegacyDocumentExportSnapshotReports {
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)
{
if (path.empty()) {
@@ -846,7 +768,7 @@ public:
auto* app = &app_;
auto path_string = std::string(path);
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();
app->rec_export(path_string);
app->ui_task([app, path_string = std::move(path_string)]() mutable {

View File

@@ -9,6 +9,7 @@
#include "legacy_history_services.h"
#include "legacy_ui_overlay_services.h"
#include "log.h"
#include "node_dialog_browse.h"
#include "node_panel_brush.h"
#include "node_panel_layer.h"
@@ -154,6 +155,32 @@ private:
} // 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(
App& app,
pp::app::DocumentOpenPlanAction action,

View File

@@ -2,11 +2,15 @@
#include "app_core/document_session.h"
#include "foundation/result.h"
#include "ui_core/overlay_lifetime.h"
#include <string>
#include <string_view>
#include <memory>
class App;
class Node;
class NodeDialogBrowse;
namespace pp::panopainter {
@@ -19,6 +23,12 @@ namespace pp::panopainter {
pp::app::DocumentOpenPlanAction action,
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(
App& app,
std::string_view path,