Route live save snapshots through PPI policy

This commit is contained in:
2026-06-06 11:43:50 +02:00
parent 772dc7332b
commit 9d9b93abb1
8 changed files with 148 additions and 23 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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<std::byte> 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<DocumentCanvasPpiExportResult>
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<DocumentCanvasPpiExportResult>::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);

View File

@@ -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<int>(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<unsigned long long>(exported.value().bytes.size()),
exported.value().report.captured_face_payloads,
exported.value().report.pending_face_payloads);
return pp::foundation::Status::success();
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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