2296 lines
88 KiB
C++
2296 lines
88 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::assets::decode_png_rgba8;
|
|
using pp::paint_renderer::CanvasBlendGateRequest;
|
|
using pp::paint_renderer::CanvasStrokeBox;
|
|
using pp::paint_renderer::CanvasStrokeCommitRequest;
|
|
using pp::paint_renderer::CanvasStrokeCommitStep;
|
|
using pp::paint_renderer::CanvasStrokeCommitTextureRole;
|
|
using pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest;
|
|
using pp::paint_renderer::CanvasStrokeMaterialRequest;
|
|
using pp::paint_renderer::CanvasStrokePadRegionRequest;
|
|
using pp::paint_renderer::CanvasStrokePoint;
|
|
using pp::paint_renderer::CanvasStrokeSampleBoundsRequest;
|
|
using pp::paint_renderer::CanvasStrokeTextureRole;
|
|
using pp::paint_renderer::DocumentFaceCompositeRequest;
|
|
using pp::paint_renderer::DocumentFrameCompositeRequest;
|
|
using pp::paint_renderer::LayerCompositeView;
|
|
using pp::paint_renderer::StrokePreviewCompositeRequest;
|
|
using pp::paint_renderer::StrokePreviewCompositeStep;
|
|
using pp::paint_renderer::StrokePreviewTextureRole;
|
|
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::export_document_depth_pngs;
|
|
using pp::paint_renderer::plan_canvas_blend_gate;
|
|
using pp::paint_renderer::plan_canvas_stroke_face_dirty_update;
|
|
using pp::paint_renderer::plan_canvas_stroke_feedback;
|
|
using pp::paint_renderer::plan_canvas_stroke_commit_sequence;
|
|
using pp::paint_renderer::plan_canvas_stroke_material;
|
|
using pp::paint_renderer::plan_canvas_stroke_pad_region;
|
|
using pp::paint_renderer::plan_canvas_stroke_rasterization;
|
|
using pp::paint_renderer::plan_canvas_stroke_sample_bounds;
|
|
using pp::paint_renderer::plan_document_depth_export_render;
|
|
using pp::paint_renderer::plan_stroke_preview_composite;
|
|
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;
|
|
}
|
|
|
|
bool has_texture_binding(
|
|
const pp::paint_renderer::CanvasStrokeMaterialPlan& plan,
|
|
CanvasStrokeTextureRole role,
|
|
std::uint8_t slot)
|
|
{
|
|
for (std::size_t i = 0; i < plan.texture_binding_count; ++i) {
|
|
if (plan.texture_bindings[i].role == role && plan.texture_bindings[i].slot == slot) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool has_preview_texture_slot(
|
|
const pp::paint_renderer::StrokePreviewCompositePlan& plan,
|
|
StrokePreviewTextureRole role,
|
|
std::uint8_t slot)
|
|
{
|
|
for (std::size_t i = 0; i < plan.texture_slot_count; ++i) {
|
|
if (plan.texture_slots[i].role == role && plan.texture_slots[i].slot == slot) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool has_commit_texture_binding(
|
|
const pp::paint_renderer::CanvasStrokeCommitSequencePlan& plan,
|
|
CanvasStrokeCommitTextureRole role,
|
|
std::uint8_t slot)
|
|
{
|
|
for (std::size_t i = 0; i < plan.texture_binding_count; ++i) {
|
|
if (plan.texture_bindings[i].role == role && plan.texture_bindings[i].slot == slot) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void expect_preview_sequence(pp::tests::Harness& h, const pp::paint_renderer::StrokePreviewCompositePlan& plan)
|
|
{
|
|
PP_EXPECT(h, plan.step_count == 5U);
|
|
PP_EXPECT(h, plan.steps[0] == StrokePreviewCompositeStep::checkerboard_background);
|
|
PP_EXPECT(h, plan.steps[1] == StrokePreviewCompositeStep::capture_background_texture);
|
|
PP_EXPECT(h, plan.steps[2] == StrokePreviewCompositeStep::bind_final_composite_inputs);
|
|
PP_EXPECT(h, plan.steps[3] == StrokePreviewCompositeStep::final_composite_draw);
|
|
PP_EXPECT(h, plan.steps[4] == StrokePreviewCompositeStep::copy_preview_texture);
|
|
}
|
|
|
|
void expect_commit_prefix(pp::tests::Harness& h, const pp::paint_renderer::CanvasStrokeCommitSequencePlan& plan)
|
|
{
|
|
PP_EXPECT(h, plan.step_count == 7U);
|
|
PP_EXPECT(h, plan.steps[0] == CanvasStrokeCommitStep::readback_history_region);
|
|
PP_EXPECT(h, plan.steps[1] == CanvasStrokeCommitStep::update_layer_dirty_state);
|
|
PP_EXPECT(h, plan.steps[2] == CanvasStrokeCommitStep::copy_layer_rtt_to_scratch);
|
|
PP_EXPECT(h, plan.steps[3] == CanvasStrokeCommitStep::bind_commit_inputs);
|
|
PP_EXPECT(h, plan.steps[5] == CanvasStrokeCommitStep::copy_committed_rtt_to_scratch);
|
|
PP_EXPECT(h, plan.steps[6] == CanvasStrokeCommitStep::dilate_edges_draw);
|
|
}
|
|
|
|
std::vector<std::uint8_t> solid_rgba8(
|
|
std::uint32_t width,
|
|
std::uint32_t height,
|
|
std::uint8_t r,
|
|
std::uint8_t g,
|
|
std::uint8_t b,
|
|
std::uint8_t a)
|
|
{
|
|
std::vector<std::uint8_t> pixels(
|
|
static_cast<std::size_t>(width) * height * pp::document::rgba8_components);
|
|
for (std::size_t i = 0; i < pixels.size(); i += pp::document::rgba8_components) {
|
|
pixels[i] = r;
|
|
pixels[i + 1U] = g;
|
|
pixels[i + 2U] = b;
|
|
pixels[i + 3U] = a;
|
|
}
|
|
return pixels;
|
|
}
|
|
|
|
LayerFacePixels solid_face_payload(
|
|
std::uint32_t face_index,
|
|
std::uint32_t width,
|
|
std::uint32_t height,
|
|
std::uint8_t r,
|
|
std::uint8_t g,
|
|
std::uint8_t b,
|
|
std::uint8_t a)
|
|
{
|
|
return LayerFacePixels {
|
|
.face_index = face_index,
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = width,
|
|
.height = height,
|
|
.rgba8 = solid_rgba8(width, height, r, g, b, a),
|
|
};
|
|
}
|
|
|
|
std::vector<LayerFacePixels> solid_cube_faces(
|
|
std::uint32_t width,
|
|
std::uint32_t height,
|
|
std::uint8_t r,
|
|
std::uint8_t g,
|
|
std::uint8_t b,
|
|
std::uint8_t a)
|
|
{
|
|
std::vector<LayerFacePixels> faces;
|
|
faces.reserve(pp::document::cube_face_count);
|
|
for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) {
|
|
faces.push_back(solid_face_payload(face_index, width, height, r, g, b, a));
|
|
}
|
|
return faces;
|
|
}
|
|
|
|
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 exports_document_frame_as_equirectangular_jpeg_with_xmp(pp::tests::Harness& h)
|
|
{
|
|
const AnimationFrame root_frames[] {
|
|
{ .duration_ms = 100, .face_pixels = {} },
|
|
};
|
|
const AnimationFrame layer_frames[] {
|
|
{
|
|
.duration_ms = 100,
|
|
.face_pixels = solid_cube_faces(1, 4, 25, 125, 225, 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_jpeg(
|
|
DocumentFrameCompositeRequest {
|
|
.document = &document.value(),
|
|
.frame_index = 0,
|
|
},
|
|
90);
|
|
|
|
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 == exported.value().jpeg.size());
|
|
PP_EXPECT(h, exported.value().xmp_injected);
|
|
|
|
const std::string_view text(
|
|
reinterpret_cast<const char*>(exported.value().jpeg.data()),
|
|
exported.value().jpeg.size());
|
|
PP_EXPECT(h, text.find("GPano:ProjectionType") != std::string_view::npos);
|
|
PP_EXPECT(h, text.find("equirectangular") != std::string_view::npos);
|
|
|
|
const auto decoded = pp::assets::decode_jpeg_rgba8(exported.value().jpeg);
|
|
PP_EXPECT(h, decoded);
|
|
if (!decoded) {
|
|
return;
|
|
}
|
|
|
|
PP_EXPECT(h, decoded.value().width == 4U);
|
|
PP_EXPECT(h, decoded.value().height == 8U);
|
|
}
|
|
|
|
void exports_document_layers_as_equirectangular_pngs(pp::tests::Harness& h)
|
|
{
|
|
const AnimationFrame root_frames[] {
|
|
{ .duration_ms = 100, .face_pixels = {} },
|
|
};
|
|
const AnimationFrame base_frames[] {
|
|
{
|
|
.duration_ms = 100,
|
|
.face_pixels = solid_cube_faces(1, 4, 255, 0, 0, 255),
|
|
},
|
|
};
|
|
const AnimationFrame hidden_frames[] {
|
|
{
|
|
.duration_ms = 100,
|
|
.face_pixels = solid_cube_faces(1, 4, 0, 0, 255, 255),
|
|
},
|
|
};
|
|
const DocumentLayerConfig layers[] {
|
|
{
|
|
.name = "Base",
|
|
.frames = std::span<const AnimationFrame>(base_frames, 1),
|
|
},
|
|
{
|
|
.name = "HiddenPaint",
|
|
.visible = false,
|
|
.opacity = 0.0F,
|
|
.frames = std::span<const AnimationFrame>(hidden_frames, 1),
|
|
},
|
|
};
|
|
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
|
.width = 1,
|
|
.height = 4,
|
|
.layers = std::span<const DocumentLayerConfig>(layers, 2),
|
|
.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_layers_equirectangular_pngs(
|
|
pp::paint_renderer::DocumentLayerEquirectangularPngExportRequest {
|
|
.document = &document.value(),
|
|
.frame_index = 0,
|
|
});
|
|
PP_EXPECT(h, exported);
|
|
if (!exported) {
|
|
return;
|
|
}
|
|
|
|
PP_EXPECT(h, exported.value().layer_count == 2U);
|
|
PP_EXPECT(h, exported.value().layers.size() == 2U);
|
|
PP_EXPECT(h, exported.value().layers[0].layer_name == "Base");
|
|
PP_EXPECT(h, exported.value().layers[1].layer_name == "HiddenPaint");
|
|
PP_EXPECT(h, exported.value().layers[0].face_payload_count == pp::document::cube_face_count);
|
|
PP_EXPECT(h, exported.value().layers[1].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().layers[1].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] == 0U);
|
|
PP_EXPECT(h, decoded.value().pixels[1] == 0U);
|
|
PP_EXPECT(h, decoded.value().pixels[2] == 255U);
|
|
}
|
|
|
|
void exports_document_animation_frames_as_equirectangular_pngs(pp::tests::Harness& h)
|
|
{
|
|
const AnimationFrame root_frames[] {
|
|
{ .duration_ms = 100, .face_pixels = {} },
|
|
{ .duration_ms = 100, .face_pixels = {} },
|
|
};
|
|
const AnimationFrame layer_frames[] {
|
|
{
|
|
.duration_ms = 100,
|
|
.face_pixels = solid_cube_faces(1, 4, 255, 0, 0, 255),
|
|
},
|
|
{
|
|
.duration_ms = 100,
|
|
.face_pixels = solid_cube_faces(1, 4, 0, 255, 0, 255),
|
|
},
|
|
};
|
|
const DocumentLayerConfig layers[] {
|
|
{
|
|
.name = "Paint",
|
|
.frames = std::span<const AnimationFrame>(layer_frames, 2),
|
|
},
|
|
};
|
|
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, 2),
|
|
.selection_masks = {},
|
|
});
|
|
PP_EXPECT(h, document);
|
|
if (!document) {
|
|
return;
|
|
}
|
|
|
|
const auto exported = pp::paint_renderer::export_document_animation_frames_equirectangular_pngs(
|
|
pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {
|
|
.document = &document.value(),
|
|
});
|
|
PP_EXPECT(h, exported);
|
|
if (!exported) {
|
|
return;
|
|
}
|
|
|
|
PP_EXPECT(h, exported.value().frame_count == 2U);
|
|
PP_EXPECT(h, exported.value().frames.size() == 2U);
|
|
PP_EXPECT(h, exported.value().frames[0].frame_index == 0U);
|
|
PP_EXPECT(h, exported.value().frames[1].frame_index == 1U);
|
|
PP_EXPECT(h, exported.value().encoded_bytes > 0U);
|
|
|
|
const auto decoded = pp::assets::decode_png_rgba8(exported.value().frames[1].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] == 0U);
|
|
PP_EXPECT(h, decoded.value().pixels[1] == 255U);
|
|
PP_EXPECT(h, decoded.value().pixels[2] == 0U);
|
|
}
|
|
|
|
void plans_document_depth_export_renderer_work(pp::tests::Harness& h)
|
|
{
|
|
std::vector<LayerFacePixels> visible_faces;
|
|
visible_faces.push_back(solid_face_payload(0, 1, 1, 255, 0, 0, 255));
|
|
visible_faces.push_back(solid_face_payload(2, 1, 1, 0, 0, 255, 255));
|
|
|
|
const AnimationFrame root_frames[] {
|
|
{ .duration_ms = 100, .face_pixels = {} },
|
|
};
|
|
const AnimationFrame visible_layer_frames[] {
|
|
{ .duration_ms = 100, .face_pixels = visible_faces },
|
|
};
|
|
const AnimationFrame hidden_layer_frames[] {
|
|
{
|
|
.duration_ms = 100,
|
|
.face_pixels = solid_cube_faces(1, 1, 255, 255, 0, 255),
|
|
},
|
|
};
|
|
const AnimationFrame empty_layer_frames[] {
|
|
{ .duration_ms = 100, .face_pixels = {} },
|
|
};
|
|
const DocumentLayerConfig layers[] {
|
|
{
|
|
.name = "Visible",
|
|
.frames = std::span<const AnimationFrame>(visible_layer_frames, 1),
|
|
},
|
|
{
|
|
.name = "Hidden",
|
|
.visible = false,
|
|
.frames = std::span<const AnimationFrame>(hidden_layer_frames, 1),
|
|
},
|
|
{
|
|
.name = "Transparent",
|
|
.opacity = 0.0F,
|
|
.frames = std::span<const AnimationFrame>(hidden_layer_frames, 1),
|
|
},
|
|
{
|
|
.name = "EmptyVisible",
|
|
.frames = std::span<const AnimationFrame>(empty_layer_frames, 1),
|
|
},
|
|
};
|
|
const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
|
|
.width = 1,
|
|
.height = 1,
|
|
.layers = std::span<const DocumentLayerConfig>(layers, 4),
|
|
.frames = std::span<const AnimationFrame>(root_frames, 1),
|
|
.selection_masks = {},
|
|
});
|
|
PP_EXPECT(h, document);
|
|
if (!document) {
|
|
return;
|
|
}
|
|
|
|
const auto plan = plan_document_depth_export_render(
|
|
pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
|
|
.document = &document.value(),
|
|
.frame_index = 0,
|
|
});
|
|
PP_EXPECT(h, plan);
|
|
if (!plan) {
|
|
return;
|
|
}
|
|
|
|
PP_EXPECT(h, plan.value().output_extent.width == 1024U);
|
|
PP_EXPECT(h, plan.value().output_extent.height == 1024U);
|
|
PP_EXPECT(h, plan.value().merged_face_draw_count == pp::document::cube_face_count);
|
|
PP_EXPECT(h, plan.value().visited_layer_count == 4U);
|
|
PP_EXPECT(h, plan.value().visible_layer_count == 2U);
|
|
PP_EXPECT(h, plan.value().face_payload_count == 2U);
|
|
PP_EXPECT(h, plan.value().layer_depth_draw_count == 2U);
|
|
PP_EXPECT(h, plan.value().uses_perspective_camera);
|
|
PP_EXPECT(h, plan.value().requires_renderer_readback);
|
|
}
|
|
|
|
void exports_document_depth_as_png_payloads(pp::tests::Harness& h)
|
|
{
|
|
const AnimationFrame root_frames[] {
|
|
{ .duration_ms = 100, .face_pixels = {} },
|
|
};
|
|
const AnimationFrame base_frames[] {
|
|
{
|
|
.duration_ms = 100,
|
|
.face_pixels = {
|
|
solid_face_payload(0, 1, 1, 255, 0, 0, 255),
|
|
},
|
|
},
|
|
};
|
|
const AnimationFrame top_frames[] {
|
|
{
|
|
.duration_ms = 100,
|
|
.face_pixels = {
|
|
solid_face_payload(0, 1, 1, 0, 255, 0, 255),
|
|
},
|
|
},
|
|
};
|
|
const DocumentLayerConfig layers[] {
|
|
{
|
|
.name = "Base",
|
|
.frames = std::span<const AnimationFrame>(base_frames, 1),
|
|
},
|
|
{
|
|
.name = "Top",
|
|
.frames = std::span<const AnimationFrame>(top_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);
|
|
if (!document) {
|
|
return;
|
|
}
|
|
|
|
const auto exported = export_document_depth_pngs(
|
|
pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
|
|
.document = &document.value(),
|
|
.frame_index = 0,
|
|
.output_extent = Extent2D { .width = 1, .height = 1 },
|
|
});
|
|
PP_EXPECT(h, exported);
|
|
if (!exported) {
|
|
return;
|
|
}
|
|
|
|
PP_EXPECT(h, exported.value().output_extent.width == 1U);
|
|
PP_EXPECT(h, exported.value().output_extent.height == 1U);
|
|
PP_EXPECT(h, exported.value().image_encoded_bytes > 0U);
|
|
PP_EXPECT(h, exported.value().depth_encoded_bytes > 0U);
|
|
PP_EXPECT(h, exported.value().merged_face_draw_count == pp::document::cube_face_count);
|
|
PP_EXPECT(h, exported.value().layer_depth_draw_count == 2U);
|
|
PP_EXPECT(h, exported.value().visited_layer_count == 2U);
|
|
PP_EXPECT(h, exported.value().visible_layer_count == 2U);
|
|
PP_EXPECT(h, exported.value().face_payload_count == 2U);
|
|
PP_EXPECT(h, exported.value().uses_perspective_camera);
|
|
|
|
const auto image = decode_png_rgba8(exported.value().image_png);
|
|
const auto depth = decode_png_rgba8(exported.value().depth_png);
|
|
PP_EXPECT(h, image);
|
|
PP_EXPECT(h, depth);
|
|
if (!image || !depth) {
|
|
return;
|
|
}
|
|
|
|
PP_EXPECT(h, image.value().width == 1U);
|
|
PP_EXPECT(h, image.value().height == 1U);
|
|
PP_EXPECT(h, depth.value().width == 1U);
|
|
PP_EXPECT(h, depth.value().height == 1U);
|
|
const std::vector<std::uint8_t> expected_image { 0, 255, 0, 255 };
|
|
const std::vector<std::uint8_t> expected_depth { 170, 170, 170, 255 };
|
|
PP_EXPECT(h, image.value().pixels == expected_image);
|
|
PP_EXPECT(h, depth.value().pixels == expected_depth);
|
|
}
|
|
|
|
void depth_export_payload_boundary_rejects_malformed_face_bytes(pp::tests::Harness& h)
|
|
{
|
|
const AnimationFrame root_frames[] {
|
|
{ .duration_ms = 100, .face_pixels = {} },
|
|
};
|
|
const AnimationFrame bad_frames[] {
|
|
{
|
|
.duration_ms = 100,
|
|
.face_pixels = {
|
|
LayerFacePixels {
|
|
.face_index = 0,
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = 1,
|
|
.height = 1,
|
|
.rgba8 = { 255, 0, 0 },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const DocumentLayerConfig layers[] {
|
|
{
|
|
.name = "Broken",
|
|
.frames = std::span<const AnimationFrame>(bad_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.ok());
|
|
PP_EXPECT(h, document.status().code == StatusCode::invalid_argument);
|
|
}
|
|
|
|
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 auto no_document_jpeg = pp::paint_renderer::export_document_frame_equirectangular_jpeg(
|
|
DocumentFrameCompositeRequest {});
|
|
const auto no_document_layers = pp::paint_renderer::export_document_layers_equirectangular_pngs(
|
|
pp::paint_renderer::DocumentLayerEquirectangularPngExportRequest {});
|
|
const auto no_document_frames = pp::paint_renderer::export_document_animation_frames_equirectangular_pngs(
|
|
pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {});
|
|
const auto no_document_depth = plan_document_depth_export_render(
|
|
pp::paint_renderer::DocumentDepthExportRenderPlanRequest {});
|
|
const auto no_document_depth_pngs = export_document_depth_pngs(
|
|
pp::paint_renderer::DocumentDepthExportRenderPlanRequest {});
|
|
|
|
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,
|
|
});
|
|
const auto bad_frame_depth = plan_document_depth_export_render(
|
|
pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
|
|
.document = &document.value(),
|
|
.frame_index = 1,
|
|
});
|
|
const auto bad_extent_depth = plan_document_depth_export_render(
|
|
pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
|
|
.document = &document.value(),
|
|
.frame_index = 0,
|
|
.output_extent = Extent2D {},
|
|
});
|
|
const auto bad_frame_depth_pngs = export_document_depth_pngs(
|
|
pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
|
|
.document = &document.value(),
|
|
.frame_index = 1,
|
|
});
|
|
const auto bad_extent_depth_pngs = export_document_depth_pngs(
|
|
pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
|
|
.document = &document.value(),
|
|
.frame_index = 0,
|
|
.output_extent = Extent2D {},
|
|
});
|
|
const auto bad_jpeg_quality = pp::paint_renderer::export_document_frame_equirectangular_jpeg(
|
|
DocumentFrameCompositeRequest {
|
|
.document = &document.value(),
|
|
.frame_index = 0,
|
|
},
|
|
0);
|
|
|
|
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, !no_document_jpeg.ok());
|
|
PP_EXPECT(h, no_document_jpeg.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !no_document_layers.ok());
|
|
PP_EXPECT(h, no_document_layers.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !no_document_frames.ok());
|
|
PP_EXPECT(h, no_document_frames.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !no_document_depth.ok());
|
|
PP_EXPECT(h, no_document_depth.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !no_document_depth_pngs.ok());
|
|
PP_EXPECT(h, no_document_depth_pngs.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, !bad_frame_depth.ok());
|
|
PP_EXPECT(h, bad_frame_depth.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !bad_extent_depth.ok());
|
|
PP_EXPECT(h, bad_extent_depth.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !bad_frame_depth_pngs.ok());
|
|
PP_EXPECT(h, bad_frame_depth_pngs.status().code == StatusCode::out_of_range);
|
|
PP_EXPECT(h, !bad_extent_depth_pngs.ok());
|
|
PP_EXPECT(h, bad_extent_depth_pngs.status().code == StatusCode::invalid_argument);
|
|
PP_EXPECT(h, !bad_jpeg_quality.ok());
|
|
PP_EXPECT(h, bad_jpeg_quality.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_stroke_material_passes(pp::tests::Harness& h)
|
|
{
|
|
const auto simple = plan_canvas_stroke_material(CanvasStrokeMaterialRequest {});
|
|
PP_EXPECT(h, simple.texture_binding_count == 1U);
|
|
PP_EXPECT(h, has_texture_binding(simple, CanvasStrokeTextureRole::main_brush_tip, 0));
|
|
PP_EXPECT(h, !simple.stroke_pass.uses_destination_feedback);
|
|
PP_EXPECT(h, !simple.stroke_pass.uses_pattern);
|
|
PP_EXPECT(h, !simple.stroke_pass.uses_mixer);
|
|
PP_EXPECT(h, !simple.dual_pass.enabled);
|
|
PP_EXPECT(h, !simple.composite_pass.use_dual);
|
|
PP_EXPECT(h, !simple.composite_pass.use_pattern);
|
|
|
|
const auto eachsample = plan_canvas_stroke_material(
|
|
CanvasStrokeMaterialRequest {
|
|
.destination_feedback_needed = true,
|
|
.pattern_enabled = true,
|
|
.pattern_eachsample = true,
|
|
.wet_blend = true,
|
|
.noise_enabled = true,
|
|
});
|
|
PP_EXPECT(h, eachsample.stroke_pass.uses_destination_feedback);
|
|
PP_EXPECT(h, eachsample.stroke_pass.uses_pattern);
|
|
PP_EXPECT(h, eachsample.stroke_pass.uses_mixer);
|
|
PP_EXPECT(h, !eachsample.composite_pass.use_pattern);
|
|
PP_EXPECT(h, has_texture_binding(eachsample, CanvasStrokeTextureRole::main_brush_tip, 0));
|
|
PP_EXPECT(h, has_texture_binding(eachsample, CanvasStrokeTextureRole::destination_feedback, 1));
|
|
PP_EXPECT(h, has_texture_binding(eachsample, CanvasStrokeTextureRole::pattern, 2));
|
|
PP_EXPECT(h, has_texture_binding(eachsample, CanvasStrokeTextureRole::mixer, 3));
|
|
|
|
const auto composite_pattern = plan_canvas_stroke_material(
|
|
CanvasStrokeMaterialRequest {
|
|
.pattern_enabled = true,
|
|
.pattern_eachsample = false,
|
|
.pattern_blend_mode = 6,
|
|
});
|
|
PP_EXPECT(h, !composite_pattern.stroke_pass.uses_pattern);
|
|
PP_EXPECT(h, composite_pattern.composite_pass.use_pattern);
|
|
PP_EXPECT(h, composite_pattern.composite_pass.pattern_blend_mode == 6);
|
|
PP_EXPECT(h, has_texture_binding(composite_pattern, CanvasStrokeTextureRole::pattern, 2));
|
|
}
|
|
|
|
void plans_canvas_stroke_dual_material_intent(pp::tests::Harness& h)
|
|
{
|
|
const auto dual = plan_canvas_stroke_material(
|
|
CanvasStrokeMaterialRequest {
|
|
.pattern_enabled = true,
|
|
.pattern_eachsample = true,
|
|
.dual_brush_enabled = true,
|
|
.dual_blend_mode = 3,
|
|
.pattern_blend_mode = 4,
|
|
.dual_alpha = 0.625F,
|
|
});
|
|
|
|
PP_EXPECT(h, dual.stroke_pass.uses_pattern);
|
|
PP_EXPECT(h, dual.dual_pass.enabled);
|
|
PP_EXPECT(h, !dual.dual_pass.uses_pattern);
|
|
PP_EXPECT(h, dual.composite_pass.use_dual);
|
|
PP_EXPECT(h, !dual.composite_pass.use_pattern);
|
|
PP_EXPECT(h, dual.composite_pass.dual_blend_mode == 3);
|
|
PP_EXPECT(h, dual.composite_pass.pattern_blend_mode == 4);
|
|
PP_EXPECT(h, near(dual.composite_pass.dual_alpha, 0.625F));
|
|
PP_EXPECT(h, has_texture_binding(dual, CanvasStrokeTextureRole::dual_brush_tip, 4));
|
|
|
|
const auto dual_composite_pattern = plan_canvas_stroke_material(
|
|
CanvasStrokeMaterialRequest {
|
|
.pattern_enabled = true,
|
|
.pattern_eachsample = false,
|
|
.mix_blend = true,
|
|
.dual_brush_enabled = true,
|
|
});
|
|
PP_EXPECT(h, !dual_composite_pattern.stroke_pass.uses_pattern);
|
|
PP_EXPECT(h, dual_composite_pattern.stroke_pass.uses_mixer);
|
|
PP_EXPECT(h, dual_composite_pattern.dual_pass.enabled);
|
|
PP_EXPECT(h, !dual_composite_pattern.dual_pass.uses_pattern);
|
|
PP_EXPECT(h, dual_composite_pattern.composite_pass.use_dual);
|
|
PP_EXPECT(h, dual_composite_pattern.composite_pass.use_pattern);
|
|
PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::mixer, 3));
|
|
PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::dual_brush_tip, 4));
|
|
PP_EXPECT(h, has_texture_binding(dual_composite_pattern, CanvasStrokeTextureRole::pattern, 2));
|
|
}
|
|
|
|
void plans_canvas_stroke_commit_erase_sequence(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_canvas_stroke_commit_sequence(
|
|
CanvasStrokeCommitRequest {
|
|
.erase_mode = true,
|
|
.alpha_locked = true,
|
|
.selection_mask_active = true,
|
|
.dual_stroke_enabled = true,
|
|
.pattern_enabled = true,
|
|
});
|
|
|
|
expect_commit_prefix(h, plan);
|
|
PP_EXPECT(h, plan.steps[4] == CanvasStrokeCommitStep::erase_draw);
|
|
PP_EXPECT(h, plan.erase_mode);
|
|
PP_EXPECT(h, plan.alpha_locked);
|
|
PP_EXPECT(h, plan.selection_mask_active);
|
|
PP_EXPECT(h, !plan.uses_dual_stroke);
|
|
PP_EXPECT(h, !plan.uses_pattern);
|
|
PP_EXPECT(h, plan.requires_history_readback);
|
|
PP_EXPECT(h, !plan.updates_layer_bounds);
|
|
PP_EXPECT(h, plan.requires_layer_scratch_copy);
|
|
PP_EXPECT(h, plan.requires_committed_scratch_copy);
|
|
PP_EXPECT(h, plan.requires_dilate);
|
|
PP_EXPECT(h, plan.texture_binding_count == 3U);
|
|
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::layer_scratch, 0));
|
|
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::stroke, 1));
|
|
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::selection_mask, 2));
|
|
}
|
|
|
|
void plans_canvas_stroke_commit_composite_sequence(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_canvas_stroke_commit_sequence(
|
|
CanvasStrokeCommitRequest {
|
|
.erase_mode = false,
|
|
.alpha_locked = false,
|
|
.selection_mask_active = true,
|
|
.dual_stroke_enabled = true,
|
|
.pattern_enabled = true,
|
|
});
|
|
|
|
expect_commit_prefix(h, plan);
|
|
PP_EXPECT(h, plan.steps[4] == CanvasStrokeCommitStep::composite_draw);
|
|
PP_EXPECT(h, !plan.erase_mode);
|
|
PP_EXPECT(h, !plan.alpha_locked);
|
|
PP_EXPECT(h, plan.selection_mask_active);
|
|
PP_EXPECT(h, plan.uses_dual_stroke);
|
|
PP_EXPECT(h, plan.uses_pattern);
|
|
PP_EXPECT(h, plan.updates_layer_bounds);
|
|
PP_EXPECT(h, plan.texture_binding_count == 5U);
|
|
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::layer_scratch, 0));
|
|
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::stroke, 1));
|
|
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::selection_mask, 2));
|
|
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::dual_stroke, 3));
|
|
PP_EXPECT(h, has_commit_texture_binding(plan, CanvasStrokeCommitTextureRole::pattern, 4));
|
|
}
|
|
|
|
void plans_stroke_preview_composite_for_simple_brush(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_stroke_preview_composite(StrokePreviewCompositeRequest {});
|
|
|
|
expect_preview_sequence(h, plan);
|
|
PP_EXPECT(h, !plan.uses_mixer);
|
|
PP_EXPECT(h, !plan.uses_dual);
|
|
PP_EXPECT(h, !plan.uses_pattern);
|
|
PP_EXPECT(h, plan.texture_slot_count == 2U);
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0));
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1));
|
|
PP_EXPECT(h, !has_preview_texture_slot(plan, StrokePreviewTextureRole::mask, 2));
|
|
}
|
|
|
|
void plans_stroke_preview_composite_with_mixer_input(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_stroke_preview_composite(
|
|
StrokePreviewCompositeRequest {
|
|
.uses_mixer = true,
|
|
});
|
|
|
|
expect_preview_sequence(h, plan);
|
|
PP_EXPECT(h, plan.uses_mixer);
|
|
PP_EXPECT(h, !plan.uses_dual);
|
|
PP_EXPECT(h, !plan.uses_pattern);
|
|
PP_EXPECT(h, plan.texture_slot_count == 3U);
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0));
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1));
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::mixer, 3));
|
|
}
|
|
|
|
void plans_stroke_preview_composite_with_dual_input(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_stroke_preview_composite(
|
|
StrokePreviewCompositeRequest {
|
|
.uses_dual = true,
|
|
});
|
|
|
|
expect_preview_sequence(h, plan);
|
|
PP_EXPECT(h, !plan.uses_mixer);
|
|
PP_EXPECT(h, plan.uses_dual);
|
|
PP_EXPECT(h, !plan.uses_pattern);
|
|
PP_EXPECT(h, plan.texture_slot_count == 3U);
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0));
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1));
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::dual, 3));
|
|
}
|
|
|
|
void plans_stroke_preview_composite_with_pattern_input(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_stroke_preview_composite(
|
|
StrokePreviewCompositeRequest {
|
|
.uses_pattern = true,
|
|
});
|
|
|
|
expect_preview_sequence(h, plan);
|
|
PP_EXPECT(h, !plan.uses_mixer);
|
|
PP_EXPECT(h, !plan.uses_dual);
|
|
PP_EXPECT(h, plan.uses_pattern);
|
|
PP_EXPECT(h, plan.texture_slot_count == 3U);
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::background, 0));
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::stroke, 1));
|
|
PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void plans_canvas_stroke_rasterization_boundary(pp::tests::Harness& h)
|
|
{
|
|
const Extent2D extent { .width = 32, .height = 16 };
|
|
const auto fetch = plan_canvas_stroke_rasterization(
|
|
RenderDeviceFeatures { .framebuffer_fetch = true },
|
|
extent);
|
|
PP_EXPECT(h, fetch);
|
|
if (fetch) {
|
|
PP_EXPECT(h, fetch.value().feedback.path == StrokeCompositePath::framebuffer_fetch);
|
|
PP_EXPECT(h, !fetch.value().copy_stroke_destination);
|
|
PP_EXPECT(h, fetch.value().can_route_feedback_through_renderer);
|
|
PP_EXPECT(h, !fetch.value().compatibility_fallback);
|
|
}
|
|
|
|
const auto copy = plan_canvas_stroke_rasterization(
|
|
RenderDeviceFeatures { .texture_copy = true },
|
|
extent);
|
|
PP_EXPECT(h, copy);
|
|
if (copy) {
|
|
PP_EXPECT(h, copy.value().feedback.path == StrokeCompositePath::ping_pong_textures);
|
|
PP_EXPECT(h, copy.value().copy_stroke_destination);
|
|
PP_EXPECT(h, copy.value().can_route_feedback_through_renderer);
|
|
PP_EXPECT(h, !copy.value().compatibility_fallback);
|
|
}
|
|
|
|
const auto fallback = plan_canvas_stroke_rasterization(
|
|
RenderDeviceFeatures {},
|
|
extent);
|
|
PP_EXPECT(h, fallback);
|
|
if (fallback) {
|
|
PP_EXPECT(h, fallback.value().feedback.path == StrokeCompositePath::ping_pong_textures);
|
|
PP_EXPECT(h, fallback.value().copy_stroke_destination);
|
|
PP_EXPECT(h, !fallback.value().can_route_feedback_through_renderer);
|
|
PP_EXPECT(h, fallback.value().compatibility_fallback);
|
|
}
|
|
|
|
const auto invalid = plan_canvas_stroke_rasterization(
|
|
RenderDeviceFeatures { .texture_copy = true },
|
|
Extent2D { .width = 0, .height = 16 });
|
|
PP_EXPECT(h, !invalid.ok());
|
|
PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument);
|
|
}
|
|
|
|
void canvas_stroke_sample_bounds_empty_vertices_have_no_pixels(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_canvas_stroke_sample_bounds(
|
|
CanvasStrokeSampleBoundsRequest {
|
|
.extent = Extent2D { .width = 32, .height = 16 },
|
|
.vertices = {},
|
|
});
|
|
|
|
PP_EXPECT(h, !plan.has_pixels);
|
|
PP_EXPECT(h, plan.copy_region.width == 0);
|
|
PP_EXPECT(h, plan.copy_region.height == 0);
|
|
}
|
|
|
|
void canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad(pp::tests::Harness& h)
|
|
{
|
|
const CanvasStrokePoint vertices[] {
|
|
{ .x = 10.0F, .y = 20.0F },
|
|
{ .x = 10.0F, .y = 30.0F },
|
|
{ .x = 30.0F, .y = 30.0F },
|
|
{ .x = 30.0F, .y = 20.0F },
|
|
};
|
|
|
|
const auto plan = plan_canvas_stroke_sample_bounds(
|
|
CanvasStrokeSampleBoundsRequest {
|
|
.extent = Extent2D { .width = 64, .height = 64 },
|
|
.vertices = vertices,
|
|
});
|
|
|
|
PP_EXPECT(h, plan.has_pixels);
|
|
PP_EXPECT(h, plan.copy_region.x == 9);
|
|
PP_EXPECT(h, plan.copy_region.y == 19);
|
|
PP_EXPECT(h, plan.copy_region.width == 22);
|
|
PP_EXPECT(h, plan.copy_region.height == 12);
|
|
PP_EXPECT(h, near(plan.dirty_bounds.min_x, 9.0F));
|
|
PP_EXPECT(h, near(plan.dirty_bounds.min_y, 19.0F));
|
|
PP_EXPECT(h, near(plan.dirty_bounds.max_x, 31.0F));
|
|
PP_EXPECT(h, near(plan.dirty_bounds.max_y, 31.0F));
|
|
}
|
|
|
|
void canvas_stroke_sample_bounds_clamp_out_of_range_vertices(pp::tests::Harness& h)
|
|
{
|
|
const CanvasStrokePoint vertices[] {
|
|
{ .x = -10.0F, .y = -5.0F },
|
|
{ .x = 70.0F, .y = 80.0F },
|
|
};
|
|
|
|
const auto plan = plan_canvas_stroke_sample_bounds(
|
|
CanvasStrokeSampleBoundsRequest {
|
|
.extent = Extent2D { .width = 64, .height = 32 },
|
|
.vertices = vertices,
|
|
});
|
|
|
|
PP_EXPECT(h, plan.has_pixels);
|
|
PP_EXPECT(h, plan.copy_region.x == 0);
|
|
PP_EXPECT(h, plan.copy_region.y == 0);
|
|
PP_EXPECT(h, plan.copy_region.width == 64);
|
|
PP_EXPECT(h, plan.copy_region.height == 32);
|
|
PP_EXPECT(h, near(plan.dirty_bounds.max_x, 64.0F));
|
|
PP_EXPECT(h, near(plan.dirty_bounds.max_y, 32.0F));
|
|
}
|
|
|
|
void canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_canvas_stroke_pad_region(
|
|
CanvasStrokePadRegionRequest {
|
|
.extent = Extent2D { .width = 100, .height = 80 },
|
|
.pass_dirty_box = CanvasStrokeBox {
|
|
.min_x = 5.0F,
|
|
.min_y = 10.0F,
|
|
.max_x = 20.0F,
|
|
.max_y = 30.0F,
|
|
},
|
|
});
|
|
|
|
PP_EXPECT(h, plan.has_pixels);
|
|
PP_EXPECT(h, plan.copy_region.x == 0);
|
|
PP_EXPECT(h, plan.copy_region.y == 0);
|
|
PP_EXPECT(h, plan.copy_region.width == 40);
|
|
PP_EXPECT(h, plan.copy_region.height == 50);
|
|
PP_EXPECT(h, near(plan.ndc_quad[0].x, -1.0F));
|
|
PP_EXPECT(h, near(plan.ndc_quad[0].y, -1.0F));
|
|
PP_EXPECT(h, near(plan.ndc_quad[2].x, -0.2F));
|
|
PP_EXPECT(h, near(plan.ndc_quad[2].y, 0.25F));
|
|
PP_EXPECT(h, near(plan.ndc_quad[5].x, -0.2F));
|
|
PP_EXPECT(h, near(plan.ndc_quad[5].y, -1.0F));
|
|
}
|
|
|
|
void canvas_stroke_face_dirty_update_includes_committed_dirty_box(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_canvas_stroke_face_dirty_update(
|
|
CanvasStrokeFaceDirtyUpdateRequest {
|
|
.extent = Extent2D { .width = 64, .height = 32 },
|
|
.previous_accumulated_dirty_box = CanvasStrokeBox {
|
|
.min_x = 64.0F,
|
|
.min_y = 32.0F,
|
|
.max_x = 0.0F,
|
|
.max_y = 0.0F,
|
|
},
|
|
.previous_pass_dirty_box = CanvasStrokeBox {
|
|
.min_x = 64.0F,
|
|
.min_y = 32.0F,
|
|
.max_x = 0.0F,
|
|
.max_y = 0.0F,
|
|
},
|
|
.sample_dirty_box = CanvasStrokeBox {
|
|
.min_x = -5.0F,
|
|
.min_y = -3.0F,
|
|
.max_x = 80.0F,
|
|
.max_y = 90.0F,
|
|
},
|
|
.include_in_committed_dirty_box = true,
|
|
});
|
|
|
|
PP_EXPECT(h, plan.has_dirty_pixels);
|
|
PP_EXPECT(h, plan.committed_dirty);
|
|
PP_EXPECT(h, plan.pass_dirty);
|
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_x, 0.0F));
|
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_y, 0.0F));
|
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_x, 64.0F));
|
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_y, 64.0F));
|
|
PP_EXPECT(h, near(plan.pass_dirty_box.min_x, -5.0F));
|
|
PP_EXPECT(h, near(plan.pass_dirty_box.max_y, 90.0F));
|
|
}
|
|
|
|
void canvas_stroke_face_dirty_update_can_skip_committed_dirty_box(pp::tests::Harness& h)
|
|
{
|
|
const auto plan = plan_canvas_stroke_face_dirty_update(
|
|
CanvasStrokeFaceDirtyUpdateRequest {
|
|
.extent = Extent2D { .width = 64, .height = 32 },
|
|
.previous_accumulated_dirty_box = CanvasStrokeBox {
|
|
.min_x = 1.0F,
|
|
.min_y = 2.0F,
|
|
.max_x = 3.0F,
|
|
.max_y = 4.0F,
|
|
},
|
|
.previous_pass_dirty_box = CanvasStrokeBox {
|
|
.min_x = 10.0F,
|
|
.min_y = 10.0F,
|
|
.max_x = 20.0F,
|
|
.max_y = 20.0F,
|
|
},
|
|
.sample_dirty_box = CanvasStrokeBox {
|
|
.min_x = 0.0F,
|
|
.min_y = 0.0F,
|
|
.max_x = 30.0F,
|
|
.max_y = 30.0F,
|
|
},
|
|
.include_in_committed_dirty_box = false,
|
|
});
|
|
|
|
PP_EXPECT(h, plan.has_dirty_pixels);
|
|
PP_EXPECT(h, !plan.committed_dirty);
|
|
PP_EXPECT(h, plan.pass_dirty);
|
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_x, 1.0F));
|
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.min_y, 2.0F));
|
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_x, 3.0F));
|
|
PP_EXPECT(h, near(plan.accumulated_dirty_box.max_y, 4.0F));
|
|
PP_EXPECT(h, near(plan.pass_dirty_box.min_x, 0.0F));
|
|
PP_EXPECT(h, near(plan.pass_dirty_box.min_y, 0.0F));
|
|
PP_EXPECT(h, near(plan.pass_dirty_box.max_x, 30.0F));
|
|
PP_EXPECT(h, near(plan.pass_dirty_box.max_y, 30.0F));
|
|
}
|
|
|
|
}
|
|
|
|
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(
|
|
"exports_document_frame_as_equirectangular_jpeg_with_xmp",
|
|
exports_document_frame_as_equirectangular_jpeg_with_xmp);
|
|
harness.run("exports_document_layers_as_equirectangular_pngs", exports_document_layers_as_equirectangular_pngs);
|
|
harness.run(
|
|
"exports_document_animation_frames_as_equirectangular_pngs",
|
|
exports_document_animation_frames_as_equirectangular_pngs);
|
|
harness.run("plans_document_depth_export_renderer_work", plans_document_depth_export_renderer_work);
|
|
harness.run("exports_document_depth_as_png_payloads", exports_document_depth_as_png_payloads);
|
|
harness.run(
|
|
"depth export payload boundary rejects malformed face bytes",
|
|
depth_export_payload_boundary_rejects_malformed_face_bytes);
|
|
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_stroke_material_passes", plans_canvas_stroke_material_passes);
|
|
harness.run("plans_canvas_stroke_dual_material_intent", plans_canvas_stroke_dual_material_intent);
|
|
harness.run("plans_canvas_stroke_commit_erase_sequence", plans_canvas_stroke_commit_erase_sequence);
|
|
harness.run("plans_canvas_stroke_commit_composite_sequence", plans_canvas_stroke_commit_composite_sequence);
|
|
harness.run("plans_stroke_preview_composite_for_simple_brush", plans_stroke_preview_composite_for_simple_brush);
|
|
harness.run("plans_stroke_preview_composite_with_mixer_input", plans_stroke_preview_composite_with_mixer_input);
|
|
harness.run("plans_stroke_preview_composite_with_dual_input", plans_stroke_preview_composite_with_dual_input);
|
|
harness.run("plans_stroke_preview_composite_with_pattern_input", plans_stroke_preview_composite_with_pattern_input);
|
|
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);
|
|
harness.run("plans_canvas_stroke_rasterization_boundary", plans_canvas_stroke_rasterization_boundary);
|
|
harness.run(
|
|
"canvas_stroke_sample_bounds_empty_vertices_have_no_pixels",
|
|
canvas_stroke_sample_bounds_empty_vertices_have_no_pixels);
|
|
harness.run(
|
|
"canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad",
|
|
canvas_stroke_sample_bounds_expand_rect_with_one_pixel_pad);
|
|
harness.run(
|
|
"canvas_stroke_sample_bounds_clamp_out_of_range_vertices",
|
|
canvas_stroke_sample_bounds_clamp_out_of_range_vertices);
|
|
harness.run(
|
|
"canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad",
|
|
canvas_stroke_pad_region_clamps_edges_with_twenty_pixel_pad);
|
|
harness.run(
|
|
"canvas_stroke_face_dirty_update_includes_committed_dirty_box",
|
|
canvas_stroke_face_dirty_update_includes_committed_dirty_box);
|
|
harness.run(
|
|
"canvas_stroke_face_dirty_update_can_skip_committed_dirty_box",
|
|
canvas_stroke_face_dirty_update_can_skip_committed_dirty_box);
|
|
return harness.finish();
|
|
}
|