diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 74c94de..eb9fa26 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,6 +18,13 @@ agent or engineer to remove them without reconstructing context from chat. ## Recent Reductions +- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` face + dirty-box planning now routes through a retained stroke execution helper + wrapping `pp_paint_renderer`, retained stroke commit step dispatch clamps + malformed step counts to the fixed plan array, and compositor coverage now + exercises malformed retained commit plans plus all-input stroke-preview + composite planning. Live stroke rasterization, callback execution, texture + binding, and history mutation remain retained. - 2026-06-13: DEBT-0036 was narrowed again. Remaining live shader setup outside retained helper headers now routes through retained helper surfaces for canvas modes, equirect layer export, `NodeCanvas` debug dirty bounds, diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 5b37614..27a64f3 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -3073,6 +3073,12 @@ Results: `NodeCanvas` debug dirty bounds, atlas image drawing, and text drawing, while geometry, framebuffer flow, texture/sampler binding, blend/depth state, readback, and draw execution remain retained. +- `Canvas::stroke_draw` face dirty-box planning now shares a retained stroke + execution helper wrapping `pp_paint_renderer`, retained stroke commit step + dispatch clamps malformed step counts to the fixed plan array, and + compositor coverage now includes malformed retained commit plans plus + all-input stroke-preview composite planning. Live stroke rasterization, + callback execution, texture binding, and history mutation remain retained. - Remaining simple color, hue, color-quad, grid heightmap, and pen/line preview shader setup in UI nodes and canvas modes now shares retained helper surfaces, while geometry, texture/sampler binding, blend/depth state, diff --git a/src/canvas.cpp b/src/canvas.cpp index 6fb905b..40dc138 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -780,16 +780,16 @@ void Canvas::stroke_draw() m_tmp[i].unbindFramebuffer(); - const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update( - pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest { + const auto dirty_update = pp::panopainter::plan_legacy_canvas_stroke_face_dirty_update( + pp::panopainter::LegacyCanvasStrokeFaceDirtyRequest { .extent = stroke_extent, - .previous_accumulated_dirty_box = canvas_stroke_box(m_dirty_box[i]), - .previous_pass_dirty_box = canvas_stroke_box(box_face[i]), - .sample_dirty_box = canvas_stroke_box(box_sample), + .previous_accumulated_dirty_box = m_dirty_box[i], + .previous_pass_dirty_box = box_face[i], + .sample_dirty_box = box_sample, .include_in_committed_dirty_box = true, }); - m_dirty_box[i] = glm_box(dirty_update.accumulated_dirty_box); - box_face[i] = glm_box(dirty_update.pass_dirty_box); + m_dirty_box[i] = dirty_update.accumulated_dirty_box; + box_face[i] = dirty_update.pass_dirty_box; // TODO: maybe average color? pad_color = f.col; } @@ -891,16 +891,16 @@ void Canvas::stroke_draw() m_tmp_dual[i].unbindFramebuffer(); // this mode overflows the main brush boundries - const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update( - pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest { + const auto dirty_update = pp::panopainter::plan_legacy_canvas_stroke_face_dirty_update( + pp::panopainter::LegacyCanvasStrokeFaceDirtyRequest { .extent = stroke_extent, - .previous_accumulated_dirty_box = canvas_stroke_box(m_dirty_box[i]), - .previous_pass_dirty_box = canvas_stroke_box(box_sample), - .sample_dirty_box = canvas_stroke_box(box_sample), + .previous_accumulated_dirty_box = m_dirty_box[i], + .previous_pass_dirty_box = box_sample, + .sample_dirty_box = box_sample, .include_in_committed_dirty_box = stroke_material.composite_pass.dual_blend_mode == 0, }); - m_dirty_box[i] = glm_box(dirty_update.accumulated_dirty_box); + m_dirty_box[i] = dirty_update.accumulated_dirty_box; } } } diff --git a/src/legacy_canvas_stroke_commit_services.h b/src/legacy_canvas_stroke_commit_services.h index 405b86c..91e4477 100644 --- a/src/legacy_canvas_stroke_commit_services.h +++ b/src/legacy_canvas_stroke_commit_services.h @@ -2,6 +2,7 @@ #include "paint_renderer/compositor.h" +#include #include #include #include @@ -45,6 +46,12 @@ struct LegacyCanvasStrokeCommitResult { int committed_faces = 0; }; +[[nodiscard]] inline std::size_t legacy_canvas_stroke_commit_step_count( + const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence) noexcept +{ + return std::min(sequence.step_count, sequence.steps.size()); +} + [[nodiscard]] inline bool legacy_canvas_stroke_commit_callbacks_ready( const LegacyCanvasStrokeCommitCallbacks& callbacks) noexcept { @@ -85,7 +92,8 @@ struct LegacyCanvasStrokeCommitResult { request.callbacks.bind_layer_framebuffer(face.index); - for (std::size_t step_index = 0; step_index < request.sequence.step_count; ++step_index) { + const auto step_count = legacy_canvas_stroke_commit_step_count(request.sequence); + for (std::size_t step_index = 0; step_index < 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); diff --git a/src/legacy_canvas_stroke_execution_services.h b/src/legacy_canvas_stroke_execution_services.h index c8d3df7..bc526cd 100644 --- a/src/legacy_canvas_stroke_execution_services.h +++ b/src/legacy_canvas_stroke_execution_services.h @@ -30,6 +30,60 @@ struct LegacyStrokeSampleExecutionResult { glm::vec4 dirty_bounds {}; }; +struct LegacyCanvasStrokeFaceDirtyRequest { + pp::renderer::Extent2D extent {}; + glm::vec4 previous_accumulated_dirty_box {}; + glm::vec4 previous_pass_dirty_box {}; + glm::vec4 sample_dirty_box {}; + bool include_in_committed_dirty_box = true; +}; + +struct LegacyCanvasStrokeFaceDirtyResult { + glm::vec4 accumulated_dirty_box {}; + glm::vec4 pass_dirty_box {}; + bool has_dirty_pixels = false; + bool committed_dirty = false; + bool pass_dirty = false; +}; + +[[nodiscard]] inline pp::paint_renderer::CanvasStrokeBox legacy_canvas_stroke_box(glm::vec4 box) noexcept +{ + return pp::paint_renderer::CanvasStrokeBox { + .min_x = box.x, + .min_y = box.y, + .max_x = box.z, + .max_y = box.w, + }; +} + +[[nodiscard]] inline glm::vec4 legacy_canvas_stroke_glm_box( + pp::paint_renderer::CanvasStrokeBox box) noexcept +{ + return glm::vec4(box.min_x, box.min_y, box.max_x, box.max_y); +} + +[[nodiscard]] inline LegacyCanvasStrokeFaceDirtyResult plan_legacy_canvas_stroke_face_dirty_update( + const LegacyCanvasStrokeFaceDirtyRequest& request) noexcept +{ + const auto plan = pp::paint_renderer::plan_canvas_stroke_face_dirty_update( + pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest { + .extent = request.extent, + .previous_accumulated_dirty_box = + legacy_canvas_stroke_box(request.previous_accumulated_dirty_box), + .previous_pass_dirty_box = legacy_canvas_stroke_box(request.previous_pass_dirty_box), + .sample_dirty_box = legacy_canvas_stroke_box(request.sample_dirty_box), + .include_in_committed_dirty_box = request.include_in_committed_dirty_box, + }); + + return LegacyCanvasStrokeFaceDirtyResult { + .accumulated_dirty_box = legacy_canvas_stroke_glm_box(plan.accumulated_dirty_box), + .pass_dirty_box = legacy_canvas_stroke_glm_box(plan.pass_dirty_box), + .has_dirty_pixels = plan.has_dirty_pixels, + .committed_dirty = plan.committed_dirty, + .pass_dirty = plan.pass_dirty, + }; +} + [[nodiscard]] inline LegacyStrokeSampleExecutionResult execute_legacy_canvas_stroke_sample( const LegacyStrokeSampleExecutionRequest& request) { diff --git a/src/legacy_node_stroke_preview_execution_services.h b/src/legacy_node_stroke_preview_execution_services.h new file mode 100644 index 0000000..f9d3718 --- /dev/null +++ b/src/legacy_node_stroke_preview_execution_services.h @@ -0,0 +1,46 @@ +#pragma once + +#include "paint_renderer/compositor.h" +#include "renderer_api/renderer_api.h" + +#include +#include + +namespace pp::panopainter { + +[[nodiscard]] inline pp::paint_renderer::CanvasStrokeFeedbackPlan plan_legacy_node_stroke_preview_feedback( + pp::renderer::RenderDeviceFeatures features, + int width, + int height) noexcept +{ + const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback( + features, + pp::renderer::Extent2D { + .width = static_cast(std::max(width, 0)), + .height = static_cast(std::max(height, 0)), + }); + if (plan) { + return plan.value(); + } + + pp::paint_renderer::CanvasStrokeFeedbackPlan fallback; + fallback.compatibility_fallback = true; + fallback.path = pp::paint_renderer::StrokeCompositePath::ping_pong_textures; + fallback.requires_auxiliary_texture = true; + return fallback; +} + +[[nodiscard]] inline pp::paint_renderer::StrokePreviewCompositePlan plan_legacy_node_stroke_preview_composite( + bool uses_mixer, + bool uses_dual, + bool uses_pattern) noexcept +{ + return pp::paint_renderer::plan_stroke_preview_composite( + pp::paint_renderer::StrokePreviewCompositeRequest { + .uses_mixer = uses_mixer, + .uses_dual = uses_dual, + .uses_pattern = uses_pattern, + }); +} + +} // namespace pp::panopainter diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index 4988ec6..b7c2d3a 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -12,11 +12,11 @@ #include "legacy_canvas_stroke_preview_services.h" #include "legacy_canvas_stroke_shader_services.h" #include "legacy_canvas_stroke_services.h" +#include "legacy_node_stroke_preview_execution_services.h" #include "legacy_ui_gl_dispatch.h" #include "paint_renderer/compositor.h" #include "renderer_gl/opengl_capabilities.h" #include "util.h" -#include #include #include @@ -31,21 +31,10 @@ pp::paint_renderer::CanvasStrokeFeedbackPlan stroke_preview_destination_feedback int width, int height) noexcept { - const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback( + return pp::panopainter::plan_legacy_node_stroke_preview_feedback( stroke_preview_render_device_features(), - pp::renderer::Extent2D { - .width = static_cast(std::max(width, 0)), - .height = static_cast(std::max(height, 0)), - }); - if (plan) { - return plan.value(); - } - - pp::paint_renderer::CanvasStrokeFeedbackPlan fallback; - fallback.compatibility_fallback = true; - fallback.path = pp::paint_renderer::StrokeCompositePath::ping_pong_textures; - fallback.requires_auxiliary_texture = true; - return fallback; + width, + height); } pp::paint_renderer::CanvasStrokeMaterialPlan stroke_preview_material_plan( @@ -452,12 +441,10 @@ void NodeStrokePreview::draw_stroke_immediate() const auto stroke_feedback = stroke_preview_destination_feedback_plan(m_rtt.getWidth(), m_rtt.getHeight()); const bool copy_stroke_destination = !stroke_feedback.reads_destination_color; const auto material = stroke_preview_material_plan(*b, copy_stroke_destination); - const auto preview_composite_plan = pp::paint_renderer::plan_stroke_preview_composite( - pp::paint_renderer::StrokePreviewCompositeRequest { - .uses_mixer = b->m_tip_mix > 0.0f, - .uses_dual = material.composite_pass.use_dual, - .uses_pattern = material.composite_pass.use_pattern, - }); + const auto preview_composite_plan = pp::panopainter::plan_legacy_node_stroke_preview_composite( + b->m_tip_mix > 0.0f, + material.composite_pass.use_dual, + material.composite_pass.use_pattern); pp::panopainter::setup_legacy_stroke_shader( pp::panopainter::LegacyStrokeShaderSetupUniforms { .resolution = size, diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index ec424b4..7eb3b01 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -1,4 +1,5 @@ #include "assets/image_pixels.h" +#include "legacy_canvas_stroke_commit_services.h" #include "paint_renderer/compositor.h" #include "renderer_api/recording_renderer.h" #include "test_harness.h" @@ -1776,6 +1777,61 @@ void plans_canvas_stroke_commit_composite_sequence(pp::tests::Harness& h) PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::pattern, 4)); } +void retained_stroke_commit_runner_clamps_malformed_step_count(pp::tests::Harness& h) +{ + pp::paint_renderer::CanvasStrokeCommitSequencePlan sequence; + sequence.step_count = 99U; + sequence.steps[0] = CanvasStrokeCommitStep::bind_commit_inputs; + sequence.steps[1] = CanvasStrokeCommitStep::composite_draw; + + int bind_inputs = 0; + int paint_draws = 0; + int erase_draws = 0; + int started = 0; + int restored = 0; + int published = 0; + int timelapse = 0; + + const auto result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence( + pp::panopainter::LegacyCanvasStrokeCommitRequest { + .context = "test", + .faces = { + pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = true }, + pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = false }, + pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = true }, + }, + .sequence = sequence, + .callbacks = { + .mark_commit_started = [&]() { ++started; }, + .capture_render_state = []() {}, + .prepare_render_state = []() {}, + .restore_render_state = [&]() { ++restored; }, + .publish_history = [&]() { ++published; }, + .capture_timelapse_frame = [&]() { ++timelapse; }, + .bind_layer_framebuffer = [](int) {}, + .capture_history_region = [](int) {}, + .apply_layer_dirty_region = [](int) {}, + .copy_layer_to_commit_destination = [](int) {}, + .bind_commit_inputs = [&](int) { ++bind_inputs; }, + .execute_erase_composite = [&](int) { ++erase_draws; }, + .execute_paint_composite = [&](int) { ++paint_draws; }, + .copy_committed_to_dilate_source = [](int) {}, + .execute_commit_dilate = [](int) {}, + .unbind_layer_framebuffer = [](int) {}, + }, + }); + + PP_EXPECT(h, result.ok); + PP_EXPECT(h, result.committed_faces == 2); + PP_EXPECT(h, bind_inputs == 2); + PP_EXPECT(h, paint_draws == 2); + PP_EXPECT(h, erase_draws == 0); + PP_EXPECT(h, started == 1); + PP_EXPECT(h, restored == 1); + PP_EXPECT(h, published == 1); + PP_EXPECT(h, timelapse == 1); +} + void plans_stroke_preview_composite_for_simple_brush(pp::tests::Harness& h) { const auto plan = plan_stroke_preview_composite(StrokePreviewCompositeRequest {}); @@ -1841,6 +1897,27 @@ void plans_stroke_preview_composite_with_pattern_input(pp::tests::Harness& h) PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4)); } +void plans_stroke_preview_composite_with_all_retained_inputs(pp::tests::Harness& h) +{ + const auto plan = plan_stroke_preview_composite( + StrokePreviewCompositeRequest { + .uses_mixer = true, + .uses_dual = true, + .uses_pattern = true, + }); + + expect_preview_sequence(h, plan); + PP_EXPECT(h, plan.uses_mixer); + PP_EXPECT(h, plan.uses_dual); + PP_EXPECT(h, plan.uses_pattern); + PP_EXPECT(h, plan.texture_slot_count == 5U); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::dual, 3)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4)); + PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::mixer, 3)); +} + void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h) { const std::vector normal_layers { 0, 0, 0 }; @@ -2264,10 +2341,16 @@ int main() 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( + "retained_stroke_commit_runner_clamps_malformed_step_count", + retained_stroke_commit_runner_clamps_malformed_step_count); 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); harness.run("plans_stroke_preview_composite_with_pattern_input", plans_stroke_preview_composite_with_pattern_input); + harness.run( + "plans_stroke_preview_composite_with_all_retained_inputs", + plans_stroke_preview_composite_with_all_retained_inputs); harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices); harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks); harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);