diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index f16d18f..f7e8329 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -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. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 6855cfa..7a09bec 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -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, diff --git a/src/canvas.cpp b/src/canvas.cpp index 38922a9..a220f12 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -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::max(width, 0)), + .height = static_cast(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 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 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); } } } diff --git a/src/legacy_canvas_stroke_execution_services.h b/src/legacy_canvas_stroke_execution_services.h index b4acf02..c8d3df7 100644 --- a/src/legacy_canvas_stroke_execution_services.h +++ b/src/legacy_canvas_stroke_execution_services.h @@ -1,5 +1,6 @@ #pragma once +#include "paint_renderer/compositor.h" #include "util.h" #include @@ -13,6 +14,7 @@ struct LegacyStrokeSampleExecutionRequest { std::string_view context; glm::vec2 target_size {}; std::span vertices; + std::span sample_points; bool copy_stroke_destination = false; std::function bind_destination_texture; std::function 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(request.target_size.x), + .height = static_cast(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( diff --git a/src/node_stroke_preview.cpp b/src/node_stroke_preview.cpp index 835e5a8..99c94b6 100644 --- a/src/node_stroke_preview.cpp +++ b/src/node_stroke_preview.cpp @@ -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 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); diff --git a/src/paint_renderer/compositor.cpp b/src/paint_renderer/compositor.cpp index 955735e..4d2b18a 100644 --- a/src/paint_renderer/compositor.cpp +++ b/src/paint_renderer/compositor.cpp @@ -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(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 expected_pixel_count(pp::renderer::Extent2D extent) noexcept { const auto extent_status = pp::renderer::validate_extent(extent); @@ -1371,6 +1399,117 @@ pp::foundation::Result plan_canvas_stroke_rasteri return pp::foundation::Result::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(request.extent.width); + const auto target_height = static_cast(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(std::clamp(std::floor(min_x) - pad, 0.0F, target_width)); + const auto copy_y = static_cast(std::clamp(std::floor(min_y) - pad, 0.0F, target_height)); + const auto max_width = static_cast(request.extent.width) - copy_x; + const auto max_height = static_cast(request.extent.height) - copy_y; + const auto copy_width = static_cast(std::clamp( + std::ceil(max_x - min_x) + pad * 2.0F, + 0.0F, + static_cast(max_width))); + const auto copy_height = static_cast(std::clamp( + std::ceil(max_y - min_y) + pad * 2.0F, + 0.0F, + static_cast(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(copy_x), + .min_y = static_cast(copy_y), + .max_x = static_cast(copy_x + copy_width), + .max_y = static_cast(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(request.extent.width); + const auto height = static_cast(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(origin_x), + .y = static_cast(origin_y), + .width = static_cast(copy_width), + .height = static_cast(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) { diff --git a/src/paint_renderer/compositor.h b/src/paint_renderer/compositor.h index 00dd238..2496a2c 100644 --- a/src/paint_renderer/compositor.h +++ b/src/paint_renderer/compositor.h @@ -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 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 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; } diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp index e9f0054..300a5d4 100644 --- a/tests/paint_renderer/compositor_tests.cpp +++ b/tests/paint_renderer/compositor_tests.cpp @@ -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(); }