Extract stroke dirty bounds planning

This commit is contained in:
2026-06-13 04:35:14 +02:00
parent 458f9bef0c
commit 36861cbf97
8 changed files with 509 additions and 40 deletions

View File

@@ -18,6 +18,11 @@ agent or engineer to remove them without reconstructing context from chat.
## Recent Reductions ## Recent Reductions
- 2026-06-13: DEBT-0036 was narrowed again. Stroke sample copy bounds, live
face dirty-box accumulation, and preview padding region math now live as
tested `pp_paint_renderer` planners and are consumed by retained Canvas and
stroke sample execution adapters. Canvas/preview still own GL ordering,
RTT/texture binding, history mutation, and final dirty state storage.
- 2026-06-12: DEBT-0036 was narrowed again. `Canvas::stroke_commit` now reuses - 2026-06-12: DEBT-0036 was narrowed again. `Canvas::stroke_commit` now reuses
`legacy_canvas_stroke_composite_services.h` for commit-time final stroke `legacy_canvas_stroke_composite_services.h` for commit-time final stroke
`kShader::CompDraw` binding and composite/pattern/dual uniform writes. `kShader::CompDraw` binding and composite/pattern/dual uniform writes.

View File

@@ -1354,8 +1354,11 @@ stroke composite `kShader::CompDraw` setup now pass through
reuses that same composite service for commit-time final stroke compositing. reuses that same composite service for commit-time final stroke compositing.
Stroke padding and commit dilate `kShader::StrokePad`/`kShader::StrokeDilate` Stroke padding and commit dilate `kShader::StrokePad`/`kShader::StrokeDilate`
setup now pass through `legacy_canvas_stroke_edge_services.h`, leaving setup now pass through `legacy_canvas_stroke_edge_services.h`, leaving
RTT/texture ownership, dirty-box policy, checkerboard/non-stroke composite RTT/texture ownership, checkerboard/non-stroke composite shaders, and retained
shaders, quad expansion, and retained callback execution under `DEBT-0036`. callback execution under `DEBT-0036`. Stroke sample copy bounds, live face
dirty-box accumulation, and preview padding region math now live as tested
`pp_paint_renderer` planners while retained Canvas/preview code still owns GL
ordering, RTT/texture binding, history mutation, and final dirty state storage.
It also owns renderer API texture-format to It also owns renderer API texture-format to
OpenGL internal/pixel/component token mapping, including depth-stencil formats, OpenGL internal/pixel/component token mapping, including depth-stencil formats,
for future backend texture objects. `Texture2D` 2D texture binding, upload, for future backend texture objects. `Texture2D` 2D texture binding, upload,

View File

