From 33f21e0a1bac847f88dbe496d19515e8d1ee4e6d Mon Sep 17 00:00:00 2001 From: omigamedev Date: Fri, 12 Jun 2026 22:25:02 +0200 Subject: [PATCH] Isolate legacy stroke sample execution --- docs/modernization/debt.md | 6 + docs/modernization/roadmap.md | 8 +- src/canvas.cpp | 85 ++++++-------- src/legacy_canvas_stroke_execution_services.h | 106 ++++++++++++++++++ src/node_stroke_preview.cpp | 81 ++++++------- 5 files changed, 190 insertions(+), 96 deletions(-) create mode 100644 src/legacy_canvas_stroke_execution_services.h diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index f860998..4220ef3 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,6 +18,12 @@ agent or engineer to remove them without reconstructing context from chat. ## Recent Reductions +- 2026-06-12: DEBT-0036 was narrowed again. Canvas and `NodeStrokePreview` + stroke sample execution now delegate optional destination copy, brush vertex + upload, brush-shape draw, and destination unbind through + `execute_legacy_canvas_stroke_sample`. Shader setup, uniforms, RTT/texture + ownership, dirty-box policy, and retained callback execution remain in the + legacy callers until a renderer-owned stroke backend replaces the adapter. - 2026-06-12: DEBT-0036 was narrowed again. `NodeStrokePreview` now consumes the same `CanvasStrokeMaterialPlan` boundary for preview dual-brush, each-sample pattern, and composite material decisions. Preview GL draw calls, diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 0676383..56c4f78 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -1343,8 +1343,12 @@ composite texture/uniform intent. Live `Canvas::stroke_draw`, stroke commit, and draw-merge paths consume that material plan for destination feedback, each-sample pattern, dual-brush, and final composite decisions, and `NodeStrokePreview` now consumes the same plan for preview material decisions. -Actual retained OpenGL draw execution and preview texture-binding details remain -under `DEBT-0036`. +Canvas and `NodeStrokePreview` stroke sample execution also delegate optional +destination copy, brush vertex upload, brush-shape draw, and destination unbind +through `execute_legacy_canvas_stroke_sample`, creating the first retained +OpenGL stroke execution service seam. Shader setup, uniforms, RTT/texture +ownership, dirty-box policy, and retained callback execution remain under +`DEBT-0036`. It also owns renderer API texture-format to OpenGL internal/pixel/component token mapping, including depth-stencil formats, for future backend texture objects. `Texture2D` 2D texture binding, upload, diff --git a/src/canvas.cpp b/src/canvas.cpp index 7a2a37f..5dc4e02 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_execution_services.h" #include "legacy_canvas_stroke_services.h" #include "legacy_ui_gl_dispatch.h" #include "legacy_ui_overlay_services.h" @@ -522,58 +523,44 @@ glm::vec4 Canvas::stroke_draw_samples( std::vector& P, bool copy_stroke_destination) { - if (copy_stroke_destination) - { - set_active_texture_unit(1); - m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing) - } - - glm::vec2 bb_min(m_width, m_height); - glm::vec2 bb_max(0, 0); - for (int j = 0; j < P.size(); j++) - { - bb_min = glm::max({ 0, 0 }, glm::min(bb_min, xy(P[j].pos))); - bb_max = glm::min({ m_width, m_height }, glm::max(bb_max, xy(P[j].pos))); - } - auto bb_sz = bb_max - bb_min; - - glm::vec2 pad(1); - glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, { m_width, m_height }); - glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(m_width, m_height) - tex_pos)); - if (copy_stroke_destination) - { - copy_framebuffer_to_texture_2d(tex_pos.x, tex_pos.y, tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y); - } - - if (P.size() == 4) - { - static vertex_t rect[6]; - rect[0] = P[0]; - rect[1] = P[1]; - rect[2] = P[2]; - rect[3] = P[0]; - rect[4] = P[2]; - rect[5] = P[3]; - m_brush_shape.update_vertices(rect, 6); - } - else if (P.size() == 3) - { - m_brush_shape.update_vertices(P.data(), (int)P.size()); - } - else - { + if (P.size() != 3 && P.size() != 4) { P = triangulate_simple(P); - m_brush_shape.update_vertices(P.data(), (int)P.size()); - } - m_brush_shape.draw_fill(); - - if (copy_stroke_destination) - { - set_active_texture_unit(1); - m_tex[i].unbind(); } - return glm::vec4(tex_pos, tex_pos + tex_sz); + const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample( + pp::panopainter::LegacyStrokeSampleExecutionRequest { + .context = "Canvas::stroke_draw_samples", + .target_size = { m_width, m_height }, + .vertices = P, + .copy_stroke_destination = copy_stroke_destination, + .bind_destination_texture = [&] { + set_active_texture_unit(1); + m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing) + }, + .copy_framebuffer_to_destination_texture = []( + 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); + }, + .unbind_destination_texture = [&] { + set_active_texture_unit(1); + m_tex[i].unbind(); + }, + .upload_brush_vertices = [&](std::span vertices) { + m_brush_shape.update_vertices( + const_cast(vertices.data()), + static_cast(vertices.size())); + }, + .draw_brush_shape = [&] { + m_brush_shape.draw_fill(); + }, + }); + + return result.dirty_bounds; } std::vector Canvas::stroke_draw_compute(Stroke& stroke) const diff --git a/src/legacy_canvas_stroke_execution_services.h b/src/legacy_canvas_stroke_execution_services.h new file mode 100644 index 0000000..b4acf02 --- /dev/null +++ b/src/legacy_canvas_stroke_execution_services.h @@ -0,0 +1,106 @@ +#pragma once + +#include "util.h" + +#include +#include +#include +#include + +namespace pp::panopainter { + +struct LegacyStrokeSampleExecutionRequest { + std::string_view context; + glm::vec2 target_size {}; + std::span vertices; + bool copy_stroke_destination = false; + std::function bind_destination_texture; + std::function copy_framebuffer_to_destination_texture; + std::function unbind_destination_texture; + std::function)> upload_brush_vertices; + std::function draw_brush_shape; +}; + +struct LegacyStrokeSampleExecutionResult { + bool ok = false; + glm::ivec2 copy_position {}; + glm::ivec2 copy_size {}; + glm::vec4 dirty_bounds {}; +}; + +[[nodiscard]] inline LegacyStrokeSampleExecutionResult execute_legacy_canvas_stroke_sample( + const LegacyStrokeSampleExecutionRequest& request) +{ + LegacyStrokeSampleExecutionResult result; + if (request.target_size.x <= 0.0f || + request.target_size.y <= 0.0f || + request.vertices.empty() || + !request.upload_brush_vertices || + !request.draw_brush_shape) { + return result; + } + + if (request.copy_stroke_destination) { + if (!request.bind_destination_texture || + !request.copy_framebuffer_to_destination_texture || + !request.unbind_destination_texture) { + return result; + } + request.bind_destination_texture(); + } + + glm::vec2 bb_min(request.target_size); + glm::vec2 bb_max(0.0f, 0.0f); + for (const auto& vertex : request.vertices) { + bb_min = glm::max({ 0.0f, 0.0f }, glm::min(bb_min, xy(vertex.pos))); + bb_max = glm::min(request.target_size, glm::max(bb_max, xy(vertex.pos))); + } + + const auto bb_sz = bb_max - bb_min; + const glm::vec2 pad(1.0f); + const glm::ivec2 target_extent(request.target_size); + result.copy_position = glm::clamp( + glm::floor(bb_min) - pad, + glm::vec2(0.0f), + request.target_size); + result.copy_size = glm::clamp( + glm::ceil(bb_sz) + pad * 2.0f, + glm::vec2(0.0f), + glm::vec2(target_extent - result.copy_position)); + result.dirty_bounds = glm::vec4(result.copy_position, result.copy_position + result.copy_size); + + if (request.copy_stroke_destination) { + request.copy_framebuffer_to_destination_texture( + result.copy_position.x, + result.copy_position.y, + result.copy_position.x, + result.copy_position.y, + result.copy_size.x, + result.copy_size.y); + } + + if (request.vertices.size() == 4) { + std::array rect { + request.vertices[0], + request.vertices[1], + request.vertices[2], + request.vertices[0], + request.vertices[2], + request.vertices[3], + }; + request.upload_brush_vertices(rect); + } else { + request.upload_brush_vertices(request.vertices); + } + + request.draw_brush_shape(); + + if (request.copy_stroke_destination) { + request.unbind_destination_texture(); + } + + result.ok = true; + return result; +} + +} // namespace pp::panopainter diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index 2b990d0..e784688 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -6,6 +6,7 @@ #include "bezier.h" #include "canvas.h" #include "app.h" +#include "legacy_canvas_stroke_execution_services.h" #include "legacy_canvas_stroke_services.h" #include "legacy_ui_gl_dispatch.h" #include "paint_renderer/compositor.h" @@ -249,52 +250,42 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples( Texture2D& blend_tex, bool copy_stroke_destination) { - if (copy_stroke_destination) - { - set_active_texture_unit(1U); - blend_tex.bind(); // bg, copy of framebuffer (copied before drawing) - } + const glm::vec2 size = { m_rtt.getWidth(), m_rtt.getHeight() }; + const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample( + pp::panopainter::LegacyStrokeSampleExecutionRequest { + .context = "NodeStrokePreview::stroke_draw_samples", + .target_size = size, + .vertices = P, + .copy_stroke_destination = copy_stroke_destination, + .bind_destination_texture = [&] { + set_active_texture_unit(1U); + blend_tex.bind(); // bg, copy of framebuffer (copied before drawing) + }, + .copy_framebuffer_to_destination_texture = []( + int src_x, + int src_y, + int dst_x, + int dst_y, + int width, + int height) { + // this is also used by the mixer + copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height); + }, + .unbind_destination_texture = [&] { + set_active_texture_unit(1U); + blend_tex.unbind(); + }, + .upload_brush_vertices = [&](std::span vertices) { + m_brush_shape.update_vertices( + const_cast(vertices.data()), + static_cast(vertices.size())); + }, + .draw_brush_shape = [&] { + m_brush_shape.draw_fill(); + }, + }); - glm::vec2 size = { m_rtt.getWidth(), m_rtt.getHeight() }; - - glm::vec2 bb_min(size); - glm::vec2 bb_max(0, 0); - for (int j = 0; j < P.size(); j++) - { - bb_min = glm::max({ 0, 0 }, glm::min(bb_min, xy(P[j].pos))); - bb_max = glm::min(size, glm::max(bb_max, xy(P[j].pos))); - } - auto bb_sz = bb_max - bb_min; - - glm::vec2 pad(1); - glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, size); - glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(size) - tex_pos)); - if (copy_stroke_destination) - { - // this is also used by the mixer - copy_framebuffer_to_texture_2d(tex_pos.x, tex_pos.y, tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y); - } - - if (P.size() == 4) - { - static vertex_t rect[6]; - rect[0] = P[0]; - rect[1] = P[1]; - rect[2] = P[2]; - rect[3] = P[0]; - rect[4] = P[2]; - rect[5] = P[3]; - m_brush_shape.update_vertices(rect, 6); - } - m_brush_shape.draw_fill(); - - if (copy_stroke_destination) - { - set_active_texture_unit(1U); - blend_tex.unbind(); - } - - return glm::vec4(tex_pos, tex_pos + tex_sz); + return result.dirty_bounds; } std::vector NodeStrokePreview::stroke_draw_compute(Stroke& stroke, float zoom) const