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
- 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
outside retained helper headers now routes through retained helper surfaces
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
geometry, framebuffer flow, texture/sampler binding, blend/depth state,
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
preview shader setup in UI nodes and canvas modes now shares retained helper
surfaces, while geometry, texture/sampler binding, blend/depth state,

View File

@@ -780,16 +780,16 @@ void Canvas::stroke_draw()
m_tmp[i].unbindFramebuffer();
const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update(
pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest {
const auto dirty_update = pp::panopainter::plan_legacy_canvas_stroke_face_dirty_update(
pp::panopainter::LegacyCanvasStrokeFaceDirtyRequest {
.extent = stroke_extent,
.previous_accumulated_dirty_box = canvas_stroke_box(m_dirty_box[i]),
.previous_pass_dirty_box = canvas_stroke_box(box_face[i]),
.sample_dirty_box = canvas_stroke_box(box_sample),
.previous_accumulated_dirty_box = m_dirty_box[i],
.previous_pass_dirty_box = box_face[i],
.sample_dirty_box = box_sample,
.include_in_committed_dirty_box = true,
});
m_dirty_box[i] = glm_box(dirty_update.accumulated_dirty_box);
box_face[i] = glm_box(dirty_update.pass_dirty_box);
m_dirty_box[i] = dirty_update.accumulated_dirty_box;
box_face[i] = dirty_update.pass_dirty_box;
// TODO: maybe average color?
pad_color = f.col;
}
@@ -891,16 +891,16 @@ void Canvas::stroke_draw()
m_tmp_dual[i].unbindFramebuffer();
// this mode overflows the main brush boundries
const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update(
pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest {
const auto dirty_update = pp::panopainter::plan_legacy_canvas_stroke_face_dirty_update(
pp::panopainter::LegacyCanvasStrokeFaceDirtyRequest {
.extent = stroke_extent,
.previous_accumulated_dirty_box = canvas_stroke_box(m_dirty_box[i]),
.previous_pass_dirty_box = canvas_stroke_box(box_sample),
.sample_dirty_box = canvas_stroke_box(box_sample),
.previous_accumulated_dirty_box = m_dirty_box[i],
.previous_pass_dirty_box = box_sample,
.sample_dirty_box = box_sample,
.include_in_committed_dirty_box =
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 <algorithm>
#include <array>
#include <functional>
#include <string_view>
@@ -45,6 +46,12 @@ struct LegacyCanvasStrokeCommitResult {
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(
const LegacyCanvasStrokeCommitCallbacks& callbacks) noexcept
{
@@ -85,7 +92,8 @@ struct LegacyCanvasStrokeCommitResult {
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]) {
case pp::paint_renderer::CanvasStrokeCommitStep::readback_history_region:
request.callbacks.capture_history_region(face.index);

View File

@@ -30,6 +30,60 @@ struct LegacyStrokeSampleExecutionResult {
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(
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_shader_services.h"
#include "legacy_canvas_stroke_services.h"
#include "legacy_node_stroke_preview_execution_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h"
#include "util.h"
#include <algorithm>
#include <array>
#include <cstdint>
@@ -31,21 +31,10 @@ pp::paint_renderer::CanvasStrokeFeedbackPlan stroke_preview_destination_feedback
int width,
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(),
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;
width,
height);
}
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 bool copy_stroke_destination = !stroke_feedback.reads_destination_color;
const auto material = stroke_preview_material_plan(*b, copy_stroke_destination);
const auto preview_composite_plan = pp::paint_renderer::plan_stroke_preview_composite(
pp::paint_renderer::StrokePreviewCompositeRequest {
.uses_mixer = b->m_tip_mix > 0.0f,
.uses_dual = material.composite_pass.use_dual,
.uses_pattern = material.composite_pass.use_pattern,
});
const auto preview_composite_plan = pp::panopainter::plan_legacy_node_stroke_preview_composite(
b->m_tip_mix > 0.0f,
material.composite_pass.use_dual,
material.composite_pass.use_pattern);
pp::panopainter::setup_legacy_stroke_shader(
pp::panopainter::LegacyStrokeShaderSetupUniforms {
.resolution = size,

View File

@@ -1,4 +1,5 @@
#include "assets/image_pixels.h"
#include "legacy_canvas_stroke_commit_services.h"
#include "paint_renderer/compositor.h"
#include "renderer_api/recording_renderer.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));
}
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)
{
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));
}
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)
{
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_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(
"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_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_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("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks);
harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);