From 04a1c5d0b1587fa60e8b2709170eee49c7125449 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 17 Jun 2026 18:15:54 +0200 Subject: [PATCH] Harden app runtime and thin export shell --- cmake/PanoPainterSources.cmake | 1 - docs/modernization/debt.md | 17 ++ docs/modernization/roadmap.md | 12 +- docs/modernization/tasks.md | 8 +- src/app.h | 18 +- src/app_core/app_thread.h | 36 +++ src/app_runtime.cpp | 279 ++++++++++++++++++--- src/app_runtime.h | 140 ++--------- src/legacy_app_runtime_shell_services.cpp | 4 +- src/legacy_canvas_document_io_services.cpp | 35 +-- src/legacy_document_export_services.cpp | 44 ++++ src/legacy_document_export_services.h | 9 + tests/app_core/app_thread_tests.cpp | 29 +++ 13 files changed, 426 insertions(+), 206 deletions(-) diff --git a/cmake/PanoPainterSources.cmake b/cmake/PanoPainterSources.cmake index b0242fde..8dfccbfd 100644 --- a/cmake/PanoPainterSources.cmake +++ b/cmake/PanoPainterSources.cmake @@ -180,7 +180,6 @@ set(PP_PANOPAINTER_APP_SOURCES src/legacy_document_open_services.h src/legacy_document_session_services.cpp src/legacy_document_session_services.h - ${PP_PLATFORM_WEB_SOURCES} src/version.cpp ) diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 2b74edfc..f097793c 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,6 +18,23 @@ agent or engineer to remove them without reconstructing context from chat. ## Reductions +- 2026-06-17: `DEBT-0003` was narrowed again. `src/app_runtime.cpp` and + `src/app_runtime.h` now synchronize render/UI worker running state, reject + cross-thread work once those workers stop, and drain queued work through + explicit runtime-owned post/shutdown semantics consumed by + `src/legacy_app_runtime_shell_services.cpp`; broader singleton reach, app + shell ownership, and remaining runtime service adoption still remain. +- 2026-06-17: `DEBT-0017` was narrowed again. + `cmake/PanoPainterSources.cmake` no longer compiles + `${PP_PLATFORM_WEB_SOURCES}` into `PP_PANOPAINTER_APP_SOURCES`, so root app + source ownership no longer mixes live Web platform implementation files into + the retained app group; broader platform entrypoint/package cleanup remains. +- 2026-06-17: `DEBT-0043` was narrowed again. + `src/legacy_canvas_document_io_services.cpp` now delegates the live + equirectangular export family through `src/legacy_document_export_services.*` + instead of carrying that orchestration inline with the broader canvas + document-I/O shell; retained export directory creation, Web handoff, and + remaining legacy `Canvas` export execution still remain. - 2026-06-17: `DEBT-0003` was narrowed again. `src/platform_windows/windows_runtime_flow.*` now owns the live Win32 startup/session composition flow, including the bound app/session preflight diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 14cbeeb6..7c9255e2 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -76,12 +76,12 @@ Current conclusion: debt. - Renderer API contracts exist, but retained OpenGL resource classes still leak into app/UI/document code. -- `AppRuntime` is a useful step toward owned queues, but thread affinity and - shutdown safety are still expressed mostly by convention, mutable booleans, - and singleton call sites. -- Platform extraction improved substantially, but CMake and entrypoint cleanup - are not complete. In particular, Web platform sources still appear inside - `PP_PANOPAINTER_APP_SOURCES`, even though `pp_platform_web` also exists. +- `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. +- 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. ## Target Architecture diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 5adad175..9d8ed9e3 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -55,11 +55,9 @@ Key facts: - Raw `Node*` and callback captures remain a dominant UI lifetime risk. - `RTT`, `Texture2D`, `Shape`, `Shader`, `Font`, and `CanvasLayer` still route render work through `App::I` queues. -- `AppRuntime` uses `std::jthread`, which is progress, but queue ownership, - running flags, thread affinity, and shutdown contracts are not yet component - contracts. -- `PP_PANOPAINTER_APP_SOURCES` still includes `${PP_PLATFORM_WEB_SOURCES}`, - which should be removed in favor of concrete `pp_platform_web` ownership. +- `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. ## Parallel Assignment Rules diff --git a/src/app.h b/src/app.h index 046d5459..1d02913b 100644 --- a/src/app.h +++ b/src/app.h @@ -336,19 +336,19 @@ public: bool is_render_thread() { - return runtime_.is_render_thread(); + return runtime().is_render_thread(); } template std::future render_task_async(T task, bool unique = false) { - return runtime_.render_task_async(std::move(task), unique); + return runtime().render_task_async(std::move(task), unique); } template void render_task(T task) { - runtime_.render_task(std::move(task)); + runtime().render_task(std::move(task)); } void render_sync() @@ -367,27 +367,27 @@ public: bool is_ui_thread() { - return runtime_.is_ui_thread(); + return runtime().is_ui_thread(); } template std::future ui_task_async(T task, bool unique = false) { - return runtime_.ui_task_async(std::move(task), unique); + return runtime().ui_task_async(std::move(task), unique); } template void ui_task(T task) { - runtime_.ui_task(std::move(task)); - if (runtime_.request_redraw()) + runtime().ui_task(std::move(task)); + if (runtime().request_redraw()) redraw = true; - runtime_.clear_request_redraw(); + runtime().clear_request_redraw(); } void ui_sync() { - runtime_.ui_sync(); + runtime().ui_sync(); } private: diff --git a/src/app_core/app_thread.h b/src/app_core/app_thread.h index 859736c4..d14aa096 100644 --- a/src/app_core/app_thread.h +++ b/src/app_core/app_thread.h @@ -19,6 +19,17 @@ struct AppTaskDispatchPlan { bool reject_unsafe_cross_thread_dispatch = false; }; +struct AppRuntimeTaskDispatchPlan { + bool execute_immediately = false; + bool queue_task = false; + bool remove_matching_unique_task = false; + bool notify_worker = false; + bool wait_for_completion = false; + bool request_redraw = false; + bool reject_unsafe_cross_thread_dispatch = false; + bool reject_stopped_worker_dispatch = false; +}; + struct AppAsyncRedrawPlan { bool set_redraw = true; bool notify_ui = true; @@ -93,6 +104,31 @@ struct AppThreadStopPlan { }; } +[[nodiscard]] constexpr AppRuntimeTaskDispatchPlan plan_app_runtime_task_dispatch( + bool already_on_target_thread, + bool unique, + std::size_t queued_task_count, + bool worker_running, + bool wait_for_completion, + bool request_redraw_after_dispatch, + bool reject_unsafe_cross_thread_dispatch = false) noexcept +{ + const bool queue_task = !already_on_target_thread + && worker_running + && !reject_unsafe_cross_thread_dispatch; + return AppRuntimeTaskDispatchPlan { + .execute_immediately = already_on_target_thread, + .queue_task = queue_task, + .remove_matching_unique_task = queue_task && unique && queued_task_count > 0U, + .notify_worker = queue_task, + .wait_for_completion = queue_task && wait_for_completion, + .request_redraw = (already_on_target_thread || queue_task) && request_redraw_after_dispatch, + .reject_unsafe_cross_thread_dispatch = !already_on_target_thread + && reject_unsafe_cross_thread_dispatch, + .reject_stopped_worker_dispatch = !already_on_target_thread && !worker_running, + }; +} + [[nodiscard]] constexpr AppAsyncRedrawPlan plan_app_async_redraw() noexcept { return AppAsyncRedrawPlan {}; diff --git a/src/app_runtime.cpp b/src/app_runtime.cpp index c5043e4d..c0584511 100644 --- a/src/app_runtime.cpp +++ b/src/app_runtime.cpp @@ -3,6 +3,34 @@ #include "app.h" +namespace { + +void execute_render_worker_task(AppTask& task) noexcept +{ + try + { + task(); + } + catch (...) + { + LOG("render worker task failed"); + } +} + +void execute_ui_worker_task(AppTask& task) noexcept +{ + try + { + task(); + } + catch (...) + { + LOG("ui worker task failed"); + } +} + +} // namespace + AppRuntime::AppRuntime() : prepared_file_worker_([this](std::stop_token stop_token) { @@ -23,11 +51,13 @@ AppRuntime::~AppRuntime() bool AppRuntime::is_render_thread() const noexcept { + std::lock_guard lock(render_task_mutex_); return std::this_thread::get_id() == render_thread_id_; } bool AppRuntime::is_ui_thread() const noexcept { + std::lock_guard lock(ui_task_mutex_); return std::this_thread::get_id() == ui_thread_id_; } @@ -84,6 +114,174 @@ void AppRuntime::drain_main_thread_tasks() } } +std::future AppRuntime::render_task_async(AppTask task, bool unique) +{ + auto future = task.get_future(); + const auto on_render_thread = is_render_thread(); + const auto running = render_running_.load(std::memory_order_acquire); + const auto dispatch = pp::app::plan_app_runtime_task_dispatch( + on_render_thread, + unique, + 0U, + running, + false, + false); + if (dispatch.execute_immediately) + { + execute_render_worker_task(task); + return future; + } + + if (!dispatch.queue_task) + return future; + + bool notify_worker = false; + { + std::lock_guard lock(render_task_mutex_); + if (!render_running_.load(std::memory_order_acquire)) + return future; + + const auto queue_dispatch = pp::app::plan_app_runtime_task_dispatch( + false, + unique, + render_tasklist_.size(), + true, + false, + false); + if (queue_dispatch.remove_matching_unique_task) + { + render_tasklist_.erase( + std::remove_if(render_tasklist_.begin(), render_tasklist_.end(), + [id = task.task_id](AppTask const& t) { return t.task_id == id; }), + render_tasklist_.end()); + } + render_tasklist_.push_back(std::move(task)); + notify_worker = queue_dispatch.notify_worker; + } + if (notify_worker) + render_cv_.notify_all(); + return future; +} + +void AppRuntime::render_task(AppTask task) +{ + auto future = task.get_future(); + const auto on_render_thread = is_render_thread(); + const auto running = render_running_.load(std::memory_order_acquire); + const auto dispatch = pp::app::plan_app_runtime_task_dispatch( + on_render_thread, + false, + 0U, + running, + true, + false); + if (dispatch.execute_immediately) + { + execute_render_worker_task(task); + } + else if (dispatch.queue_task) + { + bool notify_worker = false; + { + std::lock_guard lock(render_task_mutex_); + if (!render_running_.load(std::memory_order_acquire)) + return; + render_tasklist_.push_back(std::move(task)); + notify_worker = dispatch.notify_worker; + } + if (notify_worker) + render_cv_.notify_all(); + } + + if (dispatch.wait_for_completion) + future.get(); +} + +std::future AppRuntime::ui_task_async(AppTask task, bool unique) +{ + auto future = task.get_future(); + const auto on_ui_thread = is_ui_thread(); + const auto running = ui_running_.load(std::memory_order_acquire); + const auto dispatch = pp::app::plan_app_runtime_task_dispatch( + on_ui_thread, + unique, + 0U, + running, + false, + false); + if (dispatch.execute_immediately) + { + execute_ui_worker_task(task); + return future; + } + + if (!dispatch.queue_task) + return future; + + bool notify_worker = false; + { + std::lock_guard lock(ui_task_mutex_); + if (!ui_running_.load(std::memory_order_acquire)) + return future; + + const auto queue_dispatch = pp::app::plan_app_runtime_task_dispatch( + false, + unique, + ui_tasklist_.size(), + true, + false, + false); + if (queue_dispatch.remove_matching_unique_task) + { + ui_tasklist_.erase( + std::remove_if(ui_tasklist_.begin(), ui_tasklist_.end(), + [id = task.task_id](AppTask const& t) { return t.task_id == id; }), + ui_tasklist_.end()); + } + ui_tasklist_.push_back(std::move(task)); + notify_worker = queue_dispatch.notify_worker; + } + if (notify_worker) + ui_cv_.notify_all(); + return future; +} + +void AppRuntime::ui_task(AppTask task) +{ + auto future = task.get_future(); + const auto on_ui_thread = is_ui_thread(); + const auto running = ui_running_.load(std::memory_order_acquire); + const auto dispatch = pp::app::plan_app_runtime_task_dispatch( + on_ui_thread, + false, + 0U, + running, + true, + true); + if (dispatch.execute_immediately) + { + execute_ui_worker_task(task); + } + else if (dispatch.queue_task) + { + bool notify_worker = false; + { + std::lock_guard lock(ui_task_mutex_); + if (!ui_running_.load(std::memory_order_acquire)) + return; + ui_tasklist_.push_back(std::move(task)); + notify_worker = dispatch.notify_worker; + } + if (notify_worker) + ui_cv_.notify_all(); + } + + if (dispatch.wait_for_completion) + future.get(); + if (dispatch.request_redraw) + request_redraw_.store(true, std::memory_order_release); +} + void AppRuntime::prepared_file_worker_main(std::stop_token stop_token) { for (;;) @@ -177,14 +375,17 @@ void AppRuntime::canvas_async_worker_stop() void AppRuntime::render_thread_tick(App& app) { static uint32_t count = 0; - render_thread_id_ = std::this_thread::get_id(); + { + std::lock_guard lock(render_task_mutex_); + render_thread_id_ = std::this_thread::get_id(); + } std::deque working_list; pp::app::AppQueueDrainPlan drain_plan; { std::unique_lock lock(render_task_mutex_); drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size()); - render_running_ = drain_plan.mark_running; + render_running_.store(drain_plan.mark_running, std::memory_order_release); if (!drain_plan.drain_tasks) return; working_list = std::move(render_tasklist_); @@ -196,7 +397,7 @@ void AppRuntime::render_thread_tick(App& app) while (!working_list.empty()) { count++; - working_list.front()(); + execute_render_worker_task(working_list.front()); working_list.pop_front(); } app.async_end(); @@ -207,39 +408,46 @@ void AppRuntime::render_thread_main(App& app, std::stop_token stop_token) { BT_SetTerminate(); - render_thread_id_ = std::this_thread::get_id(); - render_running_ = pp::app::plan_app_thread_start().mark_running; - while (render_running_ && !stop_token.stop_requested()) + { + std::lock_guard lock(render_task_mutex_); + render_thread_id_ = std::this_thread::get_id(); + } + render_running_.store(pp::app::plan_app_thread_start().mark_running, std::memory_order_release); + for (;;) { std::deque working_list; - pp::app::AppQueueDrainPlan drain_plan; { std::unique_lock lock(render_task_mutex_); render_cv_.wait(lock, [this, &stop_token] { - return stop_token.stop_requested() || !render_running_ || !render_tasklist_.empty(); + return stop_token.stop_requested() || !render_running_.load(std::memory_order_acquire) || !render_tasklist_.empty(); }); - drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size()); + if (render_tasklist_.empty()) + { + if (stop_token.stop_requested() || !render_running_.load(std::memory_order_acquire)) + break; + continue; + } working_list = std::move(render_tasklist_); } - if (drain_plan.wrap_in_render_context) + app.async_start(); + while (!working_list.empty()) { - app.async_start(); - while (!working_list.empty()) - { - working_list.front()(); - working_list.pop_front(); - } - app.async_end(); + execute_render_worker_task(working_list.front()); + working_list.pop_front(); } + app.async_end(); } } void AppRuntime::ui_thread_tick(App& app) { - ui_thread_id_ = std::this_thread::get_id(); + { + std::lock_guard lock(ui_task_mutex_); + ui_thread_id_ = std::this_thread::get_id(); + } std::deque working_list; pp::app::AppUiTickPlan tick_plan; @@ -247,7 +455,7 @@ void AppRuntime::ui_thread_tick(App& app) { std::unique_lock lock(ui_task_mutex_); tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist_.size(), app.redraw); - ui_running_ = tick_plan.mark_running; + ui_running_.store(tick_plan.mark_running, std::memory_order_release); working_list = std::move(ui_tasklist_); } @@ -255,7 +463,7 @@ void AppRuntime::ui_thread_tick(App& app) { while (!working_list.empty()) { - working_list.front()(); + execute_ui_worker_task(working_list.front()); working_list.pop_front(); } } @@ -282,8 +490,11 @@ void AppRuntime::ui_thread_main(App& app, std::stop_token stop_token) { BT_SetTerminate(); - ui_thread_id_ = std::this_thread::get_id(); - ui_running_ = pp::app::plan_app_thread_start().mark_running; + { + std::lock_guard lock(ui_task_mutex_); + ui_thread_id_ = std::this_thread::get_id(); + } + ui_running_.store(pp::app::plan_app_thread_start().mark_running, std::memory_order_release); app.attach_ui_thread(); @@ -295,7 +506,7 @@ void AppRuntime::ui_thread_main(App& app, std::stop_token stop_token) float t_fps_counter = 0; float t_reloader = 0; int rendered_frames = 0; - while (ui_running_ && !stop_token.stop_requested()) + for (;;) { std::deque working_list; @@ -303,16 +514,24 @@ void AppRuntime::ui_thread_main(App& app, std::stop_token stop_token) std::unique_lock lock(ui_task_mutex_); ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms), [this, &stop_token] { - return stop_token.stop_requested() || !ui_running_ || !ui_tasklist_.empty(); + return stop_token.stop_requested() || !ui_running_.load(std::memory_order_acquire) || !ui_tasklist_.empty(); }); - working_list = std::move(ui_tasklist_); + if (ui_tasklist_.empty()) + { + if (stop_token.stop_requested() || !ui_running_.load(std::memory_order_acquire)) + break; + } + else + { + working_list = std::move(ui_tasklist_); + } } if (!working_list.empty()) { while (!working_list.empty()) { - working_list.front()(); + execute_ui_worker_task(working_list.front()); working_list.pop_front(); } } @@ -383,14 +602,14 @@ void AppRuntime::render_thread_start(App& app) { render_thread_main(app, stop_token); }); - render_running_ = plan.mark_running; + render_running_.store(plan.mark_running, std::memory_order_release); } void AppRuntime::render_thread_stop() { const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable()); if (plan.mark_not_running) - render_running_ = false; + render_running_.store(false, std::memory_order_release); if (plan.join_thread) render_thread_.request_stop(); if (plan.notify_worker) @@ -407,14 +626,14 @@ void AppRuntime::ui_thread_start(App& app) { ui_thread_main(app, stop_token); }); - ui_running_ = plan.mark_running; + ui_running_.store(plan.mark_running, std::memory_order_release); } void AppRuntime::ui_thread_stop() { const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable()); if (plan.mark_not_running) - ui_running_ = false; + ui_running_.store(false, std::memory_order_release); if (plan.join_thread) ui_thread_.request_stop(); if (plan.notify_worker) diff --git a/src/app_runtime.h b/src/app_runtime.h index 2b7ee5a8..2a7d48c6 100644 --- a/src/app_runtime.h +++ b/src/app_runtime.h @@ -2,6 +2,7 @@ #include "app_core/app_thread.h" +#include #include #include #include @@ -39,8 +40,8 @@ public: [[nodiscard]] bool is_render_thread() const noexcept; [[nodiscard]] bool is_ui_thread() const noexcept; - [[nodiscard]] bool request_redraw() const noexcept { return request_redraw_; } - void clear_request_redraw() noexcept { request_redraw_ = false; } + [[nodiscard]] bool request_redraw() const noexcept { return request_redraw_.load(std::memory_order_acquire); } + void clear_request_redraw() noexcept { request_redraw_.store(false, std::memory_order_release); } void notify_render_worker() noexcept; void notify_ui_worker() noexcept; @@ -49,6 +50,11 @@ public: void main_thread_task(std::packaged_task task); void drain_main_thread_tasks(); + std::future render_task_async(AppTask task, bool unique = false); + void render_task(AppTask task); + std::future ui_task_async(AppTask task, bool unique = false); + void ui_task(AppTask task); + void render_thread_tick(App& app); void render_thread_main(App& app, std::stop_token stop_token); void render_thread_start(App& app); @@ -62,68 +68,13 @@ public: template std::future render_task_async(T task, bool unique = false) { - AppTask pt(task); - auto f = pt.get_future(); - const auto dispatch = pp::app::plan_app_task_dispatch( - is_render_thread(), - unique, - 0U, - render_running_, - false, - false); - if (dispatch.execute_immediately) - { - pt(); - } - else if (dispatch.queue_task) - { - { - std::lock_guard lock(render_task_mutex_); - const auto queue_dispatch = pp::app::plan_app_task_dispatch( - false, - unique, - render_tasklist_.size(), - render_running_, - false, - false); - if (queue_dispatch.remove_matching_unique_task) - render_tasklist_.erase(std::remove_if(render_tasklist_.begin(), render_tasklist_.end(), - [id = pt.task_id](AppTask const& t){ return t.task_id == id; }), render_tasklist_.end()); - render_tasklist_.push_back(std::move(pt)); - } - if (dispatch.notify_worker) - render_cv_.notify_all(); - } - return f; + return render_task_async(AppTask(std::move(task)), unique); } template void render_task(T task) { - AppTask pt(task); - auto f = pt.get_future(); - const auto dispatch = pp::app::plan_app_task_dispatch( - is_render_thread(), - false, - 0U, - render_running_, - true, - false); - if (dispatch.execute_immediately) - { - pt(); - } - else if (dispatch.queue_task) - { - { - std::lock_guard lock(render_task_mutex_); - render_tasklist_.push_back(std::move(pt)); - } - if (dispatch.notify_worker) - render_cv_.notify_all(); - } - if (dispatch.wait_for_completion) - f.get(); + render_task(AppTask(std::move(task))); } void render_sync() @@ -134,70 +85,13 @@ public: template std::future ui_task_async(T task, bool unique = false) { - AppTask pt(task); - auto f = pt.get_future(); - const auto dispatch = pp::app::plan_app_task_dispatch( - is_ui_thread(), - unique, - 0U, - ui_running_, - false, - false); - if (dispatch.execute_immediately) - { - pt(); - } - else if (dispatch.queue_task) - { - { - std::lock_guard lock(ui_task_mutex_); - const auto queue_dispatch = pp::app::plan_app_task_dispatch( - false, - unique, - ui_tasklist_.size(), - ui_running_, - false, - false); - if (queue_dispatch.remove_matching_unique_task) - ui_tasklist_.erase(std::remove_if(ui_tasklist_.begin(), ui_tasklist_.end(), - [id = pt.task_id](AppTask const& t){ return t.task_id == id; }), ui_tasklist_.end()); - ui_tasklist_.push_back(std::move(pt)); - } - if (dispatch.notify_worker) - ui_cv_.notify_all(); - } - return f; + return ui_task_async(AppTask(std::move(task)), unique); } template void ui_task(T task) { - AppTask pt(task); - auto f = pt.get_future(); - const auto dispatch = pp::app::plan_app_task_dispatch( - is_ui_thread(), - false, - 0U, - ui_running_, - true, - true); - if (dispatch.execute_immediately) - { - pt(); - } - else if (dispatch.queue_task) - { - { - std::lock_guard lock(ui_task_mutex_); - ui_tasklist_.push_back(std::move(pt)); - } - if (dispatch.notify_worker) - ui_cv_.notify_all(); - } - if (dispatch.wait_for_completion) - f.get(); - if (dispatch.request_redraw) - request_redraw_ = true; + ui_task(AppTask(std::move(task))); } void ui_sync() @@ -227,17 +121,17 @@ private: std::mutex main_thread_task_mutex_; std::deque render_tasklist_; - std::mutex render_task_mutex_; + mutable std::mutex render_task_mutex_; std::condition_variable render_cv_; std::jthread render_thread_; std::thread::id render_thread_id_; - bool render_running_ = false; + std::atomic_bool render_running_ = false; std::deque ui_tasklist_; - std::mutex ui_task_mutex_; + mutable std::mutex ui_task_mutex_; std::condition_variable ui_cv_; std::jthread ui_thread_; std::thread::id ui_thread_id_; - bool ui_running_ = false; - bool request_redraw_ = false; + std::atomic_bool ui_running_ = false; + std::atomic_bool request_redraw_ = false; }; diff --git a/src/legacy_app_runtime_shell_services.cpp b/src/legacy_app_runtime_shell_services.cpp index f590a3a0..bfc54f53 100644 --- a/src/legacy_app_runtime_shell_services.cpp +++ b/src/legacy_app_runtime_shell_services.cpp @@ -230,7 +230,7 @@ void App::async_redraw() if (plan.set_redraw) redraw = true; if (plan.notify_ui) - runtime_.notify_ui_worker(); + runtime().notify_ui_worker(); } void App::async_end() @@ -370,5 +370,5 @@ void App::rec_loop() void App::render_thread_tick() { - runtime_.render_thread_tick(*this); + runtime().render_thread_tick(*this); } diff --git a/src/legacy_canvas_document_io_services.cpp b/src/legacy_canvas_document_io_services.cpp index 248fec00..9dea7141 100644 --- a/src/legacy_canvas_document_io_services.cpp +++ b/src/legacy_canvas_document_io_services.cpp @@ -4,6 +4,7 @@ #include "canvas.h" #include "app.h" #include "legacy_canvas_draw_merge_services.h" +#include "legacy_document_export_services.h" #include "legacy_ui_gl_dispatch.h" #include "legacy_ui_overlay_services.h" #include "app_core/document_canvas.h" @@ -190,40 +191,14 @@ void Canvas::import_equirectangular_thread(std::string file_path, std::shared_pt void Canvas::export_equirectangular(std::string file_path, std::function on_complete) { - if (App::I->check_license()) - { - App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable { - BT_SetTerminate(); - export_equirectangular_thread(file_path); - if (on_complete) - App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); }); - }); - } + pp::panopainter::execute_legacy_document_export_equirectangular( + std::move(file_path), + std::move(on_complete)); } void Canvas::export_equirectangular_thread(std::string file_path) { - Image data; - - App::I->render_task([&] - { - draw_merge(false); - Texture2D equirect = m_layers_merge.gen_equirect(); - data = equirect.get_image(); - }); - - LOG("writing %s", file_path.c_str()); - if (file_path.substr(file_path.size() - 4) == ".jpg") - { - data.save_jpg(file_path, 100); - inject_xmp(file_path); - } - else if (file_path.substr(file_path.size() - 4) == ".png") - { - data.save_png(file_path); - } - - App::I->publish_exported_image(file_path); + pp::panopainter::execute_legacy_document_export_equirectangular_thread(std::move(file_path)); } void Canvas::inject_xmp(std::string jpg_path) diff --git a/src/legacy_document_export_services.cpp b/src/legacy_document_export_services.cpp index dadedf08..f3a541dc 100644 --- a/src/legacy_document_export_services.cpp +++ b/src/legacy_document_export_services.cpp @@ -3,6 +3,7 @@ #include "legacy_document_export_services.h" #include "app.h" +#include "canvas.h" #include "legacy_document_canvas_services.h" #include "paint_renderer/compositor.h" @@ -130,6 +131,27 @@ pp::foundation::Status write_export_binary_file(std::string_view path, std::span return pp::foundation::Status::success(); } +void execute_legacy_document_export_equirectangular_thread_impl(std::string file_path) +{ + Image data; + + App::I->render_task([&] { + Canvas::I->draw_merge(false); + Texture2D equirect = Canvas::I->m_layers_merge.gen_equirect(); + data = equirect.get_image(); + }); + + LOG("writing %s", file_path.c_str()); + if (pp::app::document_export_path_is_jpeg_target(file_path)) { + data.save_jpg(file_path, 100); + Canvas::I->inject_xmp(file_path); + } else if (pp::app::document_export_path_is_png_target(file_path)) { + data.save_png(file_path); + } + + App::I->publish_exported_image(file_path); +} + class LegacyExportWriteServices final : public pp::app::DocumentCubeFaceExportWriteServices , public pp::app::DocumentDepthExportWriteServices @@ -868,6 +890,28 @@ private: } // namespace +void execute_legacy_document_export_equirectangular( + std::string file_path, + std::function on_complete) +{ + if (App::I->check_license()) { + App::I->runtime().canvas_async_task([file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable { + BT_SetTerminate(); + execute_legacy_document_export_equirectangular_thread_impl(file_path); + if (on_complete) { + App::I->ui_task([on_complete = std::move(on_complete)]() mutable { + on_complete(); + }); + } + }); + } +} + +void execute_legacy_document_export_equirectangular_thread(std::string file_path) +{ + execute_legacy_document_export_equirectangular_thread_impl(std::move(file_path)); +} + pp::foundation::Status execute_legacy_document_export_file( App& app, const pp::app::DocumentExportFileTarget& target) diff --git a/src/legacy_document_export_services.h b/src/legacy_document_export_services.h index a6662a9d..8fe6e33c 100644 --- a/src/legacy_document_export_services.h +++ b/src/legacy_document_export_services.h @@ -5,6 +5,9 @@ class App; +#include +#include + namespace pp::panopainter { [[nodiscard]] pp::foundation::Status execute_legacy_document_export_file( @@ -35,4 +38,10 @@ namespace pp::panopainter { std::string_view path, bool asynchronous); +void execute_legacy_document_export_equirectangular( + std::string file_path, + std::function on_complete); + +void execute_legacy_document_export_equirectangular_thread(std::string file_path); + } // namespace pp::panopainter diff --git a/tests/app_core/app_thread_tests.cpp b/tests/app_core/app_thread_tests.cpp index 80fffa4a..0758ec5a 100644 --- a/tests/app_core/app_thread_tests.cpp +++ b/tests/app_core/app_thread_tests.cpp @@ -38,6 +38,33 @@ void task_dispatch_does_not_wait_for_stopped_worker(pp::tests::Harness& harness) PP_EXPECT(harness, !plan.wait_for_completion); } +void runtime_task_dispatch_rejects_stopped_worker_enqueue(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_runtime_task_dispatch(false, true, 2, false, true, true); + + PP_EXPECT(harness, !plan.execute_immediately); + PP_EXPECT(harness, !plan.queue_task); + PP_EXPECT(harness, !plan.remove_matching_unique_task); + PP_EXPECT(harness, !plan.notify_worker); + PP_EXPECT(harness, !plan.wait_for_completion); + PP_EXPECT(harness, !plan.request_redraw); + PP_EXPECT(harness, !plan.reject_unsafe_cross_thread_dispatch); + PP_EXPECT(harness, plan.reject_stopped_worker_dispatch); +} + +void runtime_task_dispatch_executes_immediately_on_target_thread(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_runtime_task_dispatch(true, true, 3, false, true, true); + + PP_EXPECT(harness, plan.execute_immediately); + PP_EXPECT(harness, !plan.queue_task); + PP_EXPECT(harness, !plan.remove_matching_unique_task); + PP_EXPECT(harness, !plan.notify_worker); + PP_EXPECT(harness, !plan.wait_for_completion); + PP_EXPECT(harness, plan.request_redraw); + PP_EXPECT(harness, !plan.reject_stopped_worker_dispatch); +} + void task_dispatch_rejects_unsafe_cross_thread_mutations(pp::tests::Harness& harness) { const auto plan = pp::app::plan_app_task_dispatch(false, true, 2, true, true, false, true); @@ -137,6 +164,8 @@ int main() harness.run("task dispatch executes immediately on target thread", task_dispatch_executes_immediately_on_target_thread); harness.run("task dispatch queues unique work and waits for running worker", task_dispatch_queues_unique_work_and_waits_for_running_worker); harness.run("task dispatch does not wait for stopped worker", task_dispatch_does_not_wait_for_stopped_worker); + harness.run("runtime task dispatch rejects stopped worker enqueue", runtime_task_dispatch_rejects_stopped_worker_enqueue); + harness.run("runtime task dispatch executes immediately on target thread", runtime_task_dispatch_executes_immediately_on_target_thread); harness.run("task dispatch can reject unsafe cross-thread mutations", task_dispatch_rejects_unsafe_cross_thread_mutations); harness.run("render queue drain wraps non empty work in context", render_queue_drain_wraps_non_empty_work_in_context); harness.run("ui thread tick runs tasks and schedules redraw", ui_thread_tick_runs_tasks_and_schedules_redraw);