Own canvas async work and thin NodeCanvas composite
This commit is contained in:
@@ -18,6 +18,21 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
|
||||
## Reductions
|
||||
|
||||
- 2026-06-16: `DEBT-0043`/`DEBT-0040`/`DEBT-0042` were narrowed again. The
|
||||
retained `Canvas` async import/export/save/open entrypoints in
|
||||
`src/canvas.cpp` no longer launch detached threads; they now use an owned
|
||||
local `std::jthread` queue and return completion callbacks to the UI thread,
|
||||
while retained progress-node mutation, render-task orchestration, and canvas
|
||||
storage/export ownership remain.
|
||||
- 2026-06-16: `DEBT-0044` was narrowed again. The retained asynchronous
|
||||
timelapse export path in `src/legacy_document_export_services.cpp` no longer
|
||||
launches a detached worker; it now uses a service-owned `std::jthread`
|
||||
queue and returns the success dialog to the UI thread, while retained
|
||||
recording/export execution still stays behind the legacy bridge.
|
||||
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` cache-to-screen
|
||||
checkerboard plus cache-texture composite now route through
|
||||
`legacy_canvas_draw_merge_services.h` instead of living inline in
|
||||
`NodeCanvas::draw()`; broader canvas draw orchestration remains retained.
|
||||
- 2026-06-16: `DEBT-0051`/`DEBT-0052`/`DEBT-0055` were narrowed again.
|
||||
`src/platform_apple/apple_platform_services.*` no longer reaches `App::I`
|
||||
for clipboard, display/share, cursor-visibility, or save-ui-state behavior;
|
||||
@@ -2161,7 +2176,7 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| DEBT-0041 | Open | Modernization | Accepted new-document planning/execution dispatch and new-document overwrite prompt metadata now consume pure `pp_app_core` through `App::dialog_newdoc`, `pano_cli plan-new-document`, `pano_cli plan-document-session-prompt`, `NewDocumentServices`, and `src/legacy_document_session_services.*`; new-document overwrite prompt creation now uses `src/legacy_app_dialog_services.*`, and New Document dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still mutates legacy app document fields, clears legacy layer UI, resizes legacy `Canvas`, clears legacy history, creates the default layer through legacy UI, mutates unsaved/new-document flags, updates the title, wires overwrite callbacks directly, and handles keyboard/dialog cleanup directly | Preserve current New Document dialog behavior while document creation moves toward app/document/UI services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-session-prompt --kind new-document-overwrite`; `pano_cli simulate-app-session --save-intent save`; `ctest --preset desktop-fast --build-config Debug` | New document creation, overwrite confirmation, canvas/document allocation, default layer creation, history clearing, title updates, dirty/new-document state, and keyboard/dialog cleanup are owned by injected app/document/UI services with `App::dialog_newdoc` acting only as a UI adapter |
|
||||
| DEBT-0042 | Open | Modernization | Accepted Save As and Save Version planning/execution dispatch, Save As overwrite prompt metadata, and Save As/Save Version history effects now consume pure `pp_app_core` through `App::dialog_save`, `App::dialog_save_ver`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-document-session-prompt`, `DocumentFileSaveServices`, `DocumentVersionSaveServices`, and `src/legacy_document_session_services.*`; Save As overwrite prompt creation now uses `src/legacy_app_dialog_services.*`, and accepted Save As/Save Version execution prepares a payload-bearing canvas snapshot report before retained saving, but the bridge still wires overwrite callbacks directly, delegates actual writing to legacy `Canvas::project_save`, mutates app document name/path/directory fields, marks version saves dirty before saving, updates the title, and handles keyboard/dialog cleanup directly | Preserve current Save As and Save Version behavior while document persistence moves toward app/document/storage/UI services | `pp_app_core_document_session_tests`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-session-prompt --kind file-overwrite --name demo`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli simulate-app-session --save-intent save-as`; `pano_cli simulate-app-session --save-intent save-version`; `ctest --preset desktop-fast --build-config Debug` | Save As overwrite prompting, project-save execution, app document metadata updates, title updates, version-save dirty-state handling, and keyboard/dialog cleanup are owned by injected app/document/storage/UI services with `App::dialog_save` and `App::dialog_save_ver` acting only as UI adapters |
|
||||
| DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`; layer/frame dialogs also consume `plan_document_export_collection_target` plus `PlatformServices::uses_work_directory_document_export_collections()` instead of spelling local iOS branches, export success/failure/license dialog metadata plus execution log labels now come from `pp_app_core`, equirectangular/layer/animation-frame/depth/cube-face execution prepares a payload-bearing document snapshot plus the shared `pp_paint_renderer::prepare_document_frame_export_readiness` report, document-snapshot writer-versus-retained fallback routing now comes from tested `pp_app_core` policy including current-platform support consumed by the live bridge, depth export target naming and two-payload write order are covered by tested `pp_app_core` helpers, cube-face export writes the pure face PNG bytes to `pp_app_core` planned work-directory face paths through `execute_document_cube_face_export_write` before falling back to retained Canvas execution on failure, PNG/JPEG equirectangular export writes the pure `pp_paint_renderer` equirectangular payload through `execute_document_export_file_write` before retained fallback, and payload-complete layer/animation-frame collections write pure `pp_paint_renderer` PNG sequences through `execute_document_export_collection_write` before retained fallback, but the bridge still adapts retained filesystem writes/exported-image publishing locally, still calls legacy `Canvas` export methods for Web/incomplete-readback collection exports and depth rendering, creates export directories, handles picker-selected stems, performs Web prepared-file handoff directly, and leaves depth render/readback plus the legacy `.png`/JPEG payload mismatch on the retained path | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pp_platform_api_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli plan-export-target --kind cube-faces --work-dir D:/Paint --doc-name demo`; `pano_cli plan-export-message --kind equirectangular --destination work --detail D:/Paint`; `pano_cli plan-export-report --kind license-disabled`; `pano_cli plan-export-snapshot-route --kind layers-collection --captured-face-payloads 3 --pending-face-payloads 6`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and remaining retained export execution, export-directory creation, Web file handoff, picker-selected stem handling, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters |
|
||||
| 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-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`; the asynchronous timelapse path now uses a service-owned `std::jthread` queue with UI-thread success-dialog handoff instead of a detached worker, but the bridge still 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`, 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 |
|
||||
|
||||
@@ -107,7 +107,7 @@ Current architecture mismatches that must be treated as real blockers:
|
||||
rather than thin composition/binding surfaces.
|
||||
- `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use
|
||||
global singleton reach, raw observer pointers, detached `std::thread`
|
||||
launches in several canvas/export/preview paths, and ad hoc
|
||||
launches in preview/recording paths, and ad hoc
|
||||
mutex/condition-variable ownership.
|
||||
- Modern C++23 usage exists in extracted components, especially `std::span`,
|
||||
explicit result/status objects, and a few concepts, but the live app still
|
||||
|
||||
@@ -49,8 +49,10 @@ Completed, blocked, and superseded task history moved to
|
||||
- `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, preview, document export, and
|
||||
recording code
|
||||
- detached workers still launch from preview and recording code
|
||||
- canvas async import/export/save/open now run through an owned in-file
|
||||
worker, but their retained progress execution is still not a clean runtime
|
||||
service boundary
|
||||
- thread-affinity rules are enforced by convention and asserts instead of
|
||||
explicit runtime contracts
|
||||
- The UI ownership boundary is not finished:
|
||||
@@ -127,9 +129,9 @@ 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.
|
||||
- `NodeCanvas` display resolve for the `m_density != 1.f` path now routes
|
||||
through `legacy_canvas_draw_merge_services.h`, but the cache-to-screen
|
||||
composite block and broader canvas draw orchestration are still inline.
|
||||
- `NodeCanvas` display resolve plus cache-to-screen checkerboard/cache-texture
|
||||
composite now route through `legacy_canvas_draw_merge_services.h`, but
|
||||
broader canvas draw orchestration is still inline.
|
||||
|
||||
Write scope:
|
||||
- `src/node_stroke_preview.cpp`
|
||||
@@ -355,8 +357,9 @@ Current slice:
|
||||
UI-thread completion handoff
|
||||
- prepared-file save work and grid lightmap launch now also use service-owned
|
||||
workers with explicit UI-thread handoff
|
||||
- canvas, preview, document export, and recording-side detached work are still
|
||||
open
|
||||
- canvas async import/export/save/open and timelapse export now also use owned
|
||||
worker queues instead of detached threads
|
||||
- preview and recording-side detached work are still open
|
||||
|
||||
Write scope:
|
||||
- `src/canvas.cpp`
|
||||
|
||||
128
src/canvas.cpp
128
src/canvas.cpp
@@ -20,6 +20,10 @@
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
#include "util.h"
|
||||
#include <array>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <stop_token>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -31,6 +35,79 @@
|
||||
|
||||
namespace {
|
||||
|
||||
class LegacyCanvasAsyncWorker final {
|
||||
public:
|
||||
LegacyCanvasAsyncWorker()
|
||||
: worker_([this](std::stop_token stop_token) {
|
||||
run(stop_token);
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
~LegacyCanvasAsyncWorker()
|
||||
{
|
||||
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("canvas async worker task failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
std::deque<std::function<void()>> tasks_;
|
||||
bool stopping_ = false;
|
||||
std::jthread worker_;
|
||||
};
|
||||
|
||||
LegacyCanvasAsyncWorker& canvas_async_worker()
|
||||
{
|
||||
static LegacyCanvasAsyncWorker worker;
|
||||
return worker;
|
||||
}
|
||||
|
||||
GLint current_canvas_stroke_internal_format()
|
||||
{
|
||||
const auto renderer_features = ShaderManager::render_device_features();
|
||||
@@ -2769,11 +2846,10 @@ void Canvas::import_equirectangular(std::string file_path, std::shared_ptr<Layer
|
||||
{
|
||||
if (App::I->check_license())
|
||||
{
|
||||
std::thread t([=] {
|
||||
canvas_async_worker().post([this, file_path = std::move(file_path), layer = std::move(layer)] {
|
||||
BT_SetTerminate();
|
||||
import_equirectangular_thread(file_path, layer);
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2862,13 +2938,12 @@ void Canvas::export_equirectangular(std::string file_path, std::function<void()>
|
||||
{
|
||||
if (App::I->check_license())
|
||||
{
|
||||
std::thread t([=] {
|
||||
canvas_async_worker().post([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
|
||||
BT_SetTerminate();
|
||||
export_equirectangular_thread(file_path);
|
||||
if (on_complete)
|
||||
on_complete();
|
||||
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2949,13 +3024,12 @@ void Canvas::export_depth(std::string file_name, std::function<void()> on_comple
|
||||
{
|
||||
if (App::I->check_license())
|
||||
{
|
||||
std::thread t([=] {
|
||||
canvas_async_worker().post([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable {
|
||||
BT_SetTerminate();
|
||||
export_depth_thread(file_name);
|
||||
if (on_complete)
|
||||
on_complete();
|
||||
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3057,13 +3131,12 @@ void Canvas::export_layers(std::string path, std::function<void()> on_complete)
|
||||
{
|
||||
if (App::I->check_license())
|
||||
{
|
||||
std::thread t([=] {
|
||||
canvas_async_worker().post([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable {
|
||||
BT_SetTerminate();
|
||||
export_layers_thread(path);
|
||||
if (on_complete)
|
||||
on_complete();
|
||||
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3084,13 +3157,12 @@ void Canvas::export_anim_frames(std::string path, std::function<void()> on_compl
|
||||
{
|
||||
if (App::I->check_license())
|
||||
{
|
||||
std::thread t([=] {
|
||||
canvas_async_worker().post([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable {
|
||||
BT_SetTerminate();
|
||||
export_anim_frames_thread(path);
|
||||
if (on_complete)
|
||||
on_complete();
|
||||
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3110,13 +3182,12 @@ void Canvas::export_anim_mp4(std::string path, std::function<void()> on_complete
|
||||
{
|
||||
if (App::I->check_license())
|
||||
{
|
||||
std::thread t([=] {
|
||||
canvas_async_worker().post([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable {
|
||||
BT_SetTerminate();
|
||||
export_anim_mp4_thread(path);
|
||||
if (on_complete)
|
||||
on_complete();
|
||||
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3149,13 +3220,12 @@ void Canvas::export_cube_faces(std::string file_name, std::function<void()> on_c
|
||||
{
|
||||
if (App::I->check_license())
|
||||
{
|
||||
std::thread t([=] {
|
||||
canvas_async_worker().post([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable {
|
||||
BT_SetTerminate();
|
||||
export_cube_faces_thread(file_name);
|
||||
if (on_complete)
|
||||
on_complete();
|
||||
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3202,13 +3272,13 @@ void Canvas::project_save(std::function<void(bool)> on_complete)
|
||||
{
|
||||
if (App::I->check_license())
|
||||
{
|
||||
std::thread t([=] {
|
||||
const auto file_path = App::I->doc_path;
|
||||
canvas_async_worker().post([this, file_path, on_complete = std::move(on_complete)]() mutable {
|
||||
BT_SetTerminate();
|
||||
bool ret = project_save_thread(App::I->doc_path, true);
|
||||
bool ret = project_save_thread(file_path, true);
|
||||
if (on_complete)
|
||||
on_complete(ret);
|
||||
App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); });
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3217,13 +3287,12 @@ void Canvas::project_save(std::string file_path, std::function<void(bool)> on_co
|
||||
LOG("saving %s", file_path.c_str());
|
||||
if (App::I->check_license())
|
||||
{
|
||||
std::thread t([=] {
|
||||
canvas_async_worker().post([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
|
||||
BT_SetTerminate();
|
||||
bool ret = project_save_thread(file_path, true);
|
||||
if (on_complete)
|
||||
on_complete(ret);
|
||||
App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); });
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3499,13 +3568,12 @@ bool Canvas::project_save_thread(std::string file_path, bool show_progress)
|
||||
|
||||
void Canvas::project_open(std::string file_path, std::function<void(bool)> on_complete)
|
||||
{
|
||||
std::thread t([=] {
|
||||
canvas_async_worker().post([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
|
||||
BT_SetTerminate();
|
||||
bool result = project_open_thread(file_path);
|
||||
if (on_complete)
|
||||
on_complete(result);
|
||||
App::I->ui_task([on_complete = std::move(on_complete), result]() mutable { on_complete(result); });
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
|
||||
bool Canvas::project_open_thread(std::string file_path)
|
||||
|
||||
@@ -175,6 +175,20 @@ struct LegacyCanvasDrawMergeFinalPlaneCompositeExecution {
|
||||
std::function<void()> unbind_merged_texture;
|
||||
};
|
||||
|
||||
struct LegacyCanvasDrawMergeCacheToScreenCompositeUniforms {
|
||||
LegacyCanvasDrawMergeCheckerboardUniforms checkerboard;
|
||||
LegacyCanvasDrawMergeTextureUniforms texture;
|
||||
};
|
||||
|
||||
struct LegacyCanvasDrawMergeCacheToScreenCompositeExecution {
|
||||
std::function<void()> enable_blend;
|
||||
std::function<void(const LegacyCanvasDrawMergeCheckerboardUniforms&, int)> draw_checkerboard_plane;
|
||||
std::function<void()> bind_sampler;
|
||||
std::function<void()> bind_cache_texture;
|
||||
std::function<void()> draw_cache_texture;
|
||||
std::function<void()> unbind_cache_texture;
|
||||
};
|
||||
|
||||
struct LegacyCanvasDrawMergeDisplayResolveUniforms {
|
||||
LegacyCanvasDrawMergeTextureUniforms texture;
|
||||
};
|
||||
@@ -447,6 +461,23 @@ inline void execute_legacy_canvas_draw_merge_final_plane_composite(
|
||||
execution.unbind_merged_texture();
|
||||
}
|
||||
|
||||
inline void execute_legacy_canvas_draw_merge_cache_to_screen_composite(
|
||||
const LegacyCanvasDrawMergeCacheToScreenCompositeUniforms& uniforms,
|
||||
const LegacyCanvasDrawMergeCacheToScreenCompositeExecution& execution)
|
||||
{
|
||||
execution.enable_blend();
|
||||
|
||||
for (int plane_index = 0; plane_index < 6; ++plane_index) {
|
||||
execution.draw_checkerboard_plane(uniforms.checkerboard, plane_index);
|
||||
}
|
||||
|
||||
execution.bind_sampler();
|
||||
execution.bind_cache_texture();
|
||||
setup_legacy_canvas_draw_merge_texture_shader(uniforms.texture);
|
||||
execution.draw_cache_texture();
|
||||
execution.unbind_cache_texture();
|
||||
}
|
||||
|
||||
inline void execute_legacy_canvas_draw_merge_display_resolve(
|
||||
const LegacyCanvasDrawMergeDisplayResolveUniforms& uniforms,
|
||||
const LegacyCanvasDrawMergeDisplayResolveExecution& execution)
|
||||
|
||||
@@ -7,10 +7,15 @@
|
||||
#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 {
|
||||
@@ -30,6 +35,79 @@ 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()) {
|
||||
@@ -746,16 +824,18 @@ public:
|
||||
auto* app = &app_;
|
||||
auto path_string = std::string(path);
|
||||
if (asynchronous_) {
|
||||
std::thread([app, path_string] {
|
||||
document_video_export_worker().post([app, path_string = std::move(path_string)]() mutable {
|
||||
BT_SetTerminate();
|
||||
app->rec_export(path_string);
|
||||
show_export_success_dialog(
|
||||
*app,
|
||||
pp::app::plan_document_export_success_dialog(
|
||||
pp::app::DocumentExportSuccessKind::timelapse,
|
||||
pp::app::DocumentExportSuccessDestination::path,
|
||||
path_string));
|
||||
}).detach();
|
||||
app->ui_task([app, path_string = std::move(path_string)]() mutable {
|
||||
show_export_success_dialog(
|
||||
*app,
|
||||
pp::app::plan_document_export_success_dialog(
|
||||
pp::app::DocumentExportSuccessKind::timelapse,
|
||||
pp::app::DocumentExportSuccessDestination::path,
|
||||
path_string));
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -676,40 +676,44 @@ void NodeCanvas::draw()
|
||||
apply_node_canvas_viewport(0, 0, m_rtt.getWidth(), m_rtt.getHeight());
|
||||
else
|
||||
apply_node_canvas_viewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
|
||||
}
|
||||
|
||||
// draw the grid behind the layers using a temporary copy
|
||||
if (use_blend)
|
||||
{
|
||||
apply_node_canvas_capability(pp::renderer::gl::blend_state(), true);
|
||||
|
||||
//draw the grid
|
||||
for (int plane_index = 0; plane_index < 6; plane_index++)
|
||||
{
|
||||
auto plane_mvp = proj * camera *
|
||||
glm::scale(glm::vec3(m_canvas->m_layers.size() + 500.f)) *
|
||||
m_canvas->m_plane_transform[plane_index] *
|
||||
glm::translate(glm::vec3(0, 0, -1.f));
|
||||
|
||||
pp::panopainter::setup_legacy_canvas_draw_merge_checkerboard_shader(
|
||||
pp::panopainter::LegacyCanvasDrawMergeCheckerboardUniforms {
|
||||
.mvp = plane_mvp,
|
||||
pp::panopainter::execute_legacy_canvas_draw_merge_cache_to_screen_composite(
|
||||
pp::panopainter::LegacyCanvasDrawMergeCacheToScreenCompositeUniforms {
|
||||
.checkerboard = {
|
||||
.colorize = false,
|
||||
});
|
||||
m_face_plane.draw_fill();
|
||||
}
|
||||
},
|
||||
.texture = {
|
||||
.mvp = glm::ortho<float>(-1, 1, -1, 1),
|
||||
.texture_slot = 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
.enable_blend = [&] {
|
||||
apply_node_canvas_capability(pp::renderer::gl::blend_state(), true);
|
||||
},
|
||||
.draw_checkerboard_plane = [&](const pp::panopainter::LegacyCanvasDrawMergeCheckerboardUniforms& uniforms, int plane_index) {
|
||||
auto checkerboard_uniforms = uniforms;
|
||||
checkerboard_uniforms.mvp = proj * camera *
|
||||
glm::scale(glm::vec3(m_canvas->m_layers.size() + 500.f)) *
|
||||
m_canvas->m_plane_transform[plane_index] *
|
||||
glm::translate(glm::vec3(0, 0, -1.f));
|
||||
|
||||
// draw the layers
|
||||
m_sampler.bind(0);
|
||||
set_active_texture_unit(0);
|
||||
m_cache_rtt.bindTexture();
|
||||
pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
|
||||
pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
|
||||
.mvp = glm::ortho<float>(-1, 1, -1, 1),
|
||||
.texture_slot = 0,
|
||||
pp::panopainter::setup_legacy_canvas_draw_merge_checkerboard_shader(checkerboard_uniforms);
|
||||
m_face_plane.draw_fill();
|
||||
},
|
||||
.bind_sampler = [&] {
|
||||
m_sampler.bind(0);
|
||||
set_active_texture_unit(0);
|
||||
},
|
||||
.bind_cache_texture = [&] {
|
||||
m_cache_rtt.bindTexture();
|
||||
},
|
||||
.draw_cache_texture = [&] {
|
||||
m_face_plane.draw_fill();
|
||||
},
|
||||
.unbind_cache_texture = [&] {
|
||||
m_cache_rtt.unbindTexture();
|
||||
},
|
||||
});
|
||||
m_face_plane.draw_fill();
|
||||
m_cache_rtt.unbindTexture();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user