Extract stroke commit callback helper

This commit is contained in:
2026-06-13 19:51:03 +02:00
parent b9f9ecaa99
commit 341d114d99
3 changed files with 221 additions and 194 deletions

View File

@@ -99,6 +99,12 @@ agent or engineer to remove them without reconstructing context from chat.
leaving the method with only shell assembly and executor dispatch. leaving the method with only shell assembly and executor dispatch.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()` - 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()`
now keeps only retained shell assembly and executor dispatch at the callsite. now keeps only retained shell assembly and executor dispatch at the callsite.
- 2026-06-13: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()`
now routes the large retained callback bundle through a local helper, leaving
the callsite with sequence planning and helper invocation.
- 2026-06-13: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()`
now keeps the commit callback bundle in a local helper, leaving the callsite
with sequence planning and retained callback invocation only.
- 2026-06-13: `DEBT-0036` was narrowed again. `NodeStrokePreview::draw_stroke_immediate()` - 2026-06-13: `DEBT-0036` was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
now routes final composite execution and preview copy-back through a retained now routes final composite execution and preview copy-back through a retained
local wrapper, leaving the call site with only sequence wiring. local wrapper, leaving the call site with only sequence wiring.

View File

@@ -644,6 +644,12 @@ Progress Notes:
- 2026-06-13: `Canvas::stroke_draw_mix()` now keeps just the retained shell - 2026-06-13: `Canvas::stroke_draw_mix()` now keeps just the retained shell
assembly and executor dispatch at the callsite; the framebuffer setup assembly and executor dispatch at the callsite; the framebuffer setup
callbacks are isolated in the helper. callbacks are isolated in the helper.
- 2026-06-13: `Canvas::stroke_commit()` now routes the large retained callback
bundle through a local helper, leaving the callsite with sequence planning
and helper invocation.
- 2026-06-13: `Canvas::stroke_commit()` now keeps the commit callback bundle
in a local helper, leaving the callsite with sequence planning and retained
callback invocation only.
- 2026-06-13: `Canvas::stroke_draw_samples()` now reuses a retained destination - 2026-06-13: `Canvas::stroke_draw_samples()` now reuses a retained destination
texture dispatch helper for the live sample path; `Canvas` still owns the texture dispatch helper for the live sample path; `Canvas` still owns the
concrete face textures and callback execution. concrete face textures and callback execution.

View File

