Plan stroke commit sequencing

This commit is contained in:
2026-06-13 04:51:16 +02:00
parent cc67159784
commit b889f26443
7 changed files with 337 additions and 0 deletions

View File

@@ -18,6 +18,14 @@ agent or engineer to remove them without reconstructing context from chat.
## Recent Reductions ## Recent Reductions
- 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,
dilate order, and commit texture slot roles. A retained
`legacy_canvas_stroke_commit_services.h` adapter skeleton consumes the
semantic plan through callbacks, but the live Canvas commit body still owns
history/layer mutation, RTT/framebuffer binding, sampler binding, and final
OpenGL execution until the adapter is wired.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix` - 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix`
now reuses `legacy_canvas_stroke_composite_services.h` for mixer-pass now reuses `legacy_canvas_stroke_composite_services.h` for mixer-pass
`kShader::CompDraw` binding and composite/pattern/dual uniform writes. `kShader::CompDraw` binding and composite/pattern/dual uniform writes.

View File

@@ -2974,6 +2974,12 @@ Results:
`pp_paint_renderer` stroke-feedback plan to decide whether framebuffer fetch `pp_paint_renderer` stroke-feedback plan to decide whether framebuffer fetch
supplies destination color or the legacy OpenGL path must copy the target supplies destination color or the legacy OpenGL path must copy the target
texture before drawing. texture before drawing.
- Canvas stroke commit ordering now has a tested `pp_paint_renderer`
`CanvasStrokeCommitSequencePlan` for history readback, dirty-state update,
scratch copies, erase/composite draw selection, dilate, and texture slot
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.
- Canvas thumbnail layer blending now uses the same canvas destination-feedback - Canvas thumbnail layer blending now uses the same canvas destination-feedback
plan for framebuffer-fetch versus texture-copy decisions; the thumbnail draw plan for framebuffer-fetch versus texture-copy decisions; the thumbnail draw
itself still executes through retained OpenGL canvas code under DEBT-0036. itself still executes through retained OpenGL canvas code under DEBT-0036.

View File

@@ -507,6 +507,13 @@ Done Checks:
- Retained stroke OpenGL execution is deleted or isolated as an OpenGL backend - Retained stroke OpenGL execution is deleted or isolated as an OpenGL backend
implementation. implementation.
Progress Notes:
- 2026-06-13: `pp_paint_renderer` owns tested Canvas stroke commit sequencing
and commit texture slot intent. Next slice should wire
`legacy_canvas_stroke_commit_services.h` into `Canvas::stroke_commit` while
keeping history/layer mutation local.
### LATER-004 - Remove Catch-All Platform Legacy Adapter ### LATER-004 - Remove Catch-All Platform Legacy Adapter
Status: Blocked Status: Blocked

View File

