1623 lines
65 KiB
C++
1623 lines
65 KiB
C++
#include "paint_renderer/compositor.h"
|
|
|
|
#include "assets/image_pixels.h"
|
|
#include "renderer_api/recording_renderer.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <utility>
|
|
|
|
namespace pp::paint_renderer {
|
|
|
|
pp::foundation::Result<DocumentFrameCompositeResult> 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<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);
|
|
if (!extent_status.ok()) {
|
|
return pp::foundation::Result<std::size_t>::failure(extent_status);
|
|
}
|
|
|
|
const auto width = static_cast<std::uint64_t>(extent.width);
|
|
const auto height = static_cast<std::uint64_t>(extent.height);
|
|
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
|
|
return pp::foundation::Result<std::size_t>::failure(
|
|
pp::foundation::Status::out_of_range("pixel count overflows uint64"));
|
|
}
|
|
|
|
const auto count = width * height;
|
|
if (count > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
|
|
return pp::foundation::Result<std::size_t>::failure(
|
|
pp::foundation::Status::out_of_range("pixel count exceeds addressable memory"));
|
|
}
|
|
|
|
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(count));
|
|
}
|
|
|
|
[[nodiscard]] pp::paint::Rgba rgba8_pixel(std::span<const std::uint8_t> bytes) noexcept
|
|
{
|
|
constexpr auto inv = 1.0F / 255.0F;
|
|
return pp::paint::Rgba {
|
|
.r = static_cast<float>(bytes[0]) * inv,
|
|
.g = static_cast<float>(bytes[1]) * inv,
|
|
.b = static_cast<float>(bytes[2]) * inv,
|
|
.a = static_cast<float>(bytes[3]) * inv,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] std::byte rgba8_channel(float value) noexcept
|
|
{
|
|
const auto clamped = std::clamp(value, 0.0F, 1.0F);
|
|
return static_cast<std::byte>(static_cast<std::uint8_t>(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<std::uint32_t>(u * static_cast<float>(width)), width - 1U);
|
|
const auto y = std::min(static_cast<std::uint32_t>(v * static_cast<float>(height)), height - 1U);
|
|
return face.pixels[static_cast<std::size_t>(y) * width + x];
|
|
}
|
|
|
|
void append_rgba8_bytes(std::vector<std::byte>& bytes, std::span<const pp::paint::Rgba> 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<pp::paint::Rgba> 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<std::uint64_t>(payload.width)
|
|
* static_cast<std::uint64_t>(payload.height);
|
|
if (payload_pixel_count > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max() / 4U)
|
|
|| payload.rgba8.size() != static_cast<std::size_t>(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<std::size_t>(y) * payload.width + x) * 4U;
|
|
const auto destination_index = static_cast<std::size_t>(payload.y + y) * extent.width
|
|
+ static_cast<std::size_t>(payload.x + x);
|
|
auto stroke = rgba8_pixel(std::span<const std::uint8_t>(&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<std::uint8_t>& bytes, std::span<const pp::paint::Rgba> pixels)
|
|
{
|
|
bytes.clear();
|
|
bytes.reserve(pixels.size() * pp::document::rgba8_components);
|
|
for (const auto& pixel : pixels) {
|
|
bytes.push_back(static_cast<std::uint8_t>(rgba8_channel(pixel.r)));
|
|
bytes.push_back(static_cast<std::uint8_t>(rgba8_channel(pixel.g)));
|
|
bytes.push_back(static_cast<std::uint8_t>(rgba8_channel(pixel.b)));
|
|
bytes.push_back(static_cast<std::uint8_t>(rgba8_channel(pixel.a)));
|
|
}
|
|
}
|
|
|
|
struct EquirectangularProjectionResult {
|
|
pp::renderer::Extent2D face_extent {};
|
|
pp::renderer::Extent2D equirectangular_extent {};
|
|
std::vector<pp::paint::Rgba> 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<CubeFaceSample> samples;
|
|
};
|
|
|
|
pp::foundation::Result<PerspectiveProjectionMap> make_perspective_projection_map(
|
|
pp::renderer::Extent2D output_extent,
|
|
float vertical_fov_degrees)
|
|
{
|
|
if (!std::isfinite(vertical_fov_degrees)) {
|
|
return pp::foundation::Result<PerspectiveProjectionMap>::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<PerspectiveProjectionMap>::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<PerspectiveProjectionMap>::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<float>(output_extent.width) / static_cast<float>(output_extent.height);
|
|
for (std::uint32_t y = 0; y < output_extent.height; ++y) {
|
|
const auto ny = 1.0F
|
|
- ((static_cast<float>(y) + 0.5F) / static_cast<float>(output_extent.height)) * 2.0F;
|
|
for (std::uint32_t x = 0; x < output_extent.width; ++x) {
|
|
const auto nx = ((static_cast<float>(x) + 0.5F) / static_cast<float>(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<PerspectiveProjectionMap>::success(std::move(map));
|
|
}
|
|
|
|
pp::foundation::Result<EquirectangularProjectionResult> 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<EquirectangularProjectionResult>::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<EquirectangularProjectionResult>::failure(
|
|
pp::foundation::Status::invalid_argument("document equirectangular export requires complete cube faces"));
|
|
}
|
|
}
|
|
|
|
const auto output_width = static_cast<std::uint64_t>(composite.extent.width) * 4U;
|
|
const auto output_height = static_cast<std::uint64_t>(composite.extent.height) * 2U;
|
|
if (output_width > std::numeric_limits<std::uint32_t>::max()
|
|
|| output_height > std::numeric_limits<std::uint32_t>::max()) {
|
|
return pp::foundation::Result<EquirectangularProjectionResult>::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<std::uint32_t>(output_width),
|
|
.height = static_cast<std::uint32_t>(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<EquirectangularProjectionResult>::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<float>(y) + 0.5F) / static_cast<float>(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<float>(x) + 0.5F) / static_cast<float>(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<std::size_t>(y) * result.equirectangular_extent.width + x] =
|
|
sample_face_nearest(composite.faces[sample.face_index], sample.s, sample.t);
|
|
}
|
|
}
|
|
|
|
return pp::foundation::Result<EquirectangularProjectionResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<std::vector<pp::paint::Rgba>> 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<std::vector<pp::paint::Rgba>>::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<std::vector<pp::paint::Rgba>>::failure(
|
|
pp::foundation::Status::invalid_argument("document depth export requires complete cube faces"));
|
|
}
|
|
}
|
|
|
|
std::vector<pp::paint::Rgba> 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<std::vector<pp::paint::Rgba>>::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<std::vector<pp::paint::Rgba>> project_document_depth_perspective(
|
|
const pp::document::CanvasDocument& document,
|
|
std::size_t frame_index,
|
|
const PerspectiveProjectionMap& projection)
|
|
{
|
|
std::vector<pp::paint::Rgba> 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<std::vector<pp::paint::Rgba>>::failure(composite.status());
|
|
}
|
|
|
|
const auto gray = static_cast<float>(layer_index + 1U) / static_cast<float>(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<std::vector<pp::paint::Rgba>>::success(std::move(pixels));
|
|
}
|
|
|
|
}
|
|
|
|
pp::foundation::Status composite_layer(
|
|
std::span<pp::paint::Rgba> 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<DocumentFaceCompositeResult> composite_document_face(
|
|
DocumentFaceCompositeRequest request)
|
|
{
|
|
if (request.document == nullptr) {
|
|
return pp::foundation::Result<DocumentFaceCompositeResult>::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<DocumentFaceCompositeResult>::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<DocumentFaceCompositeResult>::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<DocumentFaceCompositeResult>::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<DocumentFaceCompositeResult>::failure(status);
|
|
}
|
|
|
|
composited_layer = true;
|
|
++result.face_payload_count;
|
|
}
|
|
|
|
if (composited_layer) {
|
|
++result.composited_layer_count;
|
|
}
|
|
}
|
|
|
|
return pp::foundation::Result<DocumentFaceCompositeResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentFrameCompositeResult> composite_document_frame(
|
|
DocumentFrameCompositeRequest request)
|
|
{
|
|
if (request.document == nullptr) {
|
|
return pp::foundation::Result<DocumentFrameCompositeResult>::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<DocumentFrameCompositeResult>::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<DocumentFrameCompositeResult>::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<DocumentFrameCompositeResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentFrameCompositeResult> 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<DocumentFrameCompositeResult>::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<DocumentFrameCompositeResult>::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<DocumentFrameCompositeResult>::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<DocumentFrameCompositeResult>::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<DocumentFrameCompositeResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentFrameUploadResult> 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<DocumentFrameUploadResult>::failure(composite.status());
|
|
}
|
|
|
|
DocumentFrameUploadResult result;
|
|
result.composite = std::move(composite.value());
|
|
std::vector<std::byte> 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<DocumentFrameUploadResult>::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<DocumentFrameUploadResult>::failure(upload_status);
|
|
}
|
|
|
|
result.uploaded_bytes += static_cast<std::uint64_t>(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<DocumentFrameUploadResult>::failure(transition_status);
|
|
}
|
|
++result.transition_count;
|
|
}
|
|
|
|
result.face_textures[face_index] = std::move(texture.value());
|
|
++result.texture_count;
|
|
}
|
|
|
|
return pp::foundation::Result<DocumentFrameUploadResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<RecordedDocumentFrameUploadResult> 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<RecordedDocumentFrameUploadResult>::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<RecordedDocumentFrameUploadResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentFrameFacePngExportResult> export_document_frame_face_pngs(
|
|
DocumentFrameCompositeRequest request)
|
|
{
|
|
auto composite = composite_document_frame(request);
|
|
if (!composite) {
|
|
return pp::foundation::Result<DocumentFrameFacePngExportResult>::failure(composite.status());
|
|
}
|
|
|
|
DocumentFrameFacePngExportResult result;
|
|
result.composite = std::move(composite.value());
|
|
std::vector<std::uint8_t> 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<DocumentFrameFacePngExportResult>::failure(encoded.status());
|
|
}
|
|
|
|
result.encoded_bytes += static_cast<std::uint64_t>(encoded.value().size());
|
|
result.face_pngs[face_index] = std::move(encoded.value());
|
|
++result.face_count;
|
|
}
|
|
|
|
return pp::foundation::Result<DocumentFrameFacePngExportResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>
|
|
export_document_frame_equirectangular_png(const DocumentFrameCompositeResult& composite)
|
|
{
|
|
auto projection = project_document_frame_equirectangular(composite);
|
|
if (!projection) {
|
|
return pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>::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<std::uint8_t> 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<DocumentFrameEquirectangularPngExportResult>::failure(encoded.status());
|
|
}
|
|
|
|
result.encoded_bytes = static_cast<std::uint64_t>(encoded.value().size());
|
|
result.png = std::move(encoded.value());
|
|
return pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>
|
|
export_document_frame_equirectangular_png(DocumentFrameCompositeRequest request)
|
|
{
|
|
auto composite = composite_document_frame(request);
|
|
if (!composite) {
|
|
return pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>::failure(composite.status());
|
|
}
|
|
|
|
return export_document_frame_equirectangular_png(composite.value());
|
|
}
|
|
|
|
pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>
|
|
export_document_frame_equirectangular_jpeg(const DocumentFrameCompositeResult& composite, int quality)
|
|
{
|
|
auto projection = project_document_frame_equirectangular(composite);
|
|
if (!projection) {
|
|
return pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>::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<std::uint8_t> 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<DocumentFrameEquirectangularJpegExportResult>::failure(encoded.status());
|
|
}
|
|
|
|
auto with_xmp = pp::assets::inject_gpano_xmp_into_jpeg(encoded.value());
|
|
if (!with_xmp) {
|
|
return pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>::failure(with_xmp.status());
|
|
}
|
|
|
|
result.encoded_bytes = static_cast<std::uint64_t>(with_xmp.value().size());
|
|
result.jpeg = std::move(with_xmp.value());
|
|
result.xmp_injected = true;
|
|
return pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>
|
|
export_document_frame_equirectangular_jpeg(DocumentFrameCompositeRequest request, int quality)
|
|
{
|
|
auto composite = composite_document_frame(request);
|
|
if (!composite) {
|
|
return pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>::failure(composite.status());
|
|
}
|
|
|
|
return export_document_frame_equirectangular_jpeg(composite.value(), quality);
|
|
}
|
|
|
|
pp::foundation::Result<DocumentDepthExportRenderPlan> plan_document_depth_export_render(
|
|
DocumentDepthExportRenderPlanRequest request) noexcept
|
|
{
|
|
if (request.document == nullptr) {
|
|
return pp::foundation::Result<DocumentDepthExportRenderPlan>::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<DocumentDepthExportRenderPlan>::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<DocumentDepthExportRenderPlan>::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<bool, pp::document::cube_face_count> 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<DocumentDepthExportRenderPlan>::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<DocumentDepthExportRenderPlan>::success(plan);
|
|
}
|
|
|
|
pp::foundation::Result<DocumentDepthPngExportResult> export_document_depth_pngs(
|
|
DocumentDepthExportRenderPlanRequest request)
|
|
{
|
|
auto plan = plan_document_depth_export_render(request);
|
|
if (!plan) {
|
|
return pp::foundation::Result<DocumentDepthPngExportResult>::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<DocumentDepthPngExportResult>::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<DocumentDepthPngExportResult>::failure(image_composite.status());
|
|
}
|
|
|
|
auto image_pixels = project_document_frame_perspective(image_composite.value(), projection.value());
|
|
if (!image_pixels) {
|
|
return pp::foundation::Result<DocumentDepthPngExportResult>::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<DocumentDepthPngExportResult>::failure(depth_pixels.status());
|
|
}
|
|
|
|
std::vector<std::uint8_t> 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<DocumentDepthPngExportResult>::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<DocumentDepthPngExportResult>::failure(depth_png.status());
|
|
}
|
|
|
|
DocumentDepthPngExportResult result;
|
|
result.output_extent = plan.value().output_extent;
|
|
result.image_encoded_bytes = static_cast<std::uint64_t>(image_png.value().size());
|
|
result.depth_encoded_bytes = static_cast<std::uint64_t>(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<DocumentDepthPngExportResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>
|
|
export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request)
|
|
{
|
|
if (request.document == nullptr) {
|
|
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::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<DocumentLayerEquirectangularPngExportResult>::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<DocumentLayerEquirectangularPngExportResult>::failure(composite.status());
|
|
}
|
|
|
|
auto exported = export_document_frame_equirectangular_png(composite.value());
|
|
if (!exported) {
|
|
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::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<DocumentLayerEquirectangularPngExportResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>
|
|
export_document_animation_frames_equirectangular_pngs(
|
|
DocumentAnimationFrameEquirectangularPngExportRequest request)
|
|
{
|
|
if (request.document == nullptr) {
|
|
return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::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<DocumentAnimationFrameEquirectangularPngExportResult>::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<DocumentAnimationFrameEquirectangularPngExportResult>::success(std::move(result));
|
|
}
|
|
|
|
pp::foundation::Result<DocumentFrameExportReadinessResult> 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<DocumentFrameExportReadinessResult>::failure(recorded_upload.status());
|
|
}
|
|
|
|
auto face_pngs = export_document_frame_face_pngs(request);
|
|
if (!face_pngs) {
|
|
return pp::foundation::Result<DocumentFrameExportReadinessResult>::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<DocumentFrameExportReadinessResult>::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<StrokeCompositePlan> plan_stroke_composite(
|
|
pp::renderer::RenderDeviceFeatures features,
|
|
StrokeCompositeRequest request) noexcept
|
|
{
|
|
if (!is_valid_blend_mode(request.layer_blend_mode)) {
|
|
return pp::foundation::Result<StrokeCompositePlan>::failure(
|
|
pp::foundation::Status::invalid_argument("unknown layer blend mode"));
|
|
}
|
|
|
|
if (!is_valid_stroke_blend_mode(request.stroke_blend_mode)) {
|
|
return pp::foundation::Result<StrokeCompositePlan>::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<StrokeCompositePlan>::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<StrokeCompositePlan>::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<CanvasBlendGatePlan> 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<int>(i);
|
|
mark_shader_blend_fallback(gate, features);
|
|
return pp::foundation::Result<CanvasBlendGatePlan>::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<int>(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<CanvasBlendGatePlan>::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<CanvasBlendGatePlan>::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<CanvasBlendGatePlan>::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<CanvasBlendGatePlan>::success(gate);
|
|
}
|
|
|
|
pp::foundation::Result<CanvasStrokeFeedbackPlan> 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<CanvasStrokeFeedbackPlan>::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<CanvasStrokeFeedbackPlan>::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<CanvasStrokeFeedbackPlan>::success(fallback);
|
|
}
|
|
|
|
pp::foundation::Result<CanvasStrokeRasterizationPlan> 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<CanvasStrokeRasterizationPlan>::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<CanvasStrokeRasterizationPlan>::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<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) {
|
|
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";
|
|
}
|
|
|
|
}
|