Route stroke commit sequence through adapter

This commit is contained in:
2026-06-13 04:54:58 +02:00
parent b889f26443
commit 5838a8f4ce
3 changed files with 202 additions and 176 deletions

View File

@@ -18,6 +18,13 @@ agent or engineer to remove them without reconstructing context from chat.
## Recent Reductions
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_commit` now routes
its retained per-face commit order through
`execute_legacy_canvas_stroke_commit_sequence`, consuming the tested
`CanvasStrokeCommitSequencePlan` while keeping history `ActionStroke`
mutation, layer dirty state, RTT/framebuffer binding, shader execution, and
sampler/texture binding inside Canvas callbacks. The adapter remains retained
until stroke commit execution is owned by the renderer backend implementation.
- 2026-06-13: DEBT-0036 was narrowed again. `pp_paint_renderer` now owns a
tested `CanvasStrokeCommitSequencePlan` for `Canvas::stroke_commit`
readback, dirty-state, scratch-copy, erase/composite draw, committed-copy,

View File

@@ -2980,6 +2980,10 @@ Results:
roles. A retained commit adapter skeleton consumes that semantic sequence,
while the live Canvas body still owns history/layer mutation and OpenGL
execution until the next wiring slice.
- Live `Canvas::stroke_commit` now consumes that semantic commit sequence
through retained callbacks, so the legacy body no longer owns the loop order
directly. The callbacks still execute the existing OpenGL RTT, texture,
sampler, shader, history, and layer mutation work under DEBT-0036.
- Canvas thumbnail layer blending now uses the same canvas destination-feedback
plan for framebuffer-fetch versus texture-copy decisions; the thumbnail draw
itself still executes through retained OpenGL canvas code under DEBT-0036.

View File

@@ -3,6 +3,7 @@
#include "canvas.h"
#include "app.h"
#include "legacy_gl_renderbuffer_dispatch.h"
#include "legacy_canvas_stroke_commit_services.h"
#include "legacy_canvas_stroke_composite_services.h"
#include "legacy_canvas_stroke_edge_services.h"
#include "legacy_canvas_stroke_execution_services.h"
@@ -1014,10 +1015,6 @@ void Canvas::stroke_commit()
if (!m_dirty || m_layers.empty())
return;
m_dirty = false;
m_dirty_stroke = true; // new stroke ready for timelapse capture
App::I->redraw = true;
// save viewport and clear color states
const auto vp = query_canvas_viewport();
const auto cc = query_canvas_clear_color();
@@ -1027,181 +1024,199 @@ void Canvas::stroke_commit()
auto action = new ActionStroke;
action->was_saved = !m_unsaved;
m_unsaved = true;
App::I->title_update();
// prepare common states
apply_canvas_viewport(0, 0, m_width, m_height);
apply_canvas_capability(blend_state(), false);
const auto& b = m_current_stroke->m_brush;
const auto stroke_material = canvas_stroke_material_plan(*b, false);
const auto sequence = pp::paint_renderer::plan_canvas_stroke_commit_sequence(
pp::paint_renderer::CanvasStrokeCommitRequest {
.erase_mode = m_current_mode == kCanvasMode::Erase,
.alpha_locked = m_layers[m_current_layer_idx]->m_alpha_locked,
.selection_mask_active = m_smask_active,
.dual_stroke_enabled = stroke_material.composite_pass.use_dual,
.pattern_enabled = stroke_material.composite_pass.use_pattern,
});
for (int i = 0; i < 6; i++)
{
//m_dirty_box[i] = glm::vec4(0, 0, m_width, m_height); // reset bounding box
if (!m_dirty_face[i])
continue; // no stroke on this face, skip it
m_layers[m_current_layer_idx]->rtt(i).bindFramebuffer();
// 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[]>(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);
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;
// 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();
m_tmp[i].bindTexture();
set_active_texture_unit(1);
m_tex2[i].bind();
m_sampler.bind(0);
m_sampler_nearest.bind(1);
m_sampler.bind(2);
m_sampler.bind(3);
m_sampler_stencil.bind(4);
if (m_current_mode == kCanvasMode::Erase)
{
ShaderManager::use(kShader::CompErase);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_int(kShaderUniform::TexStroke, 1);
ShaderManager::u_int(kShaderUniform::TexMask, 2);
ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
ShaderManager::u_float(kShaderUniform::Alpha, 1);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
set_active_texture_unit(0);
m_tex2[i].bind();
set_active_texture_unit(1);
m_tmp[i].bindTexture();
set_active_texture_unit(2);
m_smask.rtt(i).bindTexture();
m_plane.draw_fill();
m_smask.rtt(i).unbindTexture();
set_active_texture_unit(1);
m_tmp[i].unbindTexture();
set_active_texture_unit(0);
m_tex2[i].unbind();
}
else
{
const auto stroke_material = canvas_stroke_material_plan(*b, false);
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::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,
});
set_active_texture_unit(0);
m_tex2[i].bind();
set_active_texture_unit(1);
m_tmp[i].bindTexture();
set_active_texture_unit(2);
m_smask.rtt(i).bindTexture();
set_active_texture_unit(3);
if (stroke_material.composite_pass.use_dual)
m_tmp_dual[i].bindTexture();
set_active_texture_unit(4);
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
unbind_texture_2d();
m_plane.draw_fill();
set_active_texture_unit(3);
if (stroke_material.composite_pass.use_dual)
m_tmp_dual[i].unbindTexture();
set_active_texture_unit(2);
m_smask.rtt(i).unbindTexture();
set_active_texture_unit(1);
m_tmp[i].unbindTexture();
set_active_texture_unit(0);
m_tex2[i].unbind();
}
// else
// {
// ShaderManager::use(kShader::StrokeLayer);
// ShaderManager::u_int(kShaderUniform::TexBG, 1);
// ShaderManager::u_int(kShaderUniform::Lock, m_layers[m_current_layer_idx]->m_alpha_locked);
// ShaderManager::u_float(kShaderUniform::Alpha, b->m_tip_opacity);
//
// ShaderManager::u_int(kShaderUniform::Tex, 0);
// ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
// m_plane.draw_fill();
// m_sampler.unbind();
// m_sampler_bg.unbind();
// m_tex2[i].unbind();
// m_tmp[i].unbindTexture();
// }
// 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),
});
set_active_texture_unit(0);
m_tex2[i].bind();
copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
m_plane.draw_fill();
m_layers[m_current_layer_idx]->rtt(i).unbindFramebuffer();
std::array<pp::panopainter::LegacyCanvasStrokeCommitFace, 6> faces {};
for (int i = 0; i < 6; ++i) {
faces[i] = pp::panopainter::LegacyCanvasStrokeCommitFace {
.index = i,
.dirty = m_dirty_face[i],
};
}
// restore viewport and clear color states
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);
[[maybe_unused]] const auto commit_result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence(
pp::panopainter::LegacyCanvasStrokeCommitRequest {
.context = "Canvas::stroke_commit",
.faces = faces,
.sequence = sequence,
.callbacks = {
.mark_commit_started = [&]() {
m_dirty = false;
m_dirty_stroke = true; // new stroke ready for timelapse capture
App::I->redraw = true;
m_unsaved = true;
App::I->title_update();
},
.capture_render_state = []() {},
.prepare_render_state = [&]() {
apply_canvas_viewport(0, 0, m_width, m_height);
apply_canvas_capability(blend_state(), false);
},
.restore_render_state = [&]() {
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);
},
.publish_history = [&]() {
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);
},
.capture_timelapse_frame = [&]() {
stroke_commit_timelapse();
},
.bind_layer_framebuffer = [&](int i) {
m_layers[m_current_layer_idx]->rtt(i).bindFramebuffer();
},
.capture_history_region = [&](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());
// save history
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();
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);
},
.apply_layer_dirty_region = [&](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;
},
.copy_layer_to_commit_destination = [&](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();
},
.bind_commit_inputs = [&](int i) {
m_tmp[i].bindTexture();
set_active_texture_unit(1);
m_tex2[i].bind();
m_sampler.bind(0);
m_sampler_nearest.bind(1);
m_sampler.bind(2);
m_sampler.bind(3);
m_sampler_stencil.bind(4);
},
.execute_erase_composite = [&](int i) {
ShaderManager::use(kShader::CompErase);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_int(kShaderUniform::TexStroke, 1);
ShaderManager::u_int(kShaderUniform::TexMask, 2);
ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
ShaderManager::u_float(kShaderUniform::Alpha, 1);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
set_active_texture_unit(0);
m_tex2[i].bind();
set_active_texture_unit(1);
m_tmp[i].bindTexture();
set_active_texture_unit(2);
m_smask.rtt(i).bindTexture();
m_plane.draw_fill();
m_smask.rtt(i).unbindTexture();
set_active_texture_unit(1);
m_tmp[i].unbindTexture();
set_active_texture_unit(0);
m_tex2[i].unbind();
},
.execute_paint_composite = [&](int i) {
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::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,
});
set_active_texture_unit(0);
m_tex2[i].bind();
set_active_texture_unit(1);
m_tmp[i].bindTexture();
set_active_texture_unit(2);
m_smask.rtt(i).bindTexture();
set_active_texture_unit(3);
if (stroke_material.composite_pass.use_dual)
m_tmp_dual[i].bindTexture();
set_active_texture_unit(4);
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
unbind_texture_2d();
m_plane.draw_fill();
set_active_texture_unit(3);
if (stroke_material.composite_pass.use_dual)
m_tmp_dual[i].unbindTexture();
set_active_texture_unit(2);
m_smask.rtt(i).unbindTexture();
set_active_texture_unit(1);
m_tmp[i].unbindTexture();
set_active_texture_unit(0);
m_tex2[i].unbind();
},
.copy_committed_to_dilate_source = [&](int i) {
// 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),
});
set_active_texture_unit(0);
m_tex2[i].bind();
copy_framebuffer_to_texture_2d(0, 0, 0, 0, m_width, m_height);
},
.execute_commit_dilate = [&](int) {
m_plane.draw_fill();
},
.unbind_layer_framebuffer = [&](int i) {
m_layers[m_current_layer_idx]->rtt(i).unbindFramebuffer();
},
},
});
}
void Canvas::stroke_commit_timelapse()