Files
panopainter/src/paint_renderer/compositor.cpp

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