#include "paint_renderer/compositor.h" #include "test_harness.h" #include #include #include using pp::foundation::StatusCode; using pp::paint::BlendMode; using pp::paint::Rgba; using pp::paint::StrokeBlendMode; using pp::paint_renderer::LayerCompositeView; using pp::paint_renderer::CanvasBlendGateRequest; using pp::paint_renderer::StrokeCompositePath; using pp::paint_renderer::StrokeCompositeRequest; using pp::paint_renderer::composite_layer; using pp::paint_renderer::plan_canvas_blend_gate; 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::RenderDeviceFeatures; using pp::renderer::TextureFormat; using pp::renderer::TextureUsage; namespace { bool near(float a, float b) { return std::fabs(a - b) < 0.0001F; } void composites_visible_layer_with_opacity(pp::tests::Harness& h) { std::vector 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 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_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().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); } } } 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("detects_feedback_requirements", detects_feedback_requirements); harness.run("plans_stroke_composite_paths", plans_stroke_composite_paths); harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans); harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices); harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks); return harness.finish(); }