#include "paint_renderer/compositor.h" #include "assets/image_pixels.h" #include "renderer_api/recording_renderer.h" #include #include #include #include namespace pp::paint_renderer { pp::foundation::Result composite_document_layer_frame( const pp::document::CanvasDocument& document, std::size_t layer_index, std::size_t frame_index, pp::paint::Rgba clear_color); namespace { [[nodiscard]] bool is_valid_blend_mode(pp::paint::BlendMode mode) noexcept { switch (mode) { case pp::paint::BlendMode::normal: case pp::paint::BlendMode::multiply: case pp::paint::BlendMode::screen: case pp::paint::BlendMode::color_dodge: case pp::paint::BlendMode::overlay: return true; } return false; } [[nodiscard]] bool is_valid_stroke_blend_mode(pp::paint::StrokeBlendMode mode) noexcept { switch (mode) { case pp::paint::StrokeBlendMode::normal: case pp::paint::StrokeBlendMode::multiply: case pp::paint::StrokeBlendMode::subtract: case pp::paint::StrokeBlendMode::darken: case pp::paint::StrokeBlendMode::overlay: case pp::paint::StrokeBlendMode::color_dodge: case pp::paint::StrokeBlendMode::color_burn: case pp::paint::StrokeBlendMode::linear_burn: case pp::paint::StrokeBlendMode::hard_mix: case pp::paint::StrokeBlendMode::linear_height: case pp::paint::StrokeBlendMode::height: return true; } return false; } [[nodiscard]] bool paint_blend_mode_from_persisted_index(int value, pp::paint::BlendMode& out) noexcept { switch (value) { case 0: out = pp::paint::BlendMode::normal; return true; case 1: out = pp::paint::BlendMode::multiply; return true; case 2: out = pp::paint::BlendMode::screen; return true; case 3: out = pp::paint::BlendMode::color_dodge; return true; case 4: out = pp::paint::BlendMode::overlay; return true; default: return false; } } [[nodiscard]] bool stroke_blend_mode_from_persisted_index(int value, pp::paint::StrokeBlendMode& out) noexcept { switch (value) { case 0: out = pp::paint::StrokeBlendMode::normal; return true; case 1: out = pp::paint::StrokeBlendMode::multiply; return true; case 2: out = pp::paint::StrokeBlendMode::subtract; return true; case 3: out = pp::paint::StrokeBlendMode::darken; return true; case 4: out = pp::paint::StrokeBlendMode::overlay; return true; case 5: out = pp::paint::StrokeBlendMode::color_dodge; return true; case 6: out = pp::paint::StrokeBlendMode::color_burn; return true; case 7: out = pp::paint::StrokeBlendMode::linear_burn; return true; case 8: out = pp::paint::StrokeBlendMode::hard_mix; return true; case 9: out = pp::paint::StrokeBlendMode::linear_height; return true; case 10: out = pp::paint::StrokeBlendMode::height; return true; default: return false; } } [[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); if (!extent_status.ok()) { return pp::foundation::Result::failure(extent_status); } const auto width = static_cast(extent.width); const auto height = static_cast(extent.height); if (width > std::numeric_limits::max() / height) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("pixel count overflows uint64")); } const auto count = width * height; if (count > static_cast(std::numeric_limits::max())) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("pixel count exceeds addressable memory")); } return pp::foundation::Result::success(static_cast(count)); } [[nodiscard]] pp::paint::Rgba rgba8_pixel(std::span bytes) noexcept { constexpr auto inv = 1.0F / 255.0F; return pp::paint::Rgba { .r = static_cast(bytes[0]) * inv, .g = static_cast(bytes[1]) * inv, .b = static_cast(bytes[2]) * inv, .a = static_cast(bytes[3]) * inv, }; } [[nodiscard]] std::byte rgba8_channel(float value) noexcept { const auto clamped = std::clamp(value, 0.0F, 1.0F); return static_cast(static_cast(clamped * 255.0F + 0.5F)); } struct CubeFaceSample { std::size_t face_index = 0; float s = 0.0F; float t = 0.0F; }; constexpr float document_depth_export_default_fov_degrees = 85.0F; [[nodiscard]] CubeFaceSample panopainter_cube_face_sample(float x, float y, float z) noexcept { const auto ax = std::fabs(x); const auto ay = std::fabs(y); const auto az = std::fabs(z); if (ax >= ay && ax >= az) { if (x >= 0.0F) { return CubeFaceSample { .face_index = 3U, .s = -z / ax, .t = -y / ax }; } return CubeFaceSample { .face_index = 1U, .s = z / ax, .t = -y / ax }; } if (ay >= ax && ay >= az) { if (y >= 0.0F) { return CubeFaceSample { .face_index = 5U, .s = x / ay, .t = z / ay }; } return CubeFaceSample { .face_index = 4U, .s = x / ay, .t = -z / ay }; } if (z >= 0.0F) { return CubeFaceSample { .face_index = 2U, .s = x / az, .t = -y / az }; } return CubeFaceSample { .face_index = 0U, .s = -x / az, .t = -y / az }; } [[nodiscard]] pp::paint::Rgba sample_face_nearest( const DocumentFaceCompositeResult& face, float s, float t) noexcept { const auto width = face.extent.width; const auto height = face.extent.height; const auto u = std::clamp((s + 1.0F) * 0.5F, 0.0F, 1.0F); const auto v = std::clamp((t + 1.0F) * 0.5F, 0.0F, 1.0F); const auto x = std::min(static_cast(u * static_cast(width)), width - 1U); const auto y = std::min(static_cast(v * static_cast(height)), height - 1U); return face.pixels[static_cast(y) * width + x]; } void append_rgba8_bytes(std::vector& bytes, std::span pixels) { bytes.clear(); bytes.reserve(pixels.size() * pp::document::rgba8_components); for (const auto& pixel : pixels) { bytes.push_back(rgba8_channel(pixel.r)); bytes.push_back(rgba8_channel(pixel.g)); bytes.push_back(rgba8_channel(pixel.b)); bytes.push_back(rgba8_channel(pixel.a)); } } [[nodiscard]] pp::foundation::Status composite_face_payload( std::span destination, pp::renderer::Extent2D extent, const pp::document::LayerFacePixels& payload, const pp::document::Layer& layer) noexcept { if (payload.x > extent.width || payload.width > extent.width - payload.x || payload.y > extent.height || payload.height > extent.height - payload.y) { return pp::foundation::Status::out_of_range("document face payload rectangle is outside the render extent"); } const auto payload_pixel_count = static_cast(payload.width) * static_cast(payload.height); if (payload_pixel_count > static_cast(std::numeric_limits::max() / 4U) || payload.rgba8.size() != static_cast(payload_pixel_count) * 4U) { return pp::foundation::Status::invalid_argument("document face payload byte size does not match dimensions"); } for (std::uint32_t y = 0; y < payload.height; ++y) { for (std::uint32_t x = 0; x < payload.width; ++x) { const auto payload_index = (static_cast(y) * payload.width + x) * 4U; const auto destination_index = static_cast(payload.y + y) * extent.width + static_cast(payload.x + x); auto stroke = rgba8_pixel(std::span(&payload.rgba8[payload_index], 4U)); stroke.a *= layer.opacity; destination[destination_index] = pp::paint::blend_pixels( destination[destination_index], stroke, layer.blend_mode); } } return pp::foundation::Status::success(); } [[nodiscard]] StrokeCompositePath composite_path_from_feedback(pp::renderer::PaintFeedbackPath path) noexcept { switch (path) { case pp::renderer::PaintFeedbackPath::none: return StrokeCompositePath::fixed_function_blend; case pp::renderer::PaintFeedbackPath::framebuffer_fetch: return StrokeCompositePath::framebuffer_fetch; case pp::renderer::PaintFeedbackPath::ping_pong_textures: return StrokeCompositePath::ping_pong_textures; } return StrokeCompositePath::fixed_function_blend; } void apply_stroke_plan(CanvasBlendGatePlan& gate, const StrokeCompositePlan& stroke) noexcept { gate.path = stroke.path; gate.reads_destination_color = stroke.path == StrokeCompositePath::framebuffer_fetch; gate.requires_auxiliary_texture = stroke.requires_auxiliary_texture; gate.requires_texture_copy = stroke.requires_texture_copy; gate.requires_render_target_blit = stroke.requires_render_target_blit; } void apply_feedback_plan(CanvasStrokeFeedbackPlan& plan, const pp::renderer::PaintFeedbackPlan& feedback) noexcept { plan.path = composite_path_from_feedback(feedback.path); plan.reads_destination_color = plan.path == StrokeCompositePath::framebuffer_fetch; plan.requires_auxiliary_texture = feedback.requires_auxiliary_texture; plan.requires_texture_copy = feedback.requires_texture_copy; plan.requires_render_target_blit = feedback.requires_render_target_blit; } void mark_shader_blend_fallback( CanvasBlendGatePlan& gate, pp::renderer::RenderDeviceFeatures features) noexcept { gate.shader_blend = true; gate.complex_blend = true; gate.compatibility_fallback = true; if (features.framebuffer_fetch) { gate.path = StrokeCompositePath::framebuffer_fetch; gate.reads_destination_color = true; } else if (features.texture_copy || features.render_target_blit) { gate.path = StrokeCompositePath::ping_pong_textures; gate.requires_auxiliary_texture = true; gate.requires_texture_copy = features.texture_copy; gate.requires_render_target_blit = !features.texture_copy && features.render_target_blit; } } void append_rgba8_bytes(std::vector& bytes, std::span pixels) { bytes.clear(); bytes.reserve(pixels.size() * pp::document::rgba8_components); for (const auto& pixel : pixels) { bytes.push_back(static_cast(rgba8_channel(pixel.r))); bytes.push_back(static_cast(rgba8_channel(pixel.g))); bytes.push_back(static_cast(rgba8_channel(pixel.b))); bytes.push_back(static_cast(rgba8_channel(pixel.a))); } } struct EquirectangularProjectionResult { pp::renderer::Extent2D face_extent {}; pp::renderer::Extent2D equirectangular_extent {}; std::vector pixels; std::size_t face_payload_count = 0; std::size_t composited_layer_face_count = 0; }; struct PerspectiveProjectionMap { pp::renderer::Extent2D output_extent {}; std::vector samples; }; pp::foundation::Result make_perspective_projection_map( pp::renderer::Extent2D output_extent, float vertical_fov_degrees) { if (!std::isfinite(vertical_fov_degrees)) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document depth export field of view must be finite")); } if (vertical_fov_degrees <= 0.0F || vertical_fov_degrees >= 180.0F) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document depth export field of view must be between 0 and 180")); } const auto output_pixel_count = expected_pixel_count(output_extent); if (!output_pixel_count) { return pp::foundation::Result::failure(output_pixel_count.status()); } PerspectiveProjectionMap map; map.output_extent = output_extent; map.samples.reserve(output_pixel_count.value()); constexpr auto pi = 3.14159265358979323846F; const auto tan_half_fov = std::tan(vertical_fov_degrees * pi / 360.0F); const auto aspect = static_cast(output_extent.width) / static_cast(output_extent.height); for (std::uint32_t y = 0; y < output_extent.height; ++y) { const auto ny = 1.0F - ((static_cast(y) + 0.5F) / static_cast(output_extent.height)) * 2.0F; for (std::uint32_t x = 0; x < output_extent.width; ++x) { const auto nx = ((static_cast(x) + 0.5F) / static_cast(output_extent.width)) * 2.0F - 1.0F; const auto dir_x = nx * aspect * tan_half_fov; const auto dir_y = ny * tan_half_fov; constexpr auto dir_z = -1.0F; const auto inv_length = 1.0F / std::sqrt(dir_x * dir_x + dir_y * dir_y + dir_z * dir_z); map.samples.push_back(panopainter_cube_face_sample( dir_x * inv_length, dir_y * inv_length, dir_z * inv_length)); } } return pp::foundation::Result::success(std::move(map)); } pp::foundation::Result project_document_frame_equirectangular( const DocumentFrameCompositeResult& composite) { const auto face_pixel_count = expected_pixel_count(composite.extent); if (!face_pixel_count) { return pp::foundation::Result::failure(face_pixel_count.status()); } for (const auto& face : composite.faces) { if (face.extent.width != composite.extent.width || face.extent.height != composite.extent.height || face.pixels.size() != face_pixel_count.value()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document equirectangular export requires complete cube faces")); } } const auto output_width = static_cast(composite.extent.width) * 4U; const auto output_height = static_cast(composite.extent.height) * 2U; if (output_width > std::numeric_limits::max() || output_height > std::numeric_limits::max()) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document equirectangular extent exceeds uint32")); } EquirectangularProjectionResult result; result.face_extent = composite.extent; result.equirectangular_extent = pp::renderer::Extent2D { .width = static_cast(output_width), .height = static_cast(output_height), }; result.face_payload_count = composite.face_payload_count; result.composited_layer_face_count = composite.composited_layer_face_count; const auto output_pixel_count = expected_pixel_count(result.equirectangular_extent); if (!output_pixel_count) { return pp::foundation::Result::failure(output_pixel_count.status()); } constexpr auto pi = 3.14159265358979323846F; constexpr auto two_pi = 6.28318530717958647692F; result.pixels.assign(output_pixel_count.value(), pp::paint::Rgba {}); for (std::uint32_t y = 0; y < result.equirectangular_extent.height; ++y) { const auto v = (static_cast(y) + 0.5F) / static_cast(result.equirectangular_extent.height); const auto angle_y = (1.0F - v) * pi; const auto sin_y = std::sin(angle_y); const auto cos_y = std::cos(angle_y); for (std::uint32_t x = 0; x < result.equirectangular_extent.width; ++x) { const auto u = (static_cast(x) + 0.5F) / static_cast(result.equirectangular_extent.width); const auto angle_x = (1.25F - u) * two_pi; const auto sample = panopainter_cube_face_sample( sin_y * std::cos(angle_x), cos_y, sin_y * std::sin(angle_x)); result.pixels[static_cast(y) * result.equirectangular_extent.width + x] = sample_face_nearest(composite.faces[sample.face_index], sample.s, sample.t); } } return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result> project_document_frame_perspective( const DocumentFrameCompositeResult& composite, const PerspectiveProjectionMap& projection) { const auto face_pixel_count = expected_pixel_count(composite.extent); if (!face_pixel_count) { return pp::foundation::Result>::failure(face_pixel_count.status()); } for (const auto& face : composite.faces) { if (face.extent.width != composite.extent.width || face.extent.height != composite.extent.height || face.pixels.size() != face_pixel_count.value()) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("document depth export requires complete cube faces")); } } std::vector pixels; pixels.reserve(projection.samples.size()); for (const auto& sample : projection.samples) { pixels.push_back(sample_face_nearest(composite.faces[sample.face_index], sample.s, sample.t)); } return pp::foundation::Result>::success(std::move(pixels)); } float sample_face_alpha_nearest( const DocumentFaceCompositeResult& face, float s, float t) noexcept { return sample_face_nearest(face, s, t).a; } pp::foundation::Result> project_document_depth_perspective( const pp::document::CanvasDocument& document, std::size_t frame_index, const PerspectiveProjectionMap& projection) { std::vector pixels( projection.samples.size(), pp::paint::Rgba { .r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F, }); const auto layer_count = document.layers().size(); for (std::size_t layer_index = 0; layer_index < layer_count; ++layer_index) { const auto& layer = document.layers()[layer_index]; if (!layer.visible || layer.opacity == 0.0F || frame_index >= layer.frames.size()) { continue; } auto composite = composite_document_layer_frame(document, layer_index, frame_index, {}); if (!composite) { return pp::foundation::Result>::failure(composite.status()); } const auto gray = static_cast(layer_index + 1U) / static_cast(layer_count + 1U); const pp::paint::Rgba layer_color { .r = gray, .g = gray, .b = gray, .a = 1.0F, }; for (std::size_t pixel_index = 0; pixel_index < projection.samples.size(); ++pixel_index) { const auto& sample = projection.samples[pixel_index]; const auto alpha = sample_face_alpha_nearest( composite.value().faces[sample.face_index], sample.s, sample.t); if (alpha > 0.01F) { pixels[pixel_index] = layer_color; } } } return pp::foundation::Result>::success(std::move(pixels)); } } pp::foundation::Status composite_layer( std::span destination, pp::renderer::Extent2D extent, LayerCompositeView layer) noexcept { const auto pixel_count = expected_pixel_count(extent); if (!pixel_count) { return pixel_count.status(); } if (destination.size() != pixel_count.value() || layer.pixels.size() != pixel_count.value()) { return pp::foundation::Status::invalid_argument("composite buffers must match the render extent"); } if (layer.opacity < 0.0F || layer.opacity > 1.0F) { return pp::foundation::Status::out_of_range("layer opacity must be between 0 and 1"); } if (!layer.visible || layer.opacity == 0.0F) { return pp::foundation::Status::success(); } for (std::size_t i = 0; i < destination.size(); ++i) { auto stroke = layer.pixels[i]; stroke.a *= layer.opacity; destination[i] = pp::paint::blend_pixels(destination[i], stroke, layer.blend_mode); } return pp::foundation::Status::success(); } pp::foundation::Result composite_document_face( DocumentFaceCompositeRequest request) { if (request.document == nullptr) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document composite request requires a document")); } if (request.face_index >= pp::document::cube_face_count) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document composite face index is outside the cube")); } if (request.frame_index >= request.document->frames().size()) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document composite frame index is outside the document")); } const pp::renderer::Extent2D extent { .width = request.document->width(), .height = request.document->height(), }; const auto pixel_count = expected_pixel_count(extent); if (!pixel_count) { return pp::foundation::Result::failure(pixel_count.status()); } DocumentFaceCompositeResult result; result.extent = extent; result.pixels.assign(pixel_count.value(), request.clear_color); result.visited_layer_count = request.document->layers().size(); for (const auto& layer : request.document->layers()) { if (!layer.visible || layer.opacity == 0.0F || request.frame_index >= layer.frames.size()) { continue; } bool composited_layer = false; const auto& frame = layer.frames[request.frame_index]; for (const auto& payload : frame.face_pixels) { if (payload.face_index != request.face_index) { continue; } const auto status = composite_face_payload( result.pixels, extent, payload, layer); if (!status.ok()) { return pp::foundation::Result::failure(status); } composited_layer = true; ++result.face_payload_count; } if (composited_layer) { ++result.composited_layer_count; } } return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result composite_document_frame( DocumentFrameCompositeRequest request) { if (request.document == nullptr) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document frame composite request requires a document")); } if (request.frame_index >= request.document->frames().size()) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document frame composite index is outside the document")); } DocumentFrameCompositeResult result; result.extent = pp::renderer::Extent2D { .width = request.document->width(), .height = request.document->height(), }; result.visited_layer_count = request.document->layers().size(); for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) { auto face = composite_document_face(DocumentFaceCompositeRequest { .document = request.document, .frame_index = request.frame_index, .face_index = face_index, .clear_color = request.clear_color, }); if (!face) { return pp::foundation::Result::failure(face.status()); } result.composited_layer_face_count += face.value().composited_layer_count; result.face_payload_count += face.value().face_payload_count; result.faces[face_index] = std::move(face.value()); } return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result composite_document_layer_frame( const pp::document::CanvasDocument& document, std::size_t layer_index, std::size_t frame_index, pp::paint::Rgba clear_color) { if (layer_index >= document.layers().size()) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document layer export index is outside the document")); } if (frame_index >= document.frames().size()) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document layer export frame index is outside the document")); } const pp::renderer::Extent2D extent { .width = document.width(), .height = document.height(), }; const auto pixel_count = expected_pixel_count(extent); if (!pixel_count) { return pp::foundation::Result::failure(pixel_count.status()); } const auto& source_layer = document.layers()[layer_index]; auto export_layer = source_layer; export_layer.visible = true; export_layer.opacity = 1.0F; export_layer.blend_mode = pp::paint::BlendMode::normal; DocumentFrameCompositeResult result; result.extent = extent; result.visited_layer_count = 1U; for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) { DocumentFaceCompositeResult face; face.extent = extent; face.pixels.assign(pixel_count.value(), clear_color); face.visited_layer_count = 1U; if (frame_index < source_layer.frames.size()) { bool composited_face = false; const auto& frame = source_layer.frames[frame_index]; for (const auto& payload : frame.face_pixels) { if (payload.face_index != face_index) { continue; } const auto status = composite_face_payload(face.pixels, extent, payload, export_layer); if (!status.ok()) { return pp::foundation::Result::failure(status); } composited_face = true; ++face.face_payload_count; ++result.face_payload_count; } if (composited_face) { face.composited_layer_count = 1U; ++result.composited_layer_face_count; } } result.faces[face_index] = std::move(face); } return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result upload_document_frame_faces( pp::renderer::IRenderDevice& device, DocumentFrameUploadRequest request) { auto composite = composite_document_frame(DocumentFrameCompositeRequest { .document = request.document, .frame_index = request.frame_index, .clear_color = request.clear_color, }); if (!composite) { return pp::foundation::Result::failure(composite.status()); } DocumentFrameUploadResult result; result.composite = std::move(composite.value()); std::vector upload_bytes; for (std::size_t face_index = 0; face_index < result.composite.faces.size(); ++face_index) { const pp::renderer::TextureDesc desc { .extent = result.composite.extent, .format = pp::renderer::TextureFormat::rgba8, .usage = pp::renderer::TextureUsage::sampled | pp::renderer::TextureUsage::upload_destination | pp::renderer::TextureUsage::readback_source | pp::renderer::TextureUsage::copy_source, .debug_name = "document-frame-face", }; auto texture = device.create_texture(desc); if (!texture) { return pp::foundation::Result::failure(texture.status()); } append_rgba8_bytes(upload_bytes, result.composite.faces[face_index].pixels); auto& context = device.immediate_context(); const auto upload_status = context.upload_texture( *texture.value(), pp::renderer::ReadbackRegion { .x = 0, .y = 0, .width = result.composite.extent.width, .height = result.composite.extent.height, }, upload_bytes); if (!upload_status.ok()) { return pp::foundation::Result::failure(upload_status); } result.uploaded_bytes += static_cast(upload_bytes.size()); if (request.transition_to_shader_read && device.features().explicit_texture_transitions) { const auto transition_status = context.transition_texture( *texture.value(), pp::renderer::TextureState::upload_destination, pp::renderer::TextureState::shader_read); if (!transition_status.ok()) { return pp::foundation::Result::failure(transition_status); } ++result.transition_count; } result.face_textures[face_index] = std::move(texture.value()); ++result.texture_count; } return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result record_document_frame_upload( DocumentFrameUploadRequest request) { pp::renderer::RecordingRenderDevice render_device; auto uploaded = upload_document_frame_faces(render_device, request); if (!uploaded) { return pp::foundation::Result::failure(uploaded.status()); } RecordedDocumentFrameUploadResult result; result.upload = std::move(uploaded.value()); const auto commands = render_device.commands(); result.command_count = commands.size(); for (const auto& command : commands) { if (command.kind == pp::renderer::RecordedRenderCommandKind::upload_texture) { ++result.upload_command_count; } if (command.kind == pp::renderer::RecordedRenderCommandKind::transition_texture) { ++result.transition_command_count; } } return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result export_document_frame_face_pngs( DocumentFrameCompositeRequest request) { auto composite = composite_document_frame(request); if (!composite) { return pp::foundation::Result::failure(composite.status()); } DocumentFrameFacePngExportResult result; result.composite = std::move(composite.value()); std::vector rgba8; for (std::size_t face_index = 0; face_index < result.composite.faces.size(); ++face_index) { append_rgba8_bytes(rgba8, result.composite.faces[face_index].pixels); auto encoded = pp::assets::encode_png_rgba8( result.composite.extent.width, result.composite.extent.height, rgba8); if (!encoded) { return pp::foundation::Result::failure(encoded.status()); } result.encoded_bytes += static_cast(encoded.value().size()); result.face_pngs[face_index] = std::move(encoded.value()); ++result.face_count; } return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result export_document_frame_equirectangular_png(const DocumentFrameCompositeResult& composite) { auto projection = project_document_frame_equirectangular(composite); if (!projection) { return pp::foundation::Result::failure(projection.status()); } DocumentFrameEquirectangularPngExportResult result; result.face_extent = projection.value().face_extent; result.equirectangular_extent = projection.value().equirectangular_extent; result.face_payload_count = projection.value().face_payload_count; result.composited_layer_face_count = projection.value().composited_layer_face_count; std::vector rgba8; append_rgba8_bytes(rgba8, projection.value().pixels); auto encoded = pp::assets::encode_png_rgba8( result.equirectangular_extent.width, result.equirectangular_extent.height, rgba8); if (!encoded) { return pp::foundation::Result::failure(encoded.status()); } result.encoded_bytes = static_cast(encoded.value().size()); result.png = std::move(encoded.value()); return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result export_document_frame_equirectangular_png(DocumentFrameCompositeRequest request) { auto composite = composite_document_frame(request); if (!composite) { return pp::foundation::Result::failure(composite.status()); } return export_document_frame_equirectangular_png(composite.value()); } pp::foundation::Result export_document_frame_equirectangular_jpeg(const DocumentFrameCompositeResult& composite, int quality) { auto projection = project_document_frame_equirectangular(composite); if (!projection) { return pp::foundation::Result::failure(projection.status()); } DocumentFrameEquirectangularJpegExportResult result; result.face_extent = projection.value().face_extent; result.equirectangular_extent = projection.value().equirectangular_extent; result.face_payload_count = projection.value().face_payload_count; result.composited_layer_face_count = projection.value().composited_layer_face_count; std::vector rgba8; append_rgba8_bytes(rgba8, projection.value().pixels); auto encoded = pp::assets::encode_jpeg_rgba8( result.equirectangular_extent.width, result.equirectangular_extent.height, rgba8, quality); if (!encoded) { return pp::foundation::Result::failure(encoded.status()); } auto with_xmp = pp::assets::inject_gpano_xmp_into_jpeg(encoded.value()); if (!with_xmp) { return pp::foundation::Result::failure(with_xmp.status()); } result.encoded_bytes = static_cast(with_xmp.value().size()); result.jpeg = std::move(with_xmp.value()); result.xmp_injected = true; return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result export_document_frame_equirectangular_jpeg(DocumentFrameCompositeRequest request, int quality) { auto composite = composite_document_frame(request); if (!composite) { return pp::foundation::Result::failure(composite.status()); } return export_document_frame_equirectangular_jpeg(composite.value(), quality); } pp::foundation::Result plan_document_depth_export_render( DocumentDepthExportRenderPlanRequest request) noexcept { if (request.document == nullptr) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document depth export request requires a document")); } if (request.frame_index >= request.document->frames().size()) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document depth export frame index is outside the document")); } const auto output_pixels = expected_pixel_count(request.output_extent); if (!output_pixels) { return pp::foundation::Result::failure(output_pixels.status()); } DocumentDepthExportRenderPlan plan; plan.output_extent = request.output_extent; plan.merged_face_draw_count = pp::document::cube_face_count; plan.visited_layer_count = request.document->layers().size(); for (const auto& layer : request.document->layers()) { if (!layer.visible || layer.opacity == 0.0F || request.frame_index >= layer.frames.size()) { continue; } ++plan.visible_layer_count; std::array layer_faces {}; const auto& frame = layer.frames[request.frame_index]; for (const auto& payload : frame.face_pixels) { if (payload.face_index >= pp::document::cube_face_count) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document depth export face index is outside the cube")); } layer_faces[payload.face_index] = true; ++plan.face_payload_count; } for (const auto has_payload : layer_faces) { if (has_payload) { ++plan.layer_depth_draw_count; } } } return pp::foundation::Result::success(plan); } pp::foundation::Result export_document_depth_pngs( DocumentDepthExportRenderPlanRequest request) { auto plan = plan_document_depth_export_render(request); if (!plan) { return pp::foundation::Result::failure(plan.status()); } auto projection = make_perspective_projection_map( plan.value().output_extent, document_depth_export_default_fov_degrees); if (!projection) { return pp::foundation::Result::failure(projection.status()); } auto image_composite = composite_document_frame(DocumentFrameCompositeRequest { .document = request.document, .frame_index = request.frame_index, .clear_color = pp::paint::Rgba { .r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F, }, }); if (!image_composite) { return pp::foundation::Result::failure(image_composite.status()); } auto image_pixels = project_document_frame_perspective(image_composite.value(), projection.value()); if (!image_pixels) { return pp::foundation::Result::failure(image_pixels.status()); } auto depth_pixels = project_document_depth_perspective(*request.document, request.frame_index, projection.value()); if (!depth_pixels) { return pp::foundation::Result::failure(depth_pixels.status()); } std::vector rgba8; append_rgba8_bytes(rgba8, image_pixels.value()); auto image_png = pp::assets::encode_png_rgba8( plan.value().output_extent.width, plan.value().output_extent.height, rgba8); if (!image_png) { return pp::foundation::Result::failure(image_png.status()); } append_rgba8_bytes(rgba8, depth_pixels.value()); auto depth_png = pp::assets::encode_png_rgba8( plan.value().output_extent.width, plan.value().output_extent.height, rgba8); if (!depth_png) { return pp::foundation::Result::failure(depth_png.status()); } DocumentDepthPngExportResult result; result.output_extent = plan.value().output_extent; result.image_encoded_bytes = static_cast(image_png.value().size()); result.depth_encoded_bytes = static_cast(depth_png.value().size()); result.merged_face_draw_count = plan.value().merged_face_draw_count; result.layer_depth_draw_count = plan.value().layer_depth_draw_count; result.visited_layer_count = plan.value().visited_layer_count; result.visible_layer_count = plan.value().visible_layer_count; result.face_payload_count = plan.value().face_payload_count; result.uses_perspective_camera = plan.value().uses_perspective_camera; result.image_png = std::move(image_png.value()); result.depth_png = std::move(depth_png.value()); return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request) { if (request.document == nullptr) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document layer export request requires a document")); } if (request.frame_index >= request.document->frames().size()) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document layer export frame index is outside the document")); } DocumentLayerEquirectangularPngExportResult result; result.layers.reserve(request.document->layers().size()); for (std::size_t layer_index = 0; layer_index < request.document->layers().size(); ++layer_index) { auto composite = composite_document_layer_frame( *request.document, layer_index, request.frame_index, request.clear_color); if (!composite) { return pp::foundation::Result::failure(composite.status()); } auto exported = export_document_frame_equirectangular_png(composite.value()); if (!exported) { return pp::foundation::Result::failure(exported.status()); } DocumentLayerEquirectangularPng layer; layer.layer_index = layer_index; layer.layer_name = request.document->layers()[layer_index].name; layer.face_extent = exported.value().face_extent; layer.equirectangular_extent = exported.value().equirectangular_extent; layer.encoded_bytes = exported.value().encoded_bytes; layer.face_payload_count = exported.value().face_payload_count; layer.composited_layer_face_count = exported.value().composited_layer_face_count; layer.png = std::move(exported.value().png); result.encoded_bytes += layer.encoded_bytes; result.layers.push_back(std::move(layer)); } result.layer_count = result.layers.size(); return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result export_document_animation_frames_equirectangular_pngs( DocumentAnimationFrameEquirectangularPngExportRequest request) { if (request.document == nullptr) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document animation-frame export request requires a document")); } DocumentAnimationFrameEquirectangularPngExportResult result; result.frames.reserve(request.document->frames().size()); for (std::size_t frame_index = 0; frame_index < request.document->frames().size(); ++frame_index) { auto exported = export_document_frame_equirectangular_png(DocumentFrameCompositeRequest { .document = request.document, .frame_index = frame_index, .clear_color = request.clear_color, }); if (!exported) { return pp::foundation::Result::failure( exported.status()); } DocumentAnimationFrameEquirectangularPng frame; frame.frame_index = frame_index; frame.face_extent = exported.value().face_extent; frame.equirectangular_extent = exported.value().equirectangular_extent; frame.encoded_bytes = exported.value().encoded_bytes; frame.face_payload_count = exported.value().face_payload_count; frame.composited_layer_face_count = exported.value().composited_layer_face_count; frame.png = std::move(exported.value().png); result.encoded_bytes += frame.encoded_bytes; result.frames.push_back(std::move(frame)); } result.frame_count = result.frames.size(); return pp::foundation::Result::success(std::move(result)); } pp::foundation::Result prepare_document_frame_export_readiness( DocumentFrameCompositeRequest request) { auto recorded_upload = record_document_frame_upload(DocumentFrameUploadRequest { .document = request.document, .frame_index = request.frame_index, .clear_color = request.clear_color, }); if (!recorded_upload) { return pp::foundation::Result::failure(recorded_upload.status()); } auto face_pngs = export_document_frame_face_pngs(request); if (!face_pngs) { return pp::foundation::Result::failure(face_pngs.status()); } DocumentFrameExportReadinessResult result; result.recorded_upload = std::move(recorded_upload.value()); result.face_pngs = std::move(face_pngs.value()); return pp::foundation::Result::success(std::move(result)); } bool stroke_composite_requires_feedback( pp::paint::BlendMode layer_blend_mode, pp::paint::StrokeBlendMode stroke_blend_mode, bool dual_brush_blend, bool pattern_blend) noexcept { return layer_blend_mode != pp::paint::BlendMode::normal || stroke_blend_mode != pp::paint::StrokeBlendMode::normal || dual_brush_blend || pattern_blend; } pp::foundation::Result plan_stroke_composite( pp::renderer::RenderDeviceFeatures features, StrokeCompositeRequest request) noexcept { if (!is_valid_blend_mode(request.layer_blend_mode)) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("unknown layer blend mode")); } if (!is_valid_stroke_blend_mode(request.stroke_blend_mode)) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("unknown stroke blend mode")); } const pp::renderer::TextureDesc target_desc { .extent = request.extent, .format = request.target_format, .usage = request.target_usage, .debug_name = "stroke-composite-target", }; const auto complex_blend = stroke_composite_requires_feedback( request.layer_blend_mode, request.stroke_blend_mode, request.dual_brush_blend, request.pattern_blend); const auto feedback = pp::renderer::plan_paint_feedback(features, target_desc, complex_blend); if (!feedback) { return pp::foundation::Result::failure(feedback.status()); } StrokeCompositePlan plan; plan.path = composite_path_from_feedback(feedback.value().path); plan.feedback = feedback.value(); plan.target_desc = target_desc; plan.target_bytes = feedback.value().target_bytes; plan.auxiliary_bytes = feedback.value().requires_auxiliary_texture ? feedback.value().target_bytes : 0U; plan.estimated_working_bytes = plan.target_bytes + plan.auxiliary_bytes; plan.complex_blend = complex_blend; plan.reads_destination_color = feedback.value().reads_destination_color; plan.requires_auxiliary_texture = feedback.value().requires_auxiliary_texture; plan.requires_texture_copy = feedback.value().requires_texture_copy; plan.requires_render_target_blit = feedback.value().requires_render_target_blit; plan.requires_explicit_transition = feedback.value().requires_explicit_transition; return pp::foundation::Result::success(plan); } CanvasStrokeMaterialPlan plan_canvas_stroke_material(CanvasStrokeMaterialRequest request) noexcept { CanvasStrokeMaterialPlan plan; auto bind = [&plan](CanvasStrokeTextureRole role, std::uint8_t slot) noexcept { if (plan.texture_binding_count >= plan.texture_bindings.size()) { return; } plan.texture_bindings[plan.texture_binding_count] = CanvasStrokeTextureBindingPlan { .role = role, .slot = slot, }; ++plan.texture_binding_count; }; bind(CanvasStrokeTextureRole::main_brush_tip, 0); plan.stroke_pass.uses_destination_feedback = request.destination_feedback_needed; if (request.destination_feedback_needed) { bind(CanvasStrokeTextureRole::destination_feedback, 1); } plan.stroke_pass.uses_pattern = request.pattern_enabled && request.pattern_eachsample; if (plan.stroke_pass.uses_pattern) { bind(CanvasStrokeTextureRole::pattern, 2); } plan.stroke_pass.uses_mixer = request.wet_blend || request.mix_blend || request.noise_enabled; if (plan.stroke_pass.uses_mixer) { bind(CanvasStrokeTextureRole::mixer, 3); } plan.dual_pass.enabled = request.dual_brush_enabled; plan.dual_pass.uses_pattern = false; if (request.dual_brush_enabled) { bind(CanvasStrokeTextureRole::dual_brush_tip, 4); } plan.composite_pass.use_dual = request.dual_brush_enabled; plan.composite_pass.use_pattern = request.pattern_enabled && !request.pattern_eachsample; plan.composite_pass.dual_blend_mode = request.dual_blend_mode; plan.composite_pass.pattern_blend_mode = request.pattern_blend_mode; plan.composite_pass.dual_alpha = request.dual_alpha; if (plan.composite_pass.use_pattern && !plan.stroke_pass.uses_pattern) { bind(CanvasStrokeTextureRole::pattern, 2); } return plan; } StrokePreviewCompositePlan plan_stroke_preview_composite(StrokePreviewCompositeRequest request) noexcept { StrokePreviewCompositePlan plan; plan.uses_mixer = request.uses_mixer; plan.uses_dual = request.uses_dual; plan.uses_pattern = request.uses_pattern; auto append_step = [&plan](StrokePreviewCompositeStep step) noexcept { if (plan.step_count >= plan.steps.size()) { return; } plan.steps[plan.step_count] = step; ++plan.step_count; }; auto bind = [&plan](StrokePreviewTextureRole role, std::uint8_t slot) noexcept { if (plan.texture_slot_count >= plan.texture_slots.size()) { return; } plan.texture_slots[plan.texture_slot_count] = StrokePreviewTextureSlotPlan { .role = role, .slot = slot, }; ++plan.texture_slot_count; }; append_step(StrokePreviewCompositeStep::checkerboard_background); append_step(StrokePreviewCompositeStep::capture_background_texture); append_step(StrokePreviewCompositeStep::bind_final_composite_inputs); append_step(StrokePreviewCompositeStep::final_composite_draw); append_step(StrokePreviewCompositeStep::copy_preview_texture); bind(StrokePreviewTextureRole::background, 0); bind(StrokePreviewTextureRole::stroke, 1); if (request.uses_dual) { bind(StrokePreviewTextureRole::dual, 3); } if (request.uses_pattern) { bind(StrokePreviewTextureRole::pattern, 4); } if (request.uses_mixer) { bind(StrokePreviewTextureRole::mixer, 3); } return plan; } pp::foundation::Result plan_canvas_blend_gate( pp::renderer::RenderDeviceFeatures features, CanvasBlendGateRequest request) noexcept { CanvasBlendGatePlan gate; for (std::size_t i = 0; i < request.layer_blend_modes.size(); ++i) { pp::paint::BlendMode layer_blend = pp::paint::BlendMode::normal; if (!paint_blend_mode_from_persisted_index(request.layer_blend_modes[i], layer_blend)) { if (request.layer_blend_modes[i] != 0) { gate.first_complex_layer_index = static_cast(i); mark_shader_blend_fallback(gate, features); return pp::foundation::Result::success(gate); } continue; } if (layer_blend == pp::paint::BlendMode::normal) { continue; } gate.shader_blend = true; gate.complex_blend = true; gate.first_complex_layer_index = static_cast(i); const auto stroke = plan_stroke_composite( features, StrokeCompositeRequest { .extent = request.extent, .layer_blend_mode = layer_blend, }); if (stroke) { apply_stroke_plan(gate, stroke.value()); } else { gate.compatibility_fallback = true; } return pp::foundation::Result::success(gate); } pp::paint::StrokeBlendMode stroke_blend = pp::paint::StrokeBlendMode::normal; if (request.has_stroke_blend_mode) { if (!stroke_blend_mode_from_persisted_index(request.stroke_blend_mode, stroke_blend)) { if (request.stroke_blend_mode != 0) { gate.stroke_complex = true; mark_shader_blend_fallback(gate, features); return pp::foundation::Result::success(gate); } } else if (stroke_blend != pp::paint::StrokeBlendMode::normal) { gate.stroke_complex = true; } } gate.dual_brush_complex = request.dual_brush_blend; gate.pattern_complex = request.pattern_blend; if (!gate.stroke_complex && !gate.dual_brush_complex && !gate.pattern_complex) { return pp::foundation::Result::success(gate); } gate.shader_blend = true; gate.complex_blend = true; const auto stroke = plan_stroke_composite( features, StrokeCompositeRequest { .extent = request.extent, .stroke_blend_mode = stroke_blend, .dual_brush_blend = request.dual_brush_blend, .pattern_blend = request.pattern_blend, }); if (stroke) { apply_stroke_plan(gate, stroke.value()); } else { gate.compatibility_fallback = true; } return pp::foundation::Result::success(gate); } pp::foundation::Result plan_canvas_stroke_feedback( pp::renderer::RenderDeviceFeatures features, pp::renderer::Extent2D extent) noexcept { const auto extent_status = pp::renderer::validate_extent(extent); if (!extent_status.ok()) { return pp::foundation::Result::failure(extent_status); } const pp::renderer::TextureDesc target_desc { .extent = extent, .format = pp::renderer::TextureFormat::rgba8, .usage = pp::renderer::TextureUsage::render_target | pp::renderer::TextureUsage::sampled | pp::renderer::TextureUsage::copy_source | pp::renderer::TextureUsage::copy_destination, .debug_name = "canvas-stroke-feedback-target", }; const auto feedback = pp::renderer::plan_paint_feedback(features, target_desc, true); if (feedback) { CanvasStrokeFeedbackPlan plan; apply_feedback_plan(plan, feedback.value()); return pp::foundation::Result::success(plan); } CanvasStrokeFeedbackPlan fallback; fallback.compatibility_fallback = true; if (features.framebuffer_fetch) { fallback.path = StrokeCompositePath::framebuffer_fetch; fallback.reads_destination_color = true; } else { fallback.path = StrokeCompositePath::ping_pong_textures; fallback.requires_auxiliary_texture = true; fallback.requires_texture_copy = features.texture_copy; fallback.requires_render_target_blit = !features.texture_copy && features.render_target_blit; } return pp::foundation::Result::success(fallback); } pp::foundation::Result plan_canvas_stroke_rasterization( pp::renderer::RenderDeviceFeatures features, pp::renderer::Extent2D extent) noexcept { const auto feedback = plan_canvas_stroke_feedback(features, extent); if (!feedback) { return pp::foundation::Result::failure(feedback.status()); } CanvasStrokeRasterizationPlan plan; plan.feedback = feedback.value(); plan.copy_stroke_destination = !plan.feedback.reads_destination_color; plan.can_route_feedback_through_renderer = plan.feedback.reads_destination_color || plan.feedback.requires_texture_copy || plan.feedback.requires_render_target_blit; plan.compatibility_fallback = plan.feedback.compatibility_fallback; return pp::foundation::Result::success(plan); } CanvasStrokeCommitSequencePlan plan_canvas_stroke_commit_sequence(CanvasStrokeCommitRequest request) noexcept { CanvasStrokeCommitSequencePlan plan; plan.erase_mode = request.erase_mode; plan.alpha_locked = request.alpha_locked; plan.selection_mask_active = request.selection_mask_active; plan.uses_dual_stroke = !request.erase_mode && request.dual_stroke_enabled; plan.uses_pattern = !request.erase_mode && request.pattern_enabled; plan.updates_layer_bounds = !request.alpha_locked; auto append_step = [&plan](CanvasStrokeCommitStep step) noexcept { if (plan.step_count >= plan.steps.size()) { return; } plan.steps[plan.step_count] = step; ++plan.step_count; }; auto bind = [&plan](CanvasStrokeCommitTextureRole role, std::uint8_t slot) noexcept { if (plan.texture_binding_count >= plan.texture_bindings.size()) { return; } plan.texture_bindings[plan.texture_binding_count] = CanvasStrokeCommitTextureBindingPlan { .role = role, .slot = slot, }; ++plan.texture_binding_count; }; append_step(CanvasStrokeCommitStep::readback_history_region); append_step(CanvasStrokeCommitStep::update_layer_dirty_state); append_step(CanvasStrokeCommitStep::copy_layer_rtt_to_scratch); append_step(CanvasStrokeCommitStep::bind_commit_inputs); append_step(request.erase_mode ? CanvasStrokeCommitStep::erase_draw : CanvasStrokeCommitStep::composite_draw); append_step(CanvasStrokeCommitStep::copy_committed_rtt_to_scratch); append_step(CanvasStrokeCommitStep::dilate_edges_draw); bind(CanvasStrokeCommitTextureRole::layer_scratch, 0); bind(CanvasStrokeCommitTextureRole::stroke, 1); bind(CanvasStrokeCommitTextureRole::selection_mask, 2); if (plan.uses_dual_stroke) { bind(CanvasStrokeCommitTextureRole::dual_stroke, 3); } if (plan.uses_pattern) { bind(CanvasStrokeCommitTextureRole::pattern, 4); } return 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) { case StrokeCompositePath::fixed_function_blend: return "fixed_function_blend"; case StrokeCompositePath::framebuffer_fetch: return "framebuffer_fetch"; case StrokeCompositePath::ping_pong_textures: return "ping_pong_textures"; } return "unknown"; } }