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

@@ -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) {

View File

@@ -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;
}