From fae108d5206956ec70e099ac7ee02390cce04460 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Sat, 13 Jun 2026 11:00:41 +0200 Subject: [PATCH] Extract preview mix-pass material planning --- docs/modernization/debt.md | 7 ++ docs/modernization/tasks.md | 9 ++ ...y_node_stroke_preview_execution_services.h | 89 ++++++++++++++++++- src/node_stroke_preview.cpp | 84 +++++++++++------ tests/paint_renderer/compositor_tests.cpp | 57 ++++++++++++ 5 files changed, 218 insertions(+), 28 deletions(-) diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 6538869..59cf75b 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -292,6 +292,13 @@ agent or engineer to remove them without reconstructing context from chat. `kShader::CompDraw` binding and composite/pattern/dual uniform writes. Mixer framebuffer, scissor/capability state, texture binding, and draw/copy ordering remain retained legacy execution. +- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix` + now routes retained mix-pass material planning, pattern scale/offset + derivation, and composite/pattern/dual uniform payload assembly through + `plan_legacy_node_stroke_preview_mix_pass(...)`, with semantic adapter + coverage in `pp_paint_renderer_compositor_tests`. Mixer framebuffer + ownership, retained shader execution, texture binding, and final draw/copy + ordering remain local legacy preview execution. - 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview` final preview background capture, composite input binding/draw, and preview texture copy now route through `legacy_canvas_stroke_preview_services.h`, with semantic preview diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index c85e8c7..0f99e14 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -550,6 +550,15 @@ Progress Notes: uniform setup remain local to the preview node. Next slice should target the remaining mix-pass material/setup orchestration without reopening the landed preview live-pass, binding, sample, or final composite helpers. +- 2026-06-13: `NodeStrokePreview::stroke_draw_mix()` now routes retained + mix-pass material planning plus composite/pattern/dual uniform payload + assembly through `plan_legacy_node_stroke_preview_mix_pass(...)`, with + compositor coverage locking the retained preview mix adapter semantics for + composite-pass patterning and dual state. Mixer framebuffer ownership, + retained shader execution, texture binding, and final draw/copy ordering + remain local to the preview node. Next slice should target the remaining + mix-pass execution ownership without reopening the landed preview live-pass, + binding, sample, or final composite helpers. - 2026-06-13: `pp_paint_renderer_stroke_execution_tests` now also covers retained texture-dispatch activation order and sampler-dispatch routing across brush tip, destination, pattern, and mixer helper inputs. Next test diff --git a/src/legacy_node_stroke_preview_execution_services.h b/src/legacy_node_stroke_preview_execution_services.h index f9d3718..24b1336 100644 --- a/src/legacy_node_stroke_preview_execution_services.h +++ b/src/legacy_node_stroke_preview_execution_services.h @@ -1,7 +1,10 @@ #pragma once +#include "../libs/glm/glm/glm.hpp" +#include "../libs/glm/glm/ext/matrix_clip_space.hpp" + +#include "legacy_canvas_stroke_services.h" #include "paint_renderer/compositor.h" -#include "renderer_api/renderer_api.h" #include #include @@ -43,4 +46,88 @@ namespace pp::panopainter { }); } +struct LegacyNodeStrokePreviewMixPassPlan { + pp::paint_renderer::CanvasStrokeMaterialPlan material {}; + struct ShaderPlan { + glm::vec2 resolution {}; + glm::vec2 pattern_scale {}; + float pattern_invert = 0.0f; + float pattern_brightness = 0.0f; + float pattern_contrast = 0.0f; + float pattern_depth = 0.0f; + int pattern_blend_mode = 0; + glm::vec2 pattern_offset {}; + int blend_mode = 0; + bool use_dual = false; + int dual_blend_mode = 0; + float dual_alpha = 0.0f; + bool use_pattern = false; + } shader {}; +}; + +struct LegacyNodeStrokePreviewMixPassRequest { + glm::vec2 resolution {}; + float pattern_scale = 0.0f; + bool pattern_flipx = false; + bool pattern_flipy = false; + bool pattern_invert = false; + float pattern_brightness = 0.0f; + float pattern_contrast = 0.0f; + float pattern_depth = 0.0f; + bool pattern_rand_offset = false; + bool pattern_enabled = false; + bool pattern_eachsample = false; + float tip_wet = 0.0f; + float tip_mix = 0.0f; + float tip_noise = 0.0f; + bool dual_enabled = false; + int dual_blend_mode = 0; + int pattern_blend_mode = 0; + float dual_opacity = 0.0f; + int blend_mode = 0; +}; + +[[nodiscard]] inline LegacyNodeStrokePreviewMixPassPlan plan_legacy_node_stroke_preview_mix_pass( + const LegacyNodeStrokePreviewMixPassRequest& request) noexcept +{ + glm::vec2 pattern_scale(request.pattern_scale); + if (request.pattern_flipx) { + pattern_scale.x *= -1.0f; + } + if (request.pattern_flipy) { + pattern_scale.y *= -1.0f; + } + + LegacyNodeStrokePreviewMixPassPlan plan; + plan.material = plan_legacy_canvas_stroke_material( + pp::paint_renderer::CanvasStrokeMaterialRequest { + .destination_feedback_needed = false, + .pattern_enabled = request.pattern_enabled, + .pattern_eachsample = request.pattern_eachsample, + .wet_blend = request.tip_wet > 0.0f, + .mix_blend = request.tip_mix > 0.0f, + .noise_enabled = request.tip_noise > 0.0f, + .dual_brush_enabled = request.dual_enabled, + .dual_blend_mode = request.dual_blend_mode, + .pattern_blend_mode = request.pattern_blend_mode, + .dual_alpha = request.dual_opacity, + }); + plan.shader = LegacyNodeStrokePreviewMixPassPlan::ShaderPlan { + .resolution = request.resolution, + .pattern_scale = pattern_scale, + .pattern_invert = static_cast(request.pattern_invert), + .pattern_brightness = request.pattern_brightness, + .pattern_contrast = request.pattern_contrast, + .pattern_depth = request.pattern_depth, + .pattern_blend_mode = plan.material.composite_pass.pattern_blend_mode, + .pattern_offset = glm::vec2(request.pattern_rand_offset ? 0.5f : 0.0f), + .blend_mode = request.blend_mode, + .use_dual = plan.material.composite_pass.use_dual, + .dual_blend_mode = plan.material.composite_pass.dual_blend_mode, + .dual_alpha = plan.material.composite_pass.dual_alpha, + .use_pattern = plan.material.composite_pass.use_pattern, + }; + return plan; +} + } // namespace pp::panopainter diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index 43dabcb..a2b45a5 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -136,6 +136,60 @@ struct StrokePreviewMixPassInputs { std::function draw_mix; }; +pp::panopainter::LegacyNodeStrokePreviewMixPassRequest make_stroke_preview_mix_pass_request( + const Brush& brush, + glm::vec2 resolution) noexcept +{ + return { + .resolution = resolution, + .pattern_scale = brush.m_pattern_scale, + .pattern_flipx = brush.m_pattern_flipx, + .pattern_flipy = brush.m_pattern_flipy, + .pattern_invert = brush.m_pattern_invert, + .pattern_brightness = brush.m_pattern_brightness, + .pattern_contrast = brush.m_pattern_contrast, + .pattern_depth = brush.m_pattern_depth, + .pattern_rand_offset = brush.m_pattern_rand_offset, + .pattern_enabled = brush.m_pattern_enabled, + .pattern_eachsample = brush.m_pattern_eachsample, + .tip_wet = brush.m_tip_wet, + .tip_mix = brush.m_tip_mix, + .tip_noise = brush.m_tip_noise, + .dual_enabled = brush.m_dual_enabled, + .dual_blend_mode = brush.m_dual_blend_mode, + .pattern_blend_mode = brush.m_pattern_blend_mode, + .dual_opacity = brush.m_dual_opacity, + .blend_mode = brush.m_blend_mode, + }; +} + +pp::panopainter::LegacyStrokeCompositeUniforms make_stroke_preview_mix_composite_uniforms( + const pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan& shader_plan) noexcept +{ + return { + .resolution = shader_plan.resolution, + .pattern = { + .scale = shader_plan.pattern_scale, + .invert = shader_plan.pattern_invert, + .brightness = shader_plan.pattern_brightness, + .contrast = shader_plan.pattern_contrast, + .depth = shader_plan.pattern_depth, + .blend_mode = shader_plan.pattern_blend_mode, + .offset = shader_plan.pattern_offset, + }, + .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), + .layer_alpha = 1.0f, + .alpha_lock = false, + .mask_enabled = false, + .use_fragcoord = false, + .blend_mode = shader_plan.blend_mode, + .use_dual = shader_plan.use_dual, + .dual_blend_mode = shader_plan.dual_blend_mode, + .dual_alpha = shader_plan.dual_alpha, + .use_pattern = shader_plan.use_pattern, + }; +} + void execute_stroke_preview_mix_pass(const StrokePreviewMixPassInputs& inputs) { gl_state gl; @@ -551,34 +605,10 @@ void NodeStrokePreview::clear_context() void NodeStrokePreview::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz) { const auto& b = m_brush; - 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; - - const auto material = stroke_preview_material_plan(*b, false); + 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( - pp::panopainter::LegacyStrokeCompositeUniforms { - .resolution = 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 = material.composite_pass.pattern_blend_mode, - .offset = glm::vec2(b->m_pattern_rand_offset ? 0.5f : 0.0f), - }, - .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f), - .layer_alpha = 1.0f, - .alpha_lock = false, - .mask_enabled = false, - .use_fragcoord = false, - .blend_mode = b->m_blend_mode, - .use_dual = material.composite_pass.use_dual, - .dual_blend_mode = material.composite_pass.dual_blend_mode, - .dual_alpha = material.composite_pass.dual_alpha, - .use_pattern = material.composite_pass.use_pattern, - }); + make_stroke_preview_mix_composite_uniforms(mix_pass.shader)); execute_stroke_preview_mix_pass( StrokePreviewMixPassInputs { diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index f7cce09..b322e43 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -72,6 +72,11 @@ bool near(float a, float b) return std::fabs(a - b) < 0.0001F; } +bool near(const glm::vec2& a, const glm::vec2& b) +{ + return near(a.x, b.x) && near(a.y, b.y); +} + bool has_texture_binding( const pp::paint_renderer::CanvasStrokeMaterialPlan& plan, CanvasStrokeTextureRole role, @@ -2186,6 +2191,55 @@ void legacy_node_stroke_preview_composite_adapter_preserves_retained_inputs(pp:: PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::mixer, 3)); } +void legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and_uniforms(pp::tests::Harness& h) +{ + const pp::panopainter::LegacyNodeStrokePreviewMixPassRequest request { + .resolution = glm::vec2(128.0F, 64.0F), + .pattern_scale = 0.25F, + .pattern_flipx = true, + .pattern_flipy = true, + .pattern_invert = true, + .pattern_brightness = 0.6F, + .pattern_contrast = 0.8F, + .pattern_depth = 0.9F, + .pattern_rand_offset = true, + .pattern_enabled = true, + .pattern_eachsample = false, + .tip_wet = 0.3F, + .tip_mix = 0.7F, + .tip_noise = 0.2F, + .dual_enabled = true, + .dual_blend_mode = 9, + .pattern_blend_mode = 7, + .dual_opacity = 0.4F, + .blend_mode = 5, + }; + + const auto plan = pp::panopainter::plan_legacy_node_stroke_preview_mix_pass(request); + + PP_EXPECT(h, plan.material.dual_pass.enabled); + PP_EXPECT(h, !plan.material.stroke_pass.uses_pattern); + PP_EXPECT(h, plan.material.composite_pass.use_dual); + PP_EXPECT(h, plan.material.composite_pass.use_pattern); + PP_EXPECT(h, plan.material.composite_pass.dual_blend_mode == request.dual_blend_mode); + PP_EXPECT(h, near(plan.material.composite_pass.dual_alpha, request.dual_opacity)); + PP_EXPECT(h, plan.material.composite_pass.pattern_blend_mode == request.pattern_blend_mode); + + PP_EXPECT(h, near(plan.shader.resolution, request.resolution)); + PP_EXPECT(h, near(plan.shader.pattern_scale, glm::vec2(-0.25F, -0.25F))); + PP_EXPECT(h, near(plan.shader.pattern_invert, 1.0F)); + PP_EXPECT(h, near(plan.shader.pattern_brightness, request.pattern_brightness)); + PP_EXPECT(h, near(plan.shader.pattern_contrast, request.pattern_contrast)); + PP_EXPECT(h, near(plan.shader.pattern_depth, request.pattern_depth)); + PP_EXPECT(h, plan.shader.pattern_blend_mode == request.pattern_blend_mode); + PP_EXPECT(h, near(plan.shader.pattern_offset, glm::vec2(0.5F, 0.5F))); + PP_EXPECT(h, plan.shader.blend_mode == request.blend_mode); + PP_EXPECT(h, plan.shader.use_dual == plan.material.composite_pass.use_dual); + PP_EXPECT(h, plan.shader.dual_blend_mode == plan.material.composite_pass.dual_blend_mode); + PP_EXPECT(h, near(plan.shader.dual_alpha, plan.material.composite_pass.dual_alpha)); + PP_EXPECT(h, plan.shader.use_pattern == plan.material.composite_pass.use_pattern); +} + void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h) { const std::vector normal_layers { 0, 0, 0 }; @@ -2637,6 +2691,9 @@ int main() harness.run( "legacy_node_stroke_preview_composite_adapter_preserves_retained_inputs", legacy_node_stroke_preview_composite_adapter_preserves_retained_inputs); + 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("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);