From 548b6d3ae594b454958e972d89c1ee87234c7147 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 5 Jun 2026 06:23:00 +0200 Subject: [PATCH] Extend app frame planning to tick and resize --- docs/modernization/build-inventory.md | 13 +++--- docs/modernization/debt.md | 9 +++-- docs/modernization/roadmap.md | 16 +++++--- src/app_core/app_frame.h | 57 +++++++++++++++++++++++++++ src/app_events.cpp | 29 +++++++++++--- tests/CMakeLists.txt | 17 +++++++- tests/app_core/app_frame_tests.cpp | 41 +++++++++++++++++++ tools/pano_cli/main.cpp | 47 +++++++++++++++++++++- 8 files changed, 205 insertions(+), 24 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 0fb41c0..d1a5d9f 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -242,10 +242,11 @@ Known local toolchain state: UI render-target creation, recording startup, VR-controller state mutation, settings writes, and license-warning dialogs remain legacy execution. - `src/app_core/app_frame.h` owns the current initial surface, update-gating, - and draw-pass decisions consumed by `App::create`, `App::update`, - `App::draw`, and `pano_cli plan-app-frame`; retained layout traversal, - toolbar widget writes, canvas stroke drawing, VR UI render-target drawing, - main target binding, and OpenGL/UI drawing remain in the legacy app. + tick, resize, and draw-pass decisions consumed by `App::create`, + `App::tick`, `App::resize`, `App::update`, `App::draw`, and + `pano_cli plan-app-frame`; retained layout traversal, toolbar widget writes, + UI render-target recreation, canvas stroke drawing, VR UI render-target + drawing, main target binding, and OpenGL/UI drawing remain in the legacy app. - `src/app_core/app_shutdown.h` owns the current shutdown cleanup staging consumed by `App::terminate` and `pano_cli plan-app-shutdown`; retained UI-state save, recording stop, resource invalidation, layout unload, @@ -836,7 +837,9 @@ Known local toolchain state: split persistence/runtime dispatch, and malformed startup-plan rejection. - `pp_app_core_app_frame_tests` covers the legacy initial surface default, idle/redraw/animation update gating, canvas-stroke draw eligibility, VR UI - visibility, main UI suppression in VR-only mode, and redraw reset planning. + 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_shutdown_tests` covers legacy shutdown cleanup staging for UI-state save, stroke-preview renderer shutdown, recording stop, texture/shader invalidation, layout unload, render-target/mesh destruction, diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 3dc98dd..02ea335 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -92,11 +92,12 @@ agent or engineer to remove them without reconstructing context from chat. and runtime side effects through the startup bridge. `pano_cli plan-app-startup-resources` exposes the resource path for automation. - 2026-06-05: DEBT-0003 was narrowed. Initial surface sizing, redraw/animation - update gating, canvas-stroke draw eligibility, VR UI pass selection, main UI - pass selection, and redraw reset are now tested `pp_app_core` frame plans - consumed by `App::create`, `App::update`, `App::draw`, and + update gating, layout tick selection, resize render-target recreation, + canvas-stroke draw eligibility, VR UI pass selection, main UI pass selection, + and redraw reset are now tested `pp_app_core` frame plans consumed by + `App::create`, `App::tick`, `App::resize`, `App::update`, `App::draw`, and `pano_cli plan-app-frame`; retained layout traversal, toolbar widget writes, - and OpenGL/UI drawing remain in the legacy app. + render-target recreation, and OpenGL/UI drawing remain in the legacy app. - 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 diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index d4868cd..a12b930 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -199,10 +199,11 @@ and floating-point render targets; `App::title_update`, `App::initLayout`, and `pano_cli plan-app-status` consume those contracts while legacy UI nodes still render the strings and status lights. Frame-level app decisions for the initial surface size, redraw/animation update -gating, canvas-stroke drawing, VR UI drawing, main UI drawing, and redraw reset -now live in `pp_app_core`; `App::create`, `App::update`, `App::draw`, and -`pano_cli plan-app-frame` consume those plans while retained layout traversal -and OpenGL/UI drawing stay in the legacy app. +gating, layout ticking, resize render-target recreation, canvas-stroke drawing, +VR UI drawing, main UI drawing, and redraw reset now live in `pp_app_core`; +`App::create`, `App::tick`, `App::resize`, `App::update`, `App::draw`, and +`pano_cli plan-app-frame` consume those plans while retained layout traversal, +render-target recreation, and OpenGL/UI drawing stay in the legacy app. 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 @@ -1654,10 +1655,13 @@ Results: 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_app_frame_tests`, and `pano_cli` built after - app frame surface/update/draw-pass decisions moved into `pp_app_core`. + app frame surface/update/tick/resize/draw-pass decisions moved into + `pp_app_core`. - Focused frame CTest coverage passed for `pp_app_core_app_frame_tests`, `pano_cli_plan_app_frame_vr_smoke`, and - `pano_cli_plan_app_frame_idle_missing_canvas_smoke`. + `pano_cli_plan_app_frame_idle_missing_canvas_smoke`, with resize automation + covered by `pano_cli_plan_app_frame_resize_smoke` and + `pano_cli_plan_app_frame_rejects_bad_resize`. - `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/src/app_core/app_frame.h b/src/app_core/app_frame.h index b93cbe4..8df88bc 100644 --- a/src/app_core/app_frame.h +++ b/src/app_core/app_frame.h @@ -1,5 +1,10 @@ #pragma once +#include "foundation/result.h" + +#include +#include + namespace pp::app { struct AppInitialSurfacePlan { @@ -20,6 +25,20 @@ struct AppFrameDrawPlan { bool reset_redraw = true; }; +struct AppFrameTickPlan { + bool tick_designer_layout = false; + bool tick_main_layout = false; +}; + +struct AppResizePlan { + float width = 0.0F; + float height = 0.0F; + int render_target_width = 0; + int render_target_height = 0; + bool recreate_ui_render_target = true; + bool request_redraw = true; +}; + [[nodiscard]] constexpr AppInitialSurfacePlan plan_app_initial_surface() noexcept { return AppInitialSurfacePlan { @@ -53,4 +72,42 @@ struct AppFrameDrawPlan { }; } +[[nodiscard]] constexpr AppFrameTickPlan plan_app_frame_tick( + bool has_designer_layout, + bool has_main_layout) noexcept +{ + return AppFrameTickPlan { + .tick_designer_layout = has_designer_layout, + .tick_main_layout = has_main_layout, + }; +} + +[[nodiscard]] inline pp::foundation::Result plan_app_resize(float width, float height) +{ + if (!std::isfinite(width) || !std::isfinite(height)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("resize dimensions must be finite")); + } + + if (width < 1.0F || height < 1.0F) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("resize dimensions must be positive")); + } + + if (width > static_cast(std::numeric_limits::max()) + || height > static_cast(std::numeric_limits::max())) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("resize dimensions exceed integer range")); + } + + return pp::foundation::Result::success(AppResizePlan { + .width = width, + .height = height, + .render_target_width = static_cast(width), + .render_target_height = static_cast(height), + .recreate_ui_render_target = true, + .request_redraw = true, + }); +} + } // namespace pp::app diff --git a/src/app_events.cpp b/src/app_events.cpp index 88fc49c..55bbd22 100644 --- a/src/app_events.cpp +++ b/src/app_events.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "app.h" +#include "app_core/app_frame.h" #include "app_core/document_platform_io.h" #include "app_core/document_sharing.h" #include "platform_api/platform_services.h" @@ -89,19 +90,35 @@ void App::crash_test() void App::tick(float dt) { - if (auto* main = layout_designer[main_id]) + const auto tick_plan = pp::app::plan_app_frame_tick( + layout_designer.get(main_id) != nullptr, + layout.get(main_id) != nullptr); + if (auto* main = layout_designer[main_id]; tick_plan.tick_designer_layout && main) main->tick(dt); - if (auto* main = layout[main_id]) + if (auto* main = layout[main_id]; tick_plan.tick_main_layout && main) main->tick(dt); } void App::resize(float w, float h) { LOG("App::resize %d %d", (int)w, (int)h); - uirtt.create(static_cast(w), static_cast(h), -1, rgba8_internal_format(), true); - redraw = true; - width = w; - height = h; + const auto resize_plan = pp::app::plan_app_resize(w, h); + if (!resize_plan) { + LOG("App resize plan failed: %s", resize_plan.status().message); + return; + } + + if (resize_plan.value().recreate_ui_render_target) { + uirtt.create( + resize_plan.value().render_target_width, + resize_plan.value().render_target_height, + -1, + rgba8_internal_format(), + true); + } + redraw = resize_plan.value().request_redraw; + width = resize_plan.value().width; + height = resize_plan.value().height; } void App::show_cursor() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fb00013..82a57f6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -974,10 +974,23 @@ if(TARGET pano_cli) PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-frame\".*\"surface\":\\{\"width\":960,\"height\":540\\}.*\"updateFrame\":true.*\"updateLayouts\":true.*\"refreshCanvasToolbar\":true.*\"drawCanvasStroke\":true.*\"drawVrUi\":true.*\"drawMainUi\":true.*\"resetRedraw\":true") add_test(NAME pano_cli_plan_app_frame_idle_missing_canvas_smoke - COMMAND pano_cli plan-app-frame --no-canvas --ui-hidden --vr-only) + COMMAND pano_cli plan-app-frame --no-canvas --ui-hidden --vr-only --no-designer-layout) set_tests_properties(pano_cli_plan_app_frame_idle_missing_canvas_smoke PROPERTIES LABELS "app;integration;desktop-fast;fuzz" - PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-frame\".*\"updateFrame\":false.*\"drawCanvasStroke\":false.*\"drawVrUi\":false.*\"drawMainUi\":false.*\"resetRedraw\":true") + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-frame\".*\"updateFrame\":false.*\"tickDesignerLayout\":false.*\"tickMainLayout\":true.*\"drawCanvasStroke\":false.*\"drawVrUi\":false.*\"drawMainUi\":false.*\"resetRedraw\":true") + + add_test(NAME pano_cli_plan_app_frame_resize_smoke + COMMAND pano_cli plan-app-frame --resize-width 1024.9 --resize-height 512.1) + set_tests_properties(pano_cli_plan_app_frame_resize_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-frame\".*\"resize\":\\{\"width\":1024.9,\"height\":512.1,\"renderTargetWidth\":1024,\"renderTargetHeight\":512,\"recreateUiRenderTarget\":true,\"requestRedraw\":true\\}") + + add_test(NAME pano_cli_plan_app_frame_rejects_bad_resize + COMMAND pano_cli plan-app-frame --bad-resize) + set_tests_properties(pano_cli_plan_app_frame_rejects_bad_resize PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-frame\".*\"message\":\"resize dimensions") add_test(NAME pano_cli_plan_app_shutdown_smoke COMMAND pano_cli plan-app-shutdown) diff --git a/tests/app_core/app_frame_tests.cpp b/tests/app_core/app_frame_tests.cpp index bf71599..846b72e 100644 --- a/tests/app_core/app_frame_tests.cpp +++ b/tests/app_core/app_frame_tests.cpp @@ -1,6 +1,8 @@ #include "app_core/app_frame.h" #include "test_harness.h" +#include + namespace { void initial_surface_matches_legacy_default(pp::tests::Harness& harness) @@ -63,6 +65,42 @@ void draw_plan_respects_vr_visibility_modes(pp::tests::Harness& harness) PP_EXPECT(harness, !vr_only.draw_main_ui); } +void tick_plan_selects_available_layouts(pp::tests::Harness& harness) +{ + const auto both = pp::app::plan_app_frame_tick(true, true); + const auto main_only = pp::app::plan_app_frame_tick(false, true); + const auto none = pp::app::plan_app_frame_tick(false, false); + + PP_EXPECT(harness, both.tick_designer_layout); + PP_EXPECT(harness, both.tick_main_layout); + PP_EXPECT(harness, !main_only.tick_designer_layout); + PP_EXPECT(harness, main_only.tick_main_layout); + PP_EXPECT(harness, !none.tick_designer_layout); + PP_EXPECT(harness, !none.tick_main_layout); +} + +void resize_plan_projects_render_target_and_redraw(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_resize(1280.9F, 720.1F); + + PP_EXPECT(harness, plan); + if (plan) { + PP_EXPECT(harness, plan.value().width == 1280.9F); + PP_EXPECT(harness, plan.value().height == 720.1F); + PP_EXPECT(harness, plan.value().render_target_width == 1280); + PP_EXPECT(harness, plan.value().render_target_height == 720); + PP_EXPECT(harness, plan.value().recreate_ui_render_target); + PP_EXPECT(harness, plan.value().request_redraw); + } +} + +void resize_plan_rejects_invalid_dimensions(pp::tests::Harness& harness) +{ + PP_EXPECT(harness, !pp::app::plan_app_resize(0.0F, 720.0F)); + PP_EXPECT(harness, !pp::app::plan_app_resize(1280.0F, -1.0F)); + PP_EXPECT(harness, !pp::app::plan_app_resize(std::nanf(""), 720.0F)); +} + } // namespace int main() @@ -74,5 +112,8 @@ int main() harness.run("draw plan selects canvas and UI passes", draw_plan_selects_canvas_and_ui_passes); harness.run("draw plan skips missing canvas document", draw_plan_skips_missing_canvas_document); harness.run("draw plan respects VR visibility modes", draw_plan_respects_vr_visibility_modes); + harness.run("tick plan selects available layouts", tick_plan_selects_available_layouts); + harness.run("resize plan projects render target and redraw", resize_plan_projects_render_target_and_redraw); + harness.run("resize plan rejects invalid dimensions", resize_plan_rejects_invalid_dimensions); return harness.finish(); } diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 973ef45..396af60 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -250,11 +250,16 @@ struct PlanAppStartupResourcesArgs { struct PlanAppFrameArgs { bool redraw = false; bool animate = false; + bool has_designer_layout = true; + bool has_main_layout = true; bool has_canvas_node = true; bool has_canvas_document = true; bool vr_active = false; bool ui_visible = true; bool vr_only = false; + float resize_width = 1280.0F; + float resize_height = 720.0F; + bool bad_resize = false; }; struct PlanBrushPackageExportArgs { @@ -2022,7 +2027,7 @@ void print_help() << " 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-frame [--redraw] [--animate] [--no-canvas] [--no-canvas-document] [--vr-active] [--ui-hidden] [--vr-only]\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-shutdown\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" @@ -3740,6 +3745,10 @@ pp::foundation::Status parse_plan_app_frame_args( args.redraw = true; } else if (key == "--animate") { args.animate = true; + } else if (key == "--no-designer-layout") { + args.has_designer_layout = false; + } else if (key == "--no-main-layout") { + args.has_main_layout = false; } else if (key == "--no-canvas") { args.has_canvas_node = false; args.has_canvas_document = false; @@ -3751,6 +3760,26 @@ pp::foundation::Status parse_plan_app_frame_args( args.ui_visible = false; } else if (key == "--vr-only") { args.vr_only = true; + } else if (key == "--resize-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.resize_width = value.value(); + } else if (key == "--resize-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.resize_height = value.value(); + } else if (key == "--bad-resize") { + args.bad_resize = true; } else { return pp::foundation::Status::invalid_argument("unknown option"); } @@ -3770,12 +3799,20 @@ int plan_app_frame(int argc, char** argv) const auto surface = pp::app::plan_app_initial_surface(); const auto update = pp::app::plan_app_frame_update(args.redraw, args.animate); + const auto tick = pp::app::plan_app_frame_tick(args.has_designer_layout, args.has_main_layout); const auto draw = pp::app::plan_app_frame_draw( args.has_canvas_node, args.has_canvas_document, args.vr_active, args.ui_visible, args.vr_only); + const auto resize = pp::app::plan_app_resize( + args.bad_resize ? 0.0F : args.resize_width, + args.bad_resize ? std::nanf("") : args.resize_height); + if (!resize) { + print_error("plan-app-frame", resize.status().message); + return 2; + } std::cout << "{\"ok\":true,\"command\":\"plan-app-frame\"" << ",\"surface\":{\"width\":" << surface.width @@ -3783,10 +3820,18 @@ int plan_app_frame(int argc, char** argv) << "},\"update\":{\"updateFrame\":" << json_bool(update.update_frame) << ",\"updateLayouts\":" << json_bool(update.update_layouts) << ",\"refreshCanvasToolbar\":" << json_bool(update.refresh_canvas_toolbar) + << "},\"tick\":{\"tickDesignerLayout\":" << json_bool(tick.tick_designer_layout) + << ",\"tickMainLayout\":" << json_bool(tick.tick_main_layout) << "},\"draw\":{\"drawCanvasStroke\":" << json_bool(draw.draw_canvas_stroke) << ",\"drawVrUi\":" << json_bool(draw.draw_vr_ui) << ",\"drawMainUi\":" << json_bool(draw.draw_main_ui) << ",\"resetRedraw\":" << json_bool(draw.reset_redraw) + << "},\"resize\":{\"width\":" << resize.value().width + << ",\"height\":" << resize.value().height + << ",\"renderTargetWidth\":" << resize.value().render_target_width + << ",\"renderTargetHeight\":" << resize.value().render_target_height + << ",\"recreateUiRenderTarget\":" << json_bool(resize.value().recreate_ui_render_target) + << ",\"requestRedraw\":" << json_bool(resize.value().request_redraw) << "}}\n"; return 0; }