From b889f264438758d5071b1c4b80621fb9f0463af8 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Sat, 13 Jun 2026 04:51:16 +0200 Subject: [PATCH] Plan stroke commit sequencing --- docs/modernization/debt.md | 8 ++ docs/modernization/roadmap.md | 6 + docs/modernization/tasks.md | 7 ++ src/legacy_canvas_stroke_commit_services.h | 129 +++++++++++++++++++++ src/paint_renderer/compositor.cpp | 49 ++++++++ src/paint_renderer/compositor.h | 52 +++++++++ tests/paint_renderer/compositor_tests.cpp | 86 ++++++++++++++ 7 files changed, 337 insertions(+) create mode 100644 src/legacy_canvas_stroke_commit_services.h diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 6580f88..ca5de8f 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,6 +18,14 @@ agent or engineer to remove them without reconstructing context from chat. ## Recent Reductions +- 2026-06-13: DEBT-0036 was narrowed again. `pp_paint_renderer` now owns a + tested `CanvasStrokeCommitSequencePlan` for `Canvas::stroke_commit` + readback, dirty-state, scratch-copy, erase/composite draw, committed-copy, + dilate order, and commit texture slot roles. A retained + `legacy_canvas_stroke_commit_services.h` adapter skeleton consumes the + semantic plan through callbacks, but the live Canvas commit body still owns + history/layer mutation, RTT/framebuffer binding, sampler binding, and final + OpenGL execution until the adapter is wired. - 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix` now reuses `legacy_canvas_stroke_composite_services.h` for mixer-pass `kShader::CompDraw` binding and composite/pattern/dual uniform writes. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 32d4563..0466b7f 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -2974,6 +2974,12 @@ Results: `pp_paint_renderer` stroke-feedback plan to decide whether framebuffer fetch supplies destination color or the legacy OpenGL path must copy the target texture before drawing. +- Canvas stroke commit ordering now has a tested `pp_paint_renderer` + `CanvasStrokeCommitSequencePlan` for history readback, dirty-state update, + scratch copies, erase/composite draw selection, dilate, and texture slot + roles. A retained commit adapter skeleton consumes that semantic sequence, + while the live Canvas body still owns history/layer mutation and OpenGL + execution until the next wiring slice. - Canvas thumbnail layer blending now uses the same canvas destination-feedback plan for framebuffer-fetch versus texture-copy decisions; the thumbnail draw itself still executes through retained OpenGL canvas code under DEBT-0036. diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 911171a..10d118a 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -507,6 +507,13 @@ Done Checks: - Retained stroke OpenGL execution is deleted or isolated as an OpenGL backend implementation. +Progress Notes: + +- 2026-06-13: `pp_paint_renderer` owns tested Canvas stroke commit sequencing + and commit texture slot intent. Next slice should wire + `legacy_canvas_stroke_commit_services.h` into `Canvas::stroke_commit` while + keeping history/layer mutation local. + ### LATER-004 - Remove Catch-All Platform Legacy Adapter Status: Blocked diff --git a/src/legacy_canvas_stroke_commit_services.h b/src/legacy_canvas_stroke_commit_services.h new file mode 100644 index 0000000..405b86c --- /dev/null +++ b/src/legacy_canvas_stroke_commit_services.h @@ -0,0 +1,129 @@ +#pragma once + +#include "paint_renderer/compositor.h" + +#include +#include +#include + +namespace pp::panopainter { + +struct LegacyCanvasStrokeCommitFace { + int index = 0; + bool dirty = false; +}; + +struct LegacyCanvasStrokeCommitCallbacks { + std::function mark_commit_started; + std::function capture_render_state; + std::function prepare_render_state; + std::function restore_render_state; + std::function publish_history; + std::function capture_timelapse_frame; + + std::function bind_layer_framebuffer; + std::function capture_history_region; + std::function apply_layer_dirty_region; + std::function copy_layer_to_commit_destination; + std::function bind_commit_inputs; + std::function execute_erase_composite; + std::function execute_paint_composite; + std::function copy_committed_to_dilate_source; + std::function execute_commit_dilate; + std::function unbind_layer_framebuffer; +}; + +struct LegacyCanvasStrokeCommitRequest { + std::string_view context; + std::array faces {}; + pp::paint_renderer::CanvasStrokeCommitSequencePlan sequence; + LegacyCanvasStrokeCommitCallbacks callbacks; +}; + +struct LegacyCanvasStrokeCommitResult { + bool ok = false; + int committed_faces = 0; +}; + +[[nodiscard]] inline bool legacy_canvas_stroke_commit_callbacks_ready( + const LegacyCanvasStrokeCommitCallbacks& callbacks) noexcept +{ + return callbacks.mark_commit_started && + callbacks.capture_render_state && + callbacks.prepare_render_state && + callbacks.restore_render_state && + callbacks.publish_history && + callbacks.capture_timelapse_frame && + callbacks.bind_layer_framebuffer && + callbacks.capture_history_region && + callbacks.apply_layer_dirty_region && + callbacks.copy_layer_to_commit_destination && + callbacks.bind_commit_inputs && + callbacks.execute_erase_composite && + callbacks.execute_paint_composite && + callbacks.copy_committed_to_dilate_source && + callbacks.execute_commit_dilate && + callbacks.unbind_layer_framebuffer; +} + +[[nodiscard]] inline LegacyCanvasStrokeCommitResult execute_legacy_canvas_stroke_commit_sequence( + const LegacyCanvasStrokeCommitRequest& request) +{ + LegacyCanvasStrokeCommitResult result; + if (!legacy_canvas_stroke_commit_callbacks_ready(request.callbacks)) { + return result; + } + + request.callbacks.mark_commit_started(); + request.callbacks.capture_render_state(); + request.callbacks.prepare_render_state(); + + for (const auto& face : request.faces) { + if (!face.dirty) { + continue; + } + + request.callbacks.bind_layer_framebuffer(face.index); + + for (std::size_t step_index = 0; step_index < request.sequence.step_count; ++step_index) { + switch (request.sequence.steps[step_index]) { + case pp::paint_renderer::CanvasStrokeCommitStep::readback_history_region: + request.callbacks.capture_history_region(face.index); + break; + case pp::paint_renderer::CanvasStrokeCommitStep::update_layer_dirty_state: + request.callbacks.apply_layer_dirty_region(face.index); + break; + case pp::paint_renderer::CanvasStrokeCommitStep::copy_layer_rtt_to_scratch: + request.callbacks.copy_layer_to_commit_destination(face.index); + break; + case pp::paint_renderer::CanvasStrokeCommitStep::bind_commit_inputs: + request.callbacks.bind_commit_inputs(face.index); + break; + case pp::paint_renderer::CanvasStrokeCommitStep::erase_draw: + request.callbacks.execute_erase_composite(face.index); + break; + case pp::paint_renderer::CanvasStrokeCommitStep::composite_draw: + request.callbacks.execute_paint_composite(face.index); + break; + case pp::paint_renderer::CanvasStrokeCommitStep::copy_committed_rtt_to_scratch: + request.callbacks.copy_committed_to_dilate_source(face.index); + break; + case pp::paint_renderer::CanvasStrokeCommitStep::dilate_edges_draw: + request.callbacks.execute_commit_dilate(face.index); + break; + } + } + + request.callbacks.unbind_layer_framebuffer(face.index); + ++result.committed_faces; + } + + request.callbacks.restore_render_state(); + request.callbacks.publish_history(); + request.callbacks.capture_timelapse_frame(); + + result.ok = true; + return result; +} + +} // namespace pp::panopainter diff --git a/src/paint_renderer/compositor.cpp b/src/paint_renderer/compositor.cpp index 0bc4dc4..600a70a 100644 --- a/src/paint_renderer/compositor.cpp +++ b/src/paint_renderer/compositor.cpp @@ -1445,6 +1445,55 @@ pp::foundation::Result plan_canvas_stroke_rasteri return pp::foundation::Result::success(plan); } +CanvasStrokeCommitSequencePlan plan_canvas_stroke_commit_sequence(CanvasStrokeCommitRequest request) noexcept +{ + CanvasStrokeCommitSequencePlan plan; + plan.erase_mode = request.erase_mode; + plan.alpha_locked = request.alpha_locked; + plan.selection_mask_active = request.selection_mask_active; + plan.uses_dual_stroke = !request.erase_mode && request.dual_stroke_enabled; + plan.uses_pattern = !request.erase_mode && request.pattern_enabled; + plan.updates_layer_bounds = !request.alpha_locked; + + auto append_step = [&plan](CanvasStrokeCommitStep step) noexcept { + if (plan.step_count >= plan.steps.size()) { + return; + } + plan.steps[plan.step_count] = step; + ++plan.step_count; + }; + auto bind = [&plan](CanvasStrokeCommitTextureRole role, std::uint8_t slot) noexcept { + if (plan.texture_binding_count >= plan.texture_bindings.size()) { + return; + } + plan.texture_bindings[plan.texture_binding_count] = CanvasStrokeCommitTextureBindingPlan { + .role = role, + .slot = slot, + }; + ++plan.texture_binding_count; + }; + + append_step(CanvasStrokeCommitStep::readback_history_region); + append_step(CanvasStrokeCommitStep::update_layer_dirty_state); + append_step(CanvasStrokeCommitStep::copy_layer_rtt_to_scratch); + append_step(CanvasStrokeCommitStep::bind_commit_inputs); + append_step(request.erase_mode ? CanvasStrokeCommitStep::erase_draw : CanvasStrokeCommitStep::composite_draw); + append_step(CanvasStrokeCommitStep::copy_committed_rtt_to_scratch); + append_step(CanvasStrokeCommitStep::dilate_edges_draw); + + bind(CanvasStrokeCommitTextureRole::layer_scratch, 0); + bind(CanvasStrokeCommitTextureRole::stroke, 1); + bind(CanvasStrokeCommitTextureRole::selection_mask, 2); + if (plan.uses_dual_stroke) { + bind(CanvasStrokeCommitTextureRole::dual_stroke, 3); + } + if (plan.uses_pattern) { + bind(CanvasStrokeCommitTextureRole::pattern, 4); + } + + return plan; +} + CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds( CanvasStrokeSampleBoundsRequest request) noexcept { diff --git a/src/paint_renderer/compositor.h b/src/paint_renderer/compositor.h index 27fe655..0597a84 100644 --- a/src/paint_renderer/compositor.h +++ b/src/paint_renderer/compositor.h @@ -187,6 +187,55 @@ struct CanvasStrokeRasterizationPlan { bool compatibility_fallback = false; }; +enum class CanvasStrokeCommitStep : std::uint8_t { + readback_history_region, + update_layer_dirty_state, + copy_layer_rtt_to_scratch, + bind_commit_inputs, + erase_draw, + composite_draw, + copy_committed_rtt_to_scratch, + dilate_edges_draw, +}; + +enum class CanvasStrokeCommitTextureRole : std::uint8_t { + layer_scratch, + stroke, + selection_mask, + dual_stroke, + pattern, +}; + +struct CanvasStrokeCommitTextureBindingPlan { + CanvasStrokeCommitTextureRole role = CanvasStrokeCommitTextureRole::layer_scratch; + std::uint8_t slot = 0; +}; + +struct CanvasStrokeCommitRequest { + bool erase_mode = false; + bool alpha_locked = false; + bool selection_mask_active = false; + bool dual_stroke_enabled = false; + bool pattern_enabled = false; +}; + +struct CanvasStrokeCommitSequencePlan { + std::array steps {}; + std::size_t step_count = 0; + std::array texture_bindings {}; + std::size_t texture_binding_count = 0; + bool erase_mode = false; + bool alpha_locked = false; + bool selection_mask_active = false; + bool uses_dual_stroke = false; + bool uses_pattern = false; + bool requires_history_readback = true; + bool updates_layer_bounds = true; + bool requires_layer_scratch_copy = true; + bool requires_committed_scratch_copy = true; + bool requires_dilate = true; +}; + struct CanvasStrokePoint { float x = 0.0F; float y = 0.0F; @@ -493,6 +542,9 @@ export_document_animation_frames_equirectangular_pngs( pp::renderer::RenderDeviceFeatures features, pp::renderer::Extent2D extent) noexcept; +[[nodiscard]] CanvasStrokeCommitSequencePlan plan_canvas_stroke_commit_sequence( + CanvasStrokeCommitRequest request) noexcept; + [[nodiscard]] CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds( CanvasStrokeSampleBoundsRequest request) noexcept; diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index 448b3f4..ec424b4 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -16,6 +16,9 @@ using pp::paint::StrokeBlendMode; using pp::assets::decode_png_rgba8; using pp::paint_renderer::CanvasBlendGateRequest; using pp::paint_renderer::CanvasStrokeBox; +using pp::paint_renderer::CanvasStrokeCommitRequest; +using pp::paint_renderer::CanvasStrokeCommitStep; +using pp::paint_renderer::CanvasStrokeCommitTextureRole; using pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest; using pp::paint_renderer::CanvasStrokeMaterialRequest; using pp::paint_renderer::CanvasStrokePadRegionRequest; @@ -37,6 +40,7 @@ using pp::paint_renderer::export_document_depth_pngs; using pp::paint_renderer::plan_canvas_blend_gate; using pp::paint_renderer::plan_canvas_stroke_face_dirty_update; using pp::paint_renderer::plan_canvas_stroke_feedback; +using pp::paint_renderer::plan_canvas_stroke_commit_sequence; using pp::paint_renderer::plan_canvas_stroke_material; using pp::paint_renderer::plan_canvas_stroke_pad_region; using pp::paint_renderer::plan_canvas_stroke_rasterization; @@ -91,6 +95,19 @@ bool has_preview_texture_slot( return false; } +bool has_commit_texture_binding( + const pp::paint_renderer::CanvasStrokeCommitSequencePlan& plan, + CanvasStrokeCommitTextureRole role, + std::uint8_t slot) +{ + for (std::size_t i = 0; i < plan.texture_binding_count; ++i) { + if (plan.texture_bindings[i].role == role && plan.texture_bindings[i].slot == slot) { + return true; + } + } + return false; +} + void expect_preview_sequence(pp::tests::Harness& h, const pp::paint_renderer::StrokePreviewCompositePlan& plan) { PP_EXPECT(h, plan.step_count == 5U); @@ -101,6 +118,17 @@ void expect_preview_sequence(pp::tests::Harness& h, const pp::paint_renderer::St PP_EXPECT(h, plan.steps[4] == StrokePreviewCompositeStep::copy_preview_texture); } +void expect_commit_prefix(pp::tests::Harness& h, const pp::paint_renderer::CanvasStrokeCommitSequencePlan& plan) +{ + PP_EXPECT(h, plan.step_count == 7U); + PP_EXPECT(h, plan.steps[0] == CanvasStrokeCommitStep::readback_history_region); + PP_EXPECT(h, plan.steps[1] == CanvasStrokeCommitStep::update_layer_dirty_state); + PP_EXPECT(h, plan.steps[2] == CanvasStrokeCommitStep::copy_layer_rtt_to_scratch); + PP_EXPECT(h, plan.steps[3] == CanvasStrokeCommitStep::bind_commit_inputs); + PP_EXPECT(h, plan.steps[5] == CanvasStrokeCommitStep::copy_committed_rtt_to_scratch); + PP_EXPECT(h, plan.steps[6] == CanvasStrokeCommitStep::dilate_edges_draw); +} + std::vector solid_rgba8( std::uint32_t width, std::uint32_t height, @@ -1692,6 +1720,62 @@ void plans_canvas_stroke_dual_material_intent(pp::tests::Harness& h) PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::pattern, 2)); } +void plans_canvas_stroke_commit_erase_sequence(pp::tests::Harness& h) +{ + const auto plan = plan_canvas_stroke_commit_sequence( + CanvasStrokeCommitRequest { + .erase_mode = true, + .alpha_locked = true, + .selection_mask_active = true, + .dual_stroke_enabled = true, + .pattern_enabled = true, + }); + + expect_commit_prefix(h, plan); + PP_EXPECT(h, plan.steps[4] == CanvasStrokeCommitStep::erase_draw); + PP_EXPECT(h, plan.erase_mode); + PP_EXPECT(h, plan.alpha_locked); + PP_EXPECT(h, plan.selection_mask_active); + PP_EXPECT(h, !plan.uses_dual_stroke); + PP_EXPECT(h, !plan.uses_pattern); + PP_EXPECT(h, plan.requires_history_readback); + PP_EXPECT(h, !plan.updates_layer_bounds); + PP_EXPECT(h, plan.requires_layer_scratch_copy); + PP_EXPECT(h, plan.requires_committed_scratch_copy); + PP_EXPECT(h, plan.requires_dilate); + PP_EXPECT(h, plan.texture_binding_count == 3U); + PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::layer_scratch, 0)); + PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::stroke, 1)); + PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::selection_mask, 2)); +} + +void plans_canvas_stroke_commit_composite_sequence(pp::tests::Harness& h) +{ + const auto plan = plan_canvas_stroke_commit_sequence( + CanvasStrokeCommitRequest { + .erase_mode = false, + .alpha_locked = false, + .selection_mask_active = true, + .dual_stroke_enabled = true, + .pattern_enabled = true, + }); + + expect_commit_prefix(h, plan); + PP_EXPECT(h, plan.steps[4] == CanvasStrokeCommitStep::composite_draw); + PP_EXPECT(h, !plan.erase_mode); + PP_EXPECT(h, !plan.alpha_locked); + PP_EXPECT(h, plan.selection_mask_active); + PP_EXPECT(h, plan.uses_dual_stroke); + PP_EXPECT(h, plan.uses_pattern); + PP_EXPECT(h, plan.updates_layer_bounds); + PP_EXPECT(h, plan.texture_binding_count == 5U); + PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::layer_scratch, 0)); + PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::stroke, 1)); + PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::selection_mask, 2)); + PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::dual_stroke, 3)); + PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::pattern, 4)); +} + void plans_stroke_preview_composite_for_simple_brush(pp::tests::Harness& h) { const auto plan = plan_stroke_preview_composite(StrokePreviewCompositeRequest {}); @@ -2178,6 +2262,8 @@ int main() harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans); harness.run("plans_canvas_stroke_material_passes", plans_canvas_stroke_material_passes); harness.run("plans_canvas_stroke_dual_material_intent", plans_canvas_stroke_dual_material_intent); + harness.run("plans_canvas_stroke_commit_erase_sequence", plans_canvas_stroke_commit_erase_sequence); + harness.run("plans_canvas_stroke_commit_composite_sequence", plans_canvas_stroke_commit_composite_sequence); harness.run("plans_stroke_preview_composite_for_simple_brush", plans_stroke_preview_composite_for_simple_brush); harness.run("plans_stroke_preview_composite_with_mixer_input", plans_stroke_preview_composite_with_mixer_input); harness.run("plans_stroke_preview_composite_with_dual_input", plans_stroke_preview_composite_with_dual_input);