From 33e62a1c4a9aa269a498a1cc77dd511f36686acc Mon Sep 17 00:00:00 2001 From: omigamedev Date: Sat, 13 Jun 2026 06:52:09 +0200 Subject: [PATCH] Narrow retained canvas stroke execution helpers --- docs/modernization/debt.md | 12 +- docs/modernization/roadmap.md | 11 +- docs/modernization/tasks.md | 7 +- src/canvas.cpp | 268 ++++++++++-------- src/legacy_canvas_stroke_commit_services.h | 89 ++++++ src/legacy_canvas_stroke_execution_services.h | 59 ++++ tests/paint_renderer/compositor_tests.cpp | 99 +++++++ 7 files changed, 418 insertions(+), 127 deletions(-) diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 582a0f9..9c6cbf2 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -1,7 +1,7 @@ # Modernization Debt Log Status: live -Last updated: 2026-06-06 +Last updated: 2026-06-13 Every shortcut, temporary adapter, retained vendored dependency, skipped platform gate, compatibility shim, or incomplete automation path must be @@ -18,6 +18,16 @@ agent or engineer to remove them without reconstructing context from chat. ## Recent Reductions +- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_commit` retained + commit input texture/sampler binding, erase/composite draw dispatch, + committed-copy-to-dilate-source, and dilate draw now route through + `legacy_canvas_stroke_commit_services.h`; Canvas still owns history + readback, `ActionStroke` population, layer dirty-box mutation, and retained + RTT/framebuffer ownership. +- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` pad-pass + dirty-face iteration, pad-region planning, and NDC quad assembly now route + through a retained stroke execution helper callback boundary; Canvas still + owns framebuffer copies, brush-shape uploads, and draw execution. - 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` current and dual stroke frame-face traversal now routes through the retained stroke execution helper; framebuffer binding, shader uniform timing, dirty-box diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index f3c42ec..b9a0af9 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -1,7 +1,7 @@ # PanoPainter Modernization Roadmap Status: live -Last updated: 2026-06-12 +Last updated: 2026-06-13 This is the living roadmap for modernizing PanoPainter into independently testable C++23 components while retaining all existing functionality. Keep this @@ -3083,6 +3083,15 @@ Results: execution helper wrapping `pp_paint_renderer`, while pad color selection, dirty-face iteration, framebuffer copies, quad upload, and draw execution remain retained. +- `Canvas::stroke_draw` pad-pass dirty-face iteration, pad-region planning, and + NDC quad assembly now share a retained stroke execution helper callback + boundary, while Canvas still owns framebuffer copies, brush-shape uploads, + and draw execution. +- `Canvas::stroke_commit` retained commit input texture/sampler binding, + erase/composite draw dispatch, committed-copy to the dilate scratch texture, + and dilate draw now share `legacy_canvas_stroke_commit_services.h`; history + readback, `ActionStroke` population, layer dirty-box mutation, and retained + RTT/framebuffer ownership remain in the legacy Canvas path. - `Canvas::stroke_draw_compute` frame planning now shares the retained stroke execution helper for brush-quad construction, mixer feedback bounds, 2D/3D projection selection intent, and frame assembly, while legacy projection diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 10d118a..ba53ff2 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -1,7 +1,7 @@ # Modernization Task Tracker Status: live -Last updated: 2026-06-12 +Last updated: 2026-06-13 This file turns the modernization roadmap into small, measurable work items. The roadmap explains direction, the debt log explains why shortcuts remain, and @@ -509,6 +509,11 @@ Done Checks: Progress Notes: +- 2026-06-13: `Canvas::stroke_commit` now routes retained commit input + texture/sampler binding, erase/composite draw dispatch, committed-copy, and + dilate draw through `legacy_canvas_stroke_commit_services.h`; history + readback, `ActionStroke` population, and layer dirty-box mutation remain + local to Canvas. - 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 diff --git a/src/canvas.cpp b/src/canvas.cpp index 0a502cb..acb4ae9 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -753,45 +753,41 @@ void Canvas::stroke_draw() { set_active_texture_unit(1); } - for (int i = 0; i < 6; i++) - { - if (!box_dirty[i]) - continue; - const auto pad_region = pp::panopainter::plan_legacy_canvas_stroke_pad_region( - pp::panopainter::LegacyCanvasStrokePadRegionRequest { - .extent = stroke_extent, - .pass_dirty_box = box_face[i], - }); - if (!pad_region.has_pixels) - continue; - // B(xw)--(zw)C box - // | // | coordinates - // A(xy)--(zy)D mapping - std::array pad_quad = { - vertex_t({pad_region.ndc_quad[0].x, pad_region.ndc_quad[0].y}), // A - vertex_t({pad_region.ndc_quad[1].x, pad_region.ndc_quad[1].y}), // B - vertex_t({pad_region.ndc_quad[2].x, pad_region.ndc_quad[2].y}), // C - vertex_t({pad_region.ndc_quad[3].x, pad_region.ndc_quad[3].y}), // A - vertex_t({pad_region.ndc_quad[4].x, pad_region.ndc_quad[4].y}), // C - vertex_t({pad_region.ndc_quad[5].x, pad_region.ndc_quad[5].y}), // D - }; - m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size()); + const std::array pad_faces = { + pp::panopainter::LegacyCanvasStrokePadFace { .index = 0, .dirty = box_dirty[0], .pass_dirty_box = box_face[0] }, + pp::panopainter::LegacyCanvasStrokePadFace { .index = 1, .dirty = box_dirty[1], .pass_dirty_box = box_face[1] }, + pp::panopainter::LegacyCanvasStrokePadFace { .index = 2, .dirty = box_dirty[2], .pass_dirty_box = box_face[2] }, + pp::panopainter::LegacyCanvasStrokePadFace { .index = 3, .dirty = box_dirty[3], .pass_dirty_box = box_face[3] }, + pp::panopainter::LegacyCanvasStrokePadFace { .index = 4, .dirty = box_dirty[4], .pass_dirty_box = box_face[4] }, + pp::panopainter::LegacyCanvasStrokePadFace { .index = 5, .dirty = box_dirty[5], .pass_dirty_box = box_face[5] }, + }; + [[maybe_unused]] const auto pad_result = pp::panopainter::execute_legacy_canvas_stroke_pad_faces( + pp::panopainter::LegacyCanvasStrokePadExecutionRequest { + .context = "Canvas::stroke_draw", + .extent = stroke_extent, + .faces = pad_faces, + .execute_face = + [&](int face_index, + const pp::panopainter::LegacyCanvasStrokePadRegionResult& pad_region, + std::span pad_quad) { + m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size()); - m_tmp[i].bindFramebuffer(); - if (copy_stroke_destination) - { - m_tex[i].bind(); - copy_framebuffer_to_texture_2d( - pad_region.copy_region.x, - pad_region.copy_region.y, - pad_region.copy_region.x, - pad_region.copy_region.y, - pad_region.copy_region.width, - pad_region.copy_region.height); - } - m_brush_shape.draw_fill(); - m_tmp[i].unbindFramebuffer(); - } + m_tmp[face_index].bindFramebuffer(); + if (copy_stroke_destination) + { + m_tex[face_index].bind(); + copy_framebuffer_to_texture_2d( + pad_region.copy_region.x, + pad_region.copy_region.y, + pad_region.copy_region.x, + pad_region.copy_region.y, + pad_region.copy_region.width, + pad_region.copy_region.height); + } + m_brush_shape.draw_fill(); + m_tmp[face_index].unbindFramebuffer(); + }, + }); if (copy_stroke_destination) { unbind_texture_2d(); @@ -1047,104 +1043,128 @@ void Canvas::stroke_commit() m_tex2[i].unbind(); }, .bind_commit_inputs = [&](int i) { - m_tmp[i].bindTexture(); - set_active_texture_unit(1); - m_tex2[i].bind(); - m_sampler.bind(0); - m_sampler_nearest.bind(1); - m_sampler.bind(2); - m_sampler.bind(3); - m_sampler_stencil.bind(4); - }, - .execute_erase_composite = [&](int i) { - pp::panopainter::setup_legacy_stroke_erase_shader( - pp::panopainter::LegacyStrokeEraseUniforms { - .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), - .texture_slot = 0, - .stroke_texture_slot = 1, - .mask_texture_slot = 2, - .alpha = 1.0f, - .mask_enabled = m_smask_active, + pp::panopainter::bind_legacy_canvas_stroke_commit_inputs( + sequence, + [&](int texture_slot) { + set_active_texture_unit(texture_slot); + }, + [&](pp::paint_renderer::CanvasStrokeCommitTextureRole role) { + switch (role) { + case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch: + m_tex2[i].bind(); + break; + case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke: + m_tmp[i].bindTexture(); + break; + case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask: + m_smask.rtt(i).bindTexture(); + break; + case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke: + m_tmp_dual[i].bindTexture(); + break; + case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern: + b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d(); + break; + } + }, + [&](pp::paint_renderer::CanvasStrokeCommitTextureRole role, int texture_slot) { + switch (role) { + case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch: + m_sampler.bind(texture_slot); + break; + case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke: + m_sampler_nearest.bind(texture_slot); + break; + case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask: + case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke: + m_sampler.bind(texture_slot); + break; + case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern: + m_sampler_stencil.bind(texture_slot); + break; + } }); - - set_active_texture_unit(0); - m_tex2[i].bind(); - set_active_texture_unit(1); - m_tmp[i].bindTexture(); - set_active_texture_unit(2); - m_smask.rtt(i).bindTexture(); - m_plane.draw_fill(); - m_smask.rtt(i).unbindTexture(); - set_active_texture_unit(1); - m_tmp[i].unbindTexture(); - set_active_texture_unit(0); - m_tex2[i].unbind(); }, - .execute_paint_composite = [&](int i) { + .execute_erase_composite = [&](int) { + pp::panopainter::execute_legacy_canvas_stroke_commit_erase( + [&]() { + pp::panopainter::setup_legacy_stroke_erase_shader( + pp::panopainter::LegacyStrokeEraseUniforms { + .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), + .texture_slot = 0, + .stroke_texture_slot = 1, + .mask_texture_slot = 2, + .alpha = 1.0f, + .mask_enabled = m_smask_active, + }); + }, + [&]() { + m_plane.draw_fill(); + }); + }, + .execute_paint_composite = [&](int) { glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale); if (b->m_pattern_flipx) patt_scale.x *= -1.f; if (b->m_pattern_flipy) patt_scale.y *= -1.f; - pp::panopainter::setup_legacy_stroke_composite_shader( - pp::panopainter::LegacyStrokeCompositeUniforms { - .resolution = m_size, - .pattern = { - .scale = patt_scale, - .invert = static_cast(b->m_pattern_invert), - .brightness = b->m_pattern_brightness, - .contrast = b->m_pattern_contrast, - .depth = b->m_pattern_depth, - .blend_mode = b->m_pattern_blend_mode, - .offset = m_pattern_offset, - }, - .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), - .layer_alpha = 1.0f, - .alpha_lock = m_layers[m_current_layer_idx]->m_alpha_locked, - .mask_enabled = m_smask_active, - .use_fragcoord = false, - .blend_mode = b->m_blend_mode, - .use_dual = stroke_material.composite_pass.use_dual, - .dual_blend_mode = stroke_material.composite_pass.dual_blend_mode, - .dual_alpha = stroke_material.composite_pass.dual_alpha, - .use_pattern = stroke_material.composite_pass.use_pattern, + pp::panopainter::execute_legacy_canvas_stroke_commit_paint( + [&]() { + pp::panopainter::setup_legacy_stroke_composite_shader( + pp::panopainter::LegacyStrokeCompositeUniforms { + .resolution = m_size, + .pattern = { + .scale = patt_scale, + .invert = static_cast(b->m_pattern_invert), + .brightness = b->m_pattern_brightness, + .contrast = b->m_pattern_contrast, + .depth = b->m_pattern_depth, + .blend_mode = b->m_pattern_blend_mode, + .offset = m_pattern_offset, + }, + .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), + .layer_alpha = 1.0f, + .alpha_lock = m_layers[m_current_layer_idx]->m_alpha_locked, + .mask_enabled = m_smask_active, + .use_fragcoord = false, + .blend_mode = b->m_blend_mode, + .use_dual = stroke_material.composite_pass.use_dual, + .dual_blend_mode = stroke_material.composite_pass.dual_blend_mode, + .dual_alpha = stroke_material.composite_pass.dual_alpha, + .use_pattern = stroke_material.composite_pass.use_pattern, + }); + }, + [&]() { + m_plane.draw_fill(); }); - - set_active_texture_unit(0); - m_tex2[i].bind(); - set_active_texture_unit(1); - m_tmp[i].bindTexture(); - set_active_texture_unit(2); - m_smask.rtt(i).bindTexture(); - set_active_texture_unit(3); - if (stroke_material.composite_pass.use_dual) - m_tmp_dual[i].bindTexture(); - set_active_texture_unit(4); - b->m_pattern_texture ? - b->m_pattern_texture->bind() : - unbind_texture_2d(); - m_plane.draw_fill(); - set_active_texture_unit(3); - if (stroke_material.composite_pass.use_dual) - m_tmp_dual[i].unbindTexture(); - set_active_texture_unit(2); - m_smask.rtt(i).unbindTexture(); - set_active_texture_unit(1); - m_tmp[i].unbindTexture(); - set_active_texture_unit(0); - m_tex2[i].unbind(); }, .copy_committed_to_dilate_source = [&](int i) { - // Dilate borders to avoid interpolation bleeding - pp::panopainter::setup_legacy_stroke_dilate_shader( - pp::panopainter::LegacyStrokeDilateUniforms { - .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), + pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source( + sequence, + [&]() { + // Dilate borders to avoid interpolation bleeding + pp::panopainter::setup_legacy_stroke_dilate_shader( + pp::panopainter::LegacyStrokeDilateUniforms { + .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), + }); + }, + [&](int texture_slot) { + set_active_texture_unit(texture_slot); + }, + [&]() { + m_tex2[i].bind(); + }, + [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) { + copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height); + }, + pp::panopainter::LegacyCanvasStrokeCommitCopyExtent { + .width = m_width, + .height = m_height, }); - set_active_texture_unit(0); - m_tex2[i].bind(); - copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height); }, .execute_commit_dilate = [&](int) { - m_plane.draw_fill(); + pp::panopainter::execute_legacy_canvas_stroke_commit_dilate([&]() { + m_plane.draw_fill(); + }); }, .unbind_layer_framebuffer = [&](int i) { m_layers[m_current_layer_idx]->rtt(i).unbindFramebuffer(); diff --git a/src/legacy_canvas_stroke_commit_services.h b/src/legacy_canvas_stroke_commit_services.h index 91e4477..dad356a 100644 --- a/src/legacy_canvas_stroke_commit_services.h +++ b/src/legacy_canvas_stroke_commit_services.h @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -46,12 +47,100 @@ struct LegacyCanvasStrokeCommitResult { int committed_faces = 0; }; +struct LegacyCanvasStrokeCommitCopyExtent { + int width = 0; + int height = 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 std::size_t legacy_canvas_stroke_commit_texture_binding_count( + const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence) noexcept +{ + return std::min(sequence.texture_binding_count, sequence.texture_bindings.size()); +} + +[[nodiscard]] inline int legacy_canvas_stroke_commit_texture_slot( + const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence, + pp::paint_renderer::CanvasStrokeCommitTextureRole role) noexcept +{ + const auto binding_count = legacy_canvas_stroke_commit_texture_binding_count(sequence); + for (std::size_t binding_index = 0; binding_index < binding_count; ++binding_index) { + const auto& binding = sequence.texture_bindings[binding_index]; + if (binding.role == role) { + return static_cast(binding.slot); + } + } + + return -1; +} + +template +inline void bind_legacy_canvas_stroke_commit_inputs( + const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence, + SetActiveTextureUnit&& set_active_texture_unit, + BindTextureRole&& bind_texture_role, + BindSamplerRole&& bind_sampler_role) +{ + const auto binding_count = legacy_canvas_stroke_commit_texture_binding_count(sequence); + for (std::size_t binding_index = 0; binding_index < binding_count; ++binding_index) { + const auto& binding = sequence.texture_bindings[binding_index]; + set_active_texture_unit(static_cast(binding.slot)); + bind_texture_role(binding.role); + bind_sampler_role(binding.role, static_cast(binding.slot)); + } +} + +template +inline void execute_legacy_canvas_stroke_commit_erase( + SetupShader&& setup_shader, + DrawPlane&& draw_plane) +{ + setup_shader(); + draw_plane(); +} + +template +inline void execute_legacy_canvas_stroke_commit_paint( + SetupShader&& setup_shader, + DrawPlane&& draw_plane) +{ + setup_shader(); + draw_plane(); +} + +template +inline void copy_legacy_canvas_stroke_commit_to_dilate_source( + const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence, + SetupShader&& setup_shader, + SetActiveTextureUnit&& set_active_texture_unit, + BindLayerScratch&& bind_layer_scratch, + CopyFramebufferToTexture&& copy_framebuffer_to_texture, + LegacyCanvasStrokeCommitCopyExtent extent) +{ + const auto layer_scratch_slot = legacy_canvas_stroke_commit_texture_slot( + sequence, + pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch); + if (layer_scratch_slot < 0 || extent.width <= 0 || extent.height <= 0) { + return; + } + + setup_shader(); + set_active_texture_unit(layer_scratch_slot); + bind_layer_scratch(); + copy_framebuffer_to_texture(0, 0, 0, 0, extent.width, extent.height); +} + +template +inline void execute_legacy_canvas_stroke_commit_dilate(DrawPlane&& draw_plane) +{ + draw_plane(); +} + [[nodiscard]] inline bool legacy_canvas_stroke_commit_callbacks_ready( const LegacyCanvasStrokeCommitCallbacks& callbacks) noexcept { diff --git a/src/legacy_canvas_stroke_execution_services.h b/src/legacy_canvas_stroke_execution_services.h index f81e4c0..406f7ab 100644 --- a/src/legacy_canvas_stroke_execution_services.h +++ b/src/legacy_canvas_stroke_execution_services.h @@ -64,6 +64,24 @@ struct LegacyCanvasStrokePadRegionResult { std::array ndc_quad {}; }; +struct LegacyCanvasStrokePadFace { + int index = 0; + bool dirty = false; + glm::vec4 pass_dirty_box {}; +}; + +struct LegacyCanvasStrokePadExecutionRequest { + std::string_view context; + pp::renderer::Extent2D extent {}; + std::span faces; + std::function)> execute_face; +}; + +struct LegacyCanvasStrokePadExecutionResult { + bool ok = false; + std::size_t padded_faces = 0; +}; + struct LegacyCanvasStrokeComputeRequest { StrokeSample previous_sample {}; std::span samples; @@ -228,6 +246,47 @@ std::size_t execute_legacy_canvas_stroke_frame_faces( }; } +[[nodiscard]] inline LegacyCanvasStrokePadExecutionResult execute_legacy_canvas_stroke_pad_faces( + const LegacyCanvasStrokePadExecutionRequest& request) +{ + LegacyCanvasStrokePadExecutionResult result; + if (request.extent.width == 0U || + request.extent.height == 0U || + !request.execute_face) { + return result; + } + + for (const auto& face : request.faces) { + if (!face.dirty) { + continue; + } + + const auto pad_region = plan_legacy_canvas_stroke_pad_region( + LegacyCanvasStrokePadRegionRequest { + .extent = request.extent, + .pass_dirty_box = face.pass_dirty_box, + }); + if (!pad_region.has_pixels) { + continue; + } + + std::array pad_quad = { + vertex_t({ pad_region.ndc_quad[0].x, pad_region.ndc_quad[0].y }), + vertex_t({ pad_region.ndc_quad[1].x, pad_region.ndc_quad[1].y }), + vertex_t({ pad_region.ndc_quad[2].x, pad_region.ndc_quad[2].y }), + vertex_t({ pad_region.ndc_quad[3].x, pad_region.ndc_quad[3].y }), + vertex_t({ pad_region.ndc_quad[4].x, pad_region.ndc_quad[4].y }), + vertex_t({ pad_region.ndc_quad[5].x, pad_region.ndc_quad[5].y }), + }; + + request.execute_face(face.index, pad_region, pad_quad); + ++result.padded_faces; + } + + result.ok = true; + return result; +} + [[nodiscard]] inline LegacyStrokeSampleExecutionResult execute_legacy_canvas_stroke_sample( const LegacyStrokeSampleExecutionRequest& request) { diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index 7eb3b01..d9e776b 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -1832,6 +1832,99 @@ void retained_stroke_commit_runner_clamps_malformed_step_count(pp::tests::Harnes PP_EXPECT(h, timelapse == 1); } +void retained_stroke_commit_input_binder_uses_sequence_slots(pp::tests::Harness& h) +{ + const auto sequence = plan_canvas_stroke_commit_sequence( + CanvasStrokeCommitRequest { + .erase_mode = false, + .alpha_locked = false, + .selection_mask_active = true, + .dual_stroke_enabled = true, + .pattern_enabled = true, + }); + + std::vector active_slots; + std::vector bound_textures; + std::vector> bound_samplers; + + pp::panopainter::bind_legacy_canvas_stroke_commit_inputs( + sequence, + [&](int texture_slot) { + active_slots.push_back(texture_slot); + }, + [&](CanvasStrokeCommitTextureRole role) { + bound_textures.push_back(role); + }, + [&](CanvasStrokeCommitTextureRole role, int texture_slot) { + bound_samplers.emplace_back(role, texture_slot); + }); + + PP_EXPECT(h, active_slots.size() == 5U); + PP_EXPECT(h, active_slots[0] == 0); + PP_EXPECT(h, active_slots[1] == 1); + PP_EXPECT(h, active_slots[2] == 2); + PP_EXPECT(h, active_slots[3] == 3); + PP_EXPECT(h, active_slots[4] == 4); + PP_EXPECT(h, bound_textures.size() == 5U); + PP_EXPECT(h, bound_textures[0] == CanvasStrokeCommitTextureRole::layer_scratch); + PP_EXPECT(h, bound_textures[1] == CanvasStrokeCommitTextureRole::stroke); + PP_EXPECT(h, bound_textures[2] == CanvasStrokeCommitTextureRole::selection_mask); + PP_EXPECT(h, bound_textures[3] == CanvasStrokeCommitTextureRole::dual_stroke); + PP_EXPECT(h, bound_textures[4] == CanvasStrokeCommitTextureRole::pattern); + PP_EXPECT(h, bound_samplers.size() == 5U); + PP_EXPECT(h, bound_samplers[0].first == CanvasStrokeCommitTextureRole::layer_scratch); + PP_EXPECT(h, bound_samplers[0].second == 0); + PP_EXPECT(h, bound_samplers[4].first == CanvasStrokeCommitTextureRole::pattern); + PP_EXPECT(h, bound_samplers[4].second == 4); +} + +void retained_stroke_commit_dilate_copy_uses_layer_scratch_slot(pp::tests::Harness& h) +{ + const auto sequence = plan_canvas_stroke_commit_sequence( + CanvasStrokeCommitRequest { + .erase_mode = true, + .alpha_locked = true, + .selection_mask_active = false, + .dual_stroke_enabled = false, + .pattern_enabled = false, + }); + + int setup_calls = 0; + std::vector active_slots; + int bind_layer_scratch_calls = 0; + std::vector copy_regions; + + pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source( + sequence, + [&]() { ++setup_calls; }, + [&](int texture_slot) { active_slots.push_back(texture_slot); }, + [&]() { ++bind_layer_scratch_calls; }, + [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) { + copy_regions.push_back(pp::paint_renderer::CanvasStrokeCopyRegion { + .x = src_x, + .y = src_y, + .width = width, + .height = height, + }); + PP_EXPECT(h, dst_x == 0); + PP_EXPECT(h, dst_y == 0); + }, + pp::panopainter::LegacyCanvasStrokeCommitCopyExtent { + .width = 256, + .height = 128, + }); + + PP_EXPECT(h, setup_calls == 1); + PP_EXPECT(h, active_slots.size() == 1U); + PP_EXPECT(h, active_slots[0] == 0); + PP_EXPECT(h, bind_layer_scratch_calls == 1); + PP_EXPECT(h, copy_regions.size() == 1U); + PP_EXPECT(h, copy_regions[0].x == 0); + PP_EXPECT(h, copy_regions[0].y == 0); + PP_EXPECT(h, copy_regions[0].width == 256); + PP_EXPECT(h, copy_regions[0].height == 128); +} + void plans_stroke_preview_composite_for_simple_brush(pp::tests::Harness& h) { const auto plan = plan_stroke_preview_composite(StrokePreviewCompositeRequest {}); @@ -2344,6 +2437,12 @@ int main() harness.run( "retained_stroke_commit_runner_clamps_malformed_step_count", retained_stroke_commit_runner_clamps_malformed_step_count); + harness.run( + "retained_stroke_commit_input_binder_uses_sequence_slots", + retained_stroke_commit_input_binder_uses_sequence_slots); + harness.run( + "retained_stroke_commit_dilate_copy_uses_layer_scratch_slot", + retained_stroke_commit_dilate_copy_uses_layer_scratch_slot); 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);