diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index ca5de8f..47c16b2 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -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, diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 0466b7f..b419dd4 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -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. diff --git a/src/canvas.cpp b/src/canvas.cpp index a220f12..9e34f3e 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -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" @@ -1013,10 +1014,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(); @@ -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(box_sz.x * box_sz.y * 4); - m_layers[m_current_layer_idx]->rtt(i).readPixelsRgba8( - static_cast(m_dirty_box[i].x), - static_cast(m_dirty_box[i].y), - static_cast(box_sz.x), - static_cast(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(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 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); - - // 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(); + + [[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( + static_cast(box_sz.x * box_sz.y * 4)); + m_layers[m_current_layer_idx]->rtt(i).readPixelsRgba8( + static_cast(m_dirty_box[i].x), + static_cast(m_dirty_box[i].y), + static_cast(box_sz.x), + static_cast(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); + }, + .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(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()