@@ -0,0 +1,129 @@
#pragma once
#include "paint_renderer/compositor.h"
#include <array>
#include <functional>
#include <string_view>
namespace pp::panopainter {
struct LegacyCanvasStrokeCommitFace {
int index = 0;
bool dirty = false;
};
struct LegacyCanvasStrokeCommitCallbacks {
std::function<void()> mark_commit_started;
std::function<void()> capture_render_state;
std::function<void()> prepare_render_state;
std::function<void()> restore_render_state;
std::function<void()> publish_history;
std::function<void()> capture_timelapse_frame;
std::function<void(int)> bind_layer_framebuffer;
std::function<void(int)> capture_history_region;
std::function<void(int)> apply_layer_dirty_region;
std::function<void(int)> copy_layer_to_commit_destination;
std::function<void(int)> bind_commit_inputs;
std::function<void(int)> execute_erase_composite;
std::function<void(int)> execute_paint_composite;
std::function<void(int)> copy_committed_to_dilate_source;
std::function<void(int)> execute_commit_dilate;
std::function<void(int)> unbind_layer_framebuffer;
};
struct LegacyCanvasStrokeCommitRequest {
std::string_view context;
std::array<LegacyCanvasStrokeCommitFace, 6> faces {};
pp::paint_renderer::CanvasStrokeCommitSequencePlan sequence;
LegacyCanvasStrokeCommitCallbacks callbacks;
};
struct LegacyCanvasStrokeCommitResult {
bool ok = false;
int committed_faces = 0;
};
[[nodiscard]] inline bool legacy_canvas_stroke_commit_callbacks_ready(
const LegacyCanvasStrokeCommitCallbacks& callbacks) noexcept
{
return callbacks.mark_commit_started &&
callbacks.capture_render_state &&
callbacks.prepare_render_state &&
callbacks.restore_render_state &&
callbacks.publish_history &&
callbacks.capture_timelapse_frame &&
callbacks.bind_layer_framebuffer &&
callbacks.capture_history_region &&
callbacks.apply_layer_dirty_region &&
callbacks.copy_layer_to_commit_destination &&
callbacks.bind_commit_inputs &&
callbacks.execute_erase_composite &&
callbacks.execute_paint_composite &&
callbacks.copy_committed_to_dilate_source &&
callbacks.execute_commit_dilate &&
callbacks.unbind_layer_framebuffer;
}
[[nodiscard]] inline LegacyCanvasStrokeCommitResult execute_legacy_canvas_stroke_commit_sequence(
const LegacyCanvasStrokeCommitRequest& request)
{
LegacyCanvasStrokeCommitResult result;
if (!legacy_canvas_stroke_commit_callbacks_ready(request.callbacks)) {
return result;
}
request.callbacks.mark_commit_started();
request.callbacks.capture_render_state();
request.callbacks.prepare_render_state();
for (const auto& face : request.faces) {
if (!face.dirty) {
continue;
}
request.callbacks.bind_layer_framebuffer(face.index);
for (std::size_t step_index = 0; step_index < request.sequence.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);
break;
case pp::paint_renderer::CanvasStrokeCommitStep::update_layer_dirty_state:
request.callbacks.apply_layer_dirty_region(face.index);
break;
case pp::paint_renderer::CanvasStrokeCommitStep::copy_layer_rtt_to_scratch:
request.callbacks.copy_layer_to_commit_destination(face.index);
break;
case pp::paint_renderer::CanvasStrokeCommitStep::bind_commit_inputs:
request.callbacks.bind_commit_inputs(face.index);
break;
case pp::paint_renderer::CanvasStrokeCommitStep::erase_draw:
request.callbacks.execute_erase_composite(face.index);
break;
case pp::paint_renderer::CanvasStrokeCommitStep::composite_draw:
request.callbacks.execute_paint_composite(face.index);
break;
case pp::paint_renderer::CanvasStrokeCommitStep::copy_committed_rtt_to_scratch:
request.callbacks.copy_committed_to_dilate_source(face.index);
break;
case pp::paint_renderer::CanvasStrokeCommitStep::dilate_edges_draw:
request.callbacks.execute_commit_dilate(face.index);
break;
}
}
request.callbacks.unbind_layer_framebuffer(face.index);
++result.committed_faces;
}
request.callbacks.restore_render_state();
request.callbacks.publish_history();
request.callbacks.capture_timelapse_frame();
result.ok = true;
return result;
}
} // namespace pp::panopainter

View File

