From 678bf2dcd61b9532b8387d41a16acadbfb7f43dd Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 5 Jun 2026 05:55:23 +0200 Subject: [PATCH] Route startup resources through app core --- docs/modernization/build-inventory.md | 7 ++ docs/modernization/debt.md | 8 +- docs/modernization/roadmap.md | 19 +++-- src/app.cpp | 21 +++--- src/app_core/app_startup.h | 76 +++++++++++++++++++ src/legacy_app_startup_services.cpp | 43 ++++++++++- src/legacy_app_startup_services.h | 3 + tests/CMakeLists.txt | 13 ++++ tests/app_core/app_startup_tests.cpp | 103 +++++++++++++++++++++++++- tools/pano_cli/main.cpp | 75 +++++++++++++++++++ 10 files changed, 347 insertions(+), 21 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 991e5fa..0ac49f2 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -234,6 +234,13 @@ Known local toolchain state: `pano_cli plan-canvas-camera-reset`, `pano_cli plan-canvas-view-density`, and `pano_cli plan-canvas-view-cursor-mode` before retained canvas mutation and settings writes. +- `src/legacy_app_startup_services.*` is the current main-app startup bridge for + run-counter persistence, startup runtime side effects, and startup resource + sequencing. `App::init` now consumes `pp_app_core` plans exposed through + `pano_cli plan-app-startup` and `pano_cli plan-app-startup-resources`, while + retained shader loading, asset initialization, layout creation, title updates, + UI render-target creation, recording startup, VR-controller state mutation, + settings writes, and license-warning dialogs remain legacy execution. - `src/legacy_app_preference_services.*` is the current app-shell bridge for options-menu preference execution. It keeps UI scale, viewport scale, RTL, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks on diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 0f70235..7af1348 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -85,6 +85,12 @@ agent or engineer to remove them without reconstructing context from chat. This also narrows DEBT-0045 for viewport-density and cursor-mode preference execution, though preference persistence remains retained in the legacy canvas-view bridge. +- 2026-06-05: DEBT-0046 was narrowed. Main startup shader, asset, layout, + title, and UI render-target sequencing now goes through tested `pp_app_core` + resource plans and `src/legacy_app_startup_services.*`; `App::init` keeps + the retained OpenGL startup task in place, then delegates startup resources + and runtime side effects through the startup bridge. `pano_cli + plan-app-startup-resources` exposes the resource path for automation. - 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit, thumbnail, and object-draw history paths now query saved blend state through tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect @@ -144,7 +150,7 @@ agent or engineer to remove them without reconstructing context from chat. | 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, but the bridge still calls legacy `Canvas` export methods, owns platform-specific export success messages, creates export directories, handles picker-selected stems, and performs Web prepared-file handoff directly | 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 simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and cube export execution, export-directory creation, platform success reporting, 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`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, owns mobile/Web save callbacks, and emits success messages directly | 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`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, mobile/Web save callbacks, and success reporting 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.*`, 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 `Settings::save` directly; VR mode callbacks now call `App` VR wrappers that dispatch to `PlatformServices`, while the actual Windows OpenVR SDK bridge still lives in `WindowsPlatformServices` | 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 now consumes pure `pp_app_core` through `App::init`, `pano_cli plan-app-startup`, `AppStartupServices`, and `src/legacy_app_startup_services.*`, but the bridge still calls legacy `Settings::set`, `Settings::save`, `App::rec_start`, app VR-controller state mutation, and message-box license warning execution directly | Preserve current startup behavior while app startup moves toward app/preferences/storage/recording/UI 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`; `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, and startup UI/runtime side effects are owned by injected app/preferences/storage/recording/UI services with `App::init` acting only as orchestration | +| 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.*`, but the bridge still calls legacy `Settings::set`, `Settings::save`, `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 and execution dispatch now consume pure `pp_app_core` through `App::dialog_ppbr_export`, `pano_cli plan-brush-package-export`, `BrushPackageExportServices`, and `src/legacy_brush_package_export_services.*`; PPBR header/path planning now consumes `pp_assets::brush_package`, and the macOS data-directory override now routes through `PlatformServices`, but the bridge still reads `NodeDialogExportPPBR`, carries the legacy `Image` header object outside the pure request, converts to `NodePanelBrushPreset::PPBRInfo`, calls `NodePanelBrushPreset::export_ppbr`, owns desktop worker-thread dispatch, dialog destruction, mobile/Web completion, and success-message behavior 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`; `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 success UI are owned by injected brush asset/storage/UI/platform services with `App::dialog_ppbr_export` acting only as a UI adapter | | DEBT-0048 | Open | Modernization | ABR/PPBR brush package import execution now consumes pure `pp_app_core` through document-open confirmation callbacks, `pano_cli plan-brush-package-import`, `BrushPackageImportServices`, and `src/legacy_brush_package_import_services.*`; imported brush tip/pattern target paths now consume `pp_assets::brush_package`, but the bridge still launches detached legacy `NodePanelBrushPreset::import_abr`/`import_ppbr` worker threads and depends on the legacy preset panel as the importer/storage owner | Preserve current brush import behavior while brush package parsing, preset storage, progress/error reporting, and UI refresh move toward asset/paint/UI services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_import_tests`; `pano_cli plan-brush-package-import --kind ppbr --path D:/Paint/Brushes/clouds.ppbr`; `pano_cli plan-brush-package-import --kind abr --path D:/Paint/Brushes/clouds.abr`; `pano_cli plan-brush-package-import --kind ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | ABR/PPBR parsing, preset creation/storage, import threading/progress, duplicate asset policy, and UI refresh are owned by injected brush asset/paint/UI services with document-open callbacks only confirming user intent | | DEBT-0049 | Open | Modernization | `pp_assets::validate_ppbr_header` intentionally preserves the legacy PPBR version check from `NodePanelBrushPreset::import_ppbr`, which accepts files when either major is `0` or minor is `1` instead of requiring exactly version `0.1` | Avoid rejecting existing brush packages before compatibility fixtures prove the stricter rule is safe | `pp_assets_brush_package_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add PPBR compatibility fixtures for accepted/rejected historical package versions, then require canonical `0.1` or an explicit supported-version matrix and update live import accordingly | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 84f6ed4..84ba0a8 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -184,11 +184,13 @@ contracts. Options-menu preference execution now dispatches through legacy widgets, settings persistence, recording toggles, and canvas cursor updates continue. It also owns tested startup plans for run-counter increments, preference-save -intent, auto-timelapse startup, stored VR-controller state, and license-warning -visibility. `App::init` now plans those decisions before heavy initialization, -executes run-counter persistence through `src/legacy_app_startup_services.*` -before asset/layout setup, and executes runtime startup side effects after the -UI layout and main render target exist. +intent, auto-timelapse startup, stored VR-controller state, license-warning +visibility, and main startup resource sequencing for shader, asset, layout, +title, and UI render-target setup. `App::init` now plans those decisions before +heavy initialization, executes run-counter persistence through +`src/legacy_app_startup_services.*` before resource setup, dispatches the +resource sequence through the same bridge, and executes runtime startup side +effects after the UI layout and main render target exist. It also owns tested app status/display plans for document title text, resolution mapping/labels, DPI text, history-memory text, and recording-frame status text, plus renderer diagnostic indicator labels for framebuffer fetch @@ -1634,10 +1636,13 @@ Results: `pp_app_core_app_preferences_tests` and the app-preferences CLI smoke tests after the live bridge split, including VR mode failed-start status coverage. - `PanoPainter`, `pp_app_core_app_startup_tests`, and `pano_cli` built after - startup preference/runtime execution moved behind app startup services. + startup preference/runtime execution and startup resource sequencing moved + behind app startup services. - Focused startup CTest coverage passed for `pp_app_core_app_startup_tests`, `pano_cli_plan_app_startup_smoke`, and - `pano_cli_plan_app_startup_rejects_negative_counter`. + `pano_cli_plan_app_startup_rejects_negative_counter`, with startup resource + sequencing also covered by `pano_cli_plan_app_startup_resources_smoke` and + `pano_cli_plan_app_startup_resources_rejects_bad_size`. - `PanoPainter`, `pp_app_core_brush_package_export_tests`, and `pano_cli` built after PPBR brush package export request validation and dispatch moved behind app-core brush package services. diff --git a/src/app.cpp b/src/app.cpp index a93cbea..7f5d45e 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -143,11 +143,6 @@ void apply_app_scissor_test(bool enabled) LOG("OpenGL scissor test failed: %s", status.message); } -[[nodiscard]] GLint rgba8_internal_format() noexcept -{ - return static_cast(pp::renderer::gl::rgba8_internal_format()); -} - [[nodiscard]] GLenum linear_texture_filter() noexcept { return static_cast(pp::renderer::gl::linear_texture_filter()); @@ -450,12 +445,16 @@ void App::init() LOG("App startup persistence failed: %s", persistence_status.message); } - initShaders(); - initAssets(); - initLayout(); - title_update(); - - uirtt.create(width, height, -1, rgba8_internal_format(), true); + const auto startup_resources = pp::app::plan_app_startup_resources(width, height); + if (!startup_resources) { + LOG("App startup resource plan failed: %s", startup_resources.status().message); + } else { + const auto resource_status = pp::panopainter::execute_legacy_app_startup_resources( + *this, + startup_resources.value()); + if (!resource_status.ok()) + LOG("App startup resources failed: %s", resource_status.message); + } if (startup_plan) { const auto startup_status = pp::panopainter::execute_legacy_app_startup_runtime_plan( diff --git a/src/app_core/app_startup.h b/src/app_core/app_startup.h index bf8c052..c603cf0 100644 --- a/src/app_core/app_startup.h +++ b/src/app_core/app_startup.h @@ -2,6 +2,7 @@ #include "foundation/result.h" +#include #include namespace pp::app { @@ -15,6 +16,16 @@ struct AppStartupPlan { bool show_license_warning = false; }; +struct AppStartupResourcePlan { + int ui_render_target_width = 0; + int ui_render_target_height = 0; + bool initialize_shaders = true; + bool initialize_assets = true; + bool initialize_layout = true; + bool update_title = true; + bool create_ui_render_target = true; +}; + class AppStartupServices { public: virtual ~AppStartupServices() = default; @@ -26,6 +37,17 @@ public: virtual void show_license_warning() = 0; }; +class AppStartupResourceServices { +public: + virtual ~AppStartupResourceServices() = default; + + virtual void initialize_shaders() = 0; + virtual void initialize_assets() = 0; + virtual void initialize_layout() = 0; + virtual void update_title() = 0; + virtual void create_ui_render_target(int width, int height) = 0; +}; + [[nodiscard]] inline pp::foundation::Result plan_app_startup( int current_run_counter, bool auto_timelapse_enabled, @@ -51,6 +73,32 @@ public: return pp::foundation::Result::success(plan); } +[[nodiscard]] inline pp::foundation::Result plan_app_startup_resources( + float ui_width, + float ui_height) +{ + if (!std::isfinite(ui_width) || !std::isfinite(ui_height)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("startup resource dimensions must be finite")); + } + + if (ui_width < 1.0F || ui_height < 1.0F) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("startup resource dimensions must be positive")); + } + + if (ui_width > static_cast(std::numeric_limits::max()) + || ui_height > static_cast(std::numeric_limits::max())) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("startup resource dimensions exceed integer range")); + } + + AppStartupResourcePlan plan; + plan.ui_render_target_width = static_cast(ui_width); + plan.ui_render_target_height = static_cast(ui_height); + return pp::foundation::Result::success(plan); +} + [[nodiscard]] inline pp::foundation::Status execute_app_startup_plan( const AppStartupPlan& plan, AppStartupServices& services) @@ -109,4 +157,32 @@ public: return pp::foundation::Status::success(); } +[[nodiscard]] inline pp::foundation::Status execute_app_startup_resources( + const AppStartupResourcePlan& plan, + AppStartupResourceServices& services) +{ + if (plan.create_ui_render_target + && (plan.ui_render_target_width <= 0 || plan.ui_render_target_height <= 0)) { + return pp::foundation::Status::invalid_argument("startup resource plan has invalid UI render target size"); + } + + if (plan.initialize_shaders) { + services.initialize_shaders(); + } + if (plan.initialize_assets) { + services.initialize_assets(); + } + if (plan.initialize_layout) { + services.initialize_layout(); + } + if (plan.update_title) { + services.update_title(); + } + if (plan.create_ui_render_target) { + services.create_ui_render_target(plan.ui_render_target_width, plan.ui_render_target_height); + } + + return pp::foundation::Status::success(); +} + } // namespace pp::app diff --git a/src/legacy_app_startup_services.cpp b/src/legacy_app_startup_services.cpp index 1130a6f..f246165 100644 --- a/src/legacy_app_startup_services.cpp +++ b/src/legacy_app_startup_services.cpp @@ -3,13 +3,16 @@ #include "legacy_app_startup_services.h" #include "app.h" +#include "renderer_gl/opengl_capabilities.h" #include "serializer.h" #include "settings.h" namespace pp::panopainter { namespace { -class LegacyAppStartupServices final : public pp::app::AppStartupServices { +class LegacyAppStartupServices final + : public pp::app::AppStartupServices + , public pp::app::AppStartupResourceServices { public: explicit LegacyAppStartupServices(App& app) noexcept : app_(app) @@ -43,6 +46,36 @@ public: app_.message_box("License", "Could not validate this license, running in demo mode."); } + void initialize_shaders() override + { + app_.initShaders(); + } + + void initialize_assets() override + { + app_.initAssets(); + } + + void initialize_layout() override + { + app_.initLayout(); + } + + void update_title() override + { + app_.title_update(); + } + + void create_ui_render_target(int width, int height) override + { + app_.uirtt.create( + width, + height, + -1, + static_cast(pp::renderer::gl::rgba8_internal_format()), + true); + } + private: App& app_; }; @@ -73,4 +106,12 @@ pp::foundation::Status execute_legacy_app_startup_runtime_plan( return pp::app::execute_app_startup_runtime_plan(plan, services); } +pp::foundation::Status execute_legacy_app_startup_resources( + App& app, + const pp::app::AppStartupResourcePlan& plan) +{ + LegacyAppStartupServices services(app); + return pp::app::execute_app_startup_resources(plan, services); +} + } // namespace pp::panopainter diff --git a/src/legacy_app_startup_services.h b/src/legacy_app_startup_services.h index 7bda81d..e554b2d 100644 --- a/src/legacy_app_startup_services.h +++ b/src/legacy_app_startup_services.h @@ -15,5 +15,8 @@ namespace pp::panopainter { [[nodiscard]] pp::foundation::Status execute_legacy_app_startup_runtime_plan( App& app, const pp::app::AppStartupPlan& plan); +[[nodiscard]] pp::foundation::Status execute_legacy_app_startup_resources( + App& app, + const pp::app::AppStartupResourcePlan& plan); } // namespace pp::panopainter diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 09ab427..9a7800f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -934,6 +934,19 @@ if(TARGET pano_cli) WILL_FAIL TRUE PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-startup\".*\"message\":\"run counter must not be negative\"") + add_test(NAME pano_cli_plan_app_startup_resources_smoke + COMMAND pano_cli plan-app-startup-resources --width 1280 --height 720) + set_tests_properties(pano_cli_plan_app_startup_resources_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-startup-resources\".*\"uiRenderTargetWidth\":1280.*\"uiRenderTargetHeight\":720.*\"initializeShaders\":true.*\"initializeAssets\":true.*\"initializeLayout\":true.*\"updateTitle\":true.*\"createUiRenderTarget\":true") + + add_test(NAME pano_cli_plan_app_startup_resources_rejects_bad_size + COMMAND pano_cli plan-app-startup-resources --bad-size) + set_tests_properties(pano_cli_plan_app_startup_resources_rejects_bad_size PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-startup-resources\".*\"message\":\"startup resource dimensions") + add_test(NAME pano_cli_plan_brush_package_import_ppbr_smoke COMMAND pano_cli plan-brush-package-import --kind ppbr diff --git a/tests/app_core/app_startup_tests.cpp b/tests/app_core/app_startup_tests.cpp index 8377728..e4ae5b0 100644 --- a/tests/app_core/app_startup_tests.cpp +++ b/tests/app_core/app_startup_tests.cpp @@ -1,12 +1,15 @@ #include "app_core/app_startup.h" #include "test_harness.h" +#include #include #include namespace { -class FakeAppStartupServices final : public pp::app::AppStartupServices { +class FakeAppStartupServices final + : public pp::app::AppStartupServices + , public pp::app::AppStartupResourceServices { public: void store_run_counter(int value) override { @@ -38,11 +41,50 @@ public: call_order += "license;"; } + void initialize_shaders() override + { + shader_initializations += 1; + call_order += "shaders;"; + } + + void initialize_assets() override + { + asset_initializations += 1; + call_order += "assets;"; + } + + void initialize_layout() override + { + layout_initializations += 1; + call_order += "layout;"; + } + + void update_title() override + { + title_updates += 1; + call_order += "title;"; + } + + void create_ui_render_target(int width, int height) override + { + ui_render_target_width = width; + ui_render_target_height = height; + ui_render_target_creations += 1; + call_order += "ui-rtt;"; + } + int stored_run_counter = 0; int save_calls = 0; int timelapse_starts = 0; bool vr_controllers_enabled = false; int license_warnings = 0; + int shader_initializations = 0; + int asset_initializations = 0; + int layout_initializations = 0; + int title_updates = 0; + int ui_render_target_creations = 0; + int ui_render_target_width = 0; + int ui_render_target_height = 0; std::string call_order; }; @@ -144,6 +186,57 @@ void startup_executor_rejects_malformed_counter_state(pp::tests::Harness& harnes PP_EXPECT(harness, services.call_order.empty()); } +void startup_resource_plan_projects_ui_target_size(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_startup_resources(1280.9F, 720.1F); + + PP_EXPECT(harness, plan); + if (plan) { + PP_EXPECT(harness, plan.value().ui_render_target_width == 1280); + PP_EXPECT(harness, plan.value().ui_render_target_height == 720); + PP_EXPECT(harness, plan.value().initialize_shaders); + PP_EXPECT(harness, plan.value().initialize_assets); + PP_EXPECT(harness, plan.value().initialize_layout); + PP_EXPECT(harness, plan.value().update_title); + PP_EXPECT(harness, plan.value().create_ui_render_target); + } +} + +void startup_resource_plan_rejects_invalid_dimensions(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_app_startup_resources(0.0F, 720.0F)); + PP_EXPECT(harness, !pp::app::plan_app_startup_resources(1280.0F, -1.0F)); + PP_EXPECT(harness, !pp::app::plan_app_startup_resources(std::nanf(""), 720.0F)); +} + +void startup_resource_executor_dispatches_in_stable_order(pp::tests::Harness& harness) +{ + FakeAppStartupServices services; + const auto plan = pp::app::plan_app_startup_resources(1024.0F, 512.0F); + + PP_EXPECT(harness, plan); + PP_EXPECT(harness, pp::app::execute_app_startup_resources(plan.value(), services).ok()); + PP_EXPECT(harness, services.shader_initializations == 1); + PP_EXPECT(harness, services.asset_initializations == 1); + PP_EXPECT(harness, services.layout_initializations == 1); + PP_EXPECT(harness, services.title_updates == 1); + PP_EXPECT(harness, services.ui_render_target_creations == 1); + PP_EXPECT(harness, services.ui_render_target_width == 1024); + PP_EXPECT(harness, services.ui_render_target_height == 512); + PP_EXPECT(harness, services.call_order == "shaders;assets;layout;title;ui-rtt;"); +} + +void startup_resource_executor_rejects_invalid_render_target_size(pp::tests::Harness& harness) +{ + FakeAppStartupServices services; + pp::app::AppStartupResourcePlan plan; + plan.ui_render_target_width = 0; + plan.ui_render_target_height = 720; + + PP_EXPECT(harness, !pp::app::execute_app_startup_resources(plan, services).ok()); + PP_EXPECT(harness, services.call_order.empty()); +} + } int main() @@ -160,5 +253,13 @@ int main() "startup split executors keep persistence and runtime separate", startup_split_executors_keep_persistence_and_runtime_separate); harness.run("startup executor rejects malformed counter state", startup_executor_rejects_malformed_counter_state); + harness.run("startup resource plan projects UI target size", startup_resource_plan_projects_ui_target_size); + harness.run("startup resource plan rejects invalid dimensions", startup_resource_plan_rejects_invalid_dimensions); + harness.run( + "startup resource executor dispatches in stable order", + startup_resource_executor_dispatches_in_stable_order); + harness.run( + "startup resource executor rejects invalid render target size", + startup_resource_executor_rejects_invalid_render_target_size); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index bbbb84c..ac16f5e 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -239,6 +239,12 @@ struct PlanAppStartupArgs { bool license_valid = true; }; +struct PlanAppStartupResourcesArgs { + float width = 1280.0F; + float height = 720.0F; + bool bad_size = false; +}; + struct PlanBrushPackageExportArgs { std::string path; std::string author; @@ -2003,6 +2009,7 @@ void print_help() << " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files]\n" << " plan-app-preferences [--ui-scale N] [--display-density N] [--current-scale N] [--scale-option N] [--viewport-scale N] [--rtl] [--timelapse-disabled] [--recording-running] [--vr-controllers-disabled] [--cursor-mode N]\n" << " 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-status [--doc-name NAME] [--unsaved] [--resolution N] [--resolution-index N] [--zoom N] [--history-bytes N] [--recording-running] [--encoder-available] [--encoded-frames N] [--framebuffer-fetch] [--float32] [--float32-linear] [--float16]\n" << " plan-brush-package-import --kind abr|ppbr --path FILE\n" << " plan-brush-package-export --path FILE [--author NAME] [--email EMAIL] [--url URL] [--description TEXT] [--dest-path DIR] [--export-data|--no-export-data] [--header-image]\n" @@ -3644,6 +3651,70 @@ int plan_app_startup(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_app_startup_resources_args( + int argc, + char** argv, + PlanAppStartupResourcesArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--width") { + 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(); + } + args.width = value.value(); + } else if (key == "--height") { + 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(); + } + args.height = value.value(); + } else if (key == "--bad-size") { + args.bad_size = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_app_startup_resources(int argc, char** argv) +{ + PlanAppStartupResourcesArgs args; + const auto status = parse_plan_app_startup_resources_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-app-startup-resources", status.message); + return 2; + } + + const auto plan = pp::app::plan_app_startup_resources( + args.bad_size ? 0.0F : args.width, + args.bad_size ? std::nanf("") : args.height); + if (!plan) { + print_error("plan-app-startup-resources", plan.status().message); + return 2; + } + + std::cout << "{\"ok\":true,\"command\":\"plan-app-startup-resources\"" + << ",\"plan\":{\"uiRenderTargetWidth\":" << plan.value().ui_render_target_width + << ",\"uiRenderTargetHeight\":" << plan.value().ui_render_target_height + << ",\"initializeShaders\":" << json_bool(plan.value().initialize_shaders) + << ",\"initializeAssets\":" << json_bool(plan.value().initialize_assets) + << ",\"initializeLayout\":" << json_bool(plan.value().initialize_layout) + << ",\"updateTitle\":" << json_bool(plan.value().update_title) + << ",\"createUiRenderTarget\":" << json_bool(plan.value().create_ui_render_target) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_brush_package_import_args( int argc, char** argv, @@ -9887,6 +9958,10 @@ int main(int argc, char** argv) return plan_app_startup(argc, argv); } + if (command == "plan-app-startup-resources") { + return plan_app_startup_resources(argc, argv); + } + if (command == "plan-brush-package-import") { return plan_brush_package_import(argc, argv); }