@@ -84,6 +84,29 @@ pp::paint_renderer::CanvasStrokeMaterialPlan canvas_stroke_material_plan(
}); });
} }
pp::renderer::Extent2D canvas_stroke_extent(int width, int height) noexcept
{
return pp::renderer::Extent2D {
.width = static_cast<std::uint32_t>(std::max(width, 0)),
.height = static_cast<std::uint32_t>(std::max(height, 0)),
};
}
pp::paint_renderer::CanvasStrokeBox 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,
};
}
glm::vec4 glm_box(pp::paint_renderer::CanvasStrokeBox box) noexcept
{
return glm::vec4(box.min_x, box.min_y, box.max_x, box.max_y);
}
pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan( pp::paint_renderer::CanvasBlendGatePlan draw_merge_blend_gate_plan(
int width, int width,
int height, int height,
@@ -530,11 +553,21 @@ glm::vec4 Canvas::stroke_draw_samples(
P = triangulate_simple(P); P = triangulate_simple(P);
} }
std::vector<pp::paint_renderer::CanvasStrokePoint> sample_points;
sample_points.reserve(P.size());
for (const auto& vertex : P) {
sample_points.push_back(pp::paint_renderer::CanvasStrokePoint {
.x = vertex.pos.x,
.y = vertex.pos.y,
});
}
const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample( const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample(
pp::panopainter::LegacyStrokeSampleExecutionRequest { pp::panopainter::LegacyStrokeSampleExecutionRequest {
.context = "Canvas::stroke_draw_samples", .context = "Canvas::stroke_draw_samples",
.target_size = { m_width, m_height }, .target_size = { m_width, m_height },
.vertices = P, .vertices = P,
.sample_points = sample_points,
.copy_stroke_destination = copy_stroke_destination, .copy_stroke_destination = copy_stroke_destination,
.bind_destination_texture = [&] { .bind_destination_texture = [&] {
set_active_texture_unit(1); set_active_texture_unit(1);
@@ -676,6 +709,7 @@ void Canvas::stroke_draw()
const auto stroke_rasterization = canvas_stroke_rasterization_plan(m_width, m_height); const auto stroke_rasterization = canvas_stroke_rasterization_plan(m_width, m_height);
const bool copy_stroke_destination = stroke_rasterization.copy_stroke_destination; const bool copy_stroke_destination = stroke_rasterization.copy_stroke_destination;
const auto stroke_material = canvas_stroke_material_plan(*brush, copy_stroke_destination); const auto stroke_material = canvas_stroke_material_plan(*brush, copy_stroke_destination);
const auto stroke_extent = canvas_stroke_extent(m_width, m_height);
apply_canvas_capability(blend_state(), false); apply_canvas_capability(blend_state(), false);
pp::panopainter::setup_legacy_stroke_shader( pp::panopainter::setup_legacy_stroke_shader(
@@ -745,8 +779,16 @@ void Canvas::stroke_draw()
m_tmp[i].unbindFramebuffer(); m_tmp[i].unbindFramebuffer();
m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], box_sample), glm::vec4(0), glm::vec4(m_width)); const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update(
box_face[i] = box_union(box_face[i], box_sample); pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest {
.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),
.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);
// TODO: maybe average color? // TODO: maybe average color?
pad_color = f.col; pad_color = f.col;
} }
@@ -779,34 +821,37 @@ void Canvas::stroke_draw()
{ {
if (!box_dirty[i]) if (!box_dirty[i])
continue; continue;
const auto& b = box_face[i]; const auto pad_region = pp::paint_renderer::plan_canvas_stroke_pad_region(
glm::vec2 box_size = zw(b) - xy(b); pp::paint_renderer::CanvasStrokePadRegionRequest {
glm::vec2 pad = { 20, 20 }; // pixels padding .extent = stroke_extent,
glm::vec4 pad_box = { .pass_dirty_box = canvas_stroke_box(box_face[i]),
glm::max({0, 0}, xy(b) - pad) * 2.f / m_size - 1.f, });
glm::min(m_size, zw(b) + pad) * 2.f / m_size - 1.f if (!pad_region.has_pixels)
}; continue;
// B(xw)--(zw)C box // B(xw)--(zw)C box
// | // | coordinates // | // | coordinates
// A(xy)--(zy)D mapping // A(xy)--(zy)D mapping
std::array<vertex_t, 6> pad_quad = { std::array<vertex_t, 6> pad_quad = {
vertex_t({pad_box.x, pad_box.y}), // A vertex_t({pad_region.ndc_quad[0].x, pad_region.ndc_quad[0].y}), // A
vertex_t({pad_box.x, pad_box.w}), // B vertex_t({pad_region.ndc_quad[1].x, pad_region.ndc_quad[1].y}), // B
vertex_t({pad_box.z, pad_box.w}), // C vertex_t({pad_region.ndc_quad[2].x, pad_region.ndc_quad[2].y}), // C
vertex_t({pad_box.x, pad_box.y}), // A vertex_t({pad_region.ndc_quad[3].x, pad_region.ndc_quad[3].y}), // A
vertex_t({pad_box.z, pad_box.w}), // C vertex_t({pad_region.ndc_quad[4].x, pad_region.ndc_quad[4].y}), // C
vertex_t({pad_box.z, pad_box.y}), // D vertex_t({pad_region.ndc_quad[5].x, pad_region.ndc_quad[5].y}), // D
}; };
m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size()); m_brush_shape.update_vertices(pad_quad.data(), pad_quad.size());
m_tmp[i].bindFramebuffer(); m_tmp[i].bindFramebuffer();
if (copy_stroke_destination) if (copy_stroke_destination)
{ {
glm::vec2 o = glm::max({0, 0}, xy(b) - pad);
glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o;
m_tex[i].bind(); m_tex[i].bind();
if (sz.x > 0 && sz.y > 0) copy_framebuffer_to_texture_2d(
copy_framebuffer_to_texture_2d(o.x, o.y, o.x, o.y, sz.x, sz.y); pad_region.copy_region.x,
pad_region.copy_region.y,
pad_region.copy_region.x,
pad_region.copy_region.y,
pad_region.copy_region.width,
pad_region.copy_region.height);
} }
m_brush_shape.draw_fill(); m_brush_shape.draw_fill();
m_tmp[i].unbindFramebuffer(); m_tmp[i].unbindFramebuffer();
@@ -845,8 +890,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
if (stroke_material.composite_pass.dual_blend_mode == 0) const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update(
m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], box_sample), glm::vec4(0), glm::vec4(m_width)); pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest {
.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),
.include_in_committed_dirty_box =
stroke_material.composite_pass.dual_blend_mode == 0,
});
m_dirty_box[i] = glm_box(dirty_update.accumulated_dirty_box);
} }
} }
} }

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "paint_renderer/compositor.h"
#include "util.h" #include "util.h"
#include <array> #include <array>
@@ -13,6 +14,7 @@ struct LegacyStrokeSampleExecutionRequest {
std::string_view context; std::string_view context;
glm::vec2 target_size {}; glm::vec2 target_size {};
std::span<const vertex_t> vertices; std::span<const vertex_t> vertices;
std::span<const pp::paint_renderer::CanvasStrokePoint> sample_points;
bool copy_stroke_destination = false; bool copy_stroke_destination = false;
std::function<void()> bind_destination_texture; std::function<void()> bind_destination_texture;
std::function<void(int, int, int, int, int, int)> copy_framebuffer_to_destination_texture; std::function<void(int, int, int, int, int, int)> copy_framebuffer_to_destination_texture;
@@ -49,25 +51,28 @@ struct LegacyStrokeSampleExecutionResult {
request.bind_destination_texture(); request.bind_destination_texture();
} }
glm::vec2 bb_min(request.target_size); const auto sample_bounds = pp::paint_renderer::plan_canvas_stroke_sample_bounds(
glm::vec2 bb_max(0.0f, 0.0f); pp::paint_renderer::CanvasStrokeSampleBoundsRequest {
for (const auto& vertex : request.vertices) { .extent = pp::renderer::Extent2D {
bb_min = glm::max({ 0.0f, 0.0f }, glm::min(bb_min, xy(vertex.pos))); .width = static_cast<std::uint32_t>(request.target_size.x),
bb_max = glm::min(request.target_size, glm::max(bb_max, xy(vertex.pos))); .height = static_cast<std::uint32_t>(request.target_size.y),
},
.vertices = request.sample_points,
});
if (!sample_bounds.has_pixels) {
if (request.copy_stroke_destination) {
request.unbind_destination_texture();
}
return result;
} }
const auto bb_sz = bb_max - bb_min; result.copy_position = glm::ivec2(sample_bounds.copy_region.x, sample_bounds.copy_region.y);
const glm::vec2 pad(1.0f); result.copy_size = glm::ivec2(sample_bounds.copy_region.width, sample_bounds.copy_region.height);
const glm::ivec2 target_extent(request.target_size); result.dirty_bounds = glm::vec4(
result.copy_position = glm::clamp( sample_bounds.dirty_bounds.min_x,
glm::floor(bb_min) - pad, sample_bounds.dirty_bounds.min_y,
glm::vec2(0.0f), sample_bounds.dirty_bounds.max_x,
request.target_size); sample_bounds.dirty_bounds.max_y);
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) { if (request.copy_stroke_destination) {
request.copy_framebuffer_to_destination_texture( request.copy_framebuffer_to_destination_texture(

View File

@@ -253,11 +253,18 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(
bool copy_stroke_destination) bool copy_stroke_destination)
{ {
const glm::vec2 size = { m_rtt.getWidth(), m_rtt.getHeight() }; const glm::vec2 size = { m_rtt.getWidth(), m_rtt.getHeight() };
const std::array<pp::paint_renderer::CanvasStrokePoint, 4> sample_points {
pp::paint_renderer::CanvasStrokePoint { .x = P[0].pos.x, .y = P[0].pos.y },
pp::paint_renderer::CanvasStrokePoint { .x = P[1].pos.x, .y = P[1].pos.y },
pp::paint_renderer::CanvasStrokePoint { .x = P[2].pos.x, .y = P[2].pos.y },
pp::paint_renderer::CanvasStrokePoint { .x = P[3].pos.x, .y = P[3].pos.y },
};
const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample( const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample(
pp::panopainter::LegacyStrokeSampleExecutionRequest { pp::panopainter::LegacyStrokeSampleExecutionRequest {
.context = "NodeStrokePreview::stroke_draw_samples", .context = "NodeStrokePreview::stroke_draw_samples",
.target_size = size, .target_size = size,
.vertices = P, .vertices = P,
.sample_points = sample_points,
.copy_stroke_destination = copy_stroke_destination, .copy_stroke_destination = copy_stroke_destination,
.bind_destination_texture = [&] { .bind_destination_texture = [&] {
set_active_texture_unit(1U); set_active_texture_unit(1U);

View File

@@ -82,6 +82,34 @@ namespace {
} }
} }
[[nodiscard]] CanvasStrokeBox box_union(CanvasStrokeBox lhs, CanvasStrokeBox rhs) noexcept
{
return CanvasStrokeBox {
.min_x = std::min(lhs.min_x, rhs.min_x),
.min_y = std::min(lhs.min_y, rhs.min_y),
.max_x = std::max(lhs.max_x, rhs.max_x),
.max_y = std::max(lhs.max_y, rhs.max_y),
};
}
[[nodiscard]] CanvasStrokeBox clamp_box_to_legacy_dirty_extent(
CanvasStrokeBox box,
pp::renderer::Extent2D extent) noexcept
{
const auto max_value = static_cast<float>(extent.width);
return CanvasStrokeBox {
.min_x = std::clamp(box.min_x, 0.0F, max_value),
.min_y = std::clamp(box.min_y, 0.0F, max_value),
.max_x = std::clamp(box.max_x, 0.0F, max_value),
.max_y = std::clamp(box.max_y, 0.0F, max_value),
};
}
[[nodiscard]] bool has_positive_area(CanvasStrokeBox box) noexcept
{
return box.max_x > box.min_x && box.max_y > box.min_y;
}
[[nodiscard]] pp::foundation::Result<std::size_t> expected_pixel_count(pp::renderer::Extent2D extent) noexcept [[nodiscard]] pp::foundation::Result<std::size_t> expected_pixel_count(pp::renderer::Extent2D extent) noexcept
{ {
const auto extent_status = pp::renderer::validate_extent(extent); const auto extent_status = pp::renderer::validate_extent(extent);
@@ -1371,6 +1399,117 @@ pp::foundation::Result<CanvasStrokeRasterizationPlan> plan_canvas_stroke_rasteri
return pp::foundation::Result<CanvasStrokeRasterizationPlan>::success(plan); return pp::foundation::Result<CanvasStrokeRasterizationPlan>::success(plan);
} }
CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds(
CanvasStrokeSampleBoundsRequest request) noexcept
{
CanvasStrokeSampleBoundsPlan plan;
if (request.extent.width == 0U || request.extent.height == 0U || request.vertices.empty()) {
return plan;
}
const auto target_width = static_cast<float>(request.extent.width);
const auto target_height = static_cast<float>(request.extent.height);
auto min_x = target_width;
auto min_y = target_height;
auto max_x = 0.0F;
auto max_y = 0.0F;
for (const auto& vertex : request.vertices) {
min_x = std::max(0.0F, std::min(min_x, vertex.x));
min_y = std::max(0.0F, std::min(min_y, vertex.y));
max_x = std::min(target_width, std::max(max_x, vertex.x));
max_y = std::min(target_height, std::max(max_y, vertex.y));
}
const auto pad = std::max(0.0F, request.sample_padding_pixels);
const auto copy_x = static_cast<int>(std::clamp(std::floor(min_x) - pad, 0.0F, target_width));
const auto copy_y = static_cast<int>(std::clamp(std::floor(min_y) - pad, 0.0F, target_height));
const auto max_width = static_cast<int>(request.extent.width) - copy_x;
const auto max_height = static_cast<int>(request.extent.height) - copy_y;
const auto copy_width = static_cast<int>(std::clamp(
std::ceil(max_x - min_x) + pad * 2.0F,
0.0F,
static_cast<float>(max_width)));
const auto copy_height = static_cast<int>(std::clamp(
std::ceil(max_y - min_y) + pad * 2.0F,
0.0F,
static_cast<float>(max_height)));
plan.copy_region = CanvasStrokeCopyRegion {
.x = copy_x,
.y = copy_y,
.width = copy_width,
.height = copy_height,
};
plan.dirty_bounds = CanvasStrokeBox {
.min_x = static_cast<float>(copy_x),
.min_y = static_cast<float>(copy_y),
.max_x = static_cast<float>(copy_x + copy_width),
.max_y = static_cast<float>(copy_y + copy_height),
};
plan.has_pixels = copy_width > 0 && copy_height > 0;
return plan;
}
CanvasStrokeFaceDirtyUpdatePlan plan_canvas_stroke_face_dirty_update(
CanvasStrokeFaceDirtyUpdateRequest request) noexcept
{
CanvasStrokeFaceDirtyUpdatePlan plan;
plan.accumulated_dirty_box = request.previous_accumulated_dirty_box;
plan.pass_dirty_box = box_union(request.previous_pass_dirty_box, request.sample_dirty_box);
plan.has_dirty_pixels = has_positive_area(request.sample_dirty_box);
plan.pass_dirty = plan.has_dirty_pixels;
if (request.include_in_committed_dirty_box && plan.has_dirty_pixels) {
plan.accumulated_dirty_box = clamp_box_to_legacy_dirty_extent(
box_union(request.previous_accumulated_dirty_box, request.sample_dirty_box),
request.extent);
plan.committed_dirty = true;
}
return plan;
}
CanvasStrokePadRegionPlan plan_canvas_stroke_pad_region(
CanvasStrokePadRegionRequest request) noexcept
{
CanvasStrokePadRegionPlan plan;
if (request.extent.width == 0U || request.extent.height == 0U) {
return plan;
}
const auto pad = std::max(0.0F, request.pad_pixels);
const auto width = static_cast<float>(request.extent.width);
const auto height = static_cast<float>(request.extent.height);
const auto origin_x = std::max(0.0F, request.pass_dirty_box.min_x - pad);
const auto origin_y = std::max(0.0F, request.pass_dirty_box.min_y - pad);
const auto max_x = std::min(width, request.pass_dirty_box.max_x + pad);
const auto max_y = std::min(height, request.pass_dirty_box.max_y + pad);
const auto copy_width = max_x - origin_x;
const auto copy_height = max_y - origin_y;
if (copy_width <= 0.0F || copy_height <= 0.0F) {
return plan;
}
const auto left = origin_x * 2.0F / width - 1.0F;
const auto bottom = origin_y * 2.0F / height - 1.0F;
const auto right = max_x * 2.0F / width - 1.0F;
const auto top = max_y * 2.0F / height - 1.0F;
plan.copy_region = CanvasStrokeCopyRegion {
.x = static_cast<int>(origin_x),
.y = static_cast<int>(origin_y),
.width = static_cast<int>(copy_width),
.height = static_cast<int>(copy_height),
};
plan.ndc_quad = {
CanvasStrokePoint { .x = left, .y = bottom },
CanvasStrokePoint { .x = left, .y = top },
CanvasStrokePoint { .x = right, .y = top },
CanvasStrokePoint { .x = left, .y = bottom },
CanvasStrokePoint { .x = right, .y = top },
CanvasStrokePoint { .x = right, .y = bottom },
};
plan.has_pixels = true;
return plan;
}
const char* stroke_composite_path_name(StrokeCompositePath path) noexcept const char* stroke_composite_path_name(StrokeCompositePath path) noexcept
{ {
switch (path) { switch (path) {

View File

@@ -149,6 +149,65 @@ struct CanvasStrokeRasterizationPlan {
bool compatibility_fallback = false; bool compatibility_fallback = false;
}; };
struct CanvasStrokePoint {
float x = 0.0F;
float y = 0.0F;
};
struct CanvasStrokeBox {
float min_x = 0.0F;
float min_y = 0.0F;
float max_x = 0.0F;
float max_y = 0.0F;
};
struct CanvasStrokeCopyRegion {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
};
struct CanvasStrokeSampleBoundsRequest {
pp::renderer::Extent2D extent {};
std::span<const CanvasStrokePoint> vertices;
float sample_padding_pixels = 1.0F;
};
struct CanvasStrokeSampleBoundsPlan {
CanvasStrokeCopyRegion copy_region {};
CanvasStrokeBox dirty_bounds {};
bool has_pixels = false;
};
struct CanvasStrokeFaceDirtyUpdateRequest {
pp::renderer::Extent2D extent {};
CanvasStrokeBox previous_accumulated_dirty_box {};
CanvasStrokeBox previous_pass_dirty_box {};
CanvasStrokeBox sample_dirty_box {};
bool include_in_committed_dirty_box = true;
};
struct CanvasStrokeFaceDirtyUpdatePlan {
CanvasStrokeBox accumulated_dirty_box {};
CanvasStrokeBox pass_dirty_box {};
bool has_dirty_pixels = false;
bool committed_dirty = false;
bool pass_dirty = false;
};
struct CanvasStrokePadRegionRequest {
pp::renderer::Extent2D extent {};
CanvasStrokeBox pass_dirty_box {};
float pad_pixels = 20.0F;
};
struct CanvasStrokePadRegionPlan {
CanvasStrokeCopyRegion copy_region {};
std::array<CanvasStrokePoint, 6> ndc_quad {};
bool has_pixels = false;
};
struct DocumentFaceCompositeRequest { struct DocumentFaceCompositeRequest {
const pp::document::CanvasDocument* document = nullptr; const pp::document::CanvasDocument* document = nullptr;
std::size_t frame_index = 0; std::size_t frame_index = 0;
@@ -393,6 +452,15 @@ 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]] CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds(
CanvasStrokeSampleBoundsRequest request) noexcept;
[[nodiscard]] CanvasStrokeFaceDirtyUpdatePlan plan_canvas_stroke_face_dirty_update(
CanvasStrokeFaceDirtyUpdateRequest request) noexcept;
[[nodiscard]] CanvasStrokePadRegionPlan plan_canvas_stroke_pad_region(
CanvasStrokePadRegionRequest request) noexcept;
[[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept; [[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept;
} }

View File

@@ -15,7 +15,12 @@ using pp::paint::Rgba;
using pp::paint::StrokeBlendMode; 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::CanvasStrokeFaceDirtyUpdateRequest;
using pp::paint_renderer::CanvasStrokeMaterialRequest; using pp::paint_renderer::CanvasStrokeMaterialRequest;
using pp::paint_renderer::CanvasStrokePadRegionRequest;
using pp::paint_renderer::CanvasStrokePoint;
using pp::paint_renderer::CanvasStrokeSampleBoundsRequest;
using pp::paint_renderer::CanvasStrokeTextureRole; using pp::paint_renderer::CanvasStrokeTextureRole;
using pp::paint_renderer::DocumentFaceCompositeRequest; using pp::paint_renderer::DocumentFaceCompositeRequest;
using pp::paint_renderer::DocumentFrameCompositeRequest; using pp::paint_renderer::DocumentFrameCompositeRequest;
@@ -27,9 +32,12 @@ using pp::paint_renderer::composite_document_face;
using pp::paint_renderer::composite_document_frame; using pp::paint_renderer::composite_document_frame;
using pp::paint_renderer::export_document_depth_pngs; 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_feedback; using pp::paint_renderer::plan_canvas_stroke_feedback;
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_rasterization; using pp::paint_renderer::plan_canvas_stroke_rasterization;
using pp::paint_renderer::plan_canvas_stroke_sample_bounds;
using pp::paint_renderer::plan_document_depth_export_render; using pp::paint_renderer::plan_document_depth_export_render;
using pp::paint_renderer::plan_stroke_composite; using pp::paint_renderer::plan_stroke_composite;
using pp::paint_renderer::stroke_composite_path_name; using pp::paint_renderer::stroke_composite_path_name;
@@ -1879,6 +1887,169 @@ void plans_canvas_stroke_rasterization_boundary(pp::tests::Harness& h)
PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument); PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument);
} }
void canvas_stroke_sample_bounds_empty_vertices_have_no_pixels(pp::tests::Harness& h)
{
const auto plan = plan_canvas_stroke_sample_bounds(
CanvasStrokeSampleBoundsRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.vertices = {},
});
PP_EXPECT(h, !plan.has_pixels);
PP_EXPECT(h, plan.copy_region.width == 0);
PP_EXPECT(h, plan.copy_region.height == 0);
}
void canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad(pp::tests::Harness& h)
{
const CanvasStrokePoint vertices[] {
{ .x = 10.0F, .y = 20.0F },
{ .x = 10.0F, .y = 30.0F },
{ .x = 30.0F, .y = 30.0F },
{ .x = 30.0F, .y = 20.0F },
};
const auto plan = plan_canvas_stroke_sample_bounds(
CanvasStrokeSampleBoundsRequest {
.extent = Extent2D { .width = 64, .height = 64 },
.vertices = vertices,
});
PP_EXPECT(h, plan.has_pixels);
PP_EXPECT(h, plan.copy_region.x == 9);
PP_EXPECT(h, plan.copy_region.y == 19);
PP_EXPECT(h, plan.copy_region.width == 22);
PP_EXPECT(h, plan.copy_region.height == 12);
PP_EXPECT(h, near(plan.dirty_bounds.min_x, 9.0F));
PP_EXPECT(h, near(plan.dirty_bounds.min_y, 19.0F));
PP_EXPECT(h, near(plan.dirty_bounds.max_x, 31.0F));
PP_EXPECT(h, near(plan.dirty_bounds.max_y, 31.0F));
}
void canvas_stroke_sample_bounds_clamp_out_of_range_vertices(pp::tests::Harness& h)
{
const CanvasStrokePoint vertices[] {
{ .x = -10.0F, .y = -5.0F },
{ .x = 70.0F, .y = 80.0F },
};
const auto plan = plan_canvas_stroke_sample_bounds(
CanvasStrokeSampleBoundsRequest {
.extent = Extent2D { .width = 64, .height = 32 },
.vertices = vertices,
});
PP_EXPECT(h, plan.has_pixels);
PP_EXPECT(h, plan.copy_region.x == 0);
PP_EXPECT(h, plan.copy_region.y == 0);
PP_EXPECT(h, plan.copy_region.width == 64);
PP_EXPECT(h, plan.copy_region.height == 32);
PP_EXPECT(h, near(plan.dirty_bounds.max_x, 64.0F));
PP_EXPECT(h, near(plan.dirty_bounds.max_y, 32.0F));
}
void canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad(pp::tests::Harness& h)
{
const auto plan = plan_canvas_stroke_pad_region(
CanvasStrokePadRegionRequest {
.extent = Extent2D { .width = 100, .height = 80 },
.pass_dirty_box = CanvasStrokeBox {
.min_x = 5.0F,
.min_y = 10.0F,
.max_x = 20.0F,
.max_y = 30.0F,
},
});
PP_EXPECT(h, plan.has_pixels);
PP_EXPECT(h, plan.copy_region.x == 0);
PP_EXPECT(h, plan.copy_region.y == 0);
PP_EXPECT(h, plan.copy_region.width == 40);
PP_EXPECT(h, plan.copy_region.height == 50);
PP_EXPECT(h, near(plan.ndc_quad[0].x, -1.0F));
PP_EXPECT(h, near(plan.ndc_quad[0].y, -1.0F));
PP_EXPECT(h, near(plan.ndc_quad[2].x, -0.2F));
PP_EXPECT(h, near(plan.ndc_quad[2].y, 0.25F));
PP_EXPECT(h, near(plan.ndc_quad[5].x, -0.2F));
PP_EXPECT(h, near(plan.ndc_quad[5].y, -1.0F));
}
void canvas_stroke_face_dirty_update_includes_committed_dirty_box(pp::tests::Harness& h)
{
const auto plan = plan_canvas_stroke_face_dirty_update(
CanvasStrokeFaceDirtyUpdateRequest {
.extent = Extent2D { .width = 64, .height = 32 },
.previous_accumulated_dirty_box = CanvasStrokeBox {
.min_x = 64.0F,
.min_y = 32.0F,
.max_x = 0.0F,
.max_y = 0.0F,
},
.previous_pass_dirty_box = CanvasStrokeBox {
.min_x = 64.0F,
.min_y = 32.0F,
.max_x = 0.0F,
.max_y = 0.0F,
},
.sample_dirty_box = CanvasStrokeBox {
.min_x = -5.0F,
.min_y = -3.0F,
.max_x = 80.0F,
.max_y = 90.0F,
},
.include_in_committed_dirty_box = true,
});
PP_EXPECT(h, plan.has_dirty_pixels);
PP_EXPECT(h, plan.committed_dirty);
PP_EXPECT(h, plan.pass_dirty);
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_x, 0.0F));
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_y, 0.0F));
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_x, 64.0F));
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_y, 64.0F));
PP_EXPECT(h, near(plan.pass_dirty_box.min_x, -5.0F));
PP_EXPECT(h, near(plan.pass_dirty_box.max_y, 90.0F));
}
void canvas_stroke_face_dirty_update_can_skip_committed_dirty_box(pp::tests::Harness& h)
{
const auto plan = plan_canvas_stroke_face_dirty_update(
CanvasStrokeFaceDirtyUpdateRequest {
.extent = Extent2D { .width = 64, .height = 32 },
.previous_accumulated_dirty_box = CanvasStrokeBox {
.min_x = 1.0F,
.min_y = 2.0F,
.max_x = 3.0F,
.max_y = 4.0F,
},
.previous_pass_dirty_box = CanvasStrokeBox {
.min_x = 10.0F,
.min_y = 10.0F,
.max_x = 20.0F,
.max_y = 20.0F,
},
.sample_dirty_box = CanvasStrokeBox {
.min_x = 0.0F,
.min_y = 0.0F,
.max_x = 30.0F,
.max_y = 30.0F,
},
.include_in_committed_dirty_box = false,
});
PP_EXPECT(h, plan.has_dirty_pixels);
PP_EXPECT(h, !plan.committed_dirty);
PP_EXPECT(h, plan.pass_dirty);
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_x, 1.0F));
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_y, 2.0F));
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_x, 3.0F));
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_y, 4.0F));
PP_EXPECT(h, near(plan.pass_dirty_box.min_x, 0.0F));
PP_EXPECT(h, near(plan.pass_dirty_box.min_y, 0.0F));
PP_EXPECT(h, near(plan.pass_dirty_box.max_x, 30.0F));
PP_EXPECT(h, near(plan.pass_dirty_box.max_y, 30.0F));
}
} }
int main() int main()
@@ -1920,5 +2091,23 @@ int main()
harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths); harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);
harness.run("canvas_stroke_feedback_preserves_legacy_fallback", canvas_stroke_feedback_preserves_legacy_fallback); harness.run("canvas_stroke_feedback_preserves_legacy_fallback", canvas_stroke_feedback_preserves_legacy_fallback);
harness.run("plans_canvas_stroke_rasterization_boundary", plans_canvas_stroke_rasterization_boundary); harness.run("plans_canvas_stroke_rasterization_boundary", plans_canvas_stroke_rasterization_boundary);
harness.run(
"canvas_stroke_sample_bounds_empty_vertices_have_no_pixels",
canvas_stroke_sample_bounds_empty_vertices_have_no_pixels);
harness.run(
"canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad",
canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad);
harness.run(
"canvas_stroke_sample_bounds_clamp_out_of_range_vertices",
canvas_stroke_sample_bounds_clamp_out_of_range_vertices);
harness.run(
"canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad",
canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad);
harness.run(
"canvas_stroke_face_dirty_update_includes_committed_dirty_box",
canvas_stroke_face_dirty_update_includes_committed_dirty_box);
harness.run(
"canvas_stroke_face_dirty_update_can_skip_committed_dirty_box",
canvas_stroke_face_dirty_update_can_skip_committed_dirty_box);
return harness.finish(); return harness.finish();
} }