From 9d9b93abb122c7f8939f451bf098ee69a548c00b Mon Sep 17 00:00:00 2001 From: omigamedev Date: Sat, 6 Jun 2026 11:43:50 +0200 Subject: [PATCH] Route live save snapshots through PPI policy --- docs/modernization/build-inventory.md | 15 ++++++---- docs/modernization/debt.md | 12 ++++++-- docs/modernization/roadmap.md | 23 +++++++++------ src/app_core/document_canvas.h | 36 ++++++++++++++++++++++-- src/legacy_document_session_services.cpp | 26 +++++++++++++++++ tests/CMakeLists.txt | 4 +-- tests/app_core/document_canvas_tests.cpp | 34 ++++++++++++++++++++++ tools/pano_cli/main.cpp | 21 +++++++++++++- 8 files changed, 148 insertions(+), 23 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index cae6703..10cc860 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -283,10 +283,11 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p state toward `pp_document::CanvasDocument`, including dimensions, active layer/frame, layer visibility/opacity/alpha/blend metadata, frame durations, captured RGBA8 face payloads, and remaining renderer payload-readback counts, - plus the save-readiness report now consumed before retained live saves. For - payload-complete or metadata-only snapshots, the same app-core boundary now - exports through the pure `pp_document` PPI writer and reports generated byte - counts plus decoded dirty-face counts in `ppiExport` JSON. Payload-complete + plus the save-readiness and save-writer route reports now consumed before + retained live saves. For payload-complete or metadata-only snapshots, the + same app-core boundary now exports through the pure `pp_document` PPI writer + and reports generated byte counts plus decoded dirty-face counts in + `ppiExport` JSON. Payload-complete snapshots also feed the active frame through the shared `pp_paint_renderer` export-readiness report, reporting texture, transition, command, byte, and active-frame payload counts in `rendererUpload` JSON plus `facePngExport` @@ -1172,8 +1173,10 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.p route through this bridge before reaching legacy project-save execution, overwrite prompts, document field updates, title updates, and keyboard/dialog cleanup. Existing Save, Save As, Save Version, and save-before-workflow - prepare and log a payload-bearing canvas document snapshot report before - delegating to retained `Canvas::project_save`. Retained legacy UI/canvas + prepare and log a payload-bearing canvas document snapshot report, run the + app-core pure PPI save-writer route for payload-complete snapshots, and log + generated byte counts before delegating to retained `Canvas::project_save`. + Retained legacy UI/canvas execution and actual live save serialization remain tracked by `DEBT-0040`, `DEBT-0041`, and `DEBT-0042`; the pure snapshot-to-PPI export handoff is already validated in `pp_app_core_document_canvas_tests` and diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 66eeac5..1092d6e 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -524,8 +524,16 @@ agent or engineer to remove them without reconstructing context from chat. Save, Save As, Save Version, and save-before-workflow now prepare a payload-bearing canvas document snapshot plus `pp_app_core` save-readiness report before delegating to retained `Canvas::project_save`. The retained - writer still owns PPI serialization, progress/threading, and compatibility - quirks, so pure writer replacement remains open. + writer still owned PPI serialization, progress/threading, and compatibility + quirks, so pure writer replacement remained open. +- 2026-06-06: DEBT-0010/DEBT-0013/DEBT-0040/DEBT-0042 were narrowed again. + `pp_app_core` now owns a tested save-writer route policy for canvas snapshots, + `pano_cli plan-canvas-document-snapshot` emits that route as JSON, and live + Save, Save As, Save Version, and save-before-workflow run the pure PPI + exporter for payload-complete snapshots and log byte counts before retained + `Canvas::project_save` continues. The retained writer still owns actual save + serialization, app metadata mutation, progress/threading, and compatibility + quirks. - 2026-06-05: DEBT-0010/DEBT-0013 were narrowed again. `pp_app_core` now exports payload-complete or metadata-only canvas document snapshots through the pure `pp_document` PPI writer and rejects snapshots that still require diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 82d65a1..dab8e2a 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -699,11 +699,13 @@ now builds the same metadata snapshot from live `Canvas` state and has an opt-in dirty-face payload snapshot path backed by retained `Layer::snapshot()` readback. Live Save, Save As, Save Version, and save-before-workflow paths now prepare and log a payload-completeness report from that snapshot before -delegating to retained `Canvas::project_save`; the app-core snapshot boundary -also has a tested pure PPI export helper, and +delegating to retained `Canvas::project_save`; payload-complete live save +snapshots also run the tested pure PPI exporter and log the app-core +save-writer route/byte count before retained save continues. The app-core +snapshot boundary also has a tested pure PPI export helper, and `pano_cli plan-canvas-document-snapshot` runs that helper for payload-complete -snapshots and reports generated byte/dirty-face summaries. The same automation -now feeds payload-complete snapshots through the shared +snapshots and reports generated byte/dirty-face summaries plus the same +save-writer route JSON. The same automation now feeds payload-complete snapshots through the shared `pp_paint_renderer::prepare_document_frame_export_readiness` report, which records renderer-neutral six-face texture upload commands and encodes the active document frame's six composited faces to PNG bytes. This gives CLI @@ -2292,13 +2294,16 @@ Results: automation. - Live Save, Save As, Save Version, and save-before-workflow execution now prepare a payload-bearing canvas document snapshot and save-readiness report - through `src/legacy_document_session_services.*` before delegating to the - retained `Canvas::project_save` writer, keeping behavior stable while moving - the app path onto the document/canvas boundary. + through `src/legacy_document_session_services.*`; payload-complete snapshots + now run the pure PPI exporter and log the app-core save-writer route/byte + count before delegating to the retained `Canvas::project_save` writer, + keeping behavior stable while moving the app path onto the document/canvas + boundary. - `pano_cli plan-canvas-document-snapshot` now emits the same save-readiness report (`payloadComplete` and `canExportPpi`) used by the live save bridge, - and payload-complete snapshots now run the pure `pp_document` PPI exporter - and decoded-project summary before emitting `ppiExport` JSON. + the same save-writer route, and payload-complete snapshots now run the pure + `pp_document` PPI exporter and decoded-project summary before emitting + `ppiExport` JSON. - The same payload-complete snapshot automation now uploads the active document frame through `pp_paint_renderer::upload_document_frame_faces` and the `RecordingRenderDevice`, emitting `rendererUpload` JSON with texture, diff --git a/src/app_core/document_canvas.h b/src/app_core/document_canvas.h index 8557b25..5b0b2a6 100644 --- a/src/app_core/document_canvas.h +++ b/src/app_core/document_canvas.h @@ -78,6 +78,19 @@ struct DocumentCanvasSaveSnapshotReport { bool can_export_ppi = false; }; +enum class DocumentCanvasSaveWriterAction { + use_document_ppi_writer, + use_legacy_project_save, +}; + +struct DocumentCanvasSaveWriterRoutePlan { + DocumentCanvasSaveWriterAction action = DocumentCanvasSaveWriterAction::use_legacy_project_save; + bool payload_complete = false; + bool can_export_ppi = false; + bool uses_document_ppi_writer = false; + std::string_view fallback_reason; +}; + struct DocumentCanvasPpiExportResult { DocumentCanvasSaveSnapshotReport report; std::vector bytes; @@ -248,14 +261,31 @@ public: }; } +[[nodiscard]] constexpr DocumentCanvasSaveWriterRoutePlan plan_document_canvas_save_writer_route( + DocumentCanvasSaveSnapshotReport report) noexcept +{ + DocumentCanvasSaveWriterRoutePlan plan; + plan.payload_complete = report.payload_complete; + plan.can_export_ppi = report.can_export_ppi; + + if (!report.payload_complete || !report.can_export_ppi) { + plan.fallback_reason = "canvas document snapshot still requires renderer payload readback"; + return plan; + } + + plan.action = DocumentCanvasSaveWriterAction::use_document_ppi_writer; + plan.uses_document_ppi_writer = true; + return plan; +} + [[nodiscard]] inline pp::foundation::Result export_document_canvas_save_snapshot_to_ppi(const DocumentCanvasSnapshotResult& snapshot) { const auto report = make_document_canvas_save_snapshot_report(snapshot); - if (!report.can_export_ppi) { + const auto route = plan_document_canvas_save_writer_route(report); + if (!route.uses_document_ppi_writer) { return pp::foundation::Result::failure( - pp::foundation::Status::invalid_argument( - "canvas document snapshot still requires renderer payload readback")); + pp::foundation::Status::invalid_argument(route.fallback_reason.data())); } auto bytes = pp::document::export_ppi_project_document(snapshot.document); diff --git a/src/legacy_document_session_services.cpp b/src/legacy_document_session_services.cpp index 8589f73..ce355f1 100644 --- a/src/legacy_document_session_services.cpp +++ b/src/legacy_document_session_services.cpp @@ -40,6 +40,32 @@ pp::foundation::Status prepare_legacy_document_save_snapshot(App& app, const cha const auto report = pp::app::make_document_canvas_save_snapshot_report(snapshot.value()); log_legacy_document_save_snapshot(context, report); + const auto route = pp::app::plan_document_canvas_save_writer_route(report); + if (!route.uses_document_ppi_writer) { + LOG( + "%s document save writer retained legacy save: %.*s", + context, + static_cast(route.fallback_reason.size()), + route.fallback_reason.data()); + return pp::foundation::Status::success(); + } + + const auto exported = pp::app::export_document_canvas_save_snapshot_to_ppi(snapshot.value()); + if (!exported) { + LOG( + "%s document save writer retained legacy save after PPI export failure: %s", + context, + exported.status().message); + return exported.status(); + } + + LOG( + "%s document save writer route: document-ppi payloadComplete=%s bytes=%llu capturedFaces=%zu pendingFaces=%zu", + context, + exported.value().report.payload_complete ? "true" : "false", + static_cast(exported.value().bytes.size()), + exported.value().report.captured_face_payloads, + exported.value().report.pending_face_payloads); return pp::foundation::Status::success(); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6dcc2a7..1055e73 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1530,13 +1530,13 @@ if(TARGET pano_cli) COMMAND pano_cli plan-canvas-document-snapshot --width 128 --height 64 --layers 3 --frames 2 --current-layer 2 --current-frame 1 --hidden-layer 0 --alpha-locked-layer 2 --opacity 0.5 --blend-mode 4 --pending-face-payloads-per-layer 6) set_tests_properties(pano_cli_plan_canvas_document_snapshot_smoke PROPERTIES LABELS "app;document;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-document-snapshot\".*\"width\":128.*\"height\":64.*\"layers\":3.*\"frames\":2.*\"activeLayer\":2.*\"activeFrame\":1.*\"activeLayerName\":\"Layer 3\".*\"activeLayerOpacity\":0.5.*\"activeLayerBlend\":\"overlay\".*\"activeLayerAlphaLocked\":true.*\"pendingFacePayloads\":18.*\"metadataOnly\":true.*\"requiresRendererPayloadReadback\":true.*\"documentFacePayloads\":0.*\"saveReport\":\\{\"payloadComplete\":false,\"canExportPpi\":false\\}.*\"ppiExport\":\\{\"ready\":false,\"bytes\":0,\"dirtyFaces\":0\\}.*\"rendererUpload\":\\{\"ready\":false,\"textures\":0,\"bytes\":0,\"transitions\":0,\"facePayloads\":0,\"compositedLayerFaces\":0,\"commands\":0,\"uploadCommands\":0,\"transitionCommands\":0\\}.*\"facePngExport\":\\{\"ready\":false,\"faces\":0,\"bytes\":0,\"facePayloads\":0\\}.*\"depthExport\":\\{\"ready\":false,\"width\":0,\"height\":0,\"mergedFaceDraws\":0,\"layerDepthDraws\":0,\"visitedLayers\":0,\"visibleLayers\":0,\"facePayloads\":0,\"requiresRendererReadback\":true\\}") + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-document-snapshot\".*\"width\":128.*\"height\":64.*\"layers\":3.*\"frames\":2.*\"activeLayer\":2.*\"activeFrame\":1.*\"activeLayerName\":\"Layer 3\".*\"activeLayerOpacity\":0.5.*\"activeLayerBlend\":\"overlay\".*\"activeLayerAlphaLocked\":true.*\"pendingFacePayloads\":18.*\"metadataOnly\":true.*\"requiresRendererPayloadReadback\":true.*\"documentFacePayloads\":0.*\"saveReport\":\\{\"payloadComplete\":false,\"canExportPpi\":false\\}.*\"saveWriterRoute\":\\{\"action\":\"use-legacy-project-save\",\"usesDocumentPpiWriter\":false,\"fallbackReason\":\"canvas document snapshot still requires renderer payload readback\"\\}.*\"ppiExport\":\\{\"ready\":false,\"bytes\":0,\"dirtyFaces\":0\\}.*\"rendererUpload\":\\{\"ready\":false,\"textures\":0,\"bytes\":0,\"transitions\":0,\"facePayloads\":0,\"compositedLayerFaces\":0,\"commands\":0,\"uploadCommands\":0,\"transitionCommands\":0\\}.*\"facePngExport\":\\{\"ready\":false,\"faces\":0,\"bytes\":0,\"facePayloads\":0\\}.*\"depthExport\":\\{\"ready\":false,\"width\":0,\"height\":0,\"mergedFaceDraws\":0,\"layerDepthDraws\":0,\"visitedLayers\":0,\"visibleLayers\":0,\"facePayloads\":0,\"requiresRendererReadback\":true\\}") add_test(NAME pano_cli_plan_canvas_document_snapshot_payload_smoke COMMAND pano_cli plan-canvas-document-snapshot --width 128 --height 64 --layers 2 --frames 2 --current-layer 1 --current-frame 1 --pending-face-payloads-per-layer 2 --captured-face-payloads-per-layer 2) set_tests_properties(pano_cli_plan_canvas_document_snapshot_payload_smoke PROPERTIES LABELS "app;document;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-document-snapshot\".*\"layers\":2.*\"frames\":2.*\"activeLayer\":1.*\"activeFrame\":1.*\"pendingFacePayloads\":4.*\"capturedFacePayloads\":4.*\"metadataOnly\":false.*\"requiresRendererPayloadReadback\":false.*\"documentFacePayloads\":4.*\"saveReport\":\\{\"payloadComplete\":true,\"canExportPpi\":true\\}.*\"ppiExport\":\\{\"ready\":true,\"bytes\":[1-9][0-9]*,\"dirtyFaces\":4\\}.*\"rendererUpload\":\\{\"ready\":true,\"textures\":6,\"bytes\":[1-9][0-9]*,\"transitions\":6,\"facePayloads\":2,\"compositedLayerFaces\":2,\"commands\":12,\"uploadCommands\":6,\"transitionCommands\":6\\}.*\"facePngExport\":\\{\"ready\":true,\"faces\":6,\"bytes\":[1-9][0-9]*,\"facePayloads\":2\\}.*\"depthExport\":\\{\"ready\":true,\"width\":1024,\"height\":1024,\"mergedFaceDraws\":6,\"layerDepthDraws\":2,\"visitedLayers\":2,\"visibleLayers\":1,\"facePayloads\":2,\"requiresRendererReadback\":true\\}") + PASS_REGULAR_EXPRESSION "\"command\":\"plan-canvas-document-snapshot\".*\"layers\":2.*\"frames\":2.*\"activeLayer\":1.*\"activeFrame\":1.*\"pendingFacePayloads\":4.*\"capturedFacePayloads\":4.*\"metadataOnly\":false.*\"requiresRendererPayloadReadback\":false.*\"documentFacePayloads\":4.*\"saveReport\":\\{\"payloadComplete\":true,\"canExportPpi\":true\\}.*\"saveWriterRoute\":\\{\"action\":\"use-document-ppi-writer\",\"usesDocumentPpiWriter\":true,\"fallbackReason\":\"\"\\}.*\"ppiExport\":\\{\"ready\":true,\"bytes\":[1-9][0-9]*,\"dirtyFaces\":4\\}.*\"rendererUpload\":\\{\"ready\":true,\"textures\":6,\"bytes\":[1-9][0-9]*,\"transitions\":6,\"facePayloads\":2,\"compositedLayerFaces\":2,\"commands\":12,\"uploadCommands\":6,\"transitionCommands\":6\\}.*\"facePngExport\":\\{\"ready\":true,\"faces\":6,\"bytes\":[1-9][0-9]*,\"facePayloads\":2\\}.*\"depthExport\":\\{\"ready\":true,\"width\":1024,\"height\":1024,\"mergedFaceDraws\":6,\"layerDepthDraws\":2,\"visitedLayers\":2,\"visibleLayers\":1,\"facePayloads\":2,\"requiresRendererReadback\":true\\}") add_test(NAME pano_cli_plan_canvas_document_snapshot_no_canvas COMMAND pano_cli plan-canvas-document-snapshot --no-canvas) diff --git a/tests/app_core/document_canvas_tests.cpp b/tests/app_core/document_canvas_tests.cpp index bc6f73a..243cfb3 100644 --- a/tests/app_core/document_canvas_tests.cpp +++ b/tests/app_core/document_canvas_tests.cpp @@ -208,6 +208,38 @@ void snapshot_plan_attaches_captured_face_payloads(pp::tests::Harness& harness) } } +void save_writer_route_uses_ppi_writer_for_complete_payloads(pp::tests::Harness& harness) +{ + pp::app::DocumentCanvasSaveSnapshotReport report; + report.payload_complete = true; + report.can_export_ppi = true; + + const auto plan = pp::app::plan_document_canvas_save_writer_route(report); + + PP_EXPECT(harness, plan.action == pp::app::DocumentCanvasSaveWriterAction::use_document_ppi_writer); + PP_EXPECT(harness, plan.uses_document_ppi_writer); + PP_EXPECT(harness, plan.payload_complete); + PP_EXPECT(harness, plan.can_export_ppi); + PP_EXPECT(harness, plan.fallback_reason.empty()); +} + +void save_writer_route_falls_back_for_pending_payloads(pp::tests::Harness& harness) +{ + pp::app::DocumentCanvasSaveSnapshotReport report; + report.payload_complete = false; + report.can_export_ppi = false; + + const auto plan = pp::app::plan_document_canvas_save_writer_route(report); + + PP_EXPECT(harness, plan.action == pp::app::DocumentCanvasSaveWriterAction::use_legacy_project_save); + PP_EXPECT(harness, !plan.uses_document_ppi_writer); + PP_EXPECT(harness, !plan.payload_complete); + PP_EXPECT(harness, !plan.can_export_ppi); + PP_EXPECT( + harness, + plan.fallback_reason == "canvas document snapshot still requires renderer payload readback"); +} + void snapshot_plan_rejects_invalid_canvas_state(pp::tests::Harness& harness) { const std::uint32_t frames[] { 100U }; @@ -387,6 +419,8 @@ int main() harness.run("snapshot plan projects canvas metadata", snapshot_plan_projects_canvas_metadata); harness.run("snapshot plan defaults empty names and frames", snapshot_plan_defaults_empty_names_and_frames); harness.run("snapshot plan attaches captured face payloads", snapshot_plan_attaches_captured_face_payloads); + harness.run("save writer route uses ppi writer for complete payloads", save_writer_route_uses_ppi_writer_for_complete_payloads); + harness.run("save writer route falls back for pending payloads", save_writer_route_falls_back_for_pending_payloads); harness.run("snapshot plan rejects invalid canvas state", snapshot_plan_rejects_invalid_canvas_state); harness.run("clear plan records legacy canvas effects", clear_plan_records_legacy_canvas_effects); harness.run("clear plan noops without canvas", clear_plan_noops_without_canvas); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index a950e53..0475cff 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -949,6 +949,18 @@ const char* document_save_decision_name(pp::app::DocumentSaveDecision decision) return "no-op"; } +const char* document_canvas_save_writer_action_name(pp::app::DocumentCanvasSaveWriterAction action) noexcept +{ + switch (action) { + case pp::app::DocumentCanvasSaveWriterAction::use_document_ppi_writer: + return "use-document-ppi-writer"; + case pp::app::DocumentCanvasSaveWriterAction::use_legacy_project_save: + return "use-legacy-project-save"; + } + + return "use-legacy-project-save"; +} + const char* document_workflow_decision_name(pp::app::DocumentWorkflowDecision decision) noexcept { switch (decision) { @@ -6180,6 +6192,7 @@ int plan_canvas_document_snapshot(int argc, char** argv) const auto& document = value.document; const auto& active_layer = document.layers()[document.active_layer_index()]; const auto save_report = pp::app::make_document_canvas_save_snapshot_report(value); + const auto save_writer_route = pp::app::plan_document_canvas_save_writer_route(save_report); bool ppi_export_ready = false; std::size_t ppi_export_bytes = 0; std::uint32_t ppi_export_dirty_faces = 0; @@ -6205,7 +6218,7 @@ int plan_canvas_document_snapshot(int argc, char** argv) std::size_t depth_export_visible_layers = 0; std::size_t depth_export_face_payloads = 0; bool depth_export_requires_renderer_readback = value.requires_renderer_payload_readback; - if (save_report.can_export_ppi) { + if (save_writer_route.uses_document_ppi_writer) { const auto exported = pp::app::export_document_canvas_save_snapshot_to_ppi(value); if (!exported) { print_error("plan-canvas-document-snapshot", exported.status().message); @@ -6296,6 +6309,12 @@ int plan_canvas_document_snapshot(int argc, char** argv) << ",\"documentFacePayloads\":" << document.face_pixel_payload_count() << ",\"saveReport\":{\"payloadComplete\":" << json_bool(save_report.payload_complete) << ",\"canExportPpi\":" << json_bool(save_report.can_export_ppi) + << "},\"saveWriterRoute\":{\"action\":\"" + << document_canvas_save_writer_action_name(save_writer_route.action) + << "\",\"usesDocumentPpiWriter\":" << json_bool(save_writer_route.uses_document_ppi_writer) + << ",\"fallbackReason\":\"" + << json_escape(std::string(save_writer_route.fallback_reason)) + << "\"" << "},\"ppiExport\":{\"ready\":" << json_bool(ppi_export_ready) << ",\"bytes\":" << ppi_export_bytes << ",\"dirtyFaces\":" << ppi_export_dirty_faces