diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 59cf75b..39654b6 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -38,6 +38,11 @@ agent or engineer to remove them without reconstructing context from chat. through `execute_legacy_canvas_stroke_face_sample_polygon(...)`; the retained sample executor owns the face-aware dispatch contract while `Canvas` keeps only concrete GL object callbacks. +- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` non-erase + live temporary-stroke composite now routes setup, sampler bind, texture + bind, draw, and texture unbind ordering through + `execute_legacy_canvas_stroke_temporary_composite(...)`; erase-path and + broader final composite ownership remain retained in `Canvas`. - 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix()` now routes mixer framebuffer bind/unbind, viewport/scissor/blend state, texture-slot binding, and final plane draw through one local helper; diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 276fe9c..b9a0a2a 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -3137,6 +3137,10 @@ Results: `execute_legacy_canvas_stroke_face_sample_polygon(...)` for face-indexed destination bind/copy/unbind and brush upload/draw dispatch, while concrete GL object callbacks remain in the legacy Canvas path. +- `Canvas::draw_merge()` non-erase live temporary-stroke composite now shares + `execute_legacy_canvas_stroke_temporary_composite(...)` for setup, sampler + bind, texture bind, draw, and texture unbind ordering, while erase-path and + broader final composite ownership remain in the legacy Canvas path. - `NodeStrokePreview::stroke_draw_mix()` now shares one local helper for mixer framebuffer bind/unbind, viewport/scissor/blend state, texture-slot binding, and final plane draw, while material planning and shader uniform diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 0f99e14..5674155 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -509,6 +509,13 @@ Done Checks: Progress Notes: +- 2026-06-13: `Canvas::draw_merge()` non-erase live temporary-stroke + composite ordering now routes through + `execute_legacy_canvas_stroke_temporary_composite(...)`; erase-path and + broader final composite ownership still remain local to `Canvas`. Next slice + should target the erase temporary composite branch or another similarly + narrow final composite seam without reopening landed sample, mix, dirty, or + framebuffer helpers. - 2026-06-13: `Canvas::stroke_draw_samples()` now routes face-indexed destination bind/copy/unbind and brush upload/draw through `execute_legacy_canvas_stroke_face_sample_polygon(...)`; the retained sample diff --git a/src/canvas.cpp b/src/canvas.cpp index d09e51a..f410b10 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -1438,64 +1438,73 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array faces /*= SI } else if (m_current_stroke && m_show_tmp && m_current_layer_idx == layer_index) { - m_sampler.bind(0); - m_sampler.bind(1); - m_sampler.bind(2); - m_sampler.bind(3); - m_sampler_stencil.bind(4); - const auto stroke_material = canvas_stroke_material_plan(*b, false); 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 = Canvas::I->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 = Canvas::I->m_pattern_offset, - }, - .mvp = ortho, - .layer_alpha = m_layers[layer_index]->m_opacity, - .alpha_lock = m_layers[layer_index]->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_temporary_composite( + [&] { + pp::panopainter::setup_legacy_stroke_composite_shader( + pp::panopainter::LegacyStrokeCompositeUniforms { + .resolution = Canvas::I->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 = Canvas::I->m_pattern_offset, + }, + .mvp = ortho, + .layer_alpha = m_layers[layer_index]->m_opacity, + .alpha_lock = m_layers[layer_index]->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_sampler.bind(0); + m_sampler.bind(1); + m_sampler.bind(2); + m_sampler.bind(3); + m_sampler_stencil.bind(4); + }, + [&] { + set_active_texture_unit(0); + m_layers[layer_index]->rtt(plane_index).bindTexture(); + set_active_texture_unit(1); + m_tmp[plane_index].bindTexture(); + set_active_texture_unit(2); + m_smask.rtt(plane_index).bindTexture(); + set_active_texture_unit(3); + if (stroke_material.composite_pass.use_dual) + m_tmp_dual[plane_index].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[plane_index].unbindTexture(); + set_active_texture_unit(2); + m_smask.rtt(plane_index).unbindTexture(); + set_active_texture_unit(1); + m_tmp[plane_index].unbindTexture(); + set_active_texture_unit(0); + m_layers[layer_index]->rtt(plane_index).unbindTexture(); }); - - set_active_texture_unit(0); - m_layers[layer_index]->rtt(plane_index).bindTexture(); - set_active_texture_unit(1); - m_tmp[plane_index].bindTexture(); - set_active_texture_unit(2); - m_smask.rtt(plane_index).bindTexture(); - set_active_texture_unit(3); - if (stroke_material.composite_pass.use_dual) - m_tmp_dual[plane_index].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[plane_index].unbindTexture(); - set_active_texture_unit(2); - m_smask.rtt(plane_index).unbindTexture(); - set_active_texture_unit(1); - m_tmp[plane_index].unbindTexture(); - set_active_texture_unit(0); - m_layers[layer_index]->rtt(plane_index).unbindTexture(); } else { diff --git a/src/legacy_canvas_stroke_execution_services.h b/src/legacy_canvas_stroke_execution_services.h index 6639ad8..ba2ea4a 100644 --- a/src/legacy_canvas_stroke_execution_services.h +++ b/src/legacy_canvas_stroke_execution_services.h @@ -656,6 +656,26 @@ std::size_t execute_legacy_canvas_stroke_live_pass_with_face_framebuffers( pass_dirty_faces); } +template < + typename SetupCompositeShader, + typename BindCompositeSamplers, + typename BindCompositeTextures, + typename DrawComposite, + typename UnbindCompositeTextures> +void execute_legacy_canvas_stroke_temporary_composite( + SetupCompositeShader&& setup_composite_shader, + BindCompositeSamplers&& bind_composite_samplers, + BindCompositeTextures&& bind_composite_textures, + DrawComposite&& draw_composite, + UnbindCompositeTextures&& unbind_composite_textures) +{ + setup_composite_shader(); + bind_composite_samplers(); + bind_composite_textures(); + draw_composite(); + unbind_composite_textures(); +} + [[nodiscard]] inline pp::paint_renderer::CanvasStrokeBox legacy_canvas_stroke_box(glm::vec4 box) noexcept { return pp::paint_renderer::CanvasStrokeBox { diff --git a/tests/paint_renderer/stroke_execution_tests.cpp b/tests/paint_renderer/stroke_execution_tests.cpp index 950ef44..979a36a 100644 --- a/tests/paint_renderer/stroke_execution_tests.cpp +++ b/tests/paint_renderer/stroke_execution_tests.cpp @@ -709,6 +709,27 @@ void retained_stroke_preview_texture_copy_binds_before_copy(pp::tests::Harness& PP_EXPECT(h, copy_args[5] == 64); } +void retained_stroke_temporary_composite_preserves_retained_call_order(pp::tests::Harness& h) +{ + std::vector events; + + pp::panopainter::execute_legacy_canvas_stroke_temporary_composite( + [&]() { events.emplace_back("setup"); }, + [&]() { events.emplace_back("bind-samplers"); }, + [&]() { events.emplace_back("bind-textures"); }, + [&]() { events.emplace_back("draw"); }, + [&]() { events.emplace_back("unbind-textures"); }); + + const std::vector expected_events { + "setup", + "bind-samplers", + "bind-textures", + "draw", + "unbind-textures", + }; + PP_EXPECT(h, events == expected_events); +} + void retained_stroke_mix_pass_skips_inactive_planes_and_preserves_texture_order(pp::tests::Harness& h) { const std::array planes { @@ -825,6 +846,9 @@ int main() harness.run( "retained_stroke_preview_texture_copy_binds_before_copy", retained_stroke_preview_texture_copy_binds_before_copy); + harness.run( + "retained_stroke_temporary_composite_preserves_retained_call_order", + retained_stroke_temporary_composite_preserves_retained_call_order); harness.run( "retained_stroke_mix_pass_skips_inactive_planes_and_preserves_texture_order", retained_stroke_mix_pass_skips_inactive_planes_and_preserves_texture_order);