Narrow stroke execution planning helpers

This commit is contained in:
2026-06-13 06:28:21 +02:00
parent 493282264d
commit 13f334ae55
8 changed files with 226 additions and 35 deletions

View File

@@ -18,6 +18,13 @@ agent or engineer to remove them without reconstructing context from chat.
## Recent Reductions ## Recent Reductions
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` face
dirty-box planning now routes through a retained stroke execution helper
wrapping `pp_paint_renderer`, retained stroke commit step dispatch clamps
malformed step counts to the fixed plan array, and compositor coverage now
exercises malformed retained commit plans plus all-input stroke-preview
composite planning. Live stroke rasterization, callback execution, texture
binding, and history mutation remain retained.
- 2026-06-13: DEBT-0036 was narrowed again. Remaining live shader setup - 2026-06-13: DEBT-0036 was narrowed again. Remaining live shader setup
outside retained helper headers now routes through retained helper surfaces outside retained helper headers now routes through retained helper surfaces
for canvas modes, equirect layer export, `NodeCanvas` debug dirty bounds, for canvas modes, equirect layer export, `NodeCanvas` debug dirty bounds,

View File

@@ -3073,6 +3073,12 @@ Results:
`NodeCanvas` debug dirty bounds, atlas image drawing, and text drawing, while `NodeCanvas` debug dirty bounds, atlas image drawing, and text drawing, while
geometry, framebuffer flow, texture/sampler binding, blend/depth state, geometry, framebuffer flow, texture/sampler binding, blend/depth state,
readback, and draw execution remain retained. readback, and draw execution remain retained.
- `Canvas::stroke_draw` face dirty-box planning now shares a retained stroke
execution helper wrapping `pp_paint_renderer`, retained stroke commit step
dispatch clamps malformed step counts to the fixed plan array, and
compositor coverage now includes malformed retained commit plans plus
all-input stroke-preview composite planning. Live stroke rasterization,
callback execution, texture binding, and history mutation remain retained.
- Remaining simple color, hue, color-quad, grid heightmap, and pen/line - Remaining simple color, hue, color-quad, grid heightmap, and pen/line
preview shader setup in UI nodes and canvas modes now shares retained helper preview shader setup in UI nodes and canvas modes now shares retained helper
surfaces, while geometry, texture/sampler binding, blend/depth state, surfaces, while geometry, texture/sampler binding, blend/depth state,

View File

@@ -780,16 +780,16 @@ void Canvas::stroke_draw()
m_tmp[i].unbindFramebuffer(); m_tmp[i].unbindFramebuffer();
const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update( const auto dirty_update = pp::panopainter::plan_legacy_canvas_stroke_face_dirty_update(
pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest { pp::panopainter::LegacyCanvasStrokeFaceDirtyRequest {
.extent = stroke_extent, .extent = stroke_extent,
.previous_accumulated_dirty_box = canvas_stroke_box(m_dirty_box[i]), .previous_accumulated_dirty_box = m_dirty_box[i],
.previous_pass_dirty_box = canvas_stroke_box(box_face[i]), .previous_pass_dirty_box = box_face[i],
.sample_dirty_box = canvas_stroke_box(box_sample), .sample_dirty_box = box_sample,
.include_in_committed_dirty_box = true, .include_in_committed_dirty_box = true,
}); });
m_dirty_box[i] = glm_box(dirty_update.accumulated_dirty_box); m_dirty_box[i] = dirty_update.accumulated_dirty_box;
box_face[i] = glm_box(dirty_update.pass_dirty_box); box_face[i] = dirty_update.pass_dirty_box;
// TODO: maybe average color? // TODO: maybe average color?
pad_color = f.col; pad_color = f.col;
} }
@@ -891,16 +891,16 @@ void Canvas::stroke_draw()
m_tmp_dual[i].unbindFramebuffer(); m_tmp_dual[i].unbindFramebuffer();
// this mode overflows the main brush boundries // this mode overflows the main brush boundries
const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update( const auto dirty_update = pp::panopainter::plan_legacy_canvas_stroke_face_dirty_update(
pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest { pp::panopainter::LegacyCanvasStrokeFaceDirtyRequest {
.extent = stroke_extent, .extent = stroke_extent,
.previous_accumulated_dirty_box = canvas_stroke_box(m_dirty_box[i]), .previous_accumulated_dirty_box = m_dirty_box[i],
.previous_pass_dirty_box = canvas_stroke_box(box_sample), .previous_pass_dirty_box = box_sample,
.sample_dirty_box = canvas_stroke_box(box_sample), .sample_dirty_box = box_sample,
.include_in_committed_dirty_box = .include_in_committed_dirty_box =
stroke_material.composite_pass.dual_blend_mode == 0, stroke_material.composite_pass.dual_blend_mode == 0,
}); });
m_dirty_box[i] = glm_box(dirty_update.accumulated_dirty_box); m_dirty_box[i] = dirty_update.accumulated_dirty_box;
} }
} }
} }

View File

@@ -2,6 +2,7 @@
#include "paint_renderer/compositor.h" #include "paint_renderer/compositor.h"
#include <algorithm>
#include <array> #include <array>
#include <functional> #include <functional>
#include <string_view> #include <string_view>
@@ -45,6 +46,12 @@ struct LegacyCanvasStrokeCommitResult {
int committed_faces = 0; int committed_faces = 0;
}; };
[[nodiscard]] inline std::size_t legacy_canvas_stroke_commit_step_count(
const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence) noexcept
{
return std::min(sequence.step_count, sequence.steps.size());
}
[[nodiscard]] inline bool legacy_canvas_stroke_commit_callbacks_ready( [[nodiscard]] inline bool legacy_canvas_stroke_commit_callbacks_ready(
const LegacyCanvasStrokeCommitCallbacks& callbacks) noexcept const LegacyCanvasStrokeCommitCallbacks& callbacks) noexcept
{ {
@@ -85,7 +92,8 @@ struct LegacyCanvasStrokeCommitResult {
request.callbacks.bind_layer_framebuffer(face.index); request.callbacks.bind_layer_framebuffer(face.index);
for (std::size_t step_index = 0; step_index < request.sequence.step_count; ++step_index) { const auto step_count = legacy_canvas_stroke_commit_step_count(request.sequence);
for (std::size_t step_index = 0; step_index < step_count; ++step_index) {
switch (request.sequence.steps[step_index]) { switch (request.sequence.steps[step_index]) {
case pp::paint_renderer::CanvasStrokeCommitStep::readback_history_region: case pp::paint_renderer::CanvasStrokeCommitStep::readback_history_region:
request.callbacks.capture_history_region(face.index); request.callbacks.capture_history_region(face.index);

View File

@@ -30,6 +30,60 @@ struct LegacyStrokeSampleExecutionResult {
glm::vec4 dirty_bounds {}; glm::vec4 dirty_bounds {};
}; };
struct LegacyCanvasStrokeFaceDirtyRequest {
pp::renderer::Extent2D extent {};
glm::vec4 previous_accumulated_dirty_box {};
glm::vec4 previous_pass_dirty_box {};
glm::vec4 sample_dirty_box {};
bool include_in_committed_dirty_box = true;
};
struct LegacyCanvasStrokeFaceDirtyResult {
glm::vec4 accumulated_dirty_box {};
glm::vec4 pass_dirty_box {};
bool has_dirty_pixels = false;
bool committed_dirty = false;
bool pass_dirty = false;
};
[[nodiscard]] inline pp::paint_renderer::CanvasStrokeBox legacy_canvas_stroke_box(glm::vec4 box) noexcept
{
return pp::paint_renderer::CanvasStrokeBox {
.min_x = box.x,
.min_y = box.y,
.max_x = box.z,
.max_y = box.w,
};
}
[[nodiscard]] inline glm::vec4 legacy_canvas_stroke_glm_box(
pp::paint_renderer::CanvasStrokeBox box) noexcept
{
return glm::vec4(box.min_x, box.min_y, box.max_x, box.max_y);
}
[[nodiscard]] inline LegacyCanvasStrokeFaceDirtyResult plan_legacy_canvas_stroke_face_dirty_update(
const LegacyCanvasStrokeFaceDirtyRequest& request) noexcept
{
const auto plan = pp::paint_renderer::plan_canvas_stroke_face_dirty_update(
pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest {
.extent = request.extent,
.previous_accumulated_dirty_box =
legacy_canvas_stroke_box(request.previous_accumulated_dirty_box),
.previous_pass_dirty_box = legacy_canvas_stroke_box(request.previous_pass_dirty_box),
.sample_dirty_box = legacy_canvas_stroke_box(request.sample_dirty_box),
.include_in_committed_dirty_box = request.include_in_committed_dirty_box,
});
return LegacyCanvasStrokeFaceDirtyResult {
.accumulated_dirty_box = legacy_canvas_stroke_glm_box(plan.accumulated_dirty_box),
.pass_dirty_box = legacy_canvas_stroke_glm_box(plan.pass_dirty_box),
.has_dirty_pixels = plan.has_dirty_pixels,
.committed_dirty = plan.committed_dirty,
.pass_dirty = plan.pass_dirty,
};
}
[[nodiscard]] inline LegacyStrokeSampleExecutionResult execute_legacy_canvas_stroke_sample( [[nodiscard]] inline LegacyStrokeSampleExecutionResult execute_legacy_canvas_stroke_sample(
const LegacyStrokeSampleExecutionRequest& request) const LegacyStrokeSampleExecutionRequest& request)
{ {

View File

@@ -0,0 +1,46 @@
#pragma once
#include "paint_renderer/compositor.h"
#include "renderer_api/renderer_api.h"
#include <algorithm>
#include <cstdint>
namespace pp::panopainter {
[[nodiscard]] inline pp::paint_renderer::CanvasStrokeFeedbackPlan plan_legacy_node_stroke_preview_feedback(
pp::renderer::RenderDeviceFeatures features,
int width,
int height) noexcept
{
const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback(
features,
pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(std::max(height, 0)),
});
if (plan) {
return plan.value();
}
pp::paint_renderer::CanvasStrokeFeedbackPlan fallback;
fallback.compatibility_fallback = true;
fallback.path = pp::paint_renderer::StrokeCompositePath::ping_pong_textures;
fallback.requires_auxiliary_texture = true;
return fallback;
}
[[nodiscard]] inline pp::paint_renderer::StrokePreviewCompositePlan plan_legacy_node_stroke_preview_composite(
bool uses_mixer,
bool uses_dual,
bool uses_pattern) noexcept
{
return pp::paint_renderer::plan_stroke_preview_composite(
pp::paint_renderer::StrokePreviewCompositeRequest {
.uses_mixer = uses_mixer,
.uses_dual = uses_dual,
.uses_pattern = uses_pattern,
});
}
} // namespace pp::panopainter

View File

@@ -12,11 +12,11 @@
#include "legacy_canvas_stroke_preview_services.h" #include "legacy_canvas_stroke_preview_services.h"
#include "legacy_canvas_stroke_shader_services.h" #include "legacy_canvas_stroke_shader_services.h"
#include "legacy_canvas_stroke_services.h" #include "legacy_canvas_stroke_services.h"
#include "legacy_node_stroke_preview_execution_services.h"
#include "legacy_ui_gl_dispatch.h" #include "legacy_ui_gl_dispatch.h"
#include "paint_renderer/compositor.h" #include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h" #include "renderer_gl/opengl_capabilities.h"
#include "util.h" #include "util.h"
#include <algorithm>
#include <array> #include <array>
#include <cstdint> #include <cstdint>
@@ -31,21 +31,10 @@ pp::paint_renderer::CanvasStrokeFeedbackPlan stroke_preview_destination_feedback
int width, int width,
int height) noexcept int height) noexcept
{ {
const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback( return pp::panopainter::plan_legacy_node_stroke_preview_feedback(
stroke_preview_render_device_features(), stroke_preview_render_device_features(),
pp::renderer::Extent2D { width,
.width = static_cast<std::uint32_t>(std::max(width, 0)), height);
.height = static_cast<std::uint32_t>(std::max(height, 0)),
});
if (plan) {
return plan.value();
}
pp::paint_renderer::CanvasStrokeFeedbackPlan fallback;
fallback.compatibility_fallback = true;
fallback.path = pp::paint_renderer::StrokeCompositePath::ping_pong_textures;
fallback.requires_auxiliary_texture = true;
return fallback;
} }
pp::paint_renderer::CanvasStrokeMaterialPlan stroke_preview_material_plan( pp::paint_renderer::CanvasStrokeMaterialPlan stroke_preview_material_plan(
@@ -452,12 +441,10 @@ void NodeStrokePreview::draw_stroke_immediate()
const auto stroke_feedback = stroke_preview_destination_feedback_plan(m_rtt.getWidth(), m_rtt.getHeight()); const auto stroke_feedback = stroke_preview_destination_feedback_plan(m_rtt.getWidth(), m_rtt.getHeight());
const bool copy_stroke_destination = !stroke_feedback.reads_destination_color; const bool copy_stroke_destination = !stroke_feedback.reads_destination_color;
const auto material = stroke_preview_material_plan(*b, copy_stroke_destination); const auto material = stroke_preview_material_plan(*b, copy_stroke_destination);
const auto preview_composite_plan = pp::paint_renderer::plan_stroke_preview_composite( const auto preview_composite_plan = pp::panopainter::plan_legacy_node_stroke_preview_composite(
pp::paint_renderer::StrokePreviewCompositeRequest { b->m_tip_mix > 0.0f,
.uses_mixer = b->m_tip_mix > 0.0f, material.composite_pass.use_dual,
.uses_dual = material.composite_pass.use_dual, material.composite_pass.use_pattern);
.uses_pattern = material.composite_pass.use_pattern,
});
pp::panopainter::setup_legacy_stroke_shader( pp::panopainter::setup_legacy_stroke_shader(
pp::panopainter::LegacyStrokeShaderSetupUniforms { pp::panopainter::LegacyStrokeShaderSetupUniforms {
.resolution = size, .resolution = size,

View File

@@ -1,4 +1,5 @@
#include "assets/image_pixels.h" #include "assets/image_pixels.h"
#include "legacy_canvas_stroke_commit_services.h"
#include "paint_renderer/compositor.h" #include "paint_renderer/compositor.h"
#include "renderer_api/recording_renderer.h" #include "renderer_api/recording_renderer.h"
#include "test_harness.h" #include "test_harness.h"
@@ -1776,6 +1777,61 @@ void plans_canvas_stroke_commit_composite_sequence(pp::tests::Harness& h)
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::pattern, 4)); PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::pattern, 4));
} }
void retained_stroke_commit_runner_clamps_malformed_step_count(pp::tests::Harness& h)
{
pp::paint_renderer::CanvasStrokeCommitSequencePlan sequence;
sequence.step_count = 99U;
sequence.steps[0] = CanvasStrokeCommitStep::bind_commit_inputs;
sequence.steps[1] = CanvasStrokeCommitStep::composite_draw;
int bind_inputs = 0;
int paint_draws = 0;
int erase_draws = 0;
int started = 0;
int restored = 0;
int published = 0;
int timelapse = 0;
const auto result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence(
pp::panopainter::LegacyCanvasStrokeCommitRequest {
.context = "test",
.faces = {
pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = true },
pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = false },
pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = true },
},
.sequence = sequence,
.callbacks = {
.mark_commit_started = [&]() { ++started; },
.capture_render_state = []() {},
.prepare_render_state = []() {},
.restore_render_state = [&]() { ++restored; },
.publish_history = [&]() { ++published; },
.capture_timelapse_frame = [&]() { ++timelapse; },
.bind_layer_framebuffer = [](int) {},
.capture_history_region = [](int) {},
.apply_layer_dirty_region = [](int) {},
.copy_layer_to_commit_destination = [](int) {},
.bind_commit_inputs = [&](int) { ++bind_inputs; },
.execute_erase_composite = [&](int) { ++erase_draws; },
.execute_paint_composite = [&](int) { ++paint_draws; },
.copy_committed_to_dilate_source = [](int) {},
.execute_commit_dilate = [](int) {},
.unbind_layer_framebuffer = [](int) {},
},
});
PP_EXPECT(h, result.ok);
PP_EXPECT(h, result.committed_faces == 2);
PP_EXPECT(h, bind_inputs == 2);
PP_EXPECT(h, paint_draws == 2);
PP_EXPECT(h, erase_draws == 0);
PP_EXPECT(h, started == 1);
PP_EXPECT(h, restored == 1);
PP_EXPECT(h, published == 1);
PP_EXPECT(h, timelapse == 1);
}
void plans_stroke_preview_composite_for_simple_brush(pp::tests::Harness& h) void plans_stroke_preview_composite_for_simple_brush(pp::tests::Harness& h)
{ {
const auto plan = plan_stroke_preview_composite(StrokePreviewCompositeRequest {}); const auto plan = plan_stroke_preview_composite(StrokePreviewCompositeRequest {});
@@ -1841,6 +1897,27 @@ void plans_stroke_preview_composite_with_pattern_input(pp::tests::Harness& h)
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4)); PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4));
} }
void plans_stroke_preview_composite_with_all_retained_inputs(pp::tests::Harness& h)
{
const auto plan = plan_stroke_preview_composite(
StrokePreviewCompositeRequest {
.uses_mixer = true,
.uses_dual = true,
.uses_pattern = true,
});
expect_preview_sequence(h, plan);
PP_EXPECT(h, plan.uses_mixer);
PP_EXPECT(h, plan.uses_dual);
PP_EXPECT(h, plan.uses_pattern);
PP_EXPECT(h, plan.texture_slot_count == 5U);
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0));
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1));
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::dual, 3));
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4));
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::mixer, 3));
}
void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h) void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h)
{ {
const std::vector<int> normal_layers { 0, 0, 0 }; const std::vector<int> normal_layers { 0, 0, 0 };
@@ -2264,10 +2341,16 @@ int main()
harness.run("plans_canvas_stroke_dual_material_intent", plans_canvas_stroke_dual_material_intent); harness.run("plans_canvas_stroke_dual_material_intent", plans_canvas_stroke_dual_material_intent);
harness.run("plans_canvas_stroke_commit_erase_sequence", plans_canvas_stroke_commit_erase_sequence); harness.run("plans_canvas_stroke_commit_erase_sequence", plans_canvas_stroke_commit_erase_sequence);
harness.run("plans_canvas_stroke_commit_composite_sequence", plans_canvas_stroke_commit_composite_sequence); harness.run("plans_canvas_stroke_commit_composite_sequence", plans_canvas_stroke_commit_composite_sequence);
harness.run(
"retained_stroke_commit_runner_clamps_malformed_step_count",
retained_stroke_commit_runner_clamps_malformed_step_count);
harness.run("plans_stroke_preview_composite_for_simple_brush", plans_stroke_preview_composite_for_simple_brush); harness.run("plans_stroke_preview_composite_for_simple_brush", plans_stroke_preview_composite_for_simple_brush);
harness.run("plans_stroke_preview_composite_with_mixer_input", plans_stroke_preview_composite_with_mixer_input); harness.run("plans_stroke_preview_composite_with_mixer_input", plans_stroke_preview_composite_with_mixer_input);
harness.run("plans_stroke_preview_composite_with_dual_input", plans_stroke_preview_composite_with_dual_input); harness.run("plans_stroke_preview_composite_with_dual_input", plans_stroke_preview_composite_with_dual_input);
harness.run("plans_stroke_preview_composite_with_pattern_input", plans_stroke_preview_composite_with_pattern_input); harness.run("plans_stroke_preview_composite_with_pattern_input", plans_stroke_preview_composite_with_pattern_input);
harness.run(
"plans_stroke_preview_composite_with_all_retained_inputs",
plans_stroke_preview_composite_with_all_retained_inputs);
harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices); harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices);
harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks); harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks);
harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths); harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);