diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 39654b6..b895551 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -43,6 +43,11 @@ agent or engineer to remove them without reconstructing context from chat. 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::draw_stroke_immediate()` + now routes dual-pass/background/main-pass/final-composite/copy-back ordering + through `execute_legacy_node_stroke_preview_pass_sequence(...)`; the + remaining local preview ownership is concentrated around `stroke_draw_mix()` + setup/execution. - 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 b9a0a2a..6b1c4ee 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -3141,6 +3141,11 @@ Results: `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::draw_stroke_immediate()` now shares + `execute_legacy_node_stroke_preview_pass_sequence(...)` for + dual-pass/background/main-pass/final-composite/copy-back ordering, while the + remaining local preview ownership is concentrated around `stroke_draw_mix()` + setup/execution. - `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 5674155..38fff45 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -509,6 +509,13 @@ Done Checks: Progress Notes: +- 2026-06-13: `NodeStrokePreview::draw_stroke_immediate()` now routes + dual-pass/background/main-pass/final-composite/copy-back ordering through + `execute_legacy_node_stroke_preview_pass_sequence(...)`; the remaining local + preview ownership is concentrated around `stroke_draw_mix()` setup/execution. + Next slice should target that final mix-pass setup/execution seam without + reopening landed sample, binding, material-planning, or final-composite + helpers. - 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 diff --git a/src/legacy_node_stroke_preview_execution_services.h b/src/legacy_node_stroke_preview_execution_services.h index 24b1336..7c4e6d1 100644 --- a/src/legacy_node_stroke_preview_execution_services.h +++ b/src/legacy_node_stroke_preview_execution_services.h @@ -130,4 +130,45 @@ struct LegacyNodeStrokePreviewMixPassRequest { return plan; } +struct LegacyNodeStrokePreviewPassSequenceRequest { + bool dual_pass_enabled = false; + std::function prepare_dual_pass; + std::function execute_dual_pass; + std::function capture_background; + std::function prepare_main_pass; + std::function execute_main_pass; + std::function finish_main_pass; + std::function execute_final_composite; + std::function copy_preview_result; +}; + +[[nodiscard]] inline bool execute_legacy_node_stroke_preview_pass_sequence( + const LegacyNodeStrokePreviewPassSequenceRequest& request) +{ + if (!request.capture_background || + !request.prepare_main_pass || + !request.execute_main_pass || + !request.finish_main_pass || + !request.execute_final_composite || + !request.copy_preview_result) { + return false; + } + + if (request.dual_pass_enabled) { + if (!request.prepare_dual_pass || !request.execute_dual_pass) { + return false; + } + request.prepare_dual_pass(); + request.execute_dual_pass(); + } + + request.capture_background(); + request.prepare_main_pass(); + request.execute_main_pass(); + request.finish_main_pass(); + request.execute_final_composite(); + request.copy_preview_result(); + return true; +} + } // namespace pp::panopainter diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index a2b45a5..87a50a1 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -797,102 +797,107 @@ void NodeStrokePreview::draw_stroke_immediate() .set_opacity = false, }); - // DRAW DUAL BRUSH - - if (material.dual_pass.enabled) - { - pp::panopainter::setup_legacy_stroke_dual_shader(material.dual_pass.uses_pattern); - bind_stroke_preview_dual_pass_textures(*dual_brush); - execute_stroke_preview_live_pass( - m_tex_dual, - size, - copy_stroke_destination, - [&] { - m_rtt.clear(); + const bool sequence_ok = pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( + pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest { + .dual_pass_enabled = material.dual_pass.enabled, + .prepare_dual_pass = [&] { + pp::panopainter::setup_legacy_stroke_dual_shader(material.dual_pass.uses_pattern); + bind_stroke_preview_dual_pass_textures(*dual_brush); }, - [&] { - return stroke_draw_compute(m_dual_stroke, zoom); + .execute_dual_pass = [&] { + execute_stroke_preview_live_pass( + m_tex_dual, + size, + copy_stroke_destination, + [&] { + m_rtt.clear(); + }, + [&] { + return stroke_draw_compute(m_dual_stroke, zoom); + }, + [](auto& frame) { + frame.col = { 0, 0, 0, 1 }; + }, + [&](auto& frame, Texture2D& blend_texture, bool copy_destination) { + /*auto rect =*/ stroke_draw_samples(frame.shapes, blend_texture, copy_destination); + }); }, - [](auto& frame) { - frame.col = { 0, 0, 0, 1 }; + .capture_background = [&] { + execute_stroke_preview_background_capture_pass( + size, + b->m_tip_mix > 0.f || b->m_blend_mode != 0, + m_tex_background, + [&] { + m_plane.draw_fill(); + }); }, - [&](auto& frame, Texture2D& blend_texture, bool copy_destination) { - /*auto rect =*/ stroke_draw_samples(frame.shapes, blend_texture, copy_destination); - }); - } + .prepare_main_pass = [&] { + pp::panopainter::use_legacy_stroke_shader(); + pp::panopainter::apply_legacy_stroke_blend_uniforms( + material.stroke_pass.uses_pattern, + b->m_tip_mix, + b->m_tip_wet, + b->m_tip_noise); - // CHEKCERBOARD + bind_stroke_preview_main_pass_textures( + *b, + m_tex, + m_rtt_mixer, + copy_stroke_destination, + preview_composite_plan.uses_mixer); + }, + .execute_main_pass = [&] { + execute_stroke_preview_live_pass( + m_tex, + size, + copy_stroke_destination, + [&] { + m_rtt.clear(); + }, + [&] { + return stroke_draw_compute(m_stroke, zoom); + }, + [&](auto& frame) { + if (b->m_tip_mix > 0.f) + { + stroke_draw_mix(xy(frame.m_mixer_rect), zw(frame.m_mixer_rect)); + } - execute_stroke_preview_background_capture_pass( - size, - b->m_tip_mix > 0.f || b->m_blend_mode != 0, - m_tex_background, - [&] { - m_plane.draw_fill(); - }); - - // DRAW MAIN BRUSH - - pp::panopainter::use_legacy_stroke_shader(); - pp::panopainter::apply_legacy_stroke_blend_uniforms( - material.stroke_pass.uses_pattern, - b->m_tip_mix, - b->m_tip_wet, - b->m_tip_noise); - - bind_stroke_preview_main_pass_textures( - *b, - m_tex, - m_rtt_mixer, - copy_stroke_destination, - preview_composite_plan.uses_mixer); - execute_stroke_preview_live_pass( - m_tex, - size, - copy_stroke_destination, - [&] { - m_rtt.clear(); - }, - [&] { - return stroke_draw_compute(m_stroke, zoom); - }, - [&](auto& frame) { - if (b->m_tip_mix > 0.f) - { - stroke_draw_mix(xy(frame.m_mixer_rect), zw(frame.m_mixer_rect)); - } - - frame.col = b->m_blend_mode != 0 || b->m_tip_mix > 0.f ? - glm::vec4 { .7, .4, .1, 1 } : - glm::vec4 { 0, 0, 0, 1 }; - frame.flow = glm::max(frame.flow, m_min_flow); - }, - [&](auto& frame, Texture2D& blend_texture, bool copy_destination) { - /*auto rect =*/ stroke_draw_samples(frame.shapes, blend_texture, copy_destination); - }); - set_active_texture_unit(stroke_preview_live_slots::kMixer); - m_rtt_mixer.unbindTexture(); - - // COMPOSITE - - execute_stroke_preview_final_composite_pass( - StrokePreviewCompositePassInputs { - .resolution = size, - .pattern_scale = patt_scale, - .brush = *b, - .composite_pass = material.composite_pass, - .background_texture = m_tex_background, - .stroke_texture = m_tex, - .dual_texture = m_tex_dual, - .linear_sampler = m_sampler_linear, - .repeat_sampler = m_sampler_linear_repeat, - .draw_composite = [&] { - m_plane.draw_fill(); + frame.col = b->m_blend_mode != 0 || b->m_tip_mix > 0.f ? + glm::vec4 { .7, .4, .1, 1 } : + glm::vec4 { 0, 0, 0, 1 }; + frame.flow = glm::max(frame.flow, m_min_flow); + }, + [&](auto& frame, Texture2D& blend_texture, bool copy_destination) { + /*auto rect =*/ stroke_draw_samples(frame.shapes, blend_texture, copy_destination); + }); + }, + .finish_main_pass = [&] { + set_active_texture_unit(stroke_preview_live_slots::kMixer); + m_rtt_mixer.unbindTexture(); + }, + .execute_final_composite = [&] { + execute_stroke_preview_final_composite_pass( + StrokePreviewCompositePassInputs { + .resolution = size, + .pattern_scale = patt_scale, + .brush = *b, + .composite_pass = material.composite_pass, + .background_texture = m_tex_background, + .stroke_texture = m_tex, + .dual_texture = m_tex_dual, + .linear_sampler = m_sampler_linear, + .repeat_sampler = m_sampler_linear_repeat, + .draw_composite = [&] { + m_plane.draw_fill(); + }, + }); + }, + .copy_preview_result = [&] { + copy_stroke_preview_result_to_texture(m_tex_preview, size); }, }); - - // copy the result to the actual preview - copy_stroke_preview_result_to_texture(m_tex_preview, size); + assert(sequence_ok); m_rtt.unbindFramebuffer(); diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index b322e43..04d9755 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -2240,6 +2240,72 @@ void legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and PP_EXPECT(h, plan.shader.use_pattern == plan.material.composite_pass.use_pattern); } +void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order(pp::tests::Harness& h) +{ + std::vector steps; + const auto run_sequence = [&](bool dual_enabled) { + steps.clear(); + const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( + pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest { + .dual_pass_enabled = dual_enabled, + .prepare_dual_pass = [&] { + steps.emplace_back("prepare_dual"); + }, + .execute_dual_pass = [&] { + steps.emplace_back("execute_dual"); + }, + .capture_background = [&] { + steps.emplace_back("capture_background"); + }, + .prepare_main_pass = [&] { + steps.emplace_back("prepare_main"); + }, + .execute_main_pass = [&] { + steps.emplace_back("execute_main"); + }, + .finish_main_pass = [&] { + steps.emplace_back("finish_main"); + }, + .execute_final_composite = [&] { + steps.emplace_back("execute_composite"); + }, + .copy_preview_result = [&] { + steps.emplace_back("copy_preview"); + }, + }); + PP_EXPECT(h, ok); + }; + + run_sequence(true); + const std::vector dual_steps { + "prepare_dual", + "execute_dual", + "capture_background", + "prepare_main", + "execute_main", + "finish_main", + "execute_composite", + "copy_preview", + }; + PP_EXPECT(h, steps == dual_steps); + + run_sequence(false); + const std::vector single_steps { + "capture_background", + "prepare_main", + "execute_main", + "finish_main", + "execute_composite", + "copy_preview", + }; + PP_EXPECT(h, steps == single_steps); + + const bool missing_required = + pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( + pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {}); + PP_EXPECT(h, !missing_required); +} + void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h) { const std::vector normal_layers { 0, 0, 0 }; @@ -2694,6 +2760,9 @@ int main() harness.run( "legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and_uniforms", legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and_uniforms); + harness.run( + "legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order", + legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order); 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);