Files
panopainter/tests/paint_renderer/compositor_tests.cpp

1261 lines
47 KiB
C++

#include "assets/image_pixels.h"
#include "paint_renderer/compositor.h"
#include "renderer_api/recording_renderer.h"
#include "test_harness.h"
#include <cmath>
#include <cstdint>
#include <span>
#include <string_view>
#include <vector>
using pp::foundation::StatusCode;
using pp::paint::BlendMode;
using pp::paint::Rgba;
using pp::paint::StrokeBlendMode;
using pp::paint_renderer::CanvasBlendGateRequest;
using pp::paint_renderer::DocumentFaceCompositeRequest;
using pp::paint_renderer::DocumentFrameCompositeRequest;
using pp::paint_renderer::LayerCompositeView;
using pp::paint_renderer::StrokeCompositePath;
using pp::paint_renderer::StrokeCompositeRequest;
using pp::paint_renderer::composite_layer;
using pp::paint_renderer::composite_document_face;
using pp::paint_renderer::composite_document_frame;
using pp::paint_renderer::plan_canvas_blend_gate;
using pp::paint_renderer::plan_canvas_stroke_feedback;
using pp::paint_renderer::plan_stroke_composite;
using pp::paint_renderer::stroke_composite_path_name;
using pp::paint_renderer::stroke_composite_requires_feedback;
using pp::renderer::Extent2D;
using pp::renderer::RecordedRenderCommandKind;
using pp::renderer::RecordingRenderDevice;
using pp::renderer::RenderDeviceFeatures;
using pp::renderer::TextureFormat;
using pp::renderer::TextureUsage;
using pp::document::AnimationFrame;
using pp::document::CanvasDocument;
using pp::document::DocumentLayerConfig;
using pp::document::DocumentSnapshotConfig;
using pp::document::LayerFacePixels;
namespace {
bool near(float a, float b)
{
return std::fabs(a - b) < 0.0001F;
}
void composites_visible_layer_with_opacity(pp::tests::Harness& h)
{
std::vector<Rgba> destination {
Rgba { .r = 0.2F, .g = 0.4F, .b = 0.6F, .a = 0.5F },
};
const std::vector<Rgba> foreground {
Rgba { .r = 0.8F, .g = 0.2F, .b = 0.1F, .a = 0.5F },
};
const auto status = composite_layer(
destination,
Extent2D { .width = 1, .height = 1 },
LayerCompositeView {
.pixels = foreground,
.opacity = 0.5F,
.visible = true,
.blend_mode = BlendMode::normal,
});
PP_EXPECT(h, status.ok());
PP_EXPECT(h, near(destination[0].a, 0.625F));
PP_EXPECT(h, near(destination[0].r, 0.44F));
PP_EXPECT(h, near(destination[0].g, 0.32F));
PP_EXPECT(h, near(destination[0].b, 0.4F));
}
void invisible_and_zero_opacity_layers_are_noops(pp::tests::Harness& h)
{
const Rgba original { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 0.4F };
std::vector<Rgba> destination { original };
const std::vector<Rgba> foreground {
Rgba { .r = 1.0F, .g = 1.0F, .b = 1.0F, .a = 1.0F },
};
PP_EXPECT(h, composite_layer(
destination,
Extent2D { .width = 1, .height = 1 },
LayerCompositeView { .pixels = foreground, .opacity = 1.0F, .visible = false }).ok());
PP_EXPECT(h, near(destination[0].r, original.r));
PP_EXPECT(h, near(destination[0].g, original.g));
PP_EXPECT(h, near(destination[0].b, original.b));
PP_EXPECT(h, near(destination[0].a, original.a));
PP_EXPECT(h, composite_layer(
destination,
Extent2D { .width = 1, .height = 1 },
LayerCompositeView { .pixels = foreground, .opacity = 0.0F, .visible = true }).ok());
PP_EXPECT(h, near(destination[0].r, original.r));
PP_EXPECT(h, near(destination[0].g, original.g));
PP_EXPECT(h, near(destination[0].b, original.b));
PP_EXPECT(h, near(destination[0].a, original.a));
}
void rejects_invalid_sizes_and_opacity(pp::tests::Harness& h)
{
std::vector<Rgba> destination(2);
const std::vector<Rgba> foreground(1);
const auto mismatched = composite_layer(
destination,
Extent2D { .width = 2, .height = 1 },
LayerCompositeView { .pixels = foreground });
const auto bad_opacity = composite_layer(
destination,
Extent2D { .width = 2, .height = 1 },
LayerCompositeView { .pixels = destination, .opacity = 1.5F });
const auto bad_extent = composite_layer(
destination,
Extent2D { .width = 0, .height = 1 },
LayerCompositeView { .pixels = destination });
PP_EXPECT(h, !mismatched.ok());
PP_EXPECT(h, mismatched.code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_opacity.ok());
PP_EXPECT(h, bad_opacity.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_extent.ok());
PP_EXPECT(h, bad_extent.code == StatusCode::invalid_argument);
}
void composites_document_face_payloads_in_layer_order(pp::tests::Harness& h)
{
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const AnimationFrame base_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 0,
.x = 0,
.y = 0,
.width = 2,
.height = 1,
.rgba8 = { 255, 0, 0, 255, 0, 255, 0, 255 },
},
},
},
};
const AnimationFrame paint_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 0,
.x = 1,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 0, 0, 255, 255 },
},
},
},
};
const AnimationFrame hidden_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 0,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 255, 255, 255, 255 },
},
},
},
};
const DocumentLayerConfig layers[] {
{
.name = "Base",
.frames = std::span<const AnimationFrame>(base_frames, 1),
},
{
.name = "Paint",
.opacity = 0.5F,
.frames = std::span<const AnimationFrame>(paint_frames, 1),
},
{
.name = "Hidden",
.visible = false,
.frames = std::span<const AnimationFrame>(hidden_frames, 1),
},
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 2,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 3),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
const auto result = composite_document_face(DocumentFaceCompositeRequest {
.document = &document.value(),
.frame_index = 0,
.face_index = 0,
});
PP_EXPECT(h, result);
if (result) {
PP_EXPECT(h, result.value().extent.width == 2U);
PP_EXPECT(h, result.value().extent.height == 1U);
PP_EXPECT(h, result.value().visited_layer_count == 3U);
PP_EXPECT(h, result.value().composited_layer_count == 2U);
PP_EXPECT(h, result.value().face_payload_count == 2U);
PP_EXPECT(h, result.value().pixels.size() == 2U);
PP_EXPECT(h, near(result.value().pixels[0].r, 1.0F));
PP_EXPECT(h, near(result.value().pixels[0].g, 0.0F));
PP_EXPECT(h, near(result.value().pixels[0].b, 0.0F));
PP_EXPECT(h, near(result.value().pixels[0].a, 1.0F));
PP_EXPECT(h, near(result.value().pixels[1].r, 0.0F));
PP_EXPECT(h, near(result.value().pixels[1].g, 0.5F));
PP_EXPECT(h, near(result.value().pixels[1].b, 0.5F));
PP_EXPECT(h, near(result.value().pixels[1].a, 1.0F));
}
}
void document_face_composite_skips_layers_without_requested_frame(pp::tests::Harness& h)
{
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
{ .duration_ms = 100, .face_pixels = {} },
};
const AnimationFrame short_layer_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 0,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 255, 0, 0, 255 },
},
},
},
};
const AnimationFrame animated_layer_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 0,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 0, 0, 255, 255 },
},
},
},
};
const DocumentLayerConfig layers[] {
{
.name = "Short",
.frames = std::span<const AnimationFrame>(short_layer_frames, 1),
},
{
.name = "Animated",
.frames = std::span<const AnimationFrame>(animated_layer_frames, 2),
},
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 2),
.frames = std::span<const AnimationFrame>(root_frames, 2),
.selection_masks = {},
});
PP_EXPECT(h, document);
const auto result = composite_document_face(DocumentFaceCompositeRequest {
.document = &document.value(),
.frame_index = 1,
.face_index = 0,
});
PP_EXPECT(h, result);
if (result) {
PP_EXPECT(h, result.value().visited_layer_count == 2U);
PP_EXPECT(h, result.value().composited_layer_count == 1U);
PP_EXPECT(h, result.value().face_payload_count == 1U);
PP_EXPECT(h, near(result.value().pixels[0].r, 0.0F));
PP_EXPECT(h, near(result.value().pixels[0].g, 0.0F));
PP_EXPECT(h, near(result.value().pixels[0].b, 1.0F));
PP_EXPECT(h, near(result.value().pixels[0].a, 1.0F));
}
}
void document_face_composite_rejects_invalid_requests(pp::tests::Harness& h)
{
const auto no_document = composite_document_face(DocumentFaceCompositeRequest {});
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const DocumentLayerConfig layers[] {
{ .name = "Layer", .frames = {} },
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 1),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
const auto bad_frame = composite_document_face(DocumentFaceCompositeRequest {
.document = &document.value(),
.frame_index = 1,
.face_index = 0,
});
const auto bad_face = composite_document_face(DocumentFaceCompositeRequest {
.document = &document.value(),
.frame_index = 0,
.face_index = 6,
});
PP_EXPECT(h, !no_document.ok());
PP_EXPECT(h, no_document.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_frame.ok());
PP_EXPECT(h, bad_frame.status().code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_face.ok());
PP_EXPECT(h, bad_face.status().code == StatusCode::out_of_range);
}
void composites_document_frame_cube_faces(pp::tests::Harness& h)
{
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const AnimationFrame base_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 0,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 255, 0, 0, 255 },
},
LayerFacePixels {
.face_index = 5,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 0, 0, 255, 255 },
},
},
},
};
const AnimationFrame paint_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 5,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 0, 255, 0, 255 },
},
},
},
};
const DocumentLayerConfig layers[] {
{
.name = "Base",
.frames = std::span<const AnimationFrame>(base_frames, 1),
},
{
.name = "Paint",
.opacity = 0.5F,
.frames = std::span<const AnimationFrame>(paint_frames, 1),
},
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 2),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
const auto result = composite_document_frame(DocumentFrameCompositeRequest {
.document = &document.value(),
.frame_index = 0,
.clear_color = Rgba { .r = 0.25F, .g = 0.25F, .b = 0.25F, .a = 1.0F },
});
PP_EXPECT(h, result);
if (result) {
PP_EXPECT(h, result.value().extent.width == 1U);
PP_EXPECT(h, result.value().extent.height == 1U);
PP_EXPECT(h, result.value().faces.size() == pp::document::cube_face_count);
PP_EXPECT(h, result.value().visited_layer_count == 2U);
PP_EXPECT(h, result.value().composited_layer_face_count == 3U);
PP_EXPECT(h, result.value().face_payload_count == 3U);
PP_EXPECT(h, result.value().faces[0].pixels.size() == 1U);
PP_EXPECT(h, near(result.value().faces[0].pixels[0].r, 1.0F));
PP_EXPECT(h, near(result.value().faces[0].pixels[0].g, 0.0F));
PP_EXPECT(h, near(result.value().faces[0].pixels[0].b, 0.0F));
PP_EXPECT(h, result.value().faces[1].pixels.size() == 1U);
PP_EXPECT(h, near(result.value().faces[1].pixels[0].r, 0.25F));
PP_EXPECT(h, near(result.value().faces[1].pixels[0].g, 0.25F));
PP_EXPECT(h, near(result.value().faces[1].pixels[0].b, 0.25F));
PP_EXPECT(h, result.value().faces[5].pixels.size() == 1U);
PP_EXPECT(h, near(result.value().faces[5].pixels[0].r, 0.0F));
PP_EXPECT(h, near(result.value().faces[5].pixels[0].g, 0.5F));
PP_EXPECT(h, near(result.value().faces[5].pixels[0].b, 0.5F));
PP_EXPECT(h, near(result.value().faces[5].pixels[0].a, 1.0F));
}
}
void document_frame_composite_rejects_invalid_requests(pp::tests::Harness& h)
{
const auto no_document = composite_document_frame(DocumentFrameCompositeRequest {});
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const DocumentLayerConfig layers[] {
{ .name = "Layer", .frames = {} },
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 1),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
const auto bad_frame = composite_document_frame(DocumentFrameCompositeRequest {
.document = &document.value(),
.frame_index = 1,
});
PP_EXPECT(h, !no_document.ok());
PP_EXPECT(h, no_document.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_frame.ok());
PP_EXPECT(h, bad_frame.status().code == StatusCode::out_of_range);
}
void uploads_document_frame_faces_to_renderer_api(pp::tests::Harness& h)
{
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const AnimationFrame layer_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 0,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 255, 0, 0, 255 },
},
LayerFacePixels {
.face_index = 3,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 0, 255, 0, 128 },
},
},
},
};
const DocumentLayerConfig layers[] {
{
.name = "Paint",
.frames = std::span<const AnimationFrame>(layer_frames, 1),
},
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 1),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
RecordingRenderDevice device;
const auto result = pp::paint_renderer::upload_document_frame_faces(
device,
pp::paint_renderer::DocumentFrameUploadRequest {
.document = &document.value(),
.frame_index = 0,
});
PP_EXPECT(h, result);
if (result) {
PP_EXPECT(h, result.value().texture_count == pp::document::cube_face_count);
PP_EXPECT(h, result.value().transition_count == pp::document::cube_face_count);
PP_EXPECT(h, result.value().uploaded_bytes == 24U);
PP_EXPECT(h, result.value().composite.face_payload_count == 2U);
PP_EXPECT(h, result.value().face_textures[0] != nullptr);
PP_EXPECT(h, result.value().face_textures[0]->desc().extent.width == 1U);
PP_EXPECT(h, result.value().face_textures[0]->desc().format == TextureFormat::rgba8);
}
std::size_t upload_count = 0;
std::size_t transition_count = 0;
for (const auto& command : device.commands()) {
if (command.kind == RecordedRenderCommandKind::upload_texture) {
++upload_count;
PP_EXPECT(h, command.upload_bytes == 4U);
PP_EXPECT(h, command.readback_region.width == 1U);
PP_EXPECT(h, command.texture_desc.format == TextureFormat::rgba8);
}
if (command.kind == RecordedRenderCommandKind::transition_texture) {
++transition_count;
PP_EXPECT(h, command.before_state == pp::renderer::TextureState::upload_destination);
PP_EXPECT(h, command.after_state == pp::renderer::TextureState::shader_read);
}
}
PP_EXPECT(h, device.commands().size() == 12U);
PP_EXPECT(h, upload_count == pp::document::cube_face_count);
PP_EXPECT(h, transition_count == pp::document::cube_face_count);
}
void records_document_frame_upload_report(pp::tests::Harness& h)
{
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const AnimationFrame layer_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 1,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 0, 0, 255, 255 },
},
},
},
};
const DocumentLayerConfig layers[] {
{
.name = "Paint",
.frames = std::span<const AnimationFrame>(layer_frames, 1),
},
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 1),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
if (!document) {
return;
}
const auto report = pp::paint_renderer::record_document_frame_upload(
pp::paint_renderer::DocumentFrameUploadRequest {
.document = &document.value(),
.frame_index = 0,
});
PP_EXPECT(h, report);
if (report) {
PP_EXPECT(h, report.value().upload.texture_count == pp::document::cube_face_count);
PP_EXPECT(h, report.value().upload.transition_count == pp::document::cube_face_count);
PP_EXPECT(h, report.value().upload.uploaded_bytes == 24U);
PP_EXPECT(h, report.value().upload.composite.face_payload_count == 1U);
PP_EXPECT(h, report.value().command_count == 12U);
PP_EXPECT(h, report.value().upload_command_count == pp::document::cube_face_count);
PP_EXPECT(h, report.value().transition_command_count == pp::document::cube_face_count);
}
}
void exports_document_frame_faces_as_pngs(pp::tests::Harness& h)
{
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const AnimationFrame layer_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 4,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 64, 128, 255, 255 },
},
},
},
};
const DocumentLayerConfig layers[] {
{
.name = "Paint",
.frames = std::span<const AnimationFrame>(layer_frames, 1),
},
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 1),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
if (!document) {
return;
}
const auto exported = pp::paint_renderer::export_document_frame_face_pngs(
pp::paint_renderer::DocumentFrameCompositeRequest {
.document = &document.value(),
.frame_index = 0,
.clear_color = {},
});
PP_EXPECT(h, exported);
if (!exported) {
return;
}
PP_EXPECT(h, exported.value().face_count == pp::document::cube_face_count);
PP_EXPECT(h, exported.value().encoded_bytes > 0U);
PP_EXPECT(h, exported.value().composite.face_payload_count == 1U);
const auto decoded = pp::assets::decode_png_rgba8(exported.value().face_pngs[4]);
PP_EXPECT(h, decoded);
if (decoded) {
PP_EXPECT(h, decoded.value().width == 1U);
PP_EXPECT(h, decoded.value().height == 1U);
PP_EXPECT(h, decoded.value().pixels.size() == 4U);
PP_EXPECT(h, decoded.value().pixels[0] == 64U);
PP_EXPECT(h, decoded.value().pixels[1] == 128U);
PP_EXPECT(h, decoded.value().pixels[2] == 255U);
PP_EXPECT(h, decoded.value().pixels[3] == 255U);
}
}
void prepares_document_frame_export_readiness_report(pp::tests::Harness& h)
{
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const AnimationFrame layer_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 2,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 255, 128, 0, 255 },
},
},
},
};
const DocumentLayerConfig layers[] {
{
.name = "Paint",
.frames = std::span<const AnimationFrame>(layer_frames, 1),
},
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 1),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
if (!document) {
return;
}
const auto readiness = pp::paint_renderer::prepare_document_frame_export_readiness(
DocumentFrameCompositeRequest {
.document = &document.value(),
.frame_index = 0,
.clear_color = {},
});
PP_EXPECT(h, readiness);
if (!readiness) {
return;
}
PP_EXPECT(h, readiness.value().recorded_upload.upload.texture_count == pp::document::cube_face_count);
PP_EXPECT(h, readiness.value().recorded_upload.upload.uploaded_bytes == 24U);
PP_EXPECT(h, readiness.value().recorded_upload.command_count == 12U);
PP_EXPECT(h, readiness.value().recorded_upload.upload_command_count == pp::document::cube_face_count);
PP_EXPECT(h, readiness.value().recorded_upload.transition_command_count == pp::document::cube_face_count);
PP_EXPECT(h, readiness.value().face_pngs.face_count == pp::document::cube_face_count);
PP_EXPECT(h, readiness.value().face_pngs.encoded_bytes > 0U);
PP_EXPECT(h, readiness.value().face_pngs.composite.face_payload_count == 1U);
const auto decoded = pp::assets::decode_png_rgba8(readiness.value().face_pngs.face_pngs[2]);
PP_EXPECT(h, decoded);
if (decoded) {
PP_EXPECT(h, decoded.value().pixels[0] == 255U);
PP_EXPECT(h, decoded.value().pixels[1] == 128U);
PP_EXPECT(h, decoded.value().pixels[2] == 0U);
PP_EXPECT(h, decoded.value().pixels[3] == 255U);
}
}
void exports_document_frame_as_equirectangular_png(pp::tests::Harness& h)
{
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const AnimationFrame layer_frames[] {
{
.duration_ms = 100,
.face_pixels = {
LayerFacePixels {
.face_index = 0,
.x = 0,
.y = 0,
.width = 1,
.height = 4,
.rgba8 = { 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255 },
},
LayerFacePixels {
.face_index = 1,
.x = 0,
.y = 0,
.width = 1,
.height = 4,
.rgba8 = { 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255 },
},
LayerFacePixels {
.face_index = 2,
.x = 0,
.y = 0,
.width = 1,
.height = 4,
.rgba8 = { 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255 },
},
LayerFacePixels {
.face_index = 3,
.x = 0,
.y = 0,
.width = 1,
.height = 4,
.rgba8 = { 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255 },
},
LayerFacePixels {
.face_index = 4,
.x = 0,
.y = 0,
.width = 1,
.height = 4,
.rgba8 = { 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255 },
},
LayerFacePixels {
.face_index = 5,
.x = 0,
.y = 0,
.width = 1,
.height = 4,
.rgba8 = { 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255 },
},
},
},
};
const DocumentLayerConfig layers[] {
{
.name = "Paint",
.frames = std::span<const AnimationFrame>(layer_frames, 1),
},
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 4,
.layers = std::span<const DocumentLayerConfig>(layers, 1),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
if (!document) {
return;
}
const auto exported = pp::paint_renderer::export_document_frame_equirectangular_png(
DocumentFrameCompositeRequest {
.document = &document.value(),
.frame_index = 0,
});
PP_EXPECT(h, exported);
if (!exported) {
return;
}
PP_EXPECT(h, exported.value().face_extent.width == 1U);
PP_EXPECT(h, exported.value().face_extent.height == 4U);
PP_EXPECT(h, exported.value().equirectangular_extent.width == 4U);
PP_EXPECT(h, exported.value().equirectangular_extent.height == 8U);
PP_EXPECT(h, exported.value().face_payload_count == pp::document::cube_face_count);
PP_EXPECT(h, exported.value().encoded_bytes > 0U);
const auto decoded = pp::assets::decode_png_rgba8(exported.value().png);
PP_EXPECT(h, decoded);
if (!decoded) {
return;
}
PP_EXPECT(h, decoded.value().width == 4U);
PP_EXPECT(h, decoded.value().height == 8U);
PP_EXPECT(h, decoded.value().pixels[0] == 255U);
PP_EXPECT(h, decoded.value().pixels[1] == 0U);
PP_EXPECT(h, decoded.value().pixels[2] == 255U);
const auto bottom = (static_cast<std::size_t>(decoded.value().height) - 1U)
* decoded.value().width * pp::document::rgba8_components;
PP_EXPECT(h, decoded.value().pixels[bottom] == 0U);
PP_EXPECT(h, decoded.value().pixels[bottom + 1U] == 255U);
PP_EXPECT(h, decoded.value().pixels[bottom + 2U] == 255U);
}
void document_frame_upload_rejects_invalid_requests(pp::tests::Harness& h)
{
RecordingRenderDevice device;
const auto no_document = pp::paint_renderer::upload_document_frame_faces(
device,
pp::paint_renderer::DocumentFrameUploadRequest {});
const auto no_document_readiness = pp::paint_renderer::prepare_document_frame_export_readiness(
DocumentFrameCompositeRequest {});
const auto no_document_equirect = pp::paint_renderer::export_document_frame_equirectangular_png(
DocumentFrameCompositeRequest {});
const AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
};
const DocumentLayerConfig layers[] {
{ .name = "Layer", .frames = {} },
};
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = 1,
.height = 1,
.layers = std::span<const DocumentLayerConfig>(layers, 1),
.frames = std::span<const AnimationFrame>(root_frames, 1),
.selection_masks = {},
});
PP_EXPECT(h, document);
const auto bad_frame = pp::paint_renderer::upload_document_frame_faces(
device,
pp::paint_renderer::DocumentFrameUploadRequest {
.document = &document.value(),
.frame_index = 1,
});
const auto bad_frame_readiness = pp::paint_renderer::prepare_document_frame_export_readiness(
DocumentFrameCompositeRequest {
.document = &document.value(),
.frame_index = 1,
});
PP_EXPECT(h, !no_document.ok());
PP_EXPECT(h, no_document.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !no_document_readiness.ok());
PP_EXPECT(h, no_document_readiness.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !no_document_equirect.ok());
PP_EXPECT(h, no_document_equirect.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_frame.ok());
PP_EXPECT(h, bad_frame.status().code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_frame_readiness.ok());
PP_EXPECT(h, bad_frame_readiness.status().code == StatusCode::out_of_range);
PP_EXPECT(h, device.commands().empty());
}
void detects_feedback_requirements(pp::tests::Harness& h)
{
PP_EXPECT(h, !stroke_composite_requires_feedback(
BlendMode::normal,
StrokeBlendMode::normal,
false,
false));
PP_EXPECT(h, stroke_composite_requires_feedback(
BlendMode::multiply,
StrokeBlendMode::normal,
false,
false));
PP_EXPECT(h, stroke_composite_requires_feedback(
BlendMode::normal,
StrokeBlendMode::overlay,
false,
false));
PP_EXPECT(h, stroke_composite_requires_feedback(
BlendMode::normal,
StrokeBlendMode::normal,
true,
false));
PP_EXPECT(h, stroke_composite_requires_feedback(
BlendMode::normal,
StrokeBlendMode::normal,
false,
true));
}
void plans_stroke_composite_paths(pp::tests::Harness& h)
{
const StrokeCompositeRequest simple {
.extent = Extent2D { .width = 64, .height = 32 },
.target_usage = TextureUsage::render_target,
};
const auto fixed = plan_stroke_composite(
RenderDeviceFeatures {},
simple);
PP_EXPECT(h, fixed);
if (fixed) {
PP_EXPECT(h, fixed.value().path == StrokeCompositePath::fixed_function_blend);
PP_EXPECT(h, !fixed.value().complex_blend);
PP_EXPECT(h, !fixed.value().reads_destination_color);
PP_EXPECT(h, fixed.value().target_bytes == 8192U);
PP_EXPECT(h, fixed.value().estimated_working_bytes == 8192U);
}
const StrokeCompositeRequest complex_fetch {
.extent = Extent2D { .width = 32, .height = 16 },
.target_usage = TextureUsage::render_target,
.stroke_blend_mode = StrokeBlendMode::height,
};
const auto fetch = plan_stroke_composite(
RenderDeviceFeatures {
.framebuffer_fetch = true,
.explicit_texture_transitions = true,
},
complex_fetch);
PP_EXPECT(h, fetch);
if (fetch) {
PP_EXPECT(h, fetch.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, fetch.value().complex_blend);
PP_EXPECT(h, fetch.value().reads_destination_color);
PP_EXPECT(h, !fetch.value().requires_auxiliary_texture);
PP_EXPECT(h, fetch.value().requires_explicit_transition);
PP_EXPECT(h, fetch.value().target_bytes == 2048U);
}
const StrokeCompositeRequest complex_copy {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_mode = BlendMode::overlay,
.dual_brush_blend = true,
};
const auto copy = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
complex_copy);
PP_EXPECT(h, copy);
if (copy) {
PP_EXPECT(h, copy.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, copy.value().requires_auxiliary_texture);
PP_EXPECT(h, copy.value().requires_texture_copy);
PP_EXPECT(h, !copy.value().requires_render_target_blit);
PP_EXPECT(h, copy.value().target_bytes == 2048U);
PP_EXPECT(h, copy.value().auxiliary_bytes == 2048U);
PP_EXPECT(h, copy.value().estimated_working_bytes == 4096U);
}
const auto blit = plan_stroke_composite(
RenderDeviceFeatures { .render_target_blit = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.pattern_blend = true,
});
PP_EXPECT(h, blit);
if (blit) {
PP_EXPECT(h, blit.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, !blit.value().requires_texture_copy);
PP_EXPECT(h, blit.value().requires_render_target_blit);
}
}
void rejects_bad_stroke_composite_plans(pp::tests::Harness& h)
{
const auto unsupported = plan_stroke_composite(
RenderDeviceFeatures {},
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_mode = BlendMode::multiply,
});
const auto missing_usage = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.target_usage = TextureUsage::render_target,
.layer_blend_mode = BlendMode::multiply,
});
const auto depth = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.target_format = TextureFormat::depth24_stencil8,
.layer_blend_mode = BlendMode::multiply,
});
const auto bad_blend = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_mode = static_cast<BlendMode>(255),
});
const auto bad_stroke_blend = plan_stroke_composite(
RenderDeviceFeatures { .texture_copy = true },
StrokeCompositeRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.stroke_blend_mode = static_cast<StrokeBlendMode>(255),
});
PP_EXPECT(h, !unsupported.ok());
PP_EXPECT(h, unsupported.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !missing_usage.ok());
PP_EXPECT(h, missing_usage.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !depth.ok());
PP_EXPECT(h, depth.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_blend.ok());
PP_EXPECT(h, bad_blend.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_stroke_blend.ok());
PP_EXPECT(h, bad_stroke_blend.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::fixed_function_blend) == std::string_view("fixed_function_blend"));
PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::framebuffer_fetch) == std::string_view("framebuffer_fetch"));
PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::ping_pong_textures) == std::string_view("ping_pong_textures"));
PP_EXPECT(h, stroke_composite_path_name(static_cast<StrokeCompositePath>(255)) == std::string_view("unknown"));
}
void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h)
{
const std::vector<int> normal_layers { 0, 0, 0 };
const auto normal = plan_canvas_blend_gate(
RenderDeviceFeatures {},
CanvasBlendGateRequest {
.extent = Extent2D { .width = 0, .height = 0 },
.layer_blend_modes = normal_layers,
.has_stroke_blend_mode = true,
.stroke_blend_mode = 0,
});
PP_EXPECT(h, normal);
if (normal) {
PP_EXPECT(h, !normal.value().shader_blend);
PP_EXPECT(h, !normal.value().complex_blend);
PP_EXPECT(h, !normal.value().compatibility_fallback);
}
const std::vector<int> layer_blend { 0, 4 };
const auto layer = plan_canvas_blend_gate(
RenderDeviceFeatures { .framebuffer_fetch = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = layer_blend,
});
PP_EXPECT(h, layer);
if (layer) {
PP_EXPECT(h, layer.value().shader_blend);
PP_EXPECT(h, layer.value().complex_blend);
PP_EXPECT(h, layer.value().first_complex_layer_index == 1);
PP_EXPECT(h, layer.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, layer.value().reads_destination_color);
}
const auto stroke = plan_canvas_blend_gate(
RenderDeviceFeatures { .texture_copy = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = normal_layers,
.has_stroke_blend_mode = true,
.stroke_blend_mode = 10,
});
PP_EXPECT(h, stroke);
if (stroke) {
PP_EXPECT(h, stroke.value().shader_blend);
PP_EXPECT(h, stroke.value().stroke_complex);
PP_EXPECT(h, stroke.value().first_complex_layer_index == -1);
PP_EXPECT(h, stroke.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, !stroke.value().reads_destination_color);
PP_EXPECT(h, stroke.value().requires_texture_copy);
}
}
void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h)
{
const std::vector<int> unknown_layer { 0, 99 };
const auto unknown = plan_canvas_blend_gate(
RenderDeviceFeatures { .texture_copy = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = unknown_layer,
});
PP_EXPECT(h, unknown);
if (unknown) {
PP_EXPECT(h, unknown.value().shader_blend);
PP_EXPECT(h, unknown.value().complex_blend);
PP_EXPECT(h, unknown.value().compatibility_fallback);
PP_EXPECT(h, unknown.value().first_complex_layer_index == 1);
PP_EXPECT(h, unknown.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, unknown.value().requires_auxiliary_texture);
PP_EXPECT(h, unknown.value().requires_texture_copy);
}
const std::vector<int> normal_layers { 0 };
const auto unsupported = plan_canvas_blend_gate(
RenderDeviceFeatures {},
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = normal_layers,
.has_stroke_blend_mode = true,
.stroke_blend_mode = 10,
});
PP_EXPECT(h, unsupported);
if (unsupported) {
PP_EXPECT(h, unsupported.value().shader_blend);
PP_EXPECT(h, unsupported.value().stroke_complex);
PP_EXPECT(h, unsupported.value().compatibility_fallback);
PP_EXPECT(h, !unsupported.value().requires_texture_copy);
}
const auto unknown_fetch = plan_canvas_blend_gate(
RenderDeviceFeatures { .framebuffer_fetch = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 32, .height = 16 },
.layer_blend_modes = unknown_layer,
});
PP_EXPECT(h, unknown_fetch);
if (unknown_fetch) {
PP_EXPECT(h, unknown_fetch.value().compatibility_fallback);
PP_EXPECT(h, unknown_fetch.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, unknown_fetch.value().reads_destination_color);
PP_EXPECT(h, !unknown_fetch.value().requires_texture_copy);
}
const auto dual_pattern = plan_canvas_blend_gate(
RenderDeviceFeatures { .render_target_blit = true },
CanvasBlendGateRequest {
.extent = Extent2D { .width = 16, .height = 16 },
.layer_blend_modes = normal_layers,
.dual_brush_blend = true,
.pattern_blend = true,
});
PP_EXPECT(h, dual_pattern);
if (dual_pattern) {
PP_EXPECT(h, dual_pattern.value().shader_blend);
PP_EXPECT(h, dual_pattern.value().dual_brush_complex);
PP_EXPECT(h, dual_pattern.value().pattern_complex);
PP_EXPECT(h, dual_pattern.value().requires_render_target_blit);
}
}
void plans_canvas_stroke_feedback_paths(pp::tests::Harness& h)
{
const Extent2D extent { .width = 32, .height = 16 };
const auto fetch = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .framebuffer_fetch = true },
extent);
PP_EXPECT(h, fetch);
if (fetch) {
PP_EXPECT(h, fetch.value().path == StrokeCompositePath::framebuffer_fetch);
PP_EXPECT(h, fetch.value().reads_destination_color);
PP_EXPECT(h, !fetch.value().requires_auxiliary_texture);
PP_EXPECT(h, !fetch.value().compatibility_fallback);
}
const auto copy = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .texture_copy = true },
extent);
PP_EXPECT(h, copy);
if (copy) {
PP_EXPECT(h, copy.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, !copy.value().reads_destination_color);
PP_EXPECT(h, copy.value().requires_auxiliary_texture);
PP_EXPECT(h, copy.value().requires_texture_copy);
PP_EXPECT(h, !copy.value().requires_render_target_blit);
}
const auto blit = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .render_target_blit = true },
extent);
PP_EXPECT(h, blit);
if (blit) {
PP_EXPECT(h, blit.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, blit.value().requires_auxiliary_texture);
PP_EXPECT(h, !blit.value().requires_texture_copy);
PP_EXPECT(h, blit.value().requires_render_target_blit);
}
}
void canvas_stroke_feedback_preserves_legacy_fallback(pp::tests::Harness& h)
{
const auto fallback = plan_canvas_stroke_feedback(
RenderDeviceFeatures {},
Extent2D { .width = 32, .height = 16 });
PP_EXPECT(h, fallback);
if (fallback) {
PP_EXPECT(h, fallback.value().path == StrokeCompositePath::ping_pong_textures);
PP_EXPECT(h, fallback.value().requires_auxiliary_texture);
PP_EXPECT(h, !fallback.value().requires_texture_copy);
PP_EXPECT(h, fallback.value().compatibility_fallback);
}
const auto invalid = plan_canvas_stroke_feedback(
RenderDeviceFeatures { .texture_copy = true },
Extent2D { .width = 0, .height = 16 });
PP_EXPECT(h, !invalid.ok());
PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("composites_visible_layer_with_opacity", composites_visible_layer_with_opacity);
harness.run("invisible_and_zero_opacity_layers_are_noops", invisible_and_zero_opacity_layers_are_noops);
harness.run("rejects_invalid_sizes_and_opacity", rejects_invalid_sizes_and_opacity);
harness.run("composites_document_face_payloads_in_layer_order", composites_document_face_payloads_in_layer_order);
harness.run("document_face_composite_skips_layers_without_requested_frame", document_face_composite_skips_layers_without_requested_frame);
harness.run("document_face_composite_rejects_invalid_requests", document_face_composite_rejects_invalid_requests);
harness.run("composites_document_frame_cube_faces", composites_document_frame_cube_faces);
harness.run("document_frame_composite_rejects_invalid_requests", document_frame_composite_rejects_invalid_requests);
harness.run("uploads_document_frame_faces_to_renderer_api", uploads_document_frame_faces_to_renderer_api);
harness.run("records_document_frame_upload_report", records_document_frame_upload_report);
harness.run("exports_document_frame_faces_as_pngs", exports_document_frame_faces_as_pngs);
harness.run("prepares_document_frame_export_readiness_report", prepares_document_frame_export_readiness_report);
harness.run("exports_document_frame_as_equirectangular_png", exports_document_frame_as_equirectangular_png);
harness.run("document_frame_upload_rejects_invalid_requests", document_frame_upload_rejects_invalid_requests);
harness.run("detects_feedback_requirements", detects_feedback_requirements);
harness.run("plans_stroke_composite_paths", plans_stroke_composite_paths);
harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans);
harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices);
harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks);
harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);
harness.run("canvas_stroke_feedback_preserves_legacy_fallback", canvas_stroke_feedback_preserves_legacy_fallback);
return harness.finish();
}