From dc2d678dac6bdaf44e2d1f9addd87e07c895b47b Mon Sep 17 00:00:00 2001 From: omigamedev Date: Sat, 13 Jun 2026 11:16:27 +0200 Subject: [PATCH] Share retained stroke preview mix executor --- docs/modernization/debt.md | 5 + docs/modernization/roadmap.md | 5 + docs/modernization/tasks.md | 8 ++ ...y_node_stroke_preview_execution_services.h | 51 +++++++ src/node_stroke_preview.cpp | 116 ++++++++-------- tests/paint_renderer/compositor_tests.cpp | 124 ++++++++++++++++++ 6 files changed, 250 insertions(+), 59 deletions(-) diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index b895551..3237b8f 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,6 +18,11 @@ agent or engineer to remove them without reconstructing context from chat. ## Recent Reductions +- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix()` + now routes retained mix-pass shader setup plus framebuffer/state/input/draw + ordering through `execute_legacy_node_stroke_preview_mix_pass(...)`; the + preview node keeps only the concrete GL save/restore, texture-object bind, + and plane-draw callbacks. - 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` live-pass sampler bind/unbind plus semantic texture-input dispatch now route through retained stroke execution helpers; concrete GL object mapping, framebuffer diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 6b1c4ee..fa510ef 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -3150,6 +3150,11 @@ Results: framebuffer bind/unbind, viewport/scissor/blend state, texture-slot binding, and final plane draw, while material planning and shader uniform setup remain in the preview node. +- `NodeStrokePreview::stroke_draw_mix()` now routes retained mix-pass shader + setup plus framebuffer/state/input/draw ordering through + `execute_legacy_node_stroke_preview_mix_pass(...)`, while the preview node + keeps only the concrete GL save/restore, texture-object bind, and plane-draw + callbacks. - `pp_paint_renderer_stroke_execution_tests` now covers retained stroke texture input binding order, sample execution destination-copy behavior, live-pass face-framebuffer dirty tracking, and pad-face destination-copy behavior diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 38fff45..50f83c2 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -509,6 +509,14 @@ Done Checks: Progress Notes: +- 2026-06-13: `NodeStrokePreview::stroke_draw_mix()` now routes retained + mix-pass shader setup plus framebuffer/state/input/draw ordering through + `execute_legacy_node_stroke_preview_mix_pass(...)`, with compositor coverage + locking the retained callback order and shader-plan handoff. The preview node + keeps only the concrete GL save/restore, texture-object bind, and plane-draw + callbacks. Next slice should target another retained preview or canvas stroke + seam without reopening the landed preview sample, material-planning, + pass-sequence, or final-composite helpers. - 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 diff --git a/src/legacy_node_stroke_preview_execution_services.h b/src/legacy_node_stroke_preview_execution_services.h index 7c4e6d1..447dfd7 100644 --- a/src/legacy_node_stroke_preview_execution_services.h +++ b/src/legacy_node_stroke_preview_execution_services.h @@ -87,6 +87,24 @@ struct LegacyNodeStrokePreviewMixPassRequest { int blend_mode = 0; }; +struct LegacyNodeStrokePreviewMixExecutionRequest { + LegacyNodeStrokePreviewMixPassPlan::ShaderPlan shader {}; + int mixer_width = 0; + int mixer_height = 0; + int scissor_x = 0; + int scissor_y = 0; + int scissor_width = 0; + int scissor_height = 0; + std::function save_state; + std::function setup_mix_shader; + std::function bind_mixer_framebuffer; + std::function configure_mix_target_state; + std::function bind_mix_inputs; + std::function draw_mix; + std::function unbind_mixer_framebuffer; + std::function restore_state; +}; + [[nodiscard]] inline LegacyNodeStrokePreviewMixPassPlan plan_legacy_node_stroke_preview_mix_pass( const LegacyNodeStrokePreviewMixPassRequest& request) noexcept { @@ -130,6 +148,39 @@ struct LegacyNodeStrokePreviewMixPassRequest { return plan; } +[[nodiscard]] inline bool execute_legacy_node_stroke_preview_mix_pass( + const LegacyNodeStrokePreviewMixExecutionRequest& request) +{ + if (request.mixer_width <= 0 || + request.mixer_height <= 0 || + !request.save_state || + !request.setup_mix_shader || + !request.bind_mixer_framebuffer || + !request.configure_mix_target_state || + !request.bind_mix_inputs || + !request.draw_mix || + !request.unbind_mixer_framebuffer || + !request.restore_state) { + return false; + } + + request.save_state(); + request.setup_mix_shader(request.shader); + request.bind_mixer_framebuffer(); + request.configure_mix_target_state( + request.mixer_width, + request.mixer_height, + request.scissor_x, + request.scissor_y, + request.scissor_width, + request.scissor_height); + request.bind_mix_inputs(); + request.draw_mix(); + request.unbind_mixer_framebuffer(); + request.restore_state(); + return true; +} + struct LegacyNodeStrokePreviewPassSequenceRequest { bool dual_pass_enabled = false; std::function prepare_dual_pass; diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index 87a50a1..d25f1bd 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -124,18 +124,6 @@ struct StrokePreviewCompositePassInputs { std::function draw_composite; }; -struct StrokePreviewMixPassInputs { - glm::vec2 scissor_min; - glm::vec2 scissor_size; - RTT& mixer_rtt; - Texture2D& background_texture; - RTT& stroke_rtt; - Texture2D& dual_texture; - const Brush& brush; - Sampler& linear_sampler; - std::function draw_mix; -}; - pp::panopainter::LegacyNodeStrokePreviewMixPassRequest make_stroke_preview_mix_pass_request( const Brush& brush, glm::vec2 resolution) noexcept @@ -190,40 +178,6 @@ pp::panopainter::LegacyStrokeCompositeUniforms make_stroke_preview_mix_composite }; } -void execute_stroke_preview_mix_pass(const StrokePreviewMixPassInputs& inputs) -{ - gl_state gl; - gl.save(); - - inputs.mixer_rtt.bindFramebuffer(); - - apply_stroke_preview_viewport(0, 0, inputs.mixer_rtt.getWidth(), inputs.mixer_rtt.getHeight()); - apply_stroke_preview_capability(pp::renderer::gl::depth_test_state(), false); - apply_stroke_preview_capability(pp::renderer::gl::scissor_test_state(), true); - apply_stroke_preview_capability(pp::renderer::gl::blend_state(), false); - apply_stroke_preview_scissor( - static_cast(inputs.scissor_min.x), - static_cast(inputs.scissor_min.y), - static_cast(inputs.scissor_size.x), - static_cast(inputs.scissor_size.y)); - - inputs.linear_sampler.bind(stroke_preview_composite_slots::kBackground); - set_active_texture_unit(stroke_preview_composite_slots::kBackground); - inputs.background_texture.bind(); - set_active_texture_unit(stroke_preview_composite_slots::kStroke); - inputs.stroke_rtt.bindTexture(); - set_active_texture_unit(stroke_preview_composite_slots::kDual); - inputs.dual_texture.bind(); - set_active_texture_unit(stroke_preview_composite_slots::kPattern); - inputs.brush.m_pattern_texture ? - inputs.brush.m_pattern_texture->bind() : - unbind_texture_2d(); - inputs.draw_mix(); - - inputs.mixer_rtt.unbindFramebuffer(); - gl.restore(); -} - void execute_stroke_preview_final_composite_pass(const StrokePreviewCompositePassInputs& inputs) { pp::panopainter::execute_legacy_stroke_preview_final_composite( @@ -607,23 +561,67 @@ void NodeStrokePreview::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2 const auto& b = m_brush; const auto mix_pass = pp::panopainter::plan_legacy_node_stroke_preview_mix_pass( make_stroke_preview_mix_pass_request(*b, m_size)); - pp::panopainter::setup_legacy_stroke_composite_shader( - make_stroke_preview_mix_composite_uniforms(mix_pass.shader)); - - execute_stroke_preview_mix_pass( - StrokePreviewMixPassInputs { - .scissor_min = bb_min, - .scissor_size = bb_sz, - .mixer_rtt = m_rtt_mixer, - .background_texture = m_tex_background, - .stroke_rtt = m_rtt, - .dual_texture = m_tex_dual, - .brush = *b, - .linear_sampler = m_sampler_linear, + gl_state gl; + [[maybe_unused]] const bool mix_ok = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass( + pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest { + .shader = mix_pass.shader, + .mixer_width = m_rtt_mixer.getWidth(), + .mixer_height = m_rtt_mixer.getHeight(), + .scissor_x = static_cast(bb_min.x), + .scissor_y = static_cast(bb_min.y), + .scissor_width = static_cast(bb_sz.x), + .scissor_height = static_cast(bb_sz.y), + .save_state = [&] { + gl.save(); + }, + .setup_mix_shader = [&](const pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan& shader_plan) { + pp::panopainter::setup_legacy_stroke_composite_shader( + make_stroke_preview_mix_composite_uniforms(shader_plan)); + }, + .bind_mixer_framebuffer = [&] { + m_rtt_mixer.bindFramebuffer(); + }, + .configure_mix_target_state = [&]( + int mixer_width, + int mixer_height, + int scissor_x, + int scissor_y, + int scissor_width, + int scissor_height) { + apply_stroke_preview_viewport(0, 0, mixer_width, mixer_height); + apply_stroke_preview_capability(pp::renderer::gl::depth_test_state(), false); + apply_stroke_preview_capability(pp::renderer::gl::scissor_test_state(), true); + apply_stroke_preview_capability(pp::renderer::gl::blend_state(), false); + apply_stroke_preview_scissor( + scissor_x, + scissor_y, + scissor_width, + scissor_height); + }, + .bind_mix_inputs = [&] { + m_sampler_linear.bind(stroke_preview_composite_slots::kBackground); + set_active_texture_unit(stroke_preview_composite_slots::kBackground); + m_tex_background.bind(); + set_active_texture_unit(stroke_preview_composite_slots::kStroke); + m_rtt.bindTexture(); + set_active_texture_unit(stroke_preview_composite_slots::kDual); + m_tex_dual.bind(); + set_active_texture_unit(stroke_preview_composite_slots::kPattern); + b->m_pattern_texture ? + b->m_pattern_texture->bind() : + unbind_texture_2d(); + }, .draw_mix = [&] { m_plane.draw_fill(); }, + .unbind_mixer_framebuffer = [&] { + m_rtt_mixer.unbindFramebuffer(); + }, + .restore_state = [&] { + gl.restore(); + }, }); + assert(mix_ok); } glm::vec4 NodeStrokePreview::stroke_draw_samples( diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index 04d9755..7c41c35 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -2240,6 +2240,96 @@ 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_mix_executor_preserves_setup_and_draw_order(pp::tests::Harness& h) +{ + std::vector steps; + pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan observed_shader {}; + + const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass( + pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest { + .shader = pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan { + .resolution = glm::vec2(128.0F, 64.0F), + .pattern_scale = glm::vec2(-0.25F, 0.25F), + .pattern_invert = 1.0F, + .pattern_brightness = 0.6F, + .pattern_contrast = 0.8F, + .pattern_depth = 0.9F, + .pattern_blend_mode = 7, + .pattern_offset = glm::vec2(0.5F, 0.5F), + .blend_mode = 5, + .use_dual = true, + .dual_blend_mode = 9, + .dual_alpha = 0.4F, + .use_pattern = true, + }, + .mixer_width = 128, + .mixer_height = 64, + .scissor_x = 11, + .scissor_y = 12, + .scissor_width = 13, + .scissor_height = 14, + .save_state = [&] { + steps.emplace_back("save"); + }, + .setup_mix_shader = [&](const auto& shader) { + observed_shader = shader; + steps.emplace_back("setup"); + }, + .bind_mixer_framebuffer = [&] { + steps.emplace_back("bind-framebuffer"); + }, + .configure_mix_target_state = [&](int width, int height, int x, int y, int scissor_width, int scissor_height) { + steps.emplace_back( + "configure:" + + std::to_string(width) + "," + + std::to_string(height) + "," + + std::to_string(x) + "," + + std::to_string(y) + "," + + std::to_string(scissor_width) + "," + + std::to_string(scissor_height)); + }, + .bind_mix_inputs = [&] { + steps.emplace_back("bind-inputs"); + }, + .draw_mix = [&] { + steps.emplace_back("draw"); + }, + .unbind_mixer_framebuffer = [&] { + steps.emplace_back("unbind-framebuffer"); + }, + .restore_state = [&] { + steps.emplace_back("restore"); + }, + }); + + PP_EXPECT(h, ok); + PP_EXPECT(h, near(observed_shader.resolution, glm::vec2(128.0F, 64.0F))); + PP_EXPECT(h, near(observed_shader.pattern_scale, glm::vec2(-0.25F, 0.25F))); + PP_EXPECT(h, observed_shader.use_dual); + PP_EXPECT(h, observed_shader.use_pattern); + PP_EXPECT(h, observed_shader.dual_blend_mode == 9); + PP_EXPECT(h, near(observed_shader.dual_alpha, 0.4F)); + + const std::vector expected_steps { + "save", + "setup", + "bind-framebuffer", + "configure:128,64,11,12,13,14", + "bind-inputs", + "draw", + "unbind-framebuffer", + "restore", + }; + PP_EXPECT(h, steps == expected_steps); + + const bool invalid = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass( + pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest { + .mixer_width = 128, + .mixer_height = 64, + }); + PP_EXPECT(h, !invalid); +} + void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order(pp::tests::Harness& h) { std::vector steps; @@ -2300,6 +2390,37 @@ void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_ }; PP_EXPECT(h, steps == single_steps); + steps.clear(); + const bool missing_dual_prepare = + pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( + pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest { + .dual_pass_enabled = true, + .prepare_dual_pass = {}, + .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, !missing_dual_prepare); + PP_EXPECT(h, steps.empty()); + const bool missing_required = pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {}); @@ -2760,6 +2881,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_mix_executor_preserves_setup_and_draw_order", + legacy_node_stroke_preview_mix_executor_preserves_setup_and_draw_order); 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);