diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index f3a31d9..594663b 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -89,6 +89,9 @@ agent or engineer to remove them without reconstructing context from chat. - 2026-06-13: `LATER-003` was narrowed again. The retained stroke mix shell now has direct executor regression coverage in `retained_stroke_mix_pass_shell_builder_preserves_combined_wiring`. +- 2026-06-13: `LATER-003` was narrowed again. The retained stroke mix shell + now also has isolated executor regression coverage in + `retained_stroke_mix_pass_shell_executor_preserves_combined_wiring`. - 2026-06-13: `DEBT-0036` was narrowed again. `NodeStrokePreview::draw_stroke_immediate()` now routes final composite execution and preview copy-back through a retained local wrapper, leaving the call site with only sequence wiring. diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 44a4ebb..852878d 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -632,6 +632,9 @@ Progress Notes: - 2026-06-13: `retained_stroke_mix_pass_shell_builder_preserves_combined_wiring` now also exercises the direct shell executor path, locking the combined mix shell boundary with call-order regression coverage. +- 2026-06-13: `retained_stroke_mix_pass_shell_executor_preserves_combined_wiring` + now covers the direct shell executor path separately, keeping the shell + boundary regression isolated from the builder coverage. - 2026-06-13: `Canvas::stroke_draw_samples()` now reuses a retained destination texture dispatch helper for the live sample path; `Canvas` still owns the concrete face textures and callback execution. diff --git a/src/canvas.cpp b/src/canvas.cpp index fd04535..f91ed37 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -386,63 +386,43 @@ void Canvas::stroke_cancel() m_current_stroke = nullptr; m_show_tmp = false; } -static void execute_canvas_stroke_mix_pass( - Canvas& canvas, - const glm::vec2& bb_min, - const glm::vec2& bb_sz, - const std::array& plane_transform, - const std::vector>& layers, - std::size_t layer_index, - const Stroke* current_stroke) +void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz) { - auto& current_layer = *layers[layer_index]; + gl_state gl; + gl.save(); + const auto layer_index = m_current_layer_idx; + auto& current_layer = *m_layers[layer_index]; + std::array plane_transform {}; + std::copy(std::begin(m_plane_transform), std::end(m_plane_transform), plane_transform.begin()); const auto mix_planes = pp::panopainter::plan_legacy_canvas_stroke_mix_pass_planes( current_layer.m_visible, current_layer.m_opacity, - glm::scale(glm::vec3(1, -1, 1)) * canvas.m_proj * canvas.m_mv, + glm::scale(glm::vec3(1, -1, 1)) * m_proj * m_mv, plane_transform, [&](int plane_index) { return current_layer.face(plane_index); }); - const auto& b = current_stroke->m_brush; const auto mix_shell = pp::panopainter::make_legacy_canvas_stroke_mix_pass_shell( [&] { - canvas.m_mixer.bindFramebuffer(); - canvas.apply_canvas_viewport(0, 0, canvas.m_mixer.getWidth(), canvas.m_mixer.getHeight()); - canvas.apply_canvas_capability(canvas.depth_test_state(), false); - canvas.apply_canvas_capability(canvas.scissor_test_state(), true); - canvas.apply_canvas_capability(canvas.blend_state(), false); - canvas.apply_canvas_scissor( + m_mixer.bindFramebuffer(); + apply_canvas_viewport(0, 0, m_mixer.getWidth(), m_mixer.getHeight()); + apply_canvas_capability(depth_test_state(), false); + apply_canvas_capability(scissor_test_state(), true); + apply_canvas_capability(blend_state(), false); + apply_canvas_scissor( static_cast(bb_min.x), static_cast(bb_min.y), static_cast(bb_sz.x), static_cast(bb_sz.y)); }, [&] { - canvas.m_mixer.unbindFramebuffer(); + m_mixer.unbindFramebuffer(); }); [[maybe_unused]] const auto mix_result = pp::panopainter::execute_legacy_canvas_stroke_mix_pass_shell( mix_shell.setup.begin, mix_shell.setup.end, mix_shell.request); (void)mix_planes; - (void)b; -} - -void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz) -{ - gl_state gl; - gl.save(); - std::array plane_transform {}; - std::copy(std::begin(m_plane_transform), std::end(m_plane_transform), plane_transform.begin()); - execute_canvas_stroke_mix_pass( - *this, - bb_min, - bb_sz, - plane_transform, - m_layers, - m_current_layer_idx, - m_current_stroke); gl.restore(); } diff --git a/tests/paint_renderer/stroke_execution_tests.cpp b/tests/paint_renderer/stroke_execution_tests.cpp index 597007e..f07e721 100644 --- a/tests/paint_renderer/stroke_execution_tests.cpp +++ b/tests/paint_renderer/stroke_execution_tests.cpp @@ -1959,15 +1959,6 @@ void retained_stroke_mix_pass_shell_builder_preserves_combined_wiring(pp::tests: shell.request.unbind_mix_samplers(); shell.setup.end(); - events.clear(); - shell.setup.begin(); - const auto shell_result = pp::panopainter::execute_legacy_canvas_stroke_mix_pass_shell( - shell.setup.begin, - shell.setup.end, - shell.request); - PP_EXPECT(h, shell_result.ok); - PP_EXPECT(h, shell_result.composed_planes == 1U); - const std::vector expected_events { "begin", "bind-samplers", @@ -1985,6 +1976,68 @@ void retained_stroke_mix_pass_shell_builder_preserves_combined_wiring(pp::tests: PP_EXPECT(h, events == expected_events); } +void retained_stroke_mix_pass_shell_executor_preserves_combined_wiring(pp::tests::Harness& h) +{ + const std::array planes { + LegacyCanvasStrokeMixPassPlane { .index = 5, .visible = true, .has_target = true, .opacity = 0.5F }, + }; + + std::vector events; + const auto shell = pp::panopainter::make_legacy_canvas_stroke_mix_pass_shell( + [&] { events.emplace_back("begin"); }, + [&] { events.emplace_back("end"); }, + "mix-shell-executor", + glm::vec2(32.0F, 16.0F), + planes, + [&] { events.emplace_back("bind-samplers"); }, + [&] { events.emplace_back("unbind-samplers"); }, + [&](int plane_index, const glm::mat4&) { + events.emplace_back("setup:" + std::to_string(plane_index)); + }, + [&](int plane_index) { + events.emplace_back("bind-layer:" + std::to_string(plane_index)); + }, + [&](int plane_index) { + events.emplace_back("bind-stroke:" + std::to_string(plane_index)); + }, + [&](int plane_index) { + events.emplace_back("bind-mask:" + std::to_string(plane_index)); + }, + [&] { events.emplace_back("draw"); }, + [&](int plane_index) { + events.emplace_back("unbind-mask:" + std::to_string(plane_index)); + }, + [&](int plane_index) { + events.emplace_back("unbind-stroke:" + std::to_string(plane_index)); + }, + [&](int plane_index) { + events.emplace_back("unbind-layer:" + std::to_string(plane_index)); + }); + + const auto shell_result = pp::panopainter::execute_legacy_canvas_stroke_mix_pass_shell( + shell.setup.begin, + shell.setup.end, + shell.request); + + PP_EXPECT(h, shell_result.ok); + PP_EXPECT(h, shell_result.composed_planes == 1U); + const std::vector expected_events { + "begin", + "bind-samplers", + "setup:5", + "bind-layer:5", + "bind-stroke:5", + "bind-mask:5", + "draw", + "unbind-mask:5", + "unbind-stroke:5", + "unbind-layer:5", + "unbind-samplers", + "end", + }; + PP_EXPECT(h, events == expected_events); +} + } // namespace int main() @@ -2098,5 +2151,8 @@ int main() harness.run( "retained_stroke_mix_pass_shell_builder_preserves_combined_wiring", retained_stroke_mix_pass_shell_builder_preserves_combined_wiring); + harness.run( + "retained_stroke_mix_pass_shell_executor_preserves_combined_wiring", + retained_stroke_mix_pass_shell_executor_preserves_combined_wiring); return harness.finish(); }