@@ -531,6 +531,206 @@ static auto make_canvas_stroke_mix_pass_shell(
}); });
} }
static auto make_canvas_stroke_commit_callbacks(
Canvas& canvas,
const glm::vec4& vp,
const glm::vec4& cc,
bool blend,
ActionStroke* action,
const Stroke* current_stroke,
const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence,
const pp::paint_renderer::CanvasStrokeCommitMaterialPlan& stroke_material)
{
const auto& b = current_stroke->m_brush;
return pp::panopainter::make_legacy_canvas_stroke_commit_callbacks(
[&]() {
canvas.m_dirty = false;
canvas.m_dirty_stroke = true; // new stroke ready for timelapse capture
App::I->redraw = true;
canvas.m_unsaved = true;
App::I->title_update();
},
[]() {},
[&]() {
canvas.apply_canvas_viewport(0, 0, canvas.m_width, canvas.m_height);
canvas.apply_canvas_capability(canvas.blend_state(), false);
},
[&]() {
blend ? canvas.apply_canvas_capability(canvas.blend_state(), true) : canvas.apply_canvas_capability(canvas.blend_state(), false);
canvas.apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
canvas.apply_canvas_clear_color(cc);
set_active_texture_unit(0);
},
[&]() {
action->m_layer_idx = canvas.m_current_layer_idx;
action->m_frame_idx = canvas.layer().m_frame_index;
action->m_canvas = &canvas;
ActionManager::add(action);
},
[&]() {
canvas.stroke_commit_timelapse();
},
[&](int i) {
canvas.m_layers[canvas.m_current_layer_idx]->rtt(i).bindFramebuffer();
},
[&](int i) {
glm::vec2 box_sz = zw(canvas.m_dirty_box[i]) - xy(canvas.m_dirty_box[i]);
action->m_image[i] = std::make_unique<uint8_t[]>(
static_cast<std::size_t>(box_sz.x * box_sz.y * 4));
canvas.m_layers[canvas.m_current_layer_idx]->rtt(i).readPixelsRgba8(
static_cast<int>(canvas.m_dirty_box[i].x),
static_cast<int>(canvas.m_dirty_box[i].y),
static_cast<int>(box_sz.x),
static_cast<int>(box_sz.y),
action->m_image[i].get());
action->m_box[i] = canvas.m_dirty_box[i];
action->m_old_box[i] = canvas.m_layers[canvas.m_current_layer_idx]->box(i);
action->m_old_dirty[i] = canvas.m_layers[canvas.m_current_layer_idx]->face(i);
},
[&](int i) {
if (!canvas.m_layers[canvas.m_current_layer_idx]->m_alpha_locked) {
auto& lbox = canvas.m_layers[canvas.m_current_layer_idx]->box(i);
lbox = glm::vec4(
glm::min(xy(canvas.m_dirty_box[i]), xy(lbox)),
glm::max(zw(canvas.m_dirty_box[i]), zw(lbox)));
}
canvas.m_layers[canvas.m_current_layer_idx]->face(i) = true;
},
[&](int i) {
set_active_texture_unit(0);
canvas.m_tex2[i].bind();
copy_framebuffer_to_texture_2d(0, 0, 0, 0, canvas.m_width, canvas.m_height);
canvas.m_tex2[i].unbind();
},
[&](int i) {
pp::panopainter::bind_legacy_canvas_stroke_commit_inputs(
sequence,
[&](int texture_slot) {
set_active_texture_unit(texture_slot);
},
[&](pp::paint_renderer::CanvasStrokeCommitTextureRole role) {
switch (role) {
case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch:
canvas.m_tex2[i].bind();
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke:
canvas.m_tmp[i].bindTexture();
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask:
canvas.m_smask.rtt(i).bindTexture();
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke:
canvas.m_tmp_dual[i].bindTexture();
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern:
b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d();
break;
}
},
[&](pp::paint_renderer::CanvasStrokeCommitTextureRole role, int texture_slot) {
switch (role) {
case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch:
canvas.m_sampler.bind(texture_slot);
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke:
canvas.m_sampler_nearest.bind(texture_slot);
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask:
case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke:
canvas.m_sampler.bind(texture_slot);
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern:
canvas.m_sampler_stencil.bind(texture_slot);
break;
}
});
},
[&](int) {
pp::panopainter::execute_legacy_canvas_stroke_commit_erase(
[&]() {
pp::panopainter::setup_legacy_stroke_erase_shader(
pp::panopainter::LegacyStrokeEraseUniforms {
.mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
.texture_slot = 0,
.stroke_texture_slot = 1,
.mask_texture_slot = 2,
.alpha = 1.0f,
.mask_enabled = canvas.m_smask_active,
});
},
[&]() {
canvas.m_plane.draw_fill();
});
},
[&](int) {
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::execute_legacy_canvas_stroke_commit_paint(
[&]() {
pp::panopainter::setup_legacy_stroke_composite_shader(
pp::panopainter::LegacyStrokeCompositeUniforms {
.resolution = canvas.m_size,
.pattern = {
.scale = patt_scale,
.invert = static_cast<float>(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.m_pattern_offset,
},
.mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
.layer_alpha = 1.0f,
.alpha_lock = canvas.m_layers[canvas.m_current_layer_idx]->m_alpha_locked,
.mask_enabled = canvas.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,
});
},
[&]() {
canvas.m_plane.draw_fill();
});
},
[&](int i) {
pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source(
sequence,
[&]() {
pp::panopainter::setup_legacy_stroke_dilate_shader(
pp::panopainter::LegacyStrokeDilateUniforms {
.mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
});
},
[&](int texture_slot) {
set_active_texture_unit(texture_slot);
},
[&]() {
canvas.m_tex2[i].bind();
},
[&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height);
},
pp::panopainter::LegacyCanvasStrokeCommitCopyExtent {
.width = canvas.m_width,
.height = canvas.m_height,
});
},
[&](int) {
pp::panopainter::execute_legacy_canvas_stroke_commit_dilate([&]() {
canvas.m_plane.draw_fill();
});
},
[&](int i) {
canvas.m_layers[canvas.m_current_layer_idx]->rtt(i).unbindFramebuffer();
});
}
glm::vec4 Canvas::stroke_draw_samples( glm::vec4 Canvas::stroke_draw_samples(
int i, int i,
std::vector<vertex_t>& P, std::vector<vertex_t>& P,
@@ -1120,200 +1320,15 @@ void Canvas::stroke_commit()
}; };
} }
const auto commit_callbacks = pp::panopainter::make_legacy_canvas_stroke_commit_callbacks( const auto commit_callbacks = make_canvas_stroke_commit_callbacks(
[&]() { *this,
m_dirty = false; vp,
m_dirty_stroke = true; // new stroke ready for timelapse capture cc,
App::I->redraw = true; blend,
m_unsaved = true; action,
App::I->title_update(); m_current_stroke,
},
[]() {},
[&]() {
apply_canvas_viewport(0, 0, m_width, m_height);
apply_canvas_capability(blend_state(), false);
},
[&]() {
blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false);
apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
apply_canvas_clear_color(cc);
set_active_texture_unit(0);
},
[&]() {
action->m_layer_idx = m_current_layer_idx;
action->m_frame_idx = layer().m_frame_index;
action->m_canvas = this;
//action->m_stroke = std::move(m_current_stroke);
ActionManager::add(action);
},
[&]() {
stroke_commit_timelapse();
},
[&](int i) {
m_layers[m_current_layer_idx]->rtt(i).bindFramebuffer();
},
[&](int i) {
// save image before commit
glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
action->m_image[i] = std::make_unique<uint8_t[]>(
static_cast<std::size_t>(box_sz.x * box_sz.y * 4));
m_layers[m_current_layer_idx]->rtt(i).readPixelsRgba8(
static_cast<int>(m_dirty_box[i].x),
static_cast<int>(m_dirty_box[i].y),
static_cast<int>(box_sz.x),
static_cast<int>(box_sz.y),
action->m_image[i].get());
action->m_box[i] = m_dirty_box[i];
action->m_old_box[i] = m_layers[m_current_layer_idx]->box(i);
action->m_old_dirty[i] = m_layers[m_current_layer_idx]->face(i);
},
[&](int i) {
if (!m_layers[m_current_layer_idx]->m_alpha_locked)
{
auto& lbox = m_layers[m_current_layer_idx]->box(i);
lbox = glm::vec4(
glm::min(xy(m_dirty_box[i]), xy(lbox)),
glm::max(zw(m_dirty_box[i]), zw(lbox))
);
}
m_layers[m_current_layer_idx]->face(i) = true;
},
[&](int i) {
// copy to tmp2 for layer blending
set_active_texture_unit(0);
m_tex2[i].bind();
copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
m_tex2[i].unbind();
},
[&](int i) {
pp::panopainter::bind_legacy_canvas_stroke_commit_inputs(
sequence, sequence,
[&](int texture_slot) { stroke_material);
set_active_texture_unit(texture_slot);
},
[&](pp::paint_renderer::CanvasStrokeCommitTextureRole role) {
switch (role) {
case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch:
m_tex2[i].bind();
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke:
m_tmp[i].bindTexture();
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask:
m_smask.rtt(i).bindTexture();
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke:
m_tmp_dual[i].bindTexture();
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern:
b->m_pattern_texture ? b->m_pattern_texture->bind() : unbind_texture_2d();
break;
}
},
[&](pp::paint_renderer::CanvasStrokeCommitTextureRole role, int texture_slot) {
switch (role) {
case pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch:
m_sampler.bind(texture_slot);
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::stroke:
m_sampler_nearest.bind(texture_slot);
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::selection_mask:
case pp::paint_renderer::CanvasStrokeCommitTextureRole::dual_stroke:
m_sampler.bind(texture_slot);
break;
case pp::paint_renderer::CanvasStrokeCommitTextureRole::pattern:
m_sampler_stencil.bind(texture_slot);
break;
}
});
},
[&](int) {
pp::panopainter::execute_legacy_canvas_stroke_commit_erase(
[&]() {
pp::panopainter::setup_legacy_stroke_erase_shader(
pp::panopainter::LegacyStrokeEraseUniforms {
.mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
.texture_slot = 0,
.stroke_texture_slot = 1,
.mask_texture_slot = 2,
.alpha = 1.0f,
.mask_enabled = m_smask_active,
});
},
[&]() {
m_plane.draw_fill();
});
},
[&](int) {
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::execute_legacy_canvas_stroke_commit_paint(
[&]() {
pp::panopainter::setup_legacy_stroke_composite_shader(
pp::panopainter::LegacyStrokeCompositeUniforms {
.resolution = m_size,
.pattern = {
.scale = patt_scale,
.invert = static_cast<float>(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 = m_pattern_offset,
},
.mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
.layer_alpha = 1.0f,
.alpha_lock = m_layers[m_current_layer_idx]->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_plane.draw_fill();
});
},
[&](int i) {
pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source(
sequence,
[&]() {
// Dilate borders to avoid interpolation bleeding
pp::panopainter::setup_legacy_stroke_dilate_shader(
pp::panopainter::LegacyStrokeDilateUniforms {
.mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
});
},
[&](int texture_slot) {
set_active_texture_unit(texture_slot);
},
[&]() {
m_tex2[i].bind();
},
[&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height);
},
pp::panopainter::LegacyCanvasStrokeCommitCopyExtent {
.width = m_width,
.height = m_height,
});
},
[&](int) {
pp::panopainter::execute_legacy_canvas_stroke_commit_dilate([&]() {
m_plane.draw_fill();
});
},
[&](int i) {
m_layers[m_current_layer_idx]->rtt(i).unbindFramebuffer();
}
);
[[maybe_unused]] const auto commit_result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence( [[maybe_unused]] const auto commit_result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence(
pp::panopainter::LegacyCanvasStrokeCommitRequest { pp::panopainter::LegacyCanvasStrokeCommitRequest {