Extract stroke dirty bounds planning
This commit is contained in:
@@ -18,6 +18,11 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
|
||||
## 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
|
||||
`legacy_canvas_stroke_composite_services.h` for commit-time final stroke
|
||||
`kShader::CompDraw` binding and composite/pattern/dual uniform writes.
|
||||
|
||||
@@ -1354,8 +1354,11 @@ stroke composite `kShader::CompDraw` setup now pass through
|
||||
reuses that same composite service for commit-time final stroke compositing.
|
||||
Stroke padding and commit dilate `kShader::StrokePad`/`kShader::StrokeDilate`
|
||||
setup now pass through `legacy_canvas_stroke_edge_services.h`, leaving
|
||||
RTT/texture ownership, dirty-box policy, checkerboard/non-stroke composite
|
||||
shaders, quad expansion, and retained callback execution under `DEBT-0036`.
|
||||
RTT/texture ownership, checkerboard/non-stroke composite shaders, and retained
|
||||
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
|
||||
OpenGL internal/pixel/component token mapping, including depth-stencil formats,
|
||||
for future backend texture objects. `Texture2D` 2D texture binding, upload,
|
||||
|
||||
@@ -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(
|
||||
int width,
|
||||
int height,
|
||||
@@ -530,11 +553,21 @@ glm::vec4 Canvas::stroke_draw_samples(
|
||||
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(
|
||||
pp::panopainter::LegacyStrokeSampleExecutionRequest {
|
||||
.context = "Canvas::stroke_draw_samples",
|
||||
.target_size = { m_width, m_height },
|
||||
.vertices = P,
|
||||
.sample_points = sample_points,
|
||||
.copy_stroke_destination = copy_stroke_destination,
|
||||
.bind_destination_texture = [&] {
|
||||
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 bool copy_stroke_destination = stroke_rasterization.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);
|
||||
pp::panopainter::setup_legacy_stroke_shader(
|
||||
@@ -745,8 +779,16 @@ void Canvas::stroke_draw()
|
||||
|
||||
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));
|
||||
box_face[i] = box_union(box_face[i], box_sample);
|
||||
const auto dirty_update = pp::paint_renderer::plan_canvas_stroke_face_dirty_update(
|
||||
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?
|
||||
pad_color = f.col;
|
||||
}
|
||||
@@ -779,34 +821,37 @@ void Canvas::stroke_draw()
|
||||
{
|
||||
if (!box_dirty[i])
|
||||
continue;
|
||||
const auto& b = box_face[i];
|
||||
glm::vec2 box_size = zw(b) - xy(b);
|
||||
glm::vec2 pad = { 20, 20 }; // pixels padding
|
||||
glm::vec4 pad_box = {
|
||||
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
|
||||
};
|
||||
const auto pad_region = pp::paint_renderer::plan_canvas_stroke_pad_region(
|
||||
pp::paint_renderer::CanvasStrokePadRegionRequest {
|
||||
.extent = stroke_extent,
|
||||
.pass_dirty_box = canvas_stroke_box(box_face[i]),
|
||||
});
|
||||
if (!pad_region.has_pixels)
|
||||
continue;
|
||||
// B(xw)--(zw)C box
|
||||
// | // | coordinates
|
||||
// A(xy)--(zy)D mapping
|
||||
std::array<vertex_t, 6> pad_quad = {
|
||||
vertex_t({pad_box.x, pad_box.y}), // A
|
||||
vertex_t({pad_box.x, pad_box.w}), // B
|
||||
vertex_t({pad_box.z, pad_box.w}), // C
|
||||
vertex_t({pad_box.x, pad_box.y}), // A
|
||||
vertex_t({pad_box.z, pad_box.w}), // C
|
||||
vertex_t({pad_box.z, pad_box.y}), // D
|
||||
vertex_t({pad_region.ndc_quad[0].x, pad_region.ndc_quad[0].y}), // A
|
||||
vertex_t({pad_region.ndc_quad[1].x, pad_region.ndc_quad[1].y}), // B
|
||||
vertex_t({pad_region.ndc_quad[2].x, pad_region.ndc_quad[2].y}), // C
|
||||
vertex_t({pad_region.ndc_quad[3].x, pad_region.ndc_quad[3].y}), // A
|
||||
vertex_t({pad_region.ndc_quad[4].x, pad_region.ndc_quad[4].y}), // C
|
||||
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_tmp[i].bindFramebuffer();
|
||||
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();
|
||||
if (sz.x > 0 && sz.y > 0)
|
||||
copy_framebuffer_to_texture_2d(o.x, o.y, o.x, o.y, sz.x, sz.y);
|
||||
copy_framebuffer_to_texture_2d(
|
||||
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_tmp[i].unbindFramebuffer();
|
||||
@@ -845,8 +890,16 @@ void Canvas::stroke_draw()
|
||||
m_tmp_dual[i].unbindFramebuffer();
|
||||
|
||||
// this mode overflows the main brush boundries
|
||||
if (stroke_material.composite_pass.dual_blend_mode == 0)
|
||||
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(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "paint_renderer/compositor.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <array>
|
||||
@@ -13,6 +14,7 @@ struct LegacyStrokeSampleExecutionRequest {
|
||||
std::string_view context;
|
||||
glm::vec2 target_size {};
|
||||
std::span<const vertex_t> vertices;
|
||||
std::span<const pp::paint_renderer::CanvasStrokePoint> sample_points;
|
||||
bool copy_stroke_destination = false;
|
||||
std::function<void()> bind_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();
|
||||
}
|
||||
|
||||
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 sample_bounds = pp::paint_renderer::plan_canvas_stroke_sample_bounds(
|
||||
pp::paint_renderer::CanvasStrokeSampleBoundsRequest {
|
||||
.extent = pp::renderer::Extent2D {
|
||||
.width = static_cast<std::uint32_t>(request.target_size.x),
|
||||
.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;
|
||||
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);
|
||||
result.copy_position = glm::ivec2(sample_bounds.copy_region.x, sample_bounds.copy_region.y);
|
||||
result.copy_size = glm::ivec2(sample_bounds.copy_region.width, sample_bounds.copy_region.height);
|
||||
result.dirty_bounds = glm::vec4(
|
||||
sample_bounds.dirty_bounds.min_x,
|
||||
sample_bounds.dirty_bounds.min_y,
|
||||
sample_bounds.dirty_bounds.max_x,
|
||||
sample_bounds.dirty_bounds.max_y);
|
||||
|
||||
if (request.copy_stroke_destination) {
|
||||
request.copy_framebuffer_to_destination_texture(
|
||||
|
||||
@@ -253,11 +253,18 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(
|
||||
bool copy_stroke_destination)
|
||||
{
|
||||
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(
|
||||
pp::panopainter::LegacyStrokeSampleExecutionRequest {
|
||||
.context = "NodeStrokePreview::stroke_draw_samples",
|
||||
.target_size = size,
|
||||
.vertices = P,
|
||||
.sample_points = sample_points,
|
||||
.copy_stroke_destination = copy_stroke_destination,
|
||||
.bind_destination_texture = [&] {
|
||||
set_active_texture_unit(1U);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
switch (path) {
|
||||
|
||||
@@ -149,6 +149,65 @@ struct CanvasStrokeRasterizationPlan {
|
||||
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 {
|
||||
const pp::document::CanvasDocument* document = nullptr;
|
||||
std::size_t frame_index = 0;
|
||||
@@ -393,6 +452,15 @@ export_document_animation_frames_equirectangular_pngs(
|
||||
pp::renderer::RenderDeviceFeatures features,
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,12 @@ using pp::paint::Rgba;
|
||||
using pp::paint::StrokeBlendMode;
|
||||
using pp::assets::decode_png_rgba8;
|
||||
using pp::paint_renderer::CanvasBlendGateRequest;
|
||||
using pp::paint_renderer::CanvasStrokeBox;
|
||||
using pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest;
|
||||
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::DocumentFaceCompositeRequest;
|
||||
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::export_document_depth_pngs;
|
||||
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_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_sample_bounds;
|
||||
using pp::paint_renderer::plan_document_depth_export_render;
|
||||
using pp::paint_renderer::plan_stroke_composite;
|
||||
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);
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -1920,5 +2091,23 @@ int main()
|
||||
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("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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user