Share retained stroke preview mix executor

This commit is contained in:
2026-06-13 11:16:27 +02:00
parent f513500b3c
commit dc2d678dac
6 changed files with 250 additions and 59 deletions

View File

@@ -18,6 +18,11 @@ agent or engineer to remove them without reconstructing context from chat.
## Recent Reductions ## 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 - 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` live-pass
sampler bind/unbind plus semantic texture-input dispatch now route through sampler bind/unbind plus semantic texture-input dispatch now route through
retained stroke execution helpers; concrete GL object mapping, framebuffer retained stroke execution helpers; concrete GL object mapping, framebuffer

View File

@@ -3150,6 +3150,11 @@ Results:
framebuffer bind/unbind, viewport/scissor/blend state, texture-slot framebuffer bind/unbind, viewport/scissor/blend state, texture-slot
binding, and final plane draw, while material planning and shader uniform binding, and final plane draw, while material planning and shader uniform
setup remain in the preview node. 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 - `pp_paint_renderer_stroke_execution_tests` now covers retained stroke texture
input binding order, sample execution destination-copy behavior, live-pass input binding order, sample execution destination-copy behavior, live-pass
face-framebuffer dirty tracking, and pad-face destination-copy behavior face-framebuffer dirty tracking, and pad-face destination-copy behavior

View File

@@ -509,6 +509,14 @@ Done Checks:
Progress Notes: 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 - 2026-06-13: `NodeStrokePreview::draw_stroke_immediate()` now routes
dual-pass/background/main-pass/final-composite/copy-back ordering through dual-pass/background/main-pass/final-composite/copy-back ordering through
`execute_legacy_node_stroke_preview_pass_sequence(...)`; the remaining local `execute_legacy_node_stroke_preview_pass_sequence(...)`; the remaining local

View File

@@ -87,6 +87,24 @@ struct LegacyNodeStrokePreviewMixPassRequest {
int blend_mode = 0; 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<void()> save_state;
std::function<void(const LegacyNodeStrokePreviewMixPassPlan::ShaderPlan&)> setup_mix_shader;
std::function<void()> bind_mixer_framebuffer;
std::function<void(int, int, int, int, int, int)> configure_mix_target_state;
std::function<void()> bind_mix_inputs;
std::function<void()> draw_mix;
std::function<void()> unbind_mixer_framebuffer;
std::function<void()> restore_state;
};
[[nodiscard]] inline LegacyNodeStrokePreviewMixPassPlan plan_legacy_node_stroke_preview_mix_pass( [[nodiscard]] inline LegacyNodeStrokePreviewMixPassPlan plan_legacy_node_stroke_preview_mix_pass(
const LegacyNodeStrokePreviewMixPassRequest& request) noexcept const LegacyNodeStrokePreviewMixPassRequest& request) noexcept
{ {
@@ -130,6 +148,39 @@ struct LegacyNodeStrokePreviewMixPassRequest {
return plan; 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 { struct LegacyNodeStrokePreviewPassSequenceRequest {
bool dual_pass_enabled = false; bool dual_pass_enabled = false;
std::function<void()> prepare_dual_pass; std::function<void()> prepare_dual_pass;

View File

@@ -124,18 +124,6 @@ struct StrokePreviewCompositePassInputs {
std::function<void()> draw_composite; std::function<void()> 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<void()> draw_mix;
};
pp::panopainter::LegacyNodeStrokePreviewMixPassRequest make_stroke_preview_mix_pass_request( pp::panopainter::LegacyNodeStrokePreviewMixPassRequest make_stroke_preview_mix_pass_request(
const Brush& brush, const Brush& brush,
glm::vec2 resolution) noexcept 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<int>(inputs.scissor_min.x),
static_cast<int>(inputs.scissor_min.y),
static_cast<int>(inputs.scissor_size.x),
static_cast<int>(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) void execute_stroke_preview_final_composite_pass(const StrokePreviewCompositePassInputs& inputs)
{ {
pp::panopainter::execute_legacy_stroke_preview_final_composite( 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& b = m_brush;
const auto mix_pass = pp::panopainter::plan_legacy_node_stroke_preview_mix_pass( const auto mix_pass = pp::panopainter::plan_legacy_node_stroke_preview_mix_pass(
make_stroke_preview_mix_pass_request(*b, m_size)); make_stroke_preview_mix_pass_request(*b, m_size));
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<int>(bb_min.x),
.scissor_y = static_cast<int>(bb_min.y),
.scissor_width = static_cast<int>(bb_sz.x),
.scissor_height = static_cast<int>(bb_sz.y),
.save_state = [&] {
gl.save();
},
.setup_mix_shader = [&](const pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan& shader_plan) {
pp::panopainter::setup_legacy_stroke_composite_shader( pp::panopainter::setup_legacy_stroke_composite_shader(
make_stroke_preview_mix_composite_uniforms(mix_pass.shader)); make_stroke_preview_mix_composite_uniforms(shader_plan));
},
execute_stroke_preview_mix_pass( .bind_mixer_framebuffer = [&] {
StrokePreviewMixPassInputs { m_rtt_mixer.bindFramebuffer();
.scissor_min = bb_min, },
.scissor_size = bb_sz, .configure_mix_target_state = [&](
.mixer_rtt = m_rtt_mixer, int mixer_width,
.background_texture = m_tex_background, int mixer_height,
.stroke_rtt = m_rtt, int scissor_x,
.dual_texture = m_tex_dual, int scissor_y,
.brush = *b, int scissor_width,
.linear_sampler = m_sampler_linear, 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 = [&] { .draw_mix = [&] {
m_plane.draw_fill(); m_plane.draw_fill();
}, },
.unbind_mixer_framebuffer = [&] {
m_rtt_mixer.unbindFramebuffer();
},
.restore_state = [&] {
gl.restore();
},
}); });
assert(mix_ok);
} }
glm::vec4 NodeStrokePreview::stroke_draw_samples( glm::vec4 NodeStrokePreview::stroke_draw_samples(

View File

@@ -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); 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<std::string> 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<std::string> 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) void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order(pp::tests::Harness& h)
{ {
std::vector<std::string> steps; std::vector<std::string> steps;
@@ -2300,6 +2390,37 @@ void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_
}; };
PP_EXPECT(h, steps == single_steps); 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 = const bool missing_required =
pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence(
pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {}); pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {});
@@ -2760,6 +2881,9 @@ int main()
harness.run( 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",
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( 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",
legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order); legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order);