#include "assets/image_pixels.h" #include "legacy_canvas_stroke_commit_services.h" #include "legacy_node_stroke_preview_execution_services.h" #include "paint_renderer/compositor.h" #include "renderer_api/recording_renderer.h" #include "test_harness.h" #include #include #include #include #include #include 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 near(const glm::vec2& a, const glm::vec2& b) { return near(a.x, b.x) && near(a.y, b.y); } 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 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 pixels( static_cast(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 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 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 destination { Rgba { .r = 0.2F, .g = 0.4F, .b = 0.6F, .a = 0.5F }, }; const std::vector 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 destination { original }; const std::vector 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 destination(2); const std::vector 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(base_frames, 1), }, { .name = "Paint", .opacity = 0.5F, .frames = std::span(paint_frames, 1), }, { .name = "Hidden", .visible = false, .frames = std::span(hidden_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 2, .height = 1, .layers = std::span(layers, 3), .frames = std::span(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(short_layer_frames, 1), }, { .name = "Animated", .frames = std::span(animated_layer_frames, 2), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 1, .layers = std::span(layers, 2), .frames = std::span(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(layers, 1), .frames = std::span(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(base_frames, 1), }, { .name = "Paint", .opacity = 0.5F, .frames = std::span(paint_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 1, .layers = std::span(layers, 2), .frames = std::span(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(layers, 1), .frames = std::span(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(layer_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 1, .layers = std::span(layers, 1), .frames = std::span(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(layer_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 1, .layers = std::span(layers, 1), .frames = std::span(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(layer_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 1, .layers = std::span(layers, 1), .frames = std::span(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(layer_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 1, .layers = std::span(layers, 1), .frames = std::span(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(layer_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 4, .layers = std::span(layers, 1), .frames = std::span(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(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(layer_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 4, .layers = std::span(layers, 1), .frames = std::span(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(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(base_frames, 1), }, { .name = "HiddenPaint", .visible = false, .opacity = 0.0F, .frames = std::span(hidden_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 4, .layers = std::span(layers, 2), .frames = std::span(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(layer_frames, 2), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 4, .layers = std::span(layers, 1), .frames = std::span(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 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(visible_layer_frames, 1), }, { .name = "Hidden", .visible = false, .frames = std::span(hidden_layer_frames, 1), }, { .name = "Transparent", .opacity = 0.0F, .frames = std::span(hidden_layer_frames, 1), }, { .name = "EmptyVisible", .frames = std::span(empty_layer_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 1, .layers = std::span(layers, 4), .frames = std::span(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(base_frames, 1), }, { .name = "Top", .frames = std::span(top_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 1, .layers = std::span(layers, 2), .frames = std::span(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 expected_image { 0, 255, 0, 255 }; const std::vector 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(bad_frames, 1), }, }; const auto document = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { .width = 1, .height = 1, .layers = std::span(layers, 1), .frames = std::span(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(layers, 1), .frames = std::span(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(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(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(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 retained_stroke_commit_runner_clamps_malformed_step_count(pp::tests::Harness& h) { pp::paint_renderer::CanvasStrokeCommitSequencePlan sequence; sequence.step_count = 99U; sequence.steps[0] = CanvasStrokeCommitStep::bind_commit_inputs; sequence.steps[1] = CanvasStrokeCommitStep::composite_draw; int bind_inputs = 0; int paint_draws = 0; int erase_draws = 0; int started = 0; int restored = 0; int published = 0; int timelapse = 0; const auto result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence( pp::panopainter::LegacyCanvasStrokeCommitRequest { .context = "test", .faces = { pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = true }, pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = false }, pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = true }, }, .sequence = sequence, .callbacks = { .mark_commit_started = [&]() { ++started; }, .capture_render_state = []() {}, .prepare_render_state = []() {}, .restore_render_state = [&]() { ++restored; }, .publish_history = [&]() { ++published; }, .capture_timelapse_frame = [&]() { ++timelapse; }, .bind_layer_framebuffer = [](int) {}, .capture_history_region = [](int) {}, .apply_layer_dirty_region = [](int) {}, .copy_layer_to_commit_destination = [](int) {}, .bind_commit_inputs = [&](int) { ++bind_inputs; }, .execute_erase_composite = [&](int) { ++erase_draws; }, .execute_paint_composite = [&](int) { ++paint_draws; }, .copy_committed_to_dilate_source = [](int) {}, .execute_commit_dilate = [](int) {}, .unbind_layer_framebuffer = [](int) {}, }, }); PP_EXPECT(h, result.ok); PP_EXPECT(h, result.committed_faces == 2); PP_EXPECT(h, bind_inputs == 2); PP_EXPECT(h, paint_draws == 2); PP_EXPECT(h, erase_draws == 0); PP_EXPECT(h, started == 1); PP_EXPECT(h, restored == 1); PP_EXPECT(h, published == 1); PP_EXPECT(h, timelapse == 1); } void retained_stroke_commit_input_binder_uses_sequence_slots(pp::tests::Harness& h) { const auto sequence = plan_canvas_stroke_commit_sequence( CanvasStrokeCommitRequest { .erase_mode = false, .alpha_locked = false, .selection_mask_active = true, .dual_stroke_enabled = true, .pattern_enabled = true, }); std::vector active_slots; std::vector bound_textures; std::vector> bound_samplers; pp::panopainter::bind_legacy_canvas_stroke_commit_inputs( sequence, [&](int texture_slot) { active_slots.push_back(texture_slot); }, [&](CanvasStrokeCommitTextureRole role) { bound_textures.push_back(role); }, [&](CanvasStrokeCommitTextureRole role, int texture_slot) { bound_samplers.emplace_back(role, texture_slot); }); PP_EXPECT(h, active_slots.size() == 5U); PP_EXPECT(h, active_slots[0] == 0); PP_EXPECT(h, active_slots[1] == 1); PP_EXPECT(h, active_slots[2] == 2); PP_EXPECT(h, active_slots[3] == 3); PP_EXPECT(h, active_slots[4] == 4); PP_EXPECT(h, bound_textures.size() == 5U); PP_EXPECT(h, bound_textures[0] == CanvasStrokeCommitTextureRole::layer_scratch); PP_EXPECT(h, bound_textures[1] == CanvasStrokeCommitTextureRole::stroke); PP_EXPECT(h, bound_textures[2] == CanvasStrokeCommitTextureRole::selection_mask); PP_EXPECT(h, bound_textures[3] == CanvasStrokeCommitTextureRole::dual_stroke); PP_EXPECT(h, bound_textures[4] == CanvasStrokeCommitTextureRole::pattern); PP_EXPECT(h, bound_samplers.size() == 5U); PP_EXPECT(h, bound_samplers[0].first == CanvasStrokeCommitTextureRole::layer_scratch); PP_EXPECT(h, bound_samplers[0].second == 0); PP_EXPECT(h, bound_samplers[4].first == CanvasStrokeCommitTextureRole::pattern); PP_EXPECT(h, bound_samplers[4].second == 4); } void retained_stroke_commit_dilate_copy_uses_layer_scratch_slot(pp::tests::Harness& h) { const auto sequence = plan_canvas_stroke_commit_sequence( CanvasStrokeCommitRequest { .erase_mode = true, .alpha_locked = true, .selection_mask_active = false, .dual_stroke_enabled = false, .pattern_enabled = false, }); int setup_calls = 0; std::vector active_slots; int bind_layer_scratch_calls = 0; std::vector copy_regions; pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source( sequence, [&]() { ++setup_calls; }, [&](int texture_slot) { active_slots.push_back(texture_slot); }, [&]() { ++bind_layer_scratch_calls; }, [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) { copy_regions.push_back(pp::paint_renderer::CanvasStrokeCopyRegion { .x = src_x, .y = src_y, .width = width, .height = height, }); PP_EXPECT(h, dst_x == 0); PP_EXPECT(h, dst_y == 0); }, pp::panopainter::LegacyCanvasStrokeCommitCopyExtent { .width = 256, .height = 128, }); PP_EXPECT(h, setup_calls == 1); PP_EXPECT(h, active_slots.size() == 1U); PP_EXPECT(h, active_slots[0] == 0); PP_EXPECT(h, bind_layer_scratch_calls == 1); PP_EXPECT(h, copy_regions.size() == 1U); PP_EXPECT(h, copy_regions[0].x == 0); PP_EXPECT(h, copy_regions[0].y == 0); PP_EXPECT(h, copy_regions[0].width == 256); PP_EXPECT(h, copy_regions[0].height == 128); } void retained_stroke_commit_runner_preserves_per_face_step_order(pp::tests::Harness& h) { const auto sequence = plan_canvas_stroke_commit_sequence( CanvasStrokeCommitRequest { .erase_mode = false, .alpha_locked = false, .selection_mask_active = true, .dual_stroke_enabled = true, .pattern_enabled = true, }); std::vector events; const auto record = [&](std::string_view event, int face_index = -1) { if (face_index >= 0) { events.push_back(std::string(event) + ":" + std::to_string(face_index)); } else { events.push_back(std::string(event)); } }; const auto result = pp::panopainter::execute_legacy_canvas_stroke_commit_sequence( pp::panopainter::LegacyCanvasStrokeCommitRequest { .context = "test", .faces = { pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 0, .dirty = true }, pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 1, .dirty = false }, pp::panopainter::LegacyCanvasStrokeCommitFace { .index = 2, .dirty = true }, }, .sequence = sequence, .callbacks = { .mark_commit_started = [&]() { record("start"); }, .capture_render_state = [&]() { record("capture"); }, .prepare_render_state = [&]() { record("prepare"); }, .restore_render_state = [&]() { record("restore"); }, .publish_history = [&]() { record("publish"); }, .capture_timelapse_frame = [&]() { record("timelapse"); }, .bind_layer_framebuffer = [&](int face_index) { record("bind-fbo", face_index); }, .capture_history_region = [&](int face_index) { record("history", face_index); }, .apply_layer_dirty_region = [&](int face_index) { record("dirty", face_index); }, .copy_layer_to_commit_destination = [&](int face_index) { record("copy-layer", face_index); }, .bind_commit_inputs = [&](int face_index) { record("bind-inputs", face_index); }, .execute_erase_composite = [&](int face_index) { record("erase", face_index); }, .execute_paint_composite = [&](int face_index) { record("paint", face_index); }, .copy_committed_to_dilate_source = [&](int face_index) { record("copy-committed", face_index); }, .execute_commit_dilate = [&](int face_index) { record("dilate", face_index); }, .unbind_layer_framebuffer = [&](int face_index) { record("unbind-fbo", face_index); }, }, }); const std::vector expected { "start", "capture", "prepare", "bind-fbo:0", "history:0", "dirty:0", "copy-layer:0", "bind-inputs:0", "paint:0", "copy-committed:0", "dilate:0", "unbind-fbo:0", "bind-fbo:2", "history:2", "dirty:2", "copy-layer:2", "bind-inputs:2", "paint:2", "copy-committed:2", "dilate:2", "unbind-fbo:2", "restore", "publish", "timelapse", }; PP_EXPECT(h, result.ok); PP_EXPECT(h, result.committed_faces == 2); PP_EXPECT(h, events == expected); } void retained_stroke_commit_copy_skips_missing_layer_scratch_or_invalid_extent(pp::tests::Harness& h) { pp::paint_renderer::CanvasStrokeCommitSequencePlan missing_scratch; missing_scratch.texture_binding_count = 1U; missing_scratch.texture_bindings[0] = pp::paint_renderer::CanvasStrokeCommitTextureBindingPlan { .role = CanvasStrokeCommitTextureRole::stroke, .slot = 4, }; int setup_calls = 0; int active_texture_calls = 0; int bind_layer_scratch_calls = 0; int copy_calls = 0; pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source( missing_scratch, [&]() { ++setup_calls; }, [&](int) { ++active_texture_calls; }, [&]() { ++bind_layer_scratch_calls; }, [&](int, int, int, int, int, int) { ++copy_calls; }, pp::panopainter::LegacyCanvasStrokeCommitCopyExtent { .width = 256, .height = 128, }); const auto valid_sequence = plan_canvas_stroke_commit_sequence( CanvasStrokeCommitRequest { .erase_mode = true, .alpha_locked = true, .selection_mask_active = false, .dual_stroke_enabled = false, .pattern_enabled = false, }); pp::panopainter::copy_legacy_canvas_stroke_commit_to_dilate_source( valid_sequence, [&]() { ++setup_calls; }, [&](int) { ++active_texture_calls; }, [&]() { ++bind_layer_scratch_calls; }, [&](int, int, int, int, int, int) { ++copy_calls; }, pp::panopainter::LegacyCanvasStrokeCommitCopyExtent { .width = 0, .height = 128, }); PP_EXPECT(h, setup_calls == 0); PP_EXPECT(h, active_texture_calls == 0); PP_EXPECT(h, bind_layer_scratch_calls == 0); PP_EXPECT(h, copy_calls == 0); } 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_stroke_preview_composite_with_all_retained_inputs(pp::tests::Harness& h) { const auto plan = plan_stroke_preview_composite( StrokePreviewCompositeRequest { .uses_mixer = true, .uses_dual = true, .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 == 5U); 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)); PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4)); PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::mixer, 3)); } void legacy_node_stroke_preview_feedback_adapter_clamps_invalid_extent(pp::tests::Harness& h) { const auto fetch = pp::panopainter::plan_legacy_node_stroke_preview_feedback( RenderDeviceFeatures { .framebuffer_fetch = true }, 32, 16); PP_EXPECT(h, fetch.path == StrokeCompositePath::framebuffer_fetch); PP_EXPECT(h, fetch.reads_destination_color); PP_EXPECT(h, !fetch.requires_auxiliary_texture); PP_EXPECT(h, !fetch.compatibility_fallback); const auto fallback = pp::panopainter::plan_legacy_node_stroke_preview_feedback( RenderDeviceFeatures { .texture_copy = true }, -32, 16); PP_EXPECT(h, fallback.path == StrokeCompositePath::ping_pong_textures); PP_EXPECT(h, !fallback.reads_destination_color); PP_EXPECT(h, fallback.requires_auxiliary_texture); PP_EXPECT(h, !fallback.requires_texture_copy); PP_EXPECT(h, !fallback.requires_render_target_blit); PP_EXPECT(h, fallback.compatibility_fallback); } void legacy_node_stroke_preview_composite_adapter_preserves_retained_inputs(pp::tests::Harness& h) { const auto plan = pp::panopainter::plan_legacy_node_stroke_preview_composite( true, true, 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 == 5U); 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)); PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::pattern, 4)); PP_EXPECT(h, has_preview_texture_slot(plan, StrokePreviewTextureRole::mixer, 3)); } void legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and_uniforms(pp::tests::Harness& h) { const pp::panopainter::LegacyNodeStrokePreviewMixPassRequest request { .resolution = glm::vec2(128.0F, 64.0F), .pattern_scale = 0.25F, .pattern_flipx = true, .pattern_flipy = true, .pattern_invert = true, .pattern_brightness = 0.6F, .pattern_contrast = 0.8F, .pattern_depth = 0.9F, .pattern_rand_offset = true, .pattern_enabled = true, .pattern_eachsample = false, .tip_wet = 0.3F, .tip_mix = 0.7F, .tip_noise = 0.2F, .dual_enabled = true, .dual_blend_mode = 9, .pattern_blend_mode = 7, .dual_opacity = 0.4F, .blend_mode = 5, }; const auto plan = pp::panopainter::plan_legacy_node_stroke_preview_mix_pass(request); PP_EXPECT(h, plan.material.dual_pass.enabled); PP_EXPECT(h, !plan.material.stroke_pass.uses_pattern); PP_EXPECT(h, plan.material.composite_pass.use_dual); PP_EXPECT(h, plan.material.composite_pass.use_pattern); PP_EXPECT(h, plan.material.composite_pass.dual_blend_mode == request.dual_blend_mode); PP_EXPECT(h, near(plan.material.composite_pass.dual_alpha, request.dual_opacity)); PP_EXPECT(h, plan.material.composite_pass.pattern_blend_mode == request.pattern_blend_mode); PP_EXPECT(h, near(plan.shader.resolution, request.resolution)); PP_EXPECT(h, near(plan.shader.pattern_scale, glm::vec2(-0.25F, -0.25F))); PP_EXPECT(h, near(plan.shader.pattern_invert, 1.0F)); PP_EXPECT(h, near(plan.shader.pattern_brightness, request.pattern_brightness)); PP_EXPECT(h, near(plan.shader.pattern_contrast, request.pattern_contrast)); PP_EXPECT(h, near(plan.shader.pattern_depth, request.pattern_depth)); PP_EXPECT(h, plan.shader.pattern_blend_mode == request.pattern_blend_mode); PP_EXPECT(h, near(plan.shader.pattern_offset, glm::vec2(0.5F, 0.5F))); PP_EXPECT(h, plan.shader.blend_mode == request.blend_mode); PP_EXPECT(h, plan.shader.use_dual == plan.material.composite_pass.use_dual); PP_EXPECT(h, plan.shader.dual_blend_mode == plan.material.composite_pass.dual_blend_mode); PP_EXPECT(h, near(plan.shader.dual_alpha, plan.material.composite_pass.dual_alpha)); PP_EXPECT(h, plan.shader.use_pattern == plan.material.composite_pass.use_pattern); } void legacy_node_stroke_preview_mix_executor_preserves_setup_and_draw_order(pp::tests::Harness& h) { std::vector steps; pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan observed_shader {}; const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass( pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest { .shader = pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan { .resolution = glm::vec2(128.0F, 64.0F), .pattern_scale = glm::vec2(-0.25F, 0.25F), .pattern_invert = 1.0F, .pattern_brightness = 0.6F, .pattern_contrast = 0.8F, .pattern_depth = 0.9F, .pattern_blend_mode = 7, .pattern_offset = glm::vec2(0.5F, 0.5F), .blend_mode = 5, .use_dual = true, .dual_blend_mode = 9, .dual_alpha = 0.4F, .use_pattern = true, }, .mixer_width = 128, .mixer_height = 64, .scissor_x = 11, .scissor_y = 12, .scissor_width = 13, .scissor_height = 14, .save_state = [&] { steps.emplace_back("save"); }, .setup_mix_shader = [&](const auto& shader) { observed_shader = shader; steps.emplace_back("setup"); }, .bind_mixer_framebuffer = [&] { steps.emplace_back("bind-framebuffer"); }, .configure_mix_target_state = [&](int width, int height, int x, int y, int scissor_width, int scissor_height) { steps.emplace_back( "configure:" + std::to_string(width) + "," + std::to_string(height) + "," + std::to_string(x) + "," + std::to_string(y) + "," + std::to_string(scissor_width) + "," + std::to_string(scissor_height)); }, .bind_mix_inputs = [&] { steps.emplace_back("bind-inputs"); }, .draw_mix = [&] { steps.emplace_back("draw"); }, .unbind_mixer_framebuffer = [&] { steps.emplace_back("unbind-framebuffer"); }, .restore_state = [&] { steps.emplace_back("restore"); }, }); PP_EXPECT(h, ok); PP_EXPECT(h, near(observed_shader.resolution, glm::vec2(128.0F, 64.0F))); PP_EXPECT(h, near(observed_shader.pattern_scale, glm::vec2(-0.25F, 0.25F))); PP_EXPECT(h, observed_shader.use_dual); PP_EXPECT(h, observed_shader.use_pattern); PP_EXPECT(h, observed_shader.dual_blend_mode == 9); PP_EXPECT(h, near(observed_shader.dual_alpha, 0.4F)); const std::vector expected_steps { "save", "setup", "bind-framebuffer", "configure:128,64,11,12,13,14", "bind-inputs", "draw", "unbind-framebuffer", "restore", }; PP_EXPECT(h, steps == expected_steps); const bool invalid = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass( pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest { .mixer_width = 128, .mixer_height = 64, }); PP_EXPECT(h, !invalid); } void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order(pp::tests::Harness& h) { std::vector steps; const auto run_sequence = [&](bool dual_enabled) { steps.clear(); const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest { .dual_pass_enabled = dual_enabled, .prepare_dual_pass = [&] { steps.emplace_back("prepare_dual"); }, .execute_dual_pass = [&] { steps.emplace_back("execute_dual"); }, .capture_background = [&] { steps.emplace_back("capture_background"); }, .prepare_main_pass = [&] { steps.emplace_back("prepare_main"); }, .execute_main_pass = [&] { steps.emplace_back("execute_main"); }, .finish_main_pass = [&] { steps.emplace_back("finish_main"); }, .execute_final_composite = [&] { steps.emplace_back("execute_composite"); }, .copy_preview_result = [&] { steps.emplace_back("copy_preview"); }, }); PP_EXPECT(h, ok); }; run_sequence(true); const std::vector dual_steps { "prepare_dual", "execute_dual", "capture_background", "prepare_main", "execute_main", "finish_main", "execute_composite", "copy_preview", }; PP_EXPECT(h, steps == dual_steps); run_sequence(false); const std::vector single_steps { "capture_background", "prepare_main", "execute_main", "finish_main", "execute_composite", "copy_preview", }; PP_EXPECT(h, steps == single_steps); steps.clear(); const bool missing_dual_prepare = pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest { .dual_pass_enabled = true, .prepare_dual_pass = {}, .execute_dual_pass = [&] { steps.emplace_back("execute_dual"); }, .capture_background = [&] { steps.emplace_back("capture_background"); }, .prepare_main_pass = [&] { steps.emplace_back("prepare_main"); }, .execute_main_pass = [&] { steps.emplace_back("execute_main"); }, .finish_main_pass = [&] { steps.emplace_back("finish_main"); }, .execute_final_composite = [&] { steps.emplace_back("execute_composite"); }, .copy_preview_result = [&] { steps.emplace_back("copy_preview"); }, }); PP_EXPECT(h, !missing_dual_prepare); PP_EXPECT(h, steps.empty()); const bool missing_required = pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence( pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {}); PP_EXPECT(h, !missing_required); } void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h) { const std::vector 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 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 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 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( "retained_stroke_commit_runner_clamps_malformed_step_count", retained_stroke_commit_runner_clamps_malformed_step_count); harness.run( "retained_stroke_commit_input_binder_uses_sequence_slots", retained_stroke_commit_input_binder_uses_sequence_slots); harness.run( "retained_stroke_commit_dilate_copy_uses_layer_scratch_slot", retained_stroke_commit_dilate_copy_uses_layer_scratch_slot); harness.run( "retained_stroke_commit_runner_preserves_per_face_step_order", retained_stroke_commit_runner_preserves_per_face_step_order); harness.run( "retained_stroke_commit_copy_skips_missing_layer_scratch_or_invalid_extent", retained_stroke_commit_copy_skips_missing_layer_scratch_or_invalid_extent); 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_stroke_preview_composite_with_all_retained_inputs", plans_stroke_preview_composite_with_all_retained_inputs); harness.run( "legacy_node_stroke_preview_feedback_adapter_clamps_invalid_extent", legacy_node_stroke_preview_feedback_adapter_clamps_invalid_extent); harness.run( "legacy_node_stroke_preview_composite_adapter_preserves_retained_inputs", legacy_node_stroke_preview_composite_adapter_preserves_retained_inputs); harness.run( "legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and_uniforms", legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and_uniforms); harness.run( "legacy_node_stroke_preview_mix_executor_preserves_setup_and_draw_order", legacy_node_stroke_preview_mix_executor_preserves_setup_and_draw_order); harness.run( "legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order", legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order); 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(); }