From c50ea14a2a2318a419ce63796c039fc854d33bd0 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 5 Jun 2026 07:01:51 +0200 Subject: [PATCH] Route app thread orchestration through app core --- CMakeLists.txt | 1 + docs/modernization/build-inventory.md | 5 + docs/modernization/debt.md | 11 +- docs/modernization/roadmap.md | 16 ++ scripts/automation/platform-build.ps1 | 1 + scripts/automation/platform-build.sh | 2 +- src/app.cpp | 117 ++++++++------ src/app.h | 82 +++++++--- src/app_core/app_thread.h | 200 ++++++++++++++++++++++++ tests/CMakeLists.txt | 35 +++++ tests/app_core/app_thread_tests.cpp | 134 +++++++++++++++++ tools/pano_cli/main.cpp | 209 ++++++++++++++++++++++++++ 12 files changed, 749 insertions(+), 64 deletions(-) create mode 100644 src/app_core/app_thread.h create mode 100644 tests/app_core/app_thread_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ae3de40..b9ff6ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -246,6 +246,7 @@ add_library(pp_app_core STATIC src/app_core/app_shutdown.h src/app_core/app_status.h src/app_core/app_startup.h + src/app_core/app_thread.h src/app_core/brush_package_import.h src/app_core/brush_package_export.h src/app_core/brush_ui.h diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index b9accf9..d2a9553 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -840,6 +840,11 @@ Known local toolchain state: visibility, main UI suppression in VR-only mode, tick layout selection, resize render-target/redraw projection, invalid resize rejection, and redraw reset planning. +- `pp_app_core_app_thread_tests` covers render/UI task dispatch, immediate + same-thread execution, unique queued-task replacement, stopped-worker + no-wait behavior, render queue context wrapping, UI tick redraw scheduling, + UI-loop frame/FPS/reload timer thresholds, malformed timer rejection, redraw + frame-count projection, and thread start/stop intents. - `pp_app_core_app_input_tests` covers pointer coordinate normalization, invalid pointer/gesture inputs, designer-first mouse routing, mouse-cancel routing, gesture midpoint/distance/delta math, main-layout routing, key state diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index e77b11f..23a86eb 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -108,6 +108,15 @@ agent or engineer to remove them without reconstructing context from chat. `MouseEvent`/`GestureEvent`/`TouchEvent`/`KeyEvent` construction, UI child-node mutation, and legacy `Node` event dispatch remain in the app shell. +- 2026-06-05: DEBT-0003 was narrowed. Render/UI task dispatch, unique queued + task replacement, async redraw notification, queue draining, render-context + wrapping, UI tick redraw scheduling, UI-loop frame/FPS/live-reload timer + cadence, and thread start/stop intents are now tested `pp_app_core` plans + consumed by `App::render_task*`, `App::ui_task*`, `App::async_redraw`, + `App::render_thread_*`, `App::ui_thread_*`, and + `pano_cli plan-app-thread`; retained `std::thread`, condition-variable, + OpenGL context, live asset reload, and task execution remain in the app + shell. - 2026-06-05: DEBT-0003 was narrowed again. Shutdown cleanup staging for UI-state save, stroke-preview renderer shutdown, recording stop, texture/shader invalidation, layout unload, UI render-target and face-plane @@ -139,7 +148,7 @@ agent or engineer to remove them without reconstructing context from chat. | --- | --- | --- | --- | --- | --- | --- | | DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets | | DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation | -| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, file-menu save actions, `NodeCanvas` canvas hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/menu/target naming/path decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, app status/display and renderer diagnostic decisions, document resize decisions, layer rename/menu decisions, Tools menu/panel decisions, About menu/diagnostic decisions, main toolbar/status decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-file-menu`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-menu`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-app-status`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-about-menu`, `pano_cli plan-main-toolbar`, `pano_cli plan-document-resize`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-menu`, `pano_cli plan-canvas-hotkey`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/status/share/platform-I/O/display/keyboard/cloud/resize/layer/tools/about/toolbar/canvas-command contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, Tools panel creation/execution, About dialog/diagnostic execution, toolbar/status dialog/history/canvas execution, status/display UI rendering, renderer diagnostic capability adaptation, document resize execution, layer rename/menu execution, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4 execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_file_menu_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_app_status_tests`; `pp_app_core_tools_menu_tests`; `pp_app_core_about_menu_tests`; `pp_app_core_main_toolbar_tests`; `pp_app_core_document_resize_tests`; `pp_app_core_document_layer_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pp_app_core_canvas_hotkey_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `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-app-status --doc-name demo --unsaved --resolution 2048 --resolution-index 3 --zoom 1.25 --history-bytes 1572864 --recording-running --encoder-available --encoded-frames 12 --framebuffer-fetch --float32 --float32-linear --float16`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | +| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `App::render_task*`, `App::ui_task*`, `App::render_thread_*`, `App::ui_thread_*`, file-menu save actions, `NodeCanvas` canvas hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/menu/target naming/path decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, app status/display and renderer diagnostic decisions, app thread/task orchestration decisions, document resize decisions, layer rename/menu decisions, Tools menu/panel decisions, About menu/diagnostic decisions, main toolbar/status decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-file-menu`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-menu`, `pano_cli plan-export-target`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-app-status`, `pano_cli plan-app-thread`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-about-menu`, `pano_cli plan-main-toolbar`, `pano_cli plan-document-resize`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-menu`, `pano_cli plan-canvas-hotkey`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/status/thread/share/platform-I/O/display/keyboard/cloud/resize/layer/tools/about/toolbar/canvas-command contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, Tools panel creation/execution, About dialog/diagnostic execution, toolbar/status dialog/history/canvas execution, status/display UI rendering, renderer diagnostic capability adaptation, app task/thread execution, document resize execution, layer rename/menu execution, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4 execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_file_menu_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_app_status_tests`; `pp_app_core_app_thread_tests`; `pp_app_core_tools_menu_tests`; `pp_app_core_about_menu_tests`; `pp_app_core_main_toolbar_tests`; `pp_app_core_document_resize_tests`; `pp_app_core_document_layer_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pp_app_core_canvas_hotkey_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-recording-session --running --frame-count 12`; `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-app-status --doc-name demo --unsaved --resolution 2048 --resolution-index 3 --zoom 1.25 --history-bytes 1572864 --recording-running --encoder-available --encoded-frames 12 --framebuffer-fetch --float32 --float32-linear --float16`; `pano_cli plan-app-thread --kind ui-loop --live-reload`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | | DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path | | DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated | | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 0d46462..a73f0c4 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -212,6 +212,14 @@ touch-lock attachment now live in `pp_app_core`; `App::mouse_*`, `App::set_stylus`, and `pano_cli plan-app-input` consume those plans while retained event objects, child-node mutation, and legacy `Node` dispatch stay in the app shell. +App thread orchestration decisions for render/UI task dispatch, unique queued +task replacement, queue draining, render-context wrapping, async redraw +notification, UI tick redraw scheduling, UI-loop timer/report/reload cadence, +and thread start/stop intents now live in `pp_app_core`; `App::render_task*`, +`App::ui_task*`, `App::async_redraw`, `App::render_thread_*`, +`App::ui_thread_*`, and `pano_cli plan-app-thread` consume those plans while +retained `std::thread`, condition-variable, OpenGL context, live reload, and +task execution remain in the app shell. Shutdown lifecycle staging for UI-state save, stroke-preview renderer shutdown, recording stop, texture/shader invalidation, layout unload, render-target destruction, panel-node release, and quick-mode cleanup now lives in @@ -1685,6 +1693,14 @@ Results: `pano_cli_plan_app_input_stylus_smoke`, `pano_cli_plan_app_input_rejects_bad_float`, and `pano_cli_plan_app_input_rejects_missing_ui_panel`. +- `PanoPainter`, `pp_app_core_app_thread_tests`, and `pano_cli` built after + render/UI task dispatch, queue draining, UI-loop timer cadence, async redraw, + and start/stop decisions moved into `pp_app_core`. +- Focused app-thread CTest coverage passed for `pp_app_core_app_thread_tests`, + `pano_cli_plan_app_thread_dispatch_smoke`, + `pano_cli_plan_app_thread_ui_loop_smoke`, + `pano_cli_plan_app_thread_stop_smoke`, and + `pano_cli_plan_app_thread_rejects_bad_timer`. - `PanoPainter`, `pp_app_core_app_shutdown_tests`, and `pano_cli` built after shutdown cleanup staging moved into `pp_app_core`. - Focused shutdown CTest coverage passed for `pp_app_core_app_shutdown_tests`, diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 2bdce8b..f841a88 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -43,6 +43,7 @@ param( "pp_app_core_about_menu_tests", "pp_app_core_app_preferences_tests", "pp_app_core_app_frame_tests", + "pp_app_core_app_thread_tests", "pp_app_core_app_input_tests", "pp_app_core_app_shutdown_tests", "pp_app_core_app_startup_tests", diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index 02934c9..6274ab7 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u presets="${1:-android-arm64 android-x64 android-quest-arm64 android-focus-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_brush_package_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_about_menu_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_input_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_brush_package_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_about_menu_tests pp_app_core_app_preferences_tests pp_app_core_app_frame_tests pp_app_core_app_thread_tests pp_app_core_app_input_tests pp_app_core_app_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_tests pp_app_core_command_convert_tests pp_app_core_brush_package_export_tests pp_app_core_brush_package_import_tests pp_app_core_brush_ui_tests pp_app_core_canvas_hotkey_tests pp_app_core_canvas_tool_ui_tests pp_app_core_canvas_view_tests pp_app_core_document_animation_tests pp_app_core_document_canvas_tests pp_app_core_document_cloud_tests pp_app_core_document_export_tests pp_app_core_document_import_tests pp_app_core_document_layer_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_document_resize_tests pp_app_core_document_route_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests pp_app_core_file_menu_tests pp_app_core_grid_ui_tests pp_app_core_history_ui_tests pp_app_core_main_toolbar_tests pp_app_core_quick_ui_tests pp_app_core_tools_menu_tests}" start="$(date +%s)" overall_exit=0 diff --git a/src/app.cpp b/src/app.cpp index e8eb3fe..84ceb0c 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -9,6 +9,7 @@ #include "app_core/app_shutdown.h" #include "app_core/app_status.h" #include "app_core/app_startup.h" +#include "app_core/app_thread.h" #include "app_core/canvas_tool_ui.h" #include "app_core/document_recording.h" #include "app_core/document_route.h" @@ -475,8 +476,11 @@ void App::async_start() void App::async_redraw() { - redraw = true; - ui_cv.notify_all(); + const auto plan = pp::app::plan_app_async_redraw(); + if (plan.set_redraw) + redraw = true; + if (plan.notify_ui) + ui_cv.notify_all(); } void App::async_end() @@ -802,19 +806,21 @@ void App::render_thread_tick() { static uint32_t count = 0; render_thread_id = std::this_thread::get_id(); - render_running = true; std::deque working_list; + pp::app::AppQueueDrainPlan drain_plan; // move the task list locally to free the queue for other threads { std::unique_lock lock(render_task_mutex); - if (render_tasklist.empty()) + drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist.size()); + render_running = drain_plan.mark_running; + if (!drain_plan.drain_tasks) return; working_list = std::move(render_tasklist); } // execute the tasks - if (!working_list.empty()) + if (drain_plan.wrap_in_render_context) { async_start(); while (!working_list.empty()) @@ -835,20 +841,22 @@ void App::render_thread_main() uint32_t count = 0; render_thread_id = std::this_thread::get_id(); - render_running = true; + render_running = pp::app::plan_app_thread_start().mark_running; while (render_running) { std::deque working_list; + pp::app::AppQueueDrainPlan drain_plan; // move the task list locally to free the queue for other threads { std::unique_lock lock(render_task_mutex); render_cv.wait(lock, [this] { return render_tasklist.empty() && render_running ? false : true; }); + drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist.size()); working_list = std::move(render_tasklist); } // execute the tasks - if (!working_list.empty()) + if (drain_plan.wrap_in_render_context) { async_start(); while (!working_list.empty()) @@ -867,18 +875,20 @@ void App::render_thread_main() void App::ui_thread_tick() { ui_thread_id = std::this_thread::get_id(); - ui_running = true; std::deque working_list; + pp::app::AppUiTickPlan tick_plan; // move the task list locally to free the queue for other threads { std::unique_lock lock(ui_task_mutex); + tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist.size(), redraw); + ui_running = tick_plan.mark_running; working_list = std::move(ui_tasklist); } // execute the tasks - if (!working_list.empty()) + if (tick_plan.execute_tasks) { while (!working_list.empty()) { @@ -888,11 +898,14 @@ void App::ui_thread_tick() } } - tick(0); + if (tick_plan.tick_app) + tick(0); - if (redraw) + const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(redraw, 0); + if (redraw_plan.enqueue_render_frame) { - update(0); + if (redraw_plan.update_before_render) + update(0); render_task([this] { bind_default_render_target(); @@ -909,7 +922,7 @@ void App::ui_thread_main() uint32_t count = 0; ui_thread_id = std::this_thread::get_id(); - ui_running = true; + ui_running = pp::app::plan_app_thread_start().mark_running; attach_ui_thread(); @@ -949,25 +962,25 @@ void App::ui_thread_main() float dt = std::chrono::duration(t_now - t_start).count(); t_start = t_now; - update_platform_frame(dt); + const auto timer_plan = pp::app::plan_app_ui_loop_timers( + dt, + t_frame, + t_fps_counter, + t_reloader, + rendered_frames, + platform_enables_live_asset_reloading()); + if (timer_plan) { + if (timer_plan.value().update_platform_frame) + update_platform_frame(dt); + t_frame = timer_plan.value().frame_accumulator; + t_fps_counter = timer_plan.value().fps_accumulator; + t_reloader = timer_plan.value().reload_accumulator; + rendered_frames = timer_plan.value().rendered_frames_after_report; - // increment timers - t_frame += dt; - t_fps_counter += dt; + if (timer_plan.value().report_rendered_frames) + report_rendered_frames(timer_plan.value().reported_frame_count); - if (t_fps_counter > 1.f) - { - report_rendered_frames(rendered_frames); - t_fps_counter = 0; - rendered_frames = 0; - } - - if (platform_enables_live_asset_reloading()) - { - t_reloader += dt; - if (t_reloader > 1.0) - { - t_reloader = 0; + if (timer_plan.value().check_live_asset_reload) { if (ShaderManager::reload()) { stroke->update_controls(); @@ -980,11 +993,14 @@ void App::ui_thread_main() } } - tick(dt); + const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(redraw, rendered_frames); + if (redraw_plan.tick_app) + tick(dt); - if (redraw) + if (redraw_plan.enqueue_render_frame) { - update(t_frame); + if (redraw_plan.update_before_render) + update(t_frame); render_task([this, t_frame] { bind_default_render_target(); @@ -992,8 +1008,9 @@ void App::ui_thread_main() draw(t_frame); async_swap(); }); - t_frame = 0; - rendered_frames++; + if (redraw_plan.reset_frame_accumulator) + t_frame = 0; + rendered_frames = redraw_plan.rendered_frames; } } detach_ui_thread(); @@ -1001,28 +1018,38 @@ void App::ui_thread_main() void App::render_thread_start() { - render_thread = std::thread(&App::render_thread_main, this); - render_running = true; + const auto plan = pp::app::plan_app_thread_start(); + if (plan.start_thread) + render_thread = std::thread(&App::render_thread_main, this); + render_running = plan.mark_running; } void App::render_thread_stop() { - render_running = false; - render_cv.notify_all(); - if (render_thread.joinable()) + const auto plan = pp::app::plan_app_thread_stop(render_thread.joinable()); + if (plan.mark_not_running) + render_running = false; + if (plan.notify_worker) + render_cv.notify_all(); + if (plan.join_thread) render_thread.join(); } void App::ui_thread_start() { - ui_thread = std::thread(&App::ui_thread_main, this); - ui_running = true; + const auto plan = pp::app::plan_app_thread_start(); + if (plan.start_thread) + ui_thread = std::thread(&App::ui_thread_main, this); + ui_running = plan.mark_running; } void App::ui_thread_stop() { - ui_running = false; - ui_cv.notify_all(); - if (ui_thread.joinable()) + const auto plan = pp::app::plan_app_thread_stop(ui_thread.joinable()); + if (plan.mark_not_running) + ui_running = false; + if (plan.notify_worker) + ui_cv.notify_all(); + if (plan.join_thread) ui_thread.join(); } diff --git a/src/app.h b/src/app.h index 467fcc6..1c8d1db 100644 --- a/src/app.h +++ b/src/app.h @@ -25,6 +25,7 @@ #include "node_panel_animation.h" #include "layout.h" #include "app_core/document_session.h" +#include "app_core/app_thread.h" namespace pp::platform { class PlatformServices; @@ -374,21 +375,36 @@ public: { AppTask pt(task); auto f = pt.get_future(); - if (is_render_thread()) + const auto dispatch = pp::app::plan_app_task_dispatch( + is_render_thread(), + unique, + 0U, + render_running, + false, + false); + if (dispatch.execute_immediately) { pt(); } - else + 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); // remove any previously queued task from the same lambda - if (unique && !render_tasklist.empty()) + 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)); } - render_cv.notify_all(); + if (dispatch.notify_worker) + render_cv.notify_all(); } return f; } @@ -398,19 +414,27 @@ public: { AppTask pt(task); auto f = pt.get_future(); - if (is_render_thread()) + const auto dispatch = pp::app::plan_app_task_dispatch( + is_render_thread(), + false, + 0U, + render_running, + true, + false); + if (dispatch.execute_immediately) { pt(); } - else + else if (dispatch.queue_task) { { std::lock_guard lock(render_task_mutex); render_tasklist.push_back(std::move(pt)); } - render_cv.notify_all(); + if (dispatch.notify_worker) + render_cv.notify_all(); } - if (render_running) + if (dispatch.wait_for_completion) f.get(); } @@ -446,21 +470,36 @@ public: { AppTask pt(task); auto f = pt.get_future(); - if (is_ui_thread()) + const auto dispatch = pp::app::plan_app_task_dispatch( + is_ui_thread(), + unique, + 0U, + ui_running, + false, + false); + if (dispatch.execute_immediately) { pt(); } - else + 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); // remove any previously queued task from the same lambda - if (unique && !ui_tasklist.empty()) + 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)); } - ui_cv.notify_all(); + if (dispatch.notify_worker) + ui_cv.notify_all(); } return f; } @@ -470,21 +509,30 @@ public: { AppTask pt(task); auto f = pt.get_future(); - if (is_ui_thread()) + const auto dispatch = pp::app::plan_app_task_dispatch( + is_ui_thread(), + false, + 0U, + ui_running, + true, + true); + if (dispatch.execute_immediately) { pt(); } - else + else if (dispatch.queue_task) { { std::lock_guard lock(ui_task_mutex); ui_tasklist.push_back(std::move(pt)); } - ui_cv.notify_all(); + if (dispatch.notify_worker) + ui_cv.notify_all(); } - if (ui_running) + if (dispatch.wait_for_completion) f.get(); - redraw = true; + if (dispatch.request_redraw) + redraw = true; } void ui_sync() diff --git a/src/app_core/app_thread.h b/src/app_core/app_thread.h new file mode 100644 index 0000000..55c6603 --- /dev/null +++ b/src/app_core/app_thread.h @@ -0,0 +1,200 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include + +namespace pp::app { + +struct AppTaskDispatchPlan { + 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; +}; + +struct AppAsyncRedrawPlan { + bool set_redraw = true; + bool notify_ui = true; +}; + +struct AppQueueDrainPlan { + bool mark_running = true; + bool drain_tasks = false; + bool wrap_in_render_context = false; + std::size_t task_count = 0; +}; + +struct AppUiTickPlan { + bool mark_running = true; + bool execute_tasks = false; + bool tick_app = true; + bool update_before_render = false; + bool enqueue_render_frame = false; + std::size_t task_count = 0; +}; + +struct AppUiLoopTimerPlan { + bool update_platform_frame = true; + float frame_accumulator = 0.0F; + float fps_accumulator = 0.0F; + float reload_accumulator = 0.0F; + bool report_rendered_frames = false; + int reported_frame_count = 0; + int rendered_frames_after_report = 0; + bool check_live_asset_reload = false; +}; + +struct AppUiLoopRedrawPlan { + bool tick_app = true; + bool update_before_render = false; + bool enqueue_render_frame = false; + bool reset_frame_accumulator = false; + int rendered_frames = 0; +}; + +struct AppThreadStartPlan { + bool start_thread = true; + bool mark_running = true; +}; + +struct AppThreadStopPlan { + bool mark_not_running = true; + bool notify_worker = true; + bool join_thread = false; +}; + +[[nodiscard]] constexpr AppTaskDispatchPlan plan_app_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) noexcept +{ + const bool queue_task = !already_on_target_thread; + return AppTaskDispatchPlan { + .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 && worker_running && wait_for_completion, + .request_redraw = request_redraw_after_dispatch, + }; +} + +[[nodiscard]] constexpr AppAsyncRedrawPlan plan_app_async_redraw() noexcept +{ + return AppAsyncRedrawPlan {}; +} + +[[nodiscard]] constexpr AppQueueDrainPlan plan_app_render_queue_drain(std::size_t queued_task_count) noexcept +{ + const bool drain = queued_task_count > 0U; + return AppQueueDrainPlan { + .mark_running = true, + .drain_tasks = drain, + .wrap_in_render_context = drain, + .task_count = queued_task_count, + }; +} + +[[nodiscard]] constexpr AppQueueDrainPlan plan_app_ui_queue_drain(std::size_t queued_task_count) noexcept +{ + return AppQueueDrainPlan { + .mark_running = true, + .drain_tasks = queued_task_count > 0U, + .wrap_in_render_context = false, + .task_count = queued_task_count, + }; +} + +[[nodiscard]] constexpr AppUiTickPlan plan_app_ui_thread_tick( + std::size_t queued_task_count, + bool redraw) noexcept +{ + return AppUiTickPlan { + .mark_running = true, + .execute_tasks = queued_task_count > 0U, + .tick_app = true, + .update_before_render = redraw, + .enqueue_render_frame = redraw, + .task_count = queued_task_count, + }; +} + +[[nodiscard]] inline pp::foundation::Result plan_app_ui_loop_timers( + float delta_time_seconds, + float frame_accumulator, + float fps_accumulator, + float reload_accumulator, + int rendered_frames, + bool live_asset_reloading_enabled) +{ + if (!std::isfinite(delta_time_seconds) || !std::isfinite(frame_accumulator) + || !std::isfinite(fps_accumulator) || !std::isfinite(reload_accumulator)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("UI loop timer values must be finite")); + } + + if (delta_time_seconds < 0.0F || frame_accumulator < 0.0F + || fps_accumulator < 0.0F || reload_accumulator < 0.0F || rendered_frames < 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("UI loop timer values must not be negative")); + } + + AppUiLoopTimerPlan plan; + plan.frame_accumulator = frame_accumulator + delta_time_seconds; + plan.fps_accumulator = fps_accumulator + delta_time_seconds; + plan.reload_accumulator = reload_accumulator; + plan.rendered_frames_after_report = rendered_frames; + + if (plan.fps_accumulator > 1.0F) { + plan.report_rendered_frames = true; + plan.reported_frame_count = rendered_frames; + plan.fps_accumulator = 0.0F; + plan.rendered_frames_after_report = 0; + } + + if (live_asset_reloading_enabled) { + plan.reload_accumulator += delta_time_seconds; + if (plan.reload_accumulator > 1.0F) { + plan.reload_accumulator = 0.0F; + plan.check_live_asset_reload = true; + } + } + + return pp::foundation::Result::success(plan); +} + +[[nodiscard]] constexpr AppUiLoopRedrawPlan plan_app_ui_loop_redraw( + bool redraw, + int rendered_frames) noexcept +{ + return AppUiLoopRedrawPlan { + .tick_app = true, + .update_before_render = redraw, + .enqueue_render_frame = redraw, + .reset_frame_accumulator = redraw, + .rendered_frames = rendered_frames + (redraw ? 1 : 0), + }; +} + +[[nodiscard]] constexpr AppThreadStartPlan plan_app_thread_start() noexcept +{ + return AppThreadStartPlan {}; +} + +[[nodiscard]] constexpr AppThreadStopPlan plan_app_thread_stop(bool thread_joinable) noexcept +{ + return AppThreadStopPlan { + .mark_not_running = true, + .notify_worker = true, + .join_thread = thread_joinable, + }; +} + +} // namespace pp::app diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 867bc62..cf3b64d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -565,6 +565,16 @@ add_test(NAME pp_app_core_app_frame_tests COMMAND pp_app_core_app_frame_tests) set_tests_properties(pp_app_core_app_frame_tests PROPERTIES LABELS "app;desktop-fast;fuzz") +add_executable(pp_app_core_app_thread_tests + app_core/app_thread_tests.cpp) +target_link_libraries(pp_app_core_app_thread_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_app_thread_tests COMMAND pp_app_core_app_thread_tests) +set_tests_properties(pp_app_core_app_thread_tests PROPERTIES + LABELS "app;desktop-fast;fuzz") + add_executable(pp_app_core_app_input_tests app_core/app_input_tests.cpp) target_link_libraries(pp_app_core_app_input_tests PRIVATE @@ -1012,6 +1022,31 @@ if(TARGET pano_cli) WILL_FAIL TRUE PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-frame\".*\"message\":\"resize dimensions") + add_test(NAME pano_cli_plan_app_thread_dispatch_smoke + COMMAND pano_cli plan-app-thread --kind dispatch --unique --queued-tasks 2 --wait) + set_tests_properties(pano_cli_plan_app_thread_dispatch_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"dispatch\".*\"queueTask\":true.*\"removeMatchingUniqueTask\":true.*\"notifyWorker\":true.*\"waitForCompletion\":true") + + add_test(NAME pano_cli_plan_app_thread_ui_loop_smoke + COMMAND pano_cli plan-app-thread --kind ui-loop --dt 0.25 --frame-accumulator 0.5 --fps-accumulator 0.9 --reload-accumulator 0.9 --rendered-frames 7 --live-reload) + set_tests_properties(pano_cli_plan_app_thread_ui_loop_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"ui-loop\".*\"frameAccumulator\":0.75.*\"fpsAccumulator\":0.*\"reportRenderedFrames\":true.*\"reportedFrameCount\":7.*\"checkLiveAssetReload\":true") + + add_test(NAME pano_cli_plan_app_thread_stop_smoke + COMMAND pano_cli plan-app-thread --kind stop --not-joinable) + set_tests_properties(pano_cli_plan_app_thread_stop_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"stop\".*\"markNotRunning\":true.*\"notifyWorker\":true.*\"joinThread\":false") + + add_test(NAME pano_cli_plan_app_thread_rejects_bad_timer + COMMAND pano_cli plan-app-thread --kind ui-loop --bad-timer) + set_tests_properties(pano_cli_plan_app_thread_rejects_bad_timer PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"message\":\"UI loop timer values") + add_test(NAME pano_cli_plan_app_input_pointer_smoke COMMAND pano_cli plan-app-input --kind pointer --x 100 --y 50 --zoom 2) set_tests_properties(pano_cli_plan_app_input_pointer_smoke PROPERTIES diff --git a/tests/app_core/app_thread_tests.cpp b/tests/app_core/app_thread_tests.cpp new file mode 100644 index 0000000..4c963ff --- /dev/null +++ b/tests/app_core/app_thread_tests.cpp @@ -0,0 +1,134 @@ +#include "app_core/app_thread.h" +#include "test_harness.h" + +#include + +namespace { + +void task_dispatch_executes_immediately_on_target_thread(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_task_dispatch(true, true, 3, true, 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); +} + +void task_dispatch_queues_unique_work_and_waits_for_running_worker(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_task_dispatch(false, true, 2, true, true, false); + + 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); +} + +void task_dispatch_does_not_wait_for_stopped_worker(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_task_dispatch(false, false, 0, false, true, false); + + PP_EXPECT(harness, plan.queue_task); + PP_EXPECT(harness, plan.notify_worker); + PP_EXPECT(harness, !plan.wait_for_completion); +} + +void render_queue_drain_wraps_non_empty_work_in_context(pp::tests::Harness& harness) +{ + const auto empty = pp::app::plan_app_render_queue_drain(0); + const auto work = pp::app::plan_app_render_queue_drain(4); + + PP_EXPECT(harness, empty.mark_running); + PP_EXPECT(harness, !empty.drain_tasks); + PP_EXPECT(harness, !empty.wrap_in_render_context); + PP_EXPECT(harness, work.mark_running); + PP_EXPECT(harness, work.drain_tasks); + PP_EXPECT(harness, work.wrap_in_render_context); + PP_EXPECT(harness, work.task_count == 4U); +} + +void ui_thread_tick_runs_tasks_and_schedules_redraw(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_ui_thread_tick(3, true); + + PP_EXPECT(harness, plan.mark_running); + PP_EXPECT(harness, plan.execute_tasks); + PP_EXPECT(harness, plan.tick_app); + PP_EXPECT(harness, plan.update_before_render); + PP_EXPECT(harness, plan.enqueue_render_frame); + PP_EXPECT(harness, plan.task_count == 3U); +} + +void ui_loop_timers_report_fps_and_reload_on_threshold(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_ui_loop_timers(0.25F, 0.5F, 0.9F, 0.9F, 7, true); + + PP_EXPECT(harness, plan); + if (plan) { + PP_EXPECT(harness, plan.value().update_platform_frame); + PP_EXPECT(harness, plan.value().frame_accumulator == 0.75F); + PP_EXPECT(harness, plan.value().fps_accumulator == 0.0F); + PP_EXPECT(harness, plan.value().report_rendered_frames); + PP_EXPECT(harness, plan.value().reported_frame_count == 7); + PP_EXPECT(harness, plan.value().rendered_frames_after_report == 0); + PP_EXPECT(harness, plan.value().reload_accumulator == 0.0F); + PP_EXPECT(harness, plan.value().check_live_asset_reload); + } +} + +void ui_loop_timers_reject_invalid_values(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_app_ui_loop_timers(-0.1F, 0.0F, 0.0F, 0.0F, 0, false)); + PP_EXPECT(harness, !pp::app::plan_app_ui_loop_timers(0.1F, std::nanf(""), 0.0F, 0.0F, 0, false)); + PP_EXPECT(harness, !pp::app::plan_app_ui_loop_timers(0.1F, 0.0F, 0.0F, 0.0F, -1, false)); +} + +void ui_loop_redraw_increments_rendered_frames(pp::tests::Harness& harness) +{ + const auto idle = pp::app::plan_app_ui_loop_redraw(false, 2); + const auto redraw = pp::app::plan_app_ui_loop_redraw(true, 2); + + PP_EXPECT(harness, idle.tick_app); + PP_EXPECT(harness, !idle.enqueue_render_frame); + PP_EXPECT(harness, idle.rendered_frames == 2); + PP_EXPECT(harness, redraw.update_before_render); + PP_EXPECT(harness, redraw.enqueue_render_frame); + PP_EXPECT(harness, redraw.reset_frame_accumulator); + PP_EXPECT(harness, redraw.rendered_frames == 3); +} + +void thread_start_stop_preserve_legacy_intents(pp::tests::Harness& harness) +{ + const auto start = pp::app::plan_app_thread_start(); + const auto stop_joinable = pp::app::plan_app_thread_stop(true); + const auto stop_detached = pp::app::plan_app_thread_stop(false); + + PP_EXPECT(harness, start.start_thread); + PP_EXPECT(harness, start.mark_running); + PP_EXPECT(harness, stop_joinable.mark_not_running); + PP_EXPECT(harness, stop_joinable.notify_worker); + PP_EXPECT(harness, stop_joinable.join_thread); + PP_EXPECT(harness, !stop_detached.join_thread); +} + +} // namespace + +int main() +{ + pp::tests::Harness harness; + 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("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); + harness.run("ui loop timers report fps and reload on threshold", ui_loop_timers_report_fps_and_reload_on_threshold); + harness.run("ui loop timers reject invalid values", ui_loop_timers_reject_invalid_values); + harness.run("ui loop redraw increments rendered frames", ui_loop_redraw_increments_rendered_frames); + harness.run("thread start stop preserve legacy intents", thread_start_stop_preserve_legacy_intents); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 1c1f0f5..7246839 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -5,6 +5,7 @@ #include "app_core/app_shutdown.h" #include "app_core/app_status.h" #include "app_core/app_startup.h" +#include "app_core/app_thread.h" #include "app_core/brush_package_import.h" #include "app_core/brush_package_export.h" #include "app_core/brush_ui.h" @@ -264,6 +265,25 @@ struct PlanAppFrameArgs { bool bad_resize = false; }; +struct PlanAppThreadArgs { + std::string kind = "dispatch"; + bool on_target_thread = false; + bool unique = false; + bool worker_running = true; + bool wait = false; + bool request_redraw = false; + bool redraw = false; + bool live_reload = false; + bool joinable = true; + std::uint32_t queued_tasks = 0; + int rendered_frames = 0; + float delta_time_seconds = 0.25F; + float frame_accumulator = 0.5F; + float fps_accumulator = 0.9F; + float reload_accumulator = 0.9F; + bool bad_timer = false; +}; + struct PlanAppInputArgs { std::string kind = "pointer"; float x0 = 100.0F; @@ -2075,6 +2095,7 @@ void print_help() << " plan-app-startup [--run-counter N] [--auto-timelapse-disabled] [--vr-controllers-disabled] [--license-invalid]\n" << " plan-app-startup-resources [--width N] [--height N] [--bad-size]\n" << " plan-app-frame [--redraw] [--animate] [--no-designer-layout] [--no-main-layout] [--no-canvas] [--no-canvas-document] [--vr-active] [--ui-hidden] [--vr-only] [--resize-width N] [--resize-height N] [--bad-resize]\n" + << " plan-app-thread --kind dispatch|render-drain|ui-drain|ui-tick|ui-loop|redraw|start|stop [--on-target-thread] [--unique] [--worker-stopped] [--wait] [--request-redraw] [--redraw] [--live-reload] [--not-joinable] [--queued-tasks N] [--rendered-frames N] [--dt N] [--frame-accumulator N] [--fps-accumulator N] [--reload-accumulator N] [--bad-timer]\n" << " plan-app-input --kind pointer|gesture|cancel|main|key|ui-toggle|stylus [--x N] [--y N] [--x1 N] [--y1 N] [--prev-x N] [--prev-y N] [--prev-x1 N] [--prev-y1 N] [--zoom N] [--no-designer-layout] [--no-main-layout] [--spacebar] [--vr-active] [--key-up] [--ui-hidden] [--no-canvas] [--main-child-count N] [--panel-child-count N] [--bad-float]\n" << " plan-app-shutdown\n" << " plan-command-convert [--project FILE] [--output FILE] [--canvas-resolution N]\n" @@ -3885,6 +3906,190 @@ int plan_app_frame(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_app_thread_args( + int argc, + char** argv, + PlanAppThreadArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--kind") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.kind = argv[++i]; + } else if (key == "--on-target-thread") { + args.on_target_thread = true; + } else if (key == "--unique") { + args.unique = true; + } else if (key == "--worker-stopped") { + args.worker_running = false; + } else if (key == "--wait") { + args.wait = true; + } else if (key == "--request-redraw") { + args.request_redraw = true; + } else if (key == "--redraw") { + args.redraw = true; + } else if (key == "--live-reload") { + args.live_reload = true; + } else if (key == "--not-joinable") { + args.joinable = false; + } else if (key == "--bad-timer") { + args.bad_timer = true; + } else if (key == "--queued-tasks") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = pp::foundation::parse_u32(argv[++i]); + if (!value) { + return value.status(); + } + args.queued_tasks = value.value(); + } else if (key == "--rendered-frames") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_i32_arg(argv[++i]); + if (!value) { + return value.status(); + } + args.rendered_frames = value.value(); + } else if (key == "--dt" || key == "--frame-accumulator" + || key == "--fps-accumulator" || key == "--reload-accumulator") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_float_arg(argv[++i]); + if (!value) { + return value.status(); + } + if (key == "--dt") { + args.delta_time_seconds = value.value(); + } else if (key == "--frame-accumulator") { + args.frame_accumulator = value.value(); + } else if (key == "--fps-accumulator") { + args.fps_accumulator = value.value(); + } else { + args.reload_accumulator = value.value(); + } + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_app_thread(int argc, char** argv) +{ + PlanAppThreadArgs args; + const auto status = parse_plan_app_thread_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-app-thread", status.message); + return 2; + } + + std::cout << "{\"ok\":true,\"command\":\"plan-app-thread\",\"kind\":\"" + << json_escape(args.kind) << "\""; + + if (args.kind == "dispatch") { + const auto plan = pp::app::plan_app_task_dispatch( + args.on_target_thread, + args.unique, + args.queued_tasks, + args.worker_running, + args.wait, + args.request_redraw); + std::cout << ",\"plan\":{\"executeImmediately\":" << json_bool(plan.execute_immediately) + << ",\"queueTask\":" << json_bool(plan.queue_task) + << ",\"removeMatchingUniqueTask\":" << json_bool(plan.remove_matching_unique_task) + << ",\"notifyWorker\":" << json_bool(plan.notify_worker) + << ",\"waitForCompletion\":" << json_bool(plan.wait_for_completion) + << ",\"requestRedraw\":" << json_bool(plan.request_redraw) + << "}}\n"; + return 0; + } + + if (args.kind == "render-drain" || args.kind == "ui-drain") { + const auto plan = args.kind == "render-drain" + ? pp::app::plan_app_render_queue_drain(args.queued_tasks) + : pp::app::plan_app_ui_queue_drain(args.queued_tasks); + std::cout << ",\"plan\":{\"markRunning\":" << json_bool(plan.mark_running) + << ",\"drainTasks\":" << json_bool(plan.drain_tasks) + << ",\"wrapInRenderContext\":" << json_bool(plan.wrap_in_render_context) + << ",\"taskCount\":" << plan.task_count + << "}}\n"; + return 0; + } + + if (args.kind == "ui-tick") { + const auto plan = pp::app::plan_app_ui_thread_tick(args.queued_tasks, args.redraw); + std::cout << ",\"plan\":{\"markRunning\":" << json_bool(plan.mark_running) + << ",\"executeTasks\":" << json_bool(plan.execute_tasks) + << ",\"tickApp\":" << json_bool(plan.tick_app) + << ",\"updateBeforeRender\":" << json_bool(plan.update_before_render) + << ",\"enqueueRenderFrame\":" << json_bool(plan.enqueue_render_frame) + << ",\"taskCount\":" << plan.task_count + << "}}\n"; + return 0; + } + + if (args.kind == "ui-loop") { + const auto plan = pp::app::plan_app_ui_loop_timers( + args.bad_timer ? -1.0F : args.delta_time_seconds, + args.frame_accumulator, + args.fps_accumulator, + args.reload_accumulator, + args.rendered_frames, + args.live_reload); + if (!plan) { + print_error("plan-app-thread", plan.status().message); + return 2; + } + std::cout << ",\"plan\":{\"updatePlatformFrame\":" << json_bool(plan.value().update_platform_frame) + << ",\"frameAccumulator\":" << plan.value().frame_accumulator + << ",\"fpsAccumulator\":" << plan.value().fps_accumulator + << ",\"reloadAccumulator\":" << plan.value().reload_accumulator + << ",\"reportRenderedFrames\":" << json_bool(plan.value().report_rendered_frames) + << ",\"reportedFrameCount\":" << plan.value().reported_frame_count + << ",\"renderedFramesAfterReport\":" << plan.value().rendered_frames_after_report + << ",\"checkLiveAssetReload\":" << json_bool(plan.value().check_live_asset_reload) + << "}}\n"; + return 0; + } + + if (args.kind == "redraw") { + const auto plan = pp::app::plan_app_ui_loop_redraw(args.redraw, args.rendered_frames); + std::cout << ",\"plan\":{\"tickApp\":" << json_bool(plan.tick_app) + << ",\"updateBeforeRender\":" << json_bool(plan.update_before_render) + << ",\"enqueueRenderFrame\":" << json_bool(plan.enqueue_render_frame) + << ",\"resetFrameAccumulator\":" << json_bool(plan.reset_frame_accumulator) + << ",\"renderedFrames\":" << plan.rendered_frames + << "}}\n"; + return 0; + } + + if (args.kind == "start") { + const auto plan = pp::app::plan_app_thread_start(); + std::cout << ",\"plan\":{\"startThread\":" << json_bool(plan.start_thread) + << ",\"markRunning\":" << json_bool(plan.mark_running) + << "}}\n"; + return 0; + } + + if (args.kind == "stop") { + const auto plan = pp::app::plan_app_thread_stop(args.joinable); + std::cout << ",\"plan\":{\"markNotRunning\":" << json_bool(plan.mark_not_running) + << ",\"notifyWorker\":" << json_bool(plan.notify_worker) + << ",\"joinThread\":" << json_bool(plan.join_thread) + << "}}\n"; + return 0; + } + + print_error("plan-app-thread", "unknown app thread plan kind"); + return 2; +} + pp::foundation::Status parse_plan_app_input_args( int argc, char** argv, @@ -10469,6 +10674,10 @@ int main(int argc, char** argv) return plan_app_frame(argc, argv); } + if (command == "plan-app-thread") { + return plan_app_thread(argc, argv); + } + if (command == "plan-app-input") { return plan_app_input(argc, argv); }