From c51f79eee32eea0b056ada8a2a6bafbe0873f735 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Sat, 13 Jun 2026 16:45:35 +0200 Subject: [PATCH] Refine stroke mix pass boundary --- docs/modernization/tasks.md | 8 + src/canvas.cpp | 173 +++++++++--------- src/legacy_canvas_stroke_execution_services.h | 39 ++++ .../paint_renderer/stroke_execution_tests.cpp | 74 ++++++++ 4 files changed, 204 insertions(+), 90 deletions(-) diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 8362631..b7bd84e 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -509,6 +509,14 @@ Done Checks: Progress Notes: +- 2026-06-13: `Canvas::stroke_draw_mix()` now routes mix-pass plane planning + through `plan_legacy_canvas_stroke_mix_pass_planes(...)` and wraps retained + framebuffer setup/teardown with + `execute_legacy_canvas_stroke_mix_pass_with_setup(...)`; the live adapter + still owns concrete sampler, texture, and draw callbacks. `pp_paint_renderer_stroke_execution_tests` + now covers the default dirty-union path for live-pass face-framebuffer + execution. Next slice should target the remaining Canvas stroke execution + seam without reopening the landed mix-pass setup helper. - 2026-06-13: `Canvas::draw_merge()` now routes per-plane merge-target clear, blend-state gating, and optional checkerboard prepass through `execute_legacy_canvas_draw_merge_plane_setup(...)`; per-layer iteration, diff --git a/src/canvas.cpp b/src/canvas.cpp index ee0b4f3..fc7492f 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -390,97 +390,90 @@ void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz) { gl_state gl; gl.save(); - - 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)); - - auto layer_index = m_current_layer_idx; - std::array mix_planes {}; - const auto mix_mvp_base = - glm::scale(glm::vec3(1, -1, 1)) * - m_proj * m_mv; - for (int plane_index = 0; plane_index < 6; ++plane_index) - { - mix_planes[plane_index] = pp::panopainter::LegacyCanvasStrokeMixPassPlane { - .index = plane_index, - .visible = m_layers[layer_index]->m_visible, - .has_target = static_cast(m_layers[layer_index]->face(plane_index)), - .opacity = m_layers[layer_index]->m_opacity, - .mvp = mix_mvp_base * - m_plane_transform[plane_index] * - glm::translate(glm::vec3(0, 0, -1)), - }; - } - - const auto& b = m_current_stroke->m_brush; - [[maybe_unused]] const auto mix_result = pp::panopainter::execute_legacy_canvas_stroke_mix_pass( - pp::panopainter::LegacyCanvasStrokeMixPassRequest { - .context = "Canvas::stroke_draw_mix", - .resolution = m_size, - .planes = mix_planes, - .bind_mix_samplers = [&] { - m_sampler.bind(0); - m_sampler.bind(1); - m_sampler.bind(2); - }, - .unbind_mix_samplers = [&] { - m_sampler.unbind(); - }, - .setup_plane_shader = [&](int plane_index, const glm::mat4& plane_mvp_z) { - (void)plane_index; - pp::panopainter::setup_legacy_stroke_composite_shader( - pp::panopainter::LegacyStrokeCompositeUniforms { - .resolution = m_size, - .mvp = plane_mvp_z, - .pattern_texture_slot = 3, - .layer_alpha = 1.0f, - .alpha_lock = false, /*m_layers[layer_index]->m_alpha_locked*/ - .mask_enabled = false, /*m_smask_active*/ - .use_fragcoord = false, - .blend_mode = b->m_blend_mode, - .use_dual = false, - .use_pattern = false, - }); - }, - .bind_layer_texture = [&](int plane_index) { - set_active_texture_unit(0); - m_layers[layer_index]->rtt(plane_index).bindTexture(); - }, - .bind_stroke_texture = [&](int plane_index) { - set_active_texture_unit(1); - m_tmp[plane_index].bindTexture(); - }, - .bind_mask_texture = [&](int plane_index) { - set_active_texture_unit(2); - m_smask.rtt(plane_index).bindTexture(); - }, - .draw_plane = [&] { - m_node->m_face_plane.draw_fill(); - }, - .unbind_mask_texture = [&](int plane_index) { - set_active_texture_unit(2); - m_smask.rtt(plane_index).unbindTexture(); - }, - .unbind_stroke_texture = [&](int plane_index) { - set_active_texture_unit(1); - m_tmp[plane_index].unbindTexture(); - }, - .unbind_layer_texture = [&](int plane_index) { - set_active_texture_unit(0); - m_layers[layer_index]->rtt(plane_index).unbindTexture(); - }, + const auto layer_index = m_current_layer_idx; + auto& current_layer = *m_layers[layer_index]; + 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)) * m_proj * m_mv, + m_plane_transform, + [&](int plane_index) { + return current_layer.face(plane_index); }); - m_mixer.unbindFramebuffer(); + const auto& b = m_current_stroke->m_brush; + [[maybe_unused]] const auto mix_result = + pp::panopainter::execute_legacy_canvas_stroke_mix_pass_with_setup( + [&] { + 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)); + }, + [&] { + m_mixer.unbindFramebuffer(); + }, + pp::panopainter::LegacyCanvasStrokeMixPassRequest { + .context = "Canvas::stroke_draw_mix", + .resolution = m_size, + .planes = mix_planes, + .bind_mix_samplers = [&] { + m_sampler.bind(0); + m_sampler.bind(1); + m_sampler.bind(2); + }, + .unbind_mix_samplers = [&] { + m_sampler.unbind(); + }, + .setup_plane_shader = [&](int plane_index, const glm::mat4& plane_mvp_z) { + (void)plane_index; + pp::panopainter::setup_legacy_stroke_composite_shader( + pp::panopainter::LegacyStrokeCompositeUniforms { + .resolution = m_size, + .mvp = plane_mvp_z, + .pattern_texture_slot = 3, + .layer_alpha = 1.0f, + .alpha_lock = false, /*current_layer.m_alpha_locked*/ + .mask_enabled = false, /*m_smask_active*/ + .use_fragcoord = false, + .blend_mode = b->m_blend_mode, + .use_dual = false, + .use_pattern = false, + }); + }, + .bind_layer_texture = [&](int plane_index) { + set_active_texture_unit(0); + current_layer.rtt(plane_index).bindTexture(); + }, + .bind_stroke_texture = [&](int plane_index) { + set_active_texture_unit(1); + m_tmp[plane_index].bindTexture(); + }, + .bind_mask_texture = [&](int plane_index) { + set_active_texture_unit(2); + m_smask.rtt(plane_index).bindTexture(); + }, + .draw_plane = [&] { + m_node->m_face_plane.draw_fill(); + }, + .unbind_mask_texture = [&](int plane_index) { + set_active_texture_unit(2); + m_smask.rtt(plane_index).unbindTexture(); + }, + .unbind_stroke_texture = [&](int plane_index) { + set_active_texture_unit(1); + m_tmp[plane_index].unbindTexture(); + }, + .unbind_layer_texture = [&](int plane_index) { + set_active_texture_unit(0); + current_layer.rtt(plane_index).unbindTexture(); + }, + }); gl.restore(); } diff --git a/src/legacy_canvas_stroke_execution_services.h b/src/legacy_canvas_stroke_execution_services.h index 2dea4b1..b6d431a 100644 --- a/src/legacy_canvas_stroke_execution_services.h +++ b/src/legacy_canvas_stroke_execution_services.h @@ -179,6 +179,45 @@ struct LegacyCanvasStrokeMixPassResult { std::size_t composed_planes = 0; }; +[[nodiscard]] inline LegacyCanvasStrokeMixPassResult execute_legacy_canvas_stroke_mix_pass( + const LegacyCanvasStrokeMixPassRequest& request); + +template +[[nodiscard]] inline std::array +plan_legacy_canvas_stroke_mix_pass_planes( + bool visible, + float opacity, + const glm::mat4& mvp_base, + const std::array& plane_transforms, + HasTarget&& has_target, + const glm::mat4& plane_offset = glm::translate(glm::vec3(0.0f, 0.0f, -1.0f))) +{ + std::array planes {}; + for (std::size_t plane_index = 0; plane_index < PlaneCount; ++plane_index) { + planes[plane_index] = LegacyCanvasStrokeMixPassPlane { + .index = static_cast(plane_index), + .visible = visible, + .has_target = static_cast(has_target(static_cast(plane_index))), + .opacity = opacity, + .mvp = mvp_base * plane_transforms[plane_index] * plane_offset, + }; + } + return planes; +} + +template +[[nodiscard]] inline LegacyCanvasStrokeMixPassResult +execute_legacy_canvas_stroke_mix_pass_with_setup( + BeginMixPass&& begin_mix_pass, + EndMixPass&& end_mix_pass, + const LegacyCanvasStrokeMixPassRequest& request) +{ + begin_mix_pass(); + const auto result = execute_legacy_canvas_stroke_mix_pass(request); + end_mix_pass(); + return result; +} + struct LegacyCanvasStrokeComputeRequest { StrokeSample previous_sample {}; std::span samples; diff --git a/tests/paint_renderer/stroke_execution_tests.cpp b/tests/paint_renderer/stroke_execution_tests.cpp index 20e1344..7feb854 100644 --- a/tests/paint_renderer/stroke_execution_tests.cpp +++ b/tests/paint_renderer/stroke_execution_tests.cpp @@ -984,6 +984,77 @@ void retained_stroke_live_pass_clears_before_traversal_and_copies_afterwards(pp: PP_EXPECT(h, pass_dirty_faces[2]); } +void retained_stroke_live_pass_retains_previous_pass_dirty_union_without_override(pp::tests::Harness& h) +{ + StrokeFrame frame; + frame.id = 11; + frame.shapes[1] = { + vertex_t(glm::vec2(4.0F, 5.0F)), + vertex_t(glm::vec2(6.0F, 5.0F)), + vertex_t(glm::vec2(6.0F, 7.0F)), + }; + + std::array frames { frame }; + std::array accumulated_dirty_boxes; + std::array pass_dirty_boxes; + accumulated_dirty_boxes.fill(glm::vec4(64.0F, 64.0F, 0.0F, 0.0F)); + pass_dirty_boxes.fill(glm::vec4(64.0F, 64.0F, 0.0F, 0.0F)); + accumulated_dirty_boxes[1] = glm::vec4(7.0F, 8.0F, 9.0F, 10.0F); + pass_dirty_boxes[1] = glm::vec4(20.0F, 20.0F, 21.0F, 21.0F); + std::array include_in_committed_dirty_box { true, true, true, true, true, true }; + std::array committed_dirty_faces {}; + std::array pass_dirty_faces {}; + + std::vector events; + std::array face_framebuffers {}; + for (int face_index = 0; face_index < 6; ++face_index) { + face_framebuffers[face_index].events = &events; + face_framebuffers[face_index].face_index = face_index; + } + + const auto executed_faces = pp::panopainter::execute_legacy_canvas_stroke_live_pass_with_face_framebuffers( + frames, + pp::renderer::Extent2D { .width = 64, .height = 64 }, + accumulated_dirty_boxes, + pass_dirty_boxes, + include_in_committed_dirty_box, + [&](StrokeFrame& current_frame) { + events.push_back("begin-frame:" + std::to_string(current_frame.id)); + }, + [&](StrokeFrame&, int face_index, std::span vertices) { + events.push_back( + "prepare:" + std::to_string(face_index) + ":" + std::to_string(vertices.size())); + }, + [&](StrokeFrame&, int face_index, std::span) { + events.push_back("execute:" + std::to_string(face_index)); + return glm::vec4(1.0F, 2.0F, 3.0F, 4.0F); + }, + face_framebuffers, + false, + committed_dirty_faces, + pass_dirty_faces); + + PP_EXPECT(h, executed_faces == 1U); + const std::vector expected_events { + "begin-frame:11", + "prepare:1:3", + "bind:1", + "execute:1", + "unbind:1", + }; + PP_EXPECT(h, events == expected_events); + PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[1].x, 1.0F)); + PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[1].y, 2.0F)); + PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[1].z, 9.0F)); + PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[1].w, 10.0F)); + PP_EXPECT(h, nearly_equal(pass_dirty_boxes[1].x, 1.0F)); + PP_EXPECT(h, nearly_equal(pass_dirty_boxes[1].y, 2.0F)); + PP_EXPECT(h, nearly_equal(pass_dirty_boxes[1].z, 21.0F)); + PP_EXPECT(h, nearly_equal(pass_dirty_boxes[1].w, 21.0F)); + PP_EXPECT(h, committed_dirty_faces[1]); + PP_EXPECT(h, pass_dirty_faces[1]); +} + void retained_stroke_pad_executor_copies_destination_for_dirty_faces_only(pp::tests::Harness& h) { const std::array dirty_faces { true, false, true }; @@ -1265,6 +1336,9 @@ int main() harness.run( "retained_stroke_live_pass_clears_before_traversal_and_copies_afterwards", retained_stroke_live_pass_clears_before_traversal_and_copies_afterwards); + harness.run( + "retained_stroke_live_pass_retains_previous_pass_dirty_union_without_override", + retained_stroke_live_pass_retains_previous_pass_dirty_union_without_override); harness.run( "retained_stroke_pad_executor_copies_destination_for_dirty_faces_only", retained_stroke_pad_executor_copies_destination_for_dirty_faces_only);