@@ -1445,6 +1445,55 @@ pp::foundation::Result<CanvasStrokeRasterizationPlan> plan_canvas_stroke_rasteri
return pp::foundation::Result<CanvasStrokeRasterizationPlan>::success(plan); return pp::foundation::Result<CanvasStrokeRasterizationPlan>::success(plan);
} }
CanvasStrokeCommitSequencePlan plan_canvas_stroke_commit_sequence(CanvasStrokeCommitRequest request) noexcept
{
CanvasStrokeCommitSequencePlan plan;
plan.erase_mode = request.erase_mode;
plan.alpha_locked = request.alpha_locked;
plan.selection_mask_active = request.selection_mask_active;
plan.uses_dual_stroke = !request.erase_mode && request.dual_stroke_enabled;
plan.uses_pattern = !request.erase_mode && request.pattern_enabled;
plan.updates_layer_bounds = !request.alpha_locked;
auto append_step = [&plan](CanvasStrokeCommitStep step) noexcept {
if (plan.step_count >= plan.steps.size()) {
return;
}
plan.steps[plan.step_count] = step;
++plan.step_count;
};
auto bind = [&plan](CanvasStrokeCommitTextureRole role, std::uint8_t slot) noexcept {
if (plan.texture_binding_count >= plan.texture_bindings.size()) {
return;
}
plan.texture_bindings[plan.texture_binding_count] = CanvasStrokeCommitTextureBindingPlan {
.role = role,
.slot = slot,
};
++plan.texture_binding_count;
};
append_step(CanvasStrokeCommitStep::readback_history_region);
append_step(CanvasStrokeCommitStep::update_layer_dirty_state);
append_step(CanvasStrokeCommitStep::copy_layer_rtt_to_scratch);
append_step(CanvasStrokeCommitStep::bind_commit_inputs);
append_step(request.erase_mode ? CanvasStrokeCommitStep::erase_draw : CanvasStrokeCommitStep::composite_draw);
append_step(CanvasStrokeCommitStep::copy_committed_rtt_to_scratch);
append_step(CanvasStrokeCommitStep::dilate_edges_draw);
bind(CanvasStrokeCommitTextureRole::layer_scratch, 0);
bind(CanvasStrokeCommitTextureRole::stroke, 1);
bind(CanvasStrokeCommitTextureRole::selection_mask, 2);
if (plan.uses_dual_stroke) {
bind(CanvasStrokeCommitTextureRole::dual_stroke, 3);
}
if (plan.uses_pattern) {
bind(CanvasStrokeCommitTextureRole::pattern, 4);
}
return plan;
}
CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds( CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds(
CanvasStrokeSampleBoundsRequest request) noexcept CanvasStrokeSampleBoundsRequest request) noexcept
{ {

View File

@@ -187,6 +187,55 @@ struct CanvasStrokeRasterizationPlan {
bool compatibility_fallback = false; bool compatibility_fallback = false;
}; };
enum class CanvasStrokeCommitStep : std::uint8_t {
readback_history_region,
update_layer_dirty_state,
copy_layer_rtt_to_scratch,
bind_commit_inputs,
erase_draw,
composite_draw,
copy_committed_rtt_to_scratch,
dilate_edges_draw,
};
enum class CanvasStrokeCommitTextureRole : std::uint8_t {
layer_scratch,
stroke,
selection_mask,
dual_stroke,
pattern,
};
struct CanvasStrokeCommitTextureBindingPlan {
CanvasStrokeCommitTextureRole role = CanvasStrokeCommitTextureRole::layer_scratch;
std::uint8_t slot = 0;
};
struct CanvasStrokeCommitRequest {
bool erase_mode = false;
bool alpha_locked = false;
bool selection_mask_active = false;
bool dual_stroke_enabled = false;
bool pattern_enabled = false;
};
struct CanvasStrokeCommitSequencePlan {
std::array<CanvasStrokeCommitStep, 8> steps {};
std::size_t step_count = 0;
std::array<CanvasStrokeCommitTextureBindingPlan, 5> texture_bindings {};
std::size_t texture_binding_count = 0;
bool erase_mode = false;
bool alpha_locked = false;
bool selection_mask_active = false;
bool uses_dual_stroke = false;
bool uses_pattern = false;
bool requires_history_readback = true;
bool updates_layer_bounds = true;
bool requires_layer_scratch_copy = true;
bool requires_committed_scratch_copy = true;
bool requires_dilate = true;
};
struct CanvasStrokePoint { struct CanvasStrokePoint {
float x = 0.0F; float x = 0.0F;
float y = 0.0F; float y = 0.0F;
@@ -493,6 +542,9 @@ export_document_animation_frames_equirectangular_pngs(
pp::renderer::RenderDeviceFeatures features, pp::renderer::RenderDeviceFeatures features,
pp::renderer::Extent2D extent) noexcept; pp::renderer::Extent2D extent) noexcept;
[[nodiscard]] CanvasStrokeCommitSequencePlan plan_canvas_stroke_commit_sequence(
CanvasStrokeCommitRequest request) noexcept;
[[nodiscard]] CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds( [[nodiscard]] CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds(
CanvasStrokeSampleBoundsRequest request) noexcept; CanvasStrokeSampleBoundsRequest request) noexcept;

View File

@@ -16,6 +16,9 @@ using pp::paint::StrokeBlendMode;
using pp::assets::decode_png_rgba8; using pp::assets::decode_png_rgba8;
using pp::paint_renderer::CanvasBlendGateRequest; using pp::paint_renderer::CanvasBlendGateRequest;
using pp::paint_renderer::CanvasStrokeBox; using pp::paint_renderer::CanvasStrokeBox;
using pp::paint_renderer::CanvasStrokeCommitRequest;
using pp::paint_renderer::CanvasStrokeCommitStep;
using pp::paint_renderer::CanvasStrokeCommitTextureRole;
using pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest; using pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest;
using pp::paint_renderer::CanvasStrokeMaterialRequest; using pp::paint_renderer::CanvasStrokeMaterialRequest;
using pp::paint_renderer::CanvasStrokePadRegionRequest; using pp::paint_renderer::CanvasStrokePadRegionRequest;
@@ -37,6 +40,7 @@ using pp::paint_renderer::export_document_depth_pngs;
using pp::paint_renderer::plan_canvas_blend_gate; using pp::paint_renderer::plan_canvas_blend_gate;
using pp::paint_renderer::plan_canvas_stroke_face_dirty_update; using pp::paint_renderer::plan_canvas_stroke_face_dirty_update;
using pp::paint_renderer::plan_canvas_stroke_feedback; using pp::paint_renderer::plan_canvas_stroke_feedback;
using pp::paint_renderer::plan_canvas_stroke_commit_sequence;
using pp::paint_renderer::plan_canvas_stroke_material; using pp::paint_renderer::plan_canvas_stroke_material;
using pp::paint_renderer::plan_canvas_stroke_pad_region; using pp::paint_renderer::plan_canvas_stroke_pad_region;
using pp::paint_renderer::plan_canvas_stroke_rasterization; using pp::paint_renderer::plan_canvas_stroke_rasterization;
@@ -91,6 +95,19 @@ bool has_preview_texture_slot(
return false; return false;
} }
bool has_commit_texture_binding(
const pp::paint_renderer::CanvasStrokeCommitSequencePlan& plan,
CanvasStrokeCommitTextureRole role,
std::uint8_t slot)
{
for (std::size_t i = 0; i < plan.texture_binding_count; ++i) {
if (plan.texture_bindings[i].role == role && plan.texture_bindings[i].slot == slot) {
return true;
}
}
return false;
}
void expect_preview_sequence(pp::tests::Harness& h, const pp::paint_renderer::StrokePreviewCompositePlan& plan) void expect_preview_sequence(pp::tests::Harness& h, const pp::paint_renderer::StrokePreviewCompositePlan& plan)
{ {
PP_EXPECT(h, plan.step_count == 5U); PP_EXPECT(h, plan.step_count == 5U);
@@ -101,6 +118,17 @@ void expect_preview_sequence(pp::tests::Harness& h, const pp::paint_renderer::St
PP_EXPECT(h, plan.steps[4] == StrokePreviewCompositeStep::copy_preview_texture); PP_EXPECT(h, plan.steps[4] == StrokePreviewCompositeStep::copy_preview_texture);
} }
void expect_commit_prefix(pp::tests::Harness& h, const pp::paint_renderer::CanvasStrokeCommitSequencePlan& plan)
{
PP_EXPECT(h, plan.step_count == 7U);
PP_EXPECT(h, plan.steps[0] == CanvasStrokeCommitStep::readback_history_region);
PP_EXPECT(h, plan.steps[1] == CanvasStrokeCommitStep::update_layer_dirty_state);
PP_EXPECT(h, plan.steps[2] == CanvasStrokeCommitStep::copy_layer_rtt_to_scratch);
PP_EXPECT(h, plan.steps[3] == CanvasStrokeCommitStep::bind_commit_inputs);
PP_EXPECT(h, plan.steps[5] == CanvasStrokeCommitStep::copy_committed_rtt_to_scratch);
PP_EXPECT(h, plan.steps[6] == CanvasStrokeCommitStep::dilate_edges_draw);
}
std::vector<std::uint8_t> solid_rgba8( std::vector<std::uint8_t> solid_rgba8(
std::uint32_t width, std::uint32_t width,
std::uint32_t height, std::uint32_t height,
@@ -1692,6 +1720,62 @@ void plans_canvas_stroke_dual_material_intent(pp::tests::Harness& h)
PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::pattern, 2)); PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::pattern, 2));
} }
void plans_canvas_stroke_commit_erase_sequence(pp::tests::Harness& h)
{
const auto plan = plan_canvas_stroke_commit_sequence(
CanvasStrokeCommitRequest {
.erase_mode = true,
.alpha_locked = true,
.selection_mask_active = true,
.dual_stroke_enabled = true,
.pattern_enabled = true,
});
expect_commit_prefix(h, plan);
PP_EXPECT(h, plan.steps[4] == CanvasStrokeCommitStep::erase_draw);
PP_EXPECT(h, plan.erase_mode);
PP_EXPECT(h, plan.alpha_locked);
PP_EXPECT(h, plan.selection_mask_active);
PP_EXPECT(h, !plan.uses_dual_stroke);
PP_EXPECT(h, !plan.uses_pattern);
PP_EXPECT(h, plan.requires_history_readback);
PP_EXPECT(h, !plan.updates_layer_bounds);
PP_EXPECT(h, plan.requires_layer_scratch_copy);
PP_EXPECT(h, plan.requires_committed_scratch_copy);
PP_EXPECT(h, plan.requires_dilate);
PP_EXPECT(h, plan.texture_binding_count == 3U);
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::layer_scratch, 0));
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::stroke, 1));
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::selection_mask, 2));
}
void plans_canvas_stroke_commit_composite_sequence(pp::tests::Harness& h)
{
const auto plan = plan_canvas_stroke_commit_sequence(
CanvasStrokeCommitRequest {
.erase_mode = false,
.alpha_locked = false,
.selection_mask_active = true,
.dual_stroke_enabled = true,
.pattern_enabled = true,
});
expect_commit_prefix(h, plan);
PP_EXPECT(h, plan.steps[4] == CanvasStrokeCommitStep::composite_draw);
PP_EXPECT(h, !plan.erase_mode);
PP_EXPECT(h, !plan.alpha_locked);
PP_EXPECT(h, plan.selection_mask_active);
PP_EXPECT(h, plan.uses_dual_stroke);
PP_EXPECT(h, plan.uses_pattern);
PP_EXPECT(h, plan.updates_layer_bounds);
PP_EXPECT(h, plan.texture_binding_count == 5U);
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::layer_scratch, 0));
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::stroke, 1));
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::selection_mask, 2));
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::dual_stroke, 3));
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::pattern, 4));
}
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 {});
@@ -2178,6 +2262,8 @@ int main()
harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans); harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans);
harness.run("plans_canvas_stroke_material_passes", plans_canvas_stroke_material_passes); harness.run("plans_canvas_stroke_material_passes", plans_canvas_stroke_material_passes);
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_composite_sequence", plans_canvas_stroke_commit_composite_sequence);
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);