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

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

View File

@@ -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(

View File

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

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