From 56c4743e664c64855a7405f7164df0dd7d4e6d9c Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 16 Jun 2026 06:54:14 +0200 Subject: [PATCH] Own brush workers and thin preview/platform seams --- docs/modernization/debt.md | 21 ++- docs/modernization/roadmap.md | 7 +- docs/modernization/tasks.md | 31 +++- src/app_events.cpp | 19 +++ src/legacy_brush_package_export_services.cpp | 101 +++++++++++- src/legacy_brush_package_import_services.cpp | 94 ++++++++++- ...y_node_stroke_preview_execution_services.h | 89 +++++++++++ src/node_panel_brush.cpp | 60 ++++--- src/node_stroke_preview.cpp | 147 +++--------------- .../linux_platform_services.cpp | 22 ++- src/platform_linux/linux_platform_services.h | 4 + 11 files changed, 415 insertions(+), 180 deletions(-) diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 5933805c..eb1a30e4 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -1,7 +1,7 @@ # Modernization Debt Log Status: live -Last updated: 2026-06-15 +Last updated: 2026-06-16 Every shortcut, temporary adapter, retained vendored dependency, skipped platform gate, compatibility shim, or incomplete automation path must be @@ -18,6 +18,21 @@ agent or engineer to remove them without reconstructing context from chat. ## Reductions +- 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 + detached worker threads; it now uses a service-owned `std::jthread` queue, + while retained preset ownership, progress UI, and storage mutation remain. +- 2026-06-16: `DEBT-0047` was narrowed again. The retained PPBR export bridge + in `src/legacy_brush_package_export_services.cpp` no longer launches a + detached desktop worker; it now uses a service-owned `std::jthread` queue + and returns dialog close plus success-message work to the UI thread, while + retained dialog data extraction, preset export ownership, and mobile/Web + completion remain. +- 2026-06-16: `DEBT-0036` was narrowed again. `NodeStrokePreview` final + composite plus preview-copy execution now routes through + `legacy_node_stroke_preview_execution_services.h` instead of living inline + in `draw_stroke_immediate()`; retained live-pass sequencing, GL resource + ownership, and node-owned draw execution remain. - 2026-06-15: `DEBT-0042` was narrowed again. The retained Save Version dialog wiring in `src/app_dialogs.cpp` now routes through a focused helper instead of living inline in `App::dialog_save_ver()`; the remaining document-session @@ -2128,8 +2143,8 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `pano_cli plan-export-message`, `pano_cli plan-export-report`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, and success/failure/license dialog metadata plus execution log labels now come from `pp_app_core`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, and owns mobile/Web save callbacks | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `pano_cli plan-export-message --kind timelapse --destination success`; `pano_cli plan-export-report --kind animation-mp4 --message "video export path must not be empty"`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, and mobile/Web save callbacks are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters | | DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`; viewport-density and cursor-mode execution now delegate to `src/legacy_canvas_view_services.*`, and retained preference reads/writes for UI scale, UI-state/RTL, whats-new dialog state, viewport density, cursor mode, VR controllers, and auto-timelapse now route through `src/legacy_preference_storage.*` snapshots/helpers without direct `settings.h` includes or retained preference keys in the UI/dialog/canvas call sites, but the bridges still call legacy `App::set_ui_scale`, `App::set_ui_rtl`, `App::rec_start`, `App::rec_stop`, retained canvas view mutation, and retained `Settings` storage through that adapter; VR mode callbacks now call `App` VR wrappers that dispatch to `PlatformServices`, whose desktop runtime policy prefers OpenXR while the actual Windows OpenVR SDK bridge still lives in `WindowsPlatformServices` under DEBT-0061 | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pp_app_core_canvas_view_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-canvas-view-density --density 1.5`; `pano_cli plan-canvas-view-cursor-mode --mode 3`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters | | DEBT-0046 | Open | Modernization | Startup preference/runtime execution and startup resource sequencing now consume pure `pp_app_core` through `App::init`, `pano_cli plan-app-startup`, `pano_cli plan-app-startup-resources`, `AppStartupServices`, `AppStartupResourceServices`, and `src/legacy_app_startup_services.*`, and startup preference load/read/write now routes through `src/legacy_preference_storage.*` with retained startup keys hidden behind `LegacyStartupPreferenceSnapshot`, but the bridge still calls legacy `Settings` storage through that adapter, `App::rec_start`, app VR-controller state mutation, message-box license warning execution, shader loading, asset initialization, layout creation, title updates, and UI render-target creation directly | Preserve current startup behavior while app startup moves toward app/preferences/storage/recording/UI/renderer services | `pp_app_core_app_startup_tests`; `pano_cli plan-app-startup --run-counter 7 --vr-controllers-disabled --license-invalid`; `pano_cli plan-app-startup --run-counter -1`; `pano_cli plan-app-startup-resources --width 1280 --height 720`; `pano_cli plan-app-startup-resources --bad-size`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Startup preference persistence, auto-timelapse startup, stored VR-controller state, license validation/warning, startup resource initialization, title updates, and UI render-target allocation are owned by injected app/preferences/storage/recording/UI/renderer services with `App::init` acting only as orchestration | -| DEBT-0047 | Open | Modernization | PPBR brush package export request validation, success-dialog metadata, and execution dispatch now consume pure `pp_app_core` through `App::dialog_ppbr_export`, `pano_cli plan-brush-package-export`, `BrushPackageExportServices`, and `src/legacy_brush_package_export_services.*`; PPBR header/path planning now consumes `pp_assets::brush_package`, and the macOS data-directory override now routes through `PlatformServices`, but the bridge still reads `NodeDialogExportPPBR`, carries the legacy `Image` header object outside the pure request, converts to `NodePanelBrushPreset::PPBRInfo`, calls `NodePanelBrushPreset::export_ppbr`, owns desktop worker-thread dispatch, dialog destruction, and mobile/Web completion directly | Preserve current PPBR export behavior while brush assets, PPBR serialization, picker completion, and UI lifetime move toward asset/storage/UI/platform services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_export_tests`; `pp_platform_api_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --author Artist --dest-path D:/Paint/BrushPreviews --export-data --header-image`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `pano_cli plan-brush-package-export`; `pano_cli plan-brush-package-export --path clouds`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --dest-path D:/Paint/BrushPreviews --no-export-data`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | PPBR metadata collection, header-image ownership, serialization, picker-selected path execution, desktop threading, dialog lifetime, and mobile/Web completion are owned by injected brush asset/storage/UI/platform services with `App::dialog_ppbr_export` acting only as a UI adapter | -| DEBT-0048 | Open | Modernization | ABR/PPBR brush package import execution now consumes pure `pp_app_core` through document-open confirmation callbacks, `pano_cli plan-brush-package-import`, `BrushPackageImportServices`, and `src/legacy_brush_package_import_services.*`; imported brush tip/pattern target paths now consume `pp_assets::brush_package`, but the bridge still launches detached legacy `NodePanelBrushPreset::import_abr`/`import_ppbr` worker threads and depends on the legacy preset panel as the importer/storage owner | Preserve current brush import behavior while brush package parsing, preset storage, progress/error reporting, and UI refresh move toward asset/paint/UI services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_import_tests`; `pano_cli plan-brush-package-import --kind ppbr --path D:/Paint/Brushes/clouds.ppbr`; `pano_cli plan-brush-package-import --kind abr --path D:/Paint/Brushes/clouds.abr`; `pano_cli plan-brush-package-import --kind ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | ABR/PPBR parsing, preset creation/storage, import threading/progress, duplicate asset policy, and UI refresh are owned by injected brush asset/paint/UI services with document-open callbacks only confirming user intent | +| DEBT-0047 | Open | Modernization | PPBR brush package export request validation, success-dialog metadata, and execution dispatch now consume pure `pp_app_core` through `App::dialog_ppbr_export`, `pano_cli plan-brush-package-export`, `BrushPackageExportServices`, and `src/legacy_brush_package_export_services.*`; PPBR header/path planning now consumes `pp_assets::brush_package`, the macOS data-directory override now routes through `PlatformServices`, and the desktop async path now uses a service-owned `std::jthread` worker with UI-thread dialog close/message handoff, but the bridge still reads `NodeDialogExportPPBR`, carries the legacy `Image` header object outside the pure request, converts to `NodePanelBrushPreset::PPBRInfo`, calls `NodePanelBrushPreset::export_ppbr`, and handles mobile/Web completion directly | Preserve current PPBR export behavior while brush assets, PPBR serialization, picker completion, and UI lifetime move toward asset/storage/UI/platform services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_export_tests`; `pp_platform_api_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --author Artist --dest-path D:/Paint/BrushPreviews --export-data --header-image`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `pano_cli plan-brush-package-export`; `pano_cli plan-brush-package-export --path clouds`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --dest-path D:/Paint/BrushPreviews --no-export-data`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | PPBR metadata collection, header-image ownership, serialization, picker-selected path execution, desktop threading, dialog lifetime, and mobile/Web completion are owned by injected brush asset/storage/UI/platform services with `App::dialog_ppbr_export` acting only as a UI adapter | +| DEBT-0048 | Open | Modernization | ABR/PPBR brush package import execution now consumes pure `pp_app_core` through document-open confirmation callbacks, `pano_cli plan-brush-package-import`, `BrushPackageImportServices`, and `src/legacy_brush_package_import_services.*`; imported brush tip/pattern target paths now consume `pp_assets::brush_package`, and the retained bridge now uses a service-owned `std::jthread` worker with UI-thread completion handoff instead of detached `NodePanelBrushPreset::import_abr`/`import_ppbr` launches, but it still depends on the legacy preset panel as the importer/storage owner | Preserve current brush import behavior while brush package parsing, preset storage, progress/error reporting, and UI refresh move toward asset/paint/UI services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_import_tests`; `pano_cli plan-brush-package-import --kind ppbr --path D:/Paint/Brushes/clouds.ppbr`; `pano_cli plan-brush-package-import --kind abr --path D:/Paint/Brushes/clouds.abr`; `pano_cli plan-brush-package-import --kind ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | ABR/PPBR parsing, preset creation/storage, import threading/progress, duplicate asset policy, and UI refresh are owned by injected brush asset/paint/UI services with document-open callbacks only confirming user intent | | DEBT-0049 | Open | Modernization | `pp_assets::validate_ppbr_header` intentionally preserves the legacy PPBR version check from `NodePanelBrushPreset::import_ppbr`, which accepts files when either major is `0` or minor is `1` instead of requiring exactly version `0.1` | Avoid rejecting existing brush packages before compatibility fixtures prove the stricter rule is safe | `pp_assets_brush_package_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add PPBR compatibility fixtures for accepted/rejected historical package versions, then require canonical `0.1` or an explicit supported-version matrix and update live import accordingly | | DEBT-0050 | Open | Modernization | iOS exported-image photo-library publishing and WebGL persistent-storage flushing now dispatch through platform service boundaries; the iOS/Web policy decision lives in tested `pp_platform_api::platform_policy`, and WebGL flushing now goes through injectable `pp::platform::WebPlatformServices`, but non-Windows execution still lives in retained fallback adapters and forwards to retained `save_image_library`/`webgl_sync` bridges | Preserve current iOS/Web export and save behavior while the Apple/Web platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; platform package smoke once Apple/Web root builds exist | Exported-image publishing and persistent-storage flushing are owned by injected Apple/Web `pp_platform_*` services with no legacy adapter branch | | DEBT-0051 | Open | Modernization | Document browser search roots, Apple file/image/save/directory picker dispatch, Browse dialog working-directory picker visibility/path formatting, iOS Inbox roots, macOS empty-selection filtering, and macOS display-path formatting now dispatch through the tested `src/platform_apple/apple_platform_services.*` boundary consumed by `PlatformServices`; retained `src/platform_legacy/legacy_platform_services.*` still creates the Apple bridge and owns other non-Apple fallback behavior | Preserve current iOS document import/browse and desktop browse picker behavior while Apple platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Apple package smoke once root Apple builds exist | Document browse roots and browse-directory picker/display formatting are owned by injected Apple and desktop `pp_platform_*` services with no legacy adapter branch | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 0e0a6741..f6bd81a6 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -95,7 +95,9 @@ Current architecture mismatches that must be treated as real blockers: - `pp_platform_api` still compiles Apple implementation files instead of only platform-neutral policy and interface code. - `src/platform_apple/apple_platform_services.cpp` and - `src/platform_linux/linux_platform_services.cpp` still reach `App::I`. + parts of the concrete platform layer still reach `App::I`; Linux FPS title + reporting now uses an injected callback, but Apple singleton reach and other + platform/app coupling remain. - `src/platform_legacy/legacy_platform_services.*` is still part of the live app shell. - `pp_panopainter_ui` still depends on `pp_legacy_app`. @@ -258,7 +260,8 @@ Required outcomes: - document workflow bridges become thin adapters over `pp_app_core` - cloud transfer and cloud browser ownership move out of retained UI nodes -- brush package import/export ownership moves out of retained panel code +- brush package import/export ownership moves out of retained panel code and + no longer depends on detached worker launch sites ### 7. Only Then Resume Future Backend Work diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 2fe57c48..8ed83d47 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -42,12 +42,13 @@ Completed, blocked, and superseded task history moved to `src/node_canvas.cpp`, `src/app.cpp`, and `src/app_dialogs.cpp`. - The platform boundary is not finished: - `pp_platform_api` still compiles Apple implementation files - - Apple and Linux platform services still reach `App::I` + - Apple platform services still reach `App::I` + - Linux FPS title reporting now uses an injected callback, but broader + platform-to-app singleton reach is still open - `platform_legacy` is still part of the live app shell - The app runtime boundary is not finished: - render/UI queues are static `App` state - - detached workers still launch from canvas, cloud, brush, grid, preview, and - event code + - detached workers still launch from canvas, grid, preview, and event code - thread-affinity rules are enforced by convention and asserts instead of explicit runtime contracts - The UI ownership boundary is not finished: @@ -114,12 +115,17 @@ Mini-model packet: #### ARC-RND-002 - Isolate Preview And Canvas View Render Execution -Status: Ready +Status: In Progress Why now: `src/node_stroke_preview.cpp` and `src/node_canvas.cpp` still own a large amount of live preview/canvas render sequencing around the renderer boundary. +Current slice: +- `NodeStrokePreview` final composite plus preview-texture copy now route + through `legacy_node_stroke_preview_execution_services.h`, but the preview + node still owns most live-pass and retained GL resource execution. + Write scope: - `src/node_stroke_preview.cpp` - `src/node_canvas.cpp` @@ -330,13 +336,20 @@ Mini-model packet: #### ARC-APP-005 - Replace Detached App Workers With Joinable Or Service-Owned Work -Status: Ready +Status: In Progress Why now: Canvas imports/exports/saves, cloud transfer, brush import/export, grid lightmap work, stroke preview, and event persistence still launch detached threads. That is not a safe modernization foundation. +Current slice: +- app-owned render/UI runtime queues and cloud worker ownership are already + moving behind owned runtime/service objects +- brush package import/export now use service-owned `std::jthread` workers and + UI-thread completion handoff +- canvas, grid, preview, and event-side detached work is still open + Write scope: - `src/canvas.cpp` - `src/app_cloud.cpp` @@ -525,12 +538,18 @@ Mini-model packet: #### ARC-PLT-002 - Remove `App::I` Reach From Apple And Linux Services -Status: Ready +Status: In Progress Why now: The current Apple and Linux service files still call into the app singleton, which means the platform layer is not a platform layer yet. +Current slice: +- Linux FPS title updates now route through an injected callback installed from + `App::set_platform_services()` +- Apple singleton reach and the remaining platform callback surface are still + open + Write scope: - `src/platform_apple/*` - `src/platform_linux/*` diff --git a/src/app_events.cpp b/src/app_events.cpp index dbf7945e..96f32c65 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -5,6 +5,10 @@ #include "app_core/document_platform_io.h" #include "app_core/document_sharing.h" #include "platform_api/platform_services.h" +#ifdef __LINUX__ +#include +#include "platform_linux/linux_platform_services.h" +#endif #include "platform_legacy/legacy_platform_services.h" #include "renderer_gl/opengl_capabilities.h" @@ -51,6 +55,21 @@ namespace { void App::set_platform_services(pp::platform::PlatformServices* services) noexcept { platform_services_ = services; +#ifdef __LINUX__ + if (services) + { + pp::platform::linux::set_fps_title_callback([this](std::string title) { + if (!glfw_window) + return; + + glfwSetWindowTitle(glfw_window, title.c_str()); + }); + } + else + { + pp::platform::linux::set_fps_title_callback({}); + } +#endif } pp::platform::PlatformServices* App::platform_services() const noexcept diff --git a/src/legacy_brush_package_export_services.cpp b/src/legacy_brush_package_export_services.cpp index f3285a1d..4fc05023 100644 --- a/src/legacy_brush_package_export_services.cpp +++ b/src/legacy_brush_package_export_services.cpp @@ -7,12 +7,90 @@ #include "node_dialog_export_ppbr.h" #include "node_panel_brush.h" +#include +#include +#include #include +#include +#include #include namespace pp::panopainter { namespace { +class LegacyBrushPackageWorker final { +public: + LegacyBrushPackageWorker() + : worker_([this](std::stop_token stop_token) { + run(stop_token); + }) + { + } + + ~LegacyBrushPackageWorker() + { + shutdown(); + } + + void post(std::function task) + { + { + std::lock_guard lock(mutex_); + if (stopping_) + return; + tasks_.push_back(std::move(task)); + } + cv_.notify_one(); + } + +private: + void shutdown() + { + { + std::lock_guard lock(mutex_); + stopping_ = true; + } + cv_.notify_all(); + } + + void run(std::stop_token stop_token) + { + for (;;) { + std::function task; + { + std::unique_lock 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 export worker task failed"); + } + } + } + } + + std::mutex mutex_; + std::condition_variable cv_; + std::deque> tasks_; + bool stopping_ = false; + std::jthread worker_; +}; + +LegacyBrushPackageWorker& brush_package_worker() +{ + static LegacyBrushPackageWorker worker; + return worker; +} + NodePanelBrushPreset::PPBRInfo to_legacy_ppbr_info( const pp::app::BrushPackageExportRequest& request, const NodeDialogExportPPBR& dialog) @@ -44,20 +122,29 @@ public: { const auto path_string = std::string(path); const auto info = to_legacy_ppbr_info(request, dialog_); + auto presets = app_.presets; if (mode_ == LegacyBrushPackageExportMode::desktop_async_close_and_message) { auto* app = &app_; - auto* dialog = &dialog_; - std::thread([app, dialog, path_string, info] { + auto dialog = std::static_pointer_cast(dialog_.shared_from_this()); + brush_package_worker().post([app, presets, dialog, path_string, info] { BT_SetTerminate(); - app->presets->export_ppbr(path_string, info); - pp::panopainter::close_legacy_dialog_node(*dialog); + if (presets) { + presets->export_ppbr(path_string, info); + } const auto plan = pp::app::plan_brush_package_export_success_dialog(path_string); - app->message_box(plan.title, plan.message, plan.show_cancel); - }).detach(); + app->ui_task([dialog, plan] { + if (dialog) { + pp::panopainter::close_legacy_dialog_node(*dialog); + } + App::I->message_box(plan.title, plan.message, plan.show_cancel); + }); + }); return; } - app_.presets->export_ppbr(path_string, info); + if (presets) { + presets->export_ppbr(path_string, info); + } } private: diff --git a/src/legacy_brush_package_import_services.cpp b/src/legacy_brush_package_import_services.cpp index d35d882d..afcdf600 100644 --- a/src/legacy_brush_package_import_services.cpp +++ b/src/legacy_brush_package_import_services.cpp @@ -5,12 +5,90 @@ #include "app.h" #include "node_panel_brush.h" +#include +#include +#include #include +#include +#include #include namespace pp::panopainter { namespace { +class LegacyBrushPackageWorker final { +public: + LegacyBrushPackageWorker() + : worker_([this](std::stop_token stop_token) { + run(stop_token); + }) + { + } + + ~LegacyBrushPackageWorker() + { + shutdown(); + } + + void post(std::function task) + { + { + std::lock_guard lock(mutex_); + if (stopping_) + return; + tasks_.push_back(std::move(task)); + } + cv_.notify_one(); + } + +private: + void shutdown() + { + { + std::lock_guard lock(mutex_); + stopping_ = true; + } + cv_.notify_all(); + } + + void run(std::stop_token stop_token) + { + for (;;) { + std::function task; + { + std::unique_lock 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> 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 @@ -22,12 +100,18 @@ public: { auto presets = app_.presets; const auto path_string = std::string(path); - if (kind == pp::app::BrushPackageImportKind::abr) { - std::thread(&NodePanelBrushPreset::import_abr, presets, path_string).detach(); - return; - } + brush_package_worker().post([presets, kind, path_string] { + BT_SetTerminate(); + if (!presets) { + return; + } + if (kind == pp::app::BrushPackageImportKind::abr) { + presets->import_abr(path_string); + return; + } - std::thread(&NodePanelBrushPreset::import_ppbr, presets, path_string).detach(); + presets->import_ppbr(path_string); + }); } private: diff --git a/src/legacy_node_stroke_preview_execution_services.h b/src/legacy_node_stroke_preview_execution_services.h index c22e378f..5f720e22 100644 --- a/src/legacy_node_stroke_preview_execution_services.h +++ b/src/legacy_node_stroke_preview_execution_services.h @@ -4,14 +4,18 @@ #include "../libs/glm/glm/ext/matrix_clip_space.hpp" #include "legacy_canvas_stroke_shader_services.h" +#include "legacy_canvas_stroke_composite_services.h" #include "legacy_canvas_stroke_preview_services.h" #include "legacy_canvas_stroke_services.h" +#include "legacy_ui_gl_dispatch.h" #include "paint_renderer/compositor.h" #include "texture.h" #include #include #include +#include +#include #include namespace pp::panopainter { @@ -241,6 +245,91 @@ template return true; } +[[nodiscard]] inline bool execute_legacy_node_stroke_preview_final_composite( + glm::vec2 size, + glm::vec2 pattern_scale, + const Brush& brush, + const pp::paint_renderer::CanvasStrokeCompositePassPlan& composite_pass, + Texture2D& background_texture, + Texture2D& stroke_texture, + Texture2D& dual_texture, + Texture2D& preview_texture, + Sampler& linear_sampler, + Sampler& repeat_sampler, + std::function bind_pattern_texture, + std::function draw_composite) +{ + if (!bind_pattern_texture || !draw_composite) { + return false; + } + + pp::panopainter::execute_legacy_stroke_preview_final_composite( + [&] { + pp::panopainter::setup_legacy_stroke_composite_shader( + pp::panopainter::LegacyStrokeCompositeUniforms { + .resolution = size, + .pattern = { + .scale = pattern_scale, + .invert = static_cast(brush.m_pattern_invert), + .brightness = brush.m_pattern_brightness, + .contrast = brush.m_pattern_contrast, + .depth = brush.m_pattern_depth, + .blend_mode = composite_pass.pattern_blend_mode, + .offset = glm::vec2(brush.m_pattern_rand_offset ? 0.5f : 0.0f), + }, + .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), + .layer_alpha = 1.0f, + .alpha_lock = false, + .mask_enabled = false, + .use_fragcoord = false, + .blend_mode = brush.m_blend_mode, + .use_dual = composite_pass.use_dual, + .dual_blend_mode = composite_pass.dual_blend_mode, + .dual_alpha = composite_pass.dual_alpha, + .use_pattern = composite_pass.use_pattern, + }); + }, + [&] { + linear_sampler.bind(0U); + linear_sampler.bind(1U); + linear_sampler.bind(2U); + linear_sampler.bind(3U); + repeat_sampler.bind(4U); + }, + [&] { + pp::legacy::ui_gl::activate_texture_unit(0U, "NodeStrokePreview"); + background_texture.bind(); + pp::legacy::ui_gl::activate_texture_unit(1U, "NodeStrokePreview"); + stroke_texture.bind(); + pp::legacy::ui_gl::activate_texture_unit(3U, "NodeStrokePreview"); + dual_texture.bind(); + pp::legacy::ui_gl::activate_texture_unit(4U, "NodeStrokePreview"); + bind_pattern_texture(); + }, + [&] { + draw_composite(); + }); + + const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture( + [&] { + preview_texture.bind(); + }, + []( + int src_x, + int src_y, + int dst_x, + int dst_y, + int width, + int height) { + copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height); + }, + pp::paint_renderer::StrokePreviewCopySize { + .width = static_cast(size.x), + .height = static_cast(size.y), + }); + return copy_status.ok(); +} + struct LegacyNodeStrokePreviewPassOrchestrationRequest { pp::renderer::RenderDeviceFeatures features {}; glm::vec2 preview_size {}; diff --git a/src/node_panel_brush.cpp b/src/node_panel_brush.cpp index 3017f48e..94be778d 100644 --- a/src/node_panel_brush.cpp +++ b/src/node_panel_brush.cpp @@ -4,6 +4,7 @@ #include "assets/brush_package.h" #include "app_core/brush_ui.h" #include "legacy_brush_ui_services.h" +#include "legacy_brush_package_import_services.h" #include "legacy_ui_overlay_services.h" #include "asset.h" #include "texture.h" @@ -600,13 +601,8 @@ void NodePanelBrushPreset::init() switch (index) { case 0: // import file - App::I->pick_file({"abr", "ppbr"}, [this] (std::string path) { - std::thread([this, path] { - BT_SetTerminate(); - import_brush(path); - for (auto p : s_panels) - p->m_notification->SetVisibility(p->m_container->m_children.size() == 0); - }).detach(); + App::I->pick_file({"abr", "ppbr"}, [presets = std::static_pointer_cast(shared_from_this())] (std::string path) { + presets->import_brush(path); }); break; case 1: // export file @@ -637,13 +633,8 @@ void NodePanelBrushPreset::init() }; m_btn_import = find("import"); m_btn_import->on_click = [this] (Node*) { - App::I->pick_file({ "abr", "ppbr" }, [this](std::string path) { - std::thread([this, path] { - BT_SetTerminate(); - import_brush(path); - for (auto p : s_panels) - p->m_notification->SetVisibility(p->m_container->m_children.size() == 0); - }).detach(); + App::I->pick_file({ "abr", "ppbr" }, [presets = std::static_pointer_cast(shared_from_this())](std::string path) { + presets->import_brush(path); }); }; m_btn_download = find("download"); @@ -1050,6 +1041,10 @@ bool NodePanelBrushPreset::import_ppbr(const std::string& path) // brush settings auto brushes_count = sr.ru32(); + std::vector> brushes_to_add; + if (brushes_count > 0) { + brushes_to_add.reserve(static_cast(brushes_count)); + } for (int i = 0; i < brushes_count; i++) { auto b = std::make_shared(); @@ -1058,16 +1053,23 @@ bool NodePanelBrushPreset::import_ppbr(const std::string& path) LOG("import_ppbr brush name %s", b->m_name.c_str()); if (b->valid()) { - for (auto p : s_panels) - p->add_brush(b); + brushes_to_add.push_back(b); } pb->increment(); } - save(); - App::I->stroke->m_brush_popup->reload(); + auto owner = std::static_pointer_cast(shared_from_this()); + App::I->ui_task([owner, brushes_to_add = std::move(brushes_to_add), pb]() mutable { + for (const auto& b : brushes_to_add) + { + for (auto p : s_panels) + p->add_brush(b); + } - pp::panopainter::close_legacy_dialog_node(*pb); + owner->save(); + App::I->stroke->m_brush_popup->reload(); + pp::panopainter::close_legacy_dialog_node(*pb); + }); return true; } @@ -1148,7 +1150,8 @@ bool NodePanelBrushPreset::import_abr(const std::string& path) }); auto brushes = abr.compute_brushes(App::I->data_path); - App::I->ui_task([&]{ + auto owner = std::static_pointer_cast(shared_from_this()); + App::I->ui_task([owner, brushes = std::move(brushes), pb]() mutable { for (const auto& b : brushes) { if (b->valid()) @@ -1159,12 +1162,11 @@ bool NodePanelBrushPreset::import_abr(const std::string& path) } pb->increment(); } + owner->save(); + App::I->stroke->m_brush_popup->reload(); + pp::panopainter::close_legacy_dialog_node(*pb); }); - save(); - App::I->stroke->m_brush_popup->reload(); - pp::panopainter::close_legacy_dialog_node(*pb); - return true; } @@ -1178,7 +1180,15 @@ bool NodePanelBrushPreset::import_brush(const std::string& path) std::string name = m[2].str(); std::string ext = m[3].str(); - return ext == "ppbr" ? import_ppbr(path) : import_abr(path); + const auto kind = ext == "ppbr" + ? pp::app::BrushPackageImportKind::ppbr + : pp::app::BrushPackageImportKind::abr; + const auto status = pp::panopainter::execute_legacy_brush_package_import(*App::I, kind, path); + if (!status.ok()) { + LOG("Brush package import request failed: %s", status.message); + return false; + } + return true; } void NodePanelBrushPreset::clear_brushes() diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index 939780e1..be32ae22 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -82,43 +82,6 @@ constexpr std::uint32_t kMixer = 3U; constexpr std::uint32_t kReservedLinear = 4U; } -struct StrokePreviewCompositePassInputs { - glm::vec2 resolution; - glm::vec2 pattern_scale; - const Brush& brush; - const pp::paint_renderer::CanvasStrokeCompositePassPlan& composite_pass; - Texture2D& background_texture; - Texture2D& stroke_texture; - Texture2D& dual_texture; - Sampler& linear_sampler; - Sampler& repeat_sampler; - std::function draw_composite; - - StrokePreviewCompositePassInputs( - glm::vec2 resolution_in, - glm::vec2 pattern_scale_in, - const Brush& brush_in, - const pp::paint_renderer::CanvasStrokeCompositePassPlan& composite_pass_in, - Texture2D& background_texture_in, - Texture2D& stroke_texture_in, - Texture2D& dual_texture_in, - Sampler& linear_sampler_in, - Sampler& repeat_sampler_in, - std::function draw_composite_in) - : resolution(resolution_in) - , pattern_scale(pattern_scale_in) - , brush(brush_in) - , composite_pass(composite_pass_in) - , background_texture(background_texture_in) - , stroke_texture(stroke_texture_in) - , dual_texture(dual_texture_in) - , linear_sampler(linear_sampler_in) - , repeat_sampler(repeat_sampler_in) - , draw_composite(std::move(draw_composite_in)) - { - } -}; - pp::panopainter::LegacyNodeStrokePreviewMixPassRequest make_stroke_preview_mix_pass_request( const Brush& brush, glm::vec2 resolution) noexcept @@ -232,65 +195,7 @@ pp::panopainter::LegacyCanvasStrokeMixPassRequest make_stroke_preview_mix_pass_e [&](int) { set_active_texture_unit(stroke_preview_composite_slots::kBackground); background_texture.unbind(); - }); -} - -void copy_stroke_preview_result_to_texture(Texture2D& texture, glm::vec2 size); - -void execute_stroke_preview_final_composite_and_copy( - const StrokePreviewCompositePassInputs& inputs, - Texture2D& preview_texture, - glm::vec2 size) -{ - pp::panopainter::execute_legacy_stroke_preview_final_composite( - [&] { - pp::panopainter::setup_legacy_stroke_composite_shader( - pp::panopainter::LegacyStrokeCompositeUniforms { - .resolution = inputs.resolution, - .pattern = { - .scale = inputs.pattern_scale, - .invert = static_cast(inputs.brush.m_pattern_invert), - .brightness = inputs.brush.m_pattern_brightness, - .contrast = inputs.brush.m_pattern_contrast, - .depth = inputs.brush.m_pattern_depth, - .blend_mode = inputs.composite_pass.pattern_blend_mode, - .offset = glm::vec2(inputs.brush.m_pattern_rand_offset ? 0.5f : 0.0f), - }, - .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), - .layer_alpha = 1.0f, - .alpha_lock = false, - .mask_enabled = false, - .use_fragcoord = false, - .blend_mode = inputs.brush.m_blend_mode, - .use_dual = inputs.composite_pass.use_dual, - .dual_blend_mode = inputs.composite_pass.dual_blend_mode, - .dual_alpha = inputs.composite_pass.dual_alpha, - .use_pattern = inputs.composite_pass.use_pattern, - }); - }, - [&] { - inputs.linear_sampler.bind(stroke_preview_composite_slots::kBackground); - inputs.linear_sampler.bind(stroke_preview_composite_slots::kStroke); - inputs.linear_sampler.bind(2U); - inputs.linear_sampler.bind(stroke_preview_composite_slots::kDual); - inputs.repeat_sampler.bind(stroke_preview_composite_slots::kPattern); - }, - [&] { - set_active_texture_unit(stroke_preview_composite_slots::kBackground); - inputs.background_texture.bind(); - set_active_texture_unit(stroke_preview_composite_slots::kStroke); - inputs.stroke_texture.bind(); - set_active_texture_unit(stroke_preview_composite_slots::kDual); - inputs.dual_texture.bind(); - set_active_texture_unit(stroke_preview_composite_slots::kPattern); - inputs.brush.m_pattern_texture ? - inputs.brush.m_pattern_texture->bind() : - unbind_texture_2d(); - }, - [&] { - inputs.draw_composite(); - }); - copy_stroke_preview_result_to_texture(preview_texture, size); + }); } void copy_stroke_preview_framebuffer_to_texture( @@ -500,22 +405,6 @@ void execute_stroke_preview_background_capture_pass( assert(copy_status.ok()); } -void copy_stroke_preview_result_to_texture(Texture2D& preview_texture, glm::vec2 size) -{ - const auto result = pp::paint_renderer::copy_stroke_preview_result_to_texture( - [&] { - preview_texture.bind(); - }, - [](int src_x, int src_y, int dst_x, int dst_y, int width, int height) { - copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height); - }, - pp::paint_renderer::StrokePreviewCopySize { - .width = static_cast(size.x), - .height = static_cast(size.y), - }); - assert(result.ok()); -} - } std::atomic_int NodeStrokePreview::s_instances{ 0 }; @@ -841,22 +730,26 @@ void NodeStrokePreview::draw_stroke_immediate() return false; } - execute_stroke_preview_final_composite_and_copy( - StrokePreviewCompositePassInputs( - size, - glm::vec2(b->m_pattern_scale), - *b, - material.composite_pass, - m_tex_background, - m_tex, - m_tex_dual, - m_sampler_linear, - m_sampler_linear_repeat, - [&] { - m_plane.draw_fill(); - }), + const bool final_composite_ok = pp::panopainter::execute_legacy_node_stroke_preview_final_composite( + size, + glm::vec2(b->m_pattern_scale), + *b, + material.composite_pass, + m_tex_background, + m_tex, + m_tex_dual, m_tex_preview, - size); + m_sampler_linear, + m_sampler_linear_repeat, + [&] { + b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d(); + }, + [&] { + m_plane.draw_fill(); + }); + if (!final_composite_ok) { + return false; + } return true; }(); assert(sequence_ok); diff --git a/src/platform_linux/linux_platform_services.cpp b/src/platform_linux/linux_platform_services.cpp index 56f750cc..0d24e00b 100644 --- a/src/platform_linux/linux_platform_services.cpp +++ b/src/platform_linux/linux_platform_services.cpp @@ -2,21 +2,28 @@ #ifdef __LINUX__ #include - -#include - -#include "app.h" +#include namespace pp::platform::linux { namespace { +std::function g_fps_title_callback; + void linux_update_fps(int frames) { - App::I->title("PanoPainter - " + std::to_string(frames) + " FPS"); + if (!g_fps_title_callback) + return; + + g_fps_title_callback("PanoPainter - " + std::to_string(frames) + " FPS"); } } +void set_fps_title_callback(std::function callback) +{ + g_fps_title_callback = std::move(callback); +} + void report_rendered_frames(int frames) { linux_update_fps(frames); @@ -26,6 +33,11 @@ void report_rendered_frames(int frames) #else namespace pp::platform::linux { +void set_fps_title_callback(std::function callback) +{ + (void)callback; +} + void report_rendered_frames(int frames) { (void)frames; diff --git a/src/platform_linux/linux_platform_services.h b/src/platform_linux/linux_platform_services.h index a146d7df..45ff3dce 100644 --- a/src/platform_linux/linux_platform_services.h +++ b/src/platform_linux/linux_platform_services.h @@ -1,7 +1,11 @@ #pragma once +#include +#include + namespace pp::platform::linux { +void set_fps_title_callback(std::function callback); void report_rendered_frames(int frames); }