diff --git a/CMakeLists.txt b/CMakeLists.txt index ff8ecb8..5aac493 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -242,6 +242,7 @@ add_library(pp_app_core STATIC src/app_core/about_menu.h src/app_core/app_frame.h src/app_core/app_preferences.h + src/app_core/app_shutdown.h src/app_core/app_status.h src/app_core/app_startup.h src/app_core/brush_package_import.h diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index ea01c9c..0fb41c0 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -246,6 +246,11 @@ Known local toolchain state: `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. +- `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, + render-target/mesh destruction, and panel-node release remain in the legacy + app. - `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 @@ -832,6 +837,10 @@ Known local toolchain state: - `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. +- `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, + panel-node release, and quick-mode cleanup. - `pp_platform_api_tests` covers service dispatch for clipboard read/write, empty clipboard writes, cursor visibility, virtual-keyboard visibility, external file display, file sharing, VR lifecycle, layout/asset file load diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index e69d668..3dc98dd 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -20,7 +20,7 @@ agent or engineer to remove them without reconstructing context from chat. - 2026-06-04: DEBT-0009 was narrowed. `platform-build.ps1` and `platform-build.sh` now include the current headless component/test matrix, - including brush-package coverage and the app-core startup/frame/file/document/ + including brush-package coverage and the app-core startup/frame/shutdown/file/document/ brush/canvas/history/grid/toolbar/tools/about/preferences/status automation tests, and `panopainter_platform_build_target_matrix_self_test` now verifies the wrapper defaults against the current CMake test executables. On @@ -97,6 +97,13 @@ agent or engineer to remove them without reconstructing context from chat. consumed by `App::create`, `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. +- 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 + destruction, panel-node release, and quick-mode cleanup now goes through + tested `pp_app_core` plans consumed by `App::terminate` and + `pano_cli plan-app-shutdown`; retained cleanup execution remains in the + legacy app. - 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 diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index f071c80..d4868cd 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -203,6 +203,11 @@ 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. +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 +`pp_app_core`; `App::terminate` and `pano_cli plan-app-shutdown` consume that +plan while retained cleanup execution stays in the legacy app. `panopainter_app` is now a real static target that owns app orchestration sources, app version metadata, and version-header generation. `pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas, @@ -1291,7 +1296,7 @@ standard x64/arm64, Android Quest arm64, Android Focus/Wave arm64, Emscripten/WebGL, macOS, iOS device, and iOS simulator. `platform-build` automation now builds the current headless component matrix, including `pp_platform_api`, `pp_app_core`, platform API tests, brush-package tests, and -the current app-core startup/frame/file/document/brush/canvas/history/grid/toolbar/ +the current app-core startup/frame/shutdown/file/document/brush/canvas/history/grid/toolbar/ tools/about/preferences/status automation tests. The PowerShell wrapper also normalizes comma-separated `-Presets` and `-Targets` values for reliable machine-driven partial matrix checks. `panopainter_platform_build_target_matrix_self_test` @@ -1653,6 +1658,11 @@ Results: - 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`. +- `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`, + `pano_cli_plan_app_shutdown_smoke`, and + `pano_cli_plan_app_shutdown_rejects_unknown_option`. - `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/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 19d5403..1471b36 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_shutdown_tests", "pp_app_core_app_startup_tests", "pp_app_core_app_status_tests", "pp_app_core_brush_package_export_tests", diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index fa040db..8e04d14 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_startup_tests pp_app_core_app_status_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_shutdown_tests pp_app_core_app_startup_tests pp_app_core_app_status_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 b3c4174..e8eb3fe 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -6,6 +6,7 @@ #include "node_progress_bar.h" #include "mp4enc.h" #include "app_core/app_frame.h" +#include "app_core/app_shutdown.h" #include "app_core/app_status.h" #include "app_core/app_startup.h" #include "app_core/canvas_tool_ui.h" @@ -642,27 +643,41 @@ void App::update(float dt) void App::terminate() { LOG("App::terminate"); - ui_save(); + const auto shutdown_plan = pp::app::plan_app_shutdown(); - NodeStrokePreview::terminate_renderer(); - rec_stop(); + if (shutdown_plan.save_ui_state) + ui_save(); + + if (shutdown_plan.terminate_stroke_preview_renderer) + NodeStrokePreview::terminate_renderer(); + if (shutdown_plan.stop_recording) + rec_stop(); - TextureManager::invalidate(); - ShaderManager::invalidate(); - layout.unload(); - layout_designer.unload(); - uirtt.destroy(); - m_face_plane.destroy(); - layers.reset(); - color.reset(); - stroke.reset(); - grid.reset(); - presets.reset(); - floating_presets.reset(); - floating_color.reset(); - floating_layers.reset(); - floating_picker.reset(); - quick_mode_state.clear(); + if (shutdown_plan.invalidate_textures) + TextureManager::invalidate(); + if (shutdown_plan.invalidate_shaders) + ShaderManager::invalidate(); + if (shutdown_plan.unload_layouts) { + layout.unload(); + layout_designer.unload(); + } + if (shutdown_plan.destroy_ui_render_target) + uirtt.destroy(); + if (shutdown_plan.destroy_face_plane) + m_face_plane.destroy(); + if (shutdown_plan.release_panel_nodes) { + layers.reset(); + color.reset(); + stroke.reset(); + grid.reset(); + presets.reset(); + floating_presets.reset(); + floating_color.reset(); + floating_layers.reset(); + floating_picker.reset(); + } + if (shutdown_plan.clear_quick_mode_state) + quick_mode_state.clear(); } void App::update_memory_usage(size_t bytes) diff --git a/src/app_core/app_shutdown.h b/src/app_core/app_shutdown.h new file mode 100644 index 0000000..c85ced5 --- /dev/null +++ b/src/app_core/app_shutdown.h @@ -0,0 +1,23 @@ +#pragma once + +namespace pp::app { + +struct AppShutdownPlan { + bool save_ui_state = true; + bool terminate_stroke_preview_renderer = true; + bool stop_recording = true; + bool invalidate_textures = true; + bool invalidate_shaders = true; + bool unload_layouts = true; + bool destroy_ui_render_target = true; + bool destroy_face_plane = true; + bool release_panel_nodes = true; + bool clear_quick_mode_state = true; +}; + +[[nodiscard]] constexpr AppShutdownPlan plan_app_shutdown() noexcept +{ + return AppShutdownPlan {}; +} + +} // namespace pp::app diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ea5b7e7..fb00013 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_shutdown_tests + app_core/app_shutdown_tests.cpp) +target_link_libraries(pp_app_core_app_shutdown_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_app_shutdown_tests COMMAND pp_app_core_app_shutdown_tests) +set_tests_properties(pp_app_core_app_shutdown_tests PROPERTIES + LABELS "app;desktop-fast") + add_executable(pp_app_core_document_sharing_tests app_core/document_sharing_tests.cpp) target_link_libraries(pp_app_core_document_sharing_tests PRIVATE @@ -969,6 +979,19 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast;fuzz" PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-frame\".*\"updateFrame\":false.*\"drawCanvasStroke\":false.*\"drawVrUi\":false.*\"drawMainUi\":false.*\"resetRedraw\":true") + add_test(NAME pano_cli_plan_app_shutdown_smoke + COMMAND pano_cli plan-app-shutdown) + set_tests_properties(pano_cli_plan_app_shutdown_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-shutdown\".*\"saveUiState\":true.*\"terminateStrokePreviewRenderer\":true.*\"stopRecording\":true.*\"invalidateTextures\":true.*\"invalidateShaders\":true.*\"unloadLayouts\":true.*\"destroyUiRenderTarget\":true.*\"destroyFacePlane\":true.*\"releasePanelNodes\":true.*\"clearQuickModeState\":true") + + add_test(NAME pano_cli_plan_app_shutdown_rejects_unknown_option + COMMAND pano_cli plan-app-shutdown --unexpected) + set_tests_properties(pano_cli_plan_app_shutdown_rejects_unknown_option PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-shutdown\".*\"message\":\"unknown option\"") + 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_shutdown_tests.cpp b/tests/app_core/app_shutdown_tests.cpp new file mode 100644 index 0000000..8f339b3 --- /dev/null +++ b/tests/app_core/app_shutdown_tests.cpp @@ -0,0 +1,29 @@ +#include "app_core/app_shutdown.h" +#include "test_harness.h" + +namespace { + +void shutdown_plan_preserves_legacy_cleanup_sequence(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_shutdown(); + + PP_EXPECT(harness, plan.save_ui_state); + PP_EXPECT(harness, plan.terminate_stroke_preview_renderer); + PP_EXPECT(harness, plan.stop_recording); + PP_EXPECT(harness, plan.invalidate_textures); + PP_EXPECT(harness, plan.invalidate_shaders); + PP_EXPECT(harness, plan.unload_layouts); + PP_EXPECT(harness, plan.destroy_ui_render_target); + PP_EXPECT(harness, plan.destroy_face_plane); + PP_EXPECT(harness, plan.release_panel_nodes); + PP_EXPECT(harness, plan.clear_quick_mode_state); +} + +} // namespace + +int main() +{ + pp::tests::Harness harness; + harness.run("shutdown plan preserves legacy cleanup sequence", shutdown_plan_preserves_legacy_cleanup_sequence); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 2902f37..973ef45 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,6 +1,7 @@ #include "app_core/about_menu.h" #include "app_core/app_preferences.h" #include "app_core/app_frame.h" +#include "app_core/app_shutdown.h" #include "app_core/app_status.h" #include "app_core/app_startup.h" #include "app_core/brush_package_import.h" @@ -2022,6 +2023,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-canvas] [--no-canvas-document] [--vr-active] [--ui-hidden] [--vr-only]\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" << " 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" @@ -3789,6 +3791,30 @@ int plan_app_frame(int argc, char** argv) return 0; } +int plan_app_shutdown(int argc, char** argv) +{ + static_cast(argv); + if (argc > 2) { + print_error("plan-app-shutdown", "unknown option"); + return 2; + } + + const auto plan = pp::app::plan_app_shutdown(); + std::cout << "{\"ok\":true,\"command\":\"plan-app-shutdown\"" + << ",\"plan\":{\"saveUiState\":" << json_bool(plan.save_ui_state) + << ",\"terminateStrokePreviewRenderer\":" << json_bool(plan.terminate_stroke_preview_renderer) + << ",\"stopRecording\":" << json_bool(plan.stop_recording) + << ",\"invalidateTextures\":" << json_bool(plan.invalidate_textures) + << ",\"invalidateShaders\":" << json_bool(plan.invalidate_shaders) + << ",\"unloadLayouts\":" << json_bool(plan.unload_layouts) + << ",\"destroyUiRenderTarget\":" << json_bool(plan.destroy_ui_render_target) + << ",\"destroyFacePlane\":" << json_bool(plan.destroy_face_plane) + << ",\"releasePanelNodes\":" << json_bool(plan.release_panel_nodes) + << ",\"clearQuickModeState\":" << json_bool(plan.clear_quick_mode_state) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_brush_package_import_args( int argc, char** argv, @@ -10040,6 +10066,10 @@ int main(int argc, char** argv) return plan_app_frame(argc, argv); } + if (command == "plan-app-shutdown") { + return plan_app_shutdown(argc, argv); + } + if (command == "plan-brush-package-import") { return plan_brush_package_import(argc, argv); }