#include "renderer_api/renderer_api.h" #include "renderer_api/recording_renderer.h" #include "renderer_api/shader_catalog.h" #include "test_harness.h" #include #include #include using pp::foundation::StatusCode; using pp::renderer::BlitFilter; using pp::renderer::blit_filter_name; using pp::renderer::BlendFactor; using pp::renderer::blend_factor_name; using pp::renderer::BlendOp; using pp::renderer::blend_op_name; using pp::renderer::BlendState; using pp::renderer::ClearColor; using pp::renderer::CompareOp; using pp::renderer::compare_op_name; using pp::renderer::DepthState; using pp::renderer::Extent2D; using pp::renderer::frame_capture_byte_size; using pp::renderer::ICommandContext; using pp::renderer::IMesh; using pp::renderer::IRenderDevice; using pp::renderer::IRenderTarget; using pp::renderer::IRenderTrace; using pp::renderer::IShaderProgram; using pp::renderer::MeshDesc; using pp::renderer::PrimitiveTopology; using pp::renderer::ReadbackRegion; using pp::renderer::RecordedRenderCommandKind; using pp::renderer::RecordingMesh; using pp::renderer::RecordingReadbackBuffer; using pp::renderer::RecordingRenderDevice; using pp::renderer::RecordingRenderTarget; using pp::renderer::RecordingShaderProgram; using pp::renderer::RecordingTexture2D; using pp::renderer::ScissorRect; using pp::renderer::ShaderProgramDesc; using pp::renderer::ShaderStageSource; using pp::renderer::TextureDesc; using pp::renderer::TextureFormat; using pp::renderer::Viewport; using pp::renderer::max_shader_source_bytes; using pp::renderer::max_texture_dimension; using pp::renderer::max_texture_slots; using pp::renderer::panopainter_shader_catalog; using pp::renderer::primitive_topology_name; using pp::renderer::readback_byte_size; using pp::renderer::recorded_render_command_kind_name; using pp::renderer::ShaderCatalogEntry; using pp::renderer::texture_byte_size; using pp::renderer::texture_format_name; using pp::renderer::validate_extent; using pp::renderer::validate_blit_descs; using pp::renderer::validate_blit_filter; using pp::renderer::validate_blend_factor; using pp::renderer::validate_blend_op; using pp::renderer::validate_blend_state; using pp::renderer::validate_compare_op; using pp::renderer::validate_depth_state; using pp::renderer::validate_mesh_desc; using pp::renderer::validate_readback_region; using pp::renderer::validate_scissor; using pp::renderer::validate_shader_catalog; using pp::renderer::validate_shader_program_desc; using pp::renderer::validate_texture_slot; using pp::renderer::validate_viewport; namespace { class FakeRenderTarget final : public IRenderTarget { public: [[nodiscard]] TextureDesc color_desc() const noexcept override { return TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .render_target = true, }; } }; class FakeShaderProgram final : public IShaderProgram { public: [[nodiscard]] const char* debug_name() const noexcept override { return "fake-shader"; } }; class FakeMesh final : public IMesh { public: [[nodiscard]] MeshDesc desc() const noexcept override { return MeshDesc { .vertex_count = 3, .index_count = 0, .topology = PrimitiveTopology::triangles }; } }; class FakeTexture final : public pp::renderer::ITexture2D { public: [[nodiscard]] TextureDesc desc() const noexcept override { return TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .render_target = true, }; } }; class FakeReadbackBuffer final : public pp::renderer::IReadbackBuffer { public: explicit FakeReadbackBuffer(std::uint64_t size) noexcept : size_(size) { } [[nodiscard]] std::uint64_t size_bytes() const noexcept override { return size_; } private: std::uint64_t size_ = 0; }; class FakeTrace final : public IRenderTrace { public: void marker(const char* component, const char* name) noexcept override { last_component = component; last_name = name; } const char* last_component = nullptr; const char* last_name = nullptr; }; class FakeCommandContext final : public ICommandContext { public: [[nodiscard]] pp::foundation::Status begin_render_pass( IRenderTarget& target, ClearColor) noexcept override { in_render_pass = true; return validate_extent(target.color_desc().extent); } [[nodiscard]] pp::foundation::Status set_viewport(Viewport viewport) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } return validate_viewport(viewport, Extent2D { .width = 64, .height = 32 }); } [[nodiscard]] pp::foundation::Status set_scissor(ScissorRect scissor) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_scissor(scissor, Extent2D { .width = 64, .height = 32 }); if (!status.ok()) { return status; } last_scissor = scissor; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept override { shader_name = shader.debug_name(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status set_blend_state(BlendState state) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_blend_state(state); if (!status.ok()) { return status; } last_blend_state = state; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status set_depth_state(DepthState state) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_depth_state(state); if (!status.ok()) { return status; } last_depth_state = state; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status bind_texture( std::uint32_t slot, pp::renderer::ITexture2D& texture) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto slot_status = validate_texture_slot(slot); if (!slot_status.ok()) { return slot_status; } const auto bytes = texture_byte_size(texture.desc()); if (!bytes) { return bytes.status(); } last_texture_slot = slot; last_texture_bytes = bytes.value(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status bind_mesh(IMesh& mesh) noexcept override { return validate_mesh_desc(mesh.desc()); } [[nodiscard]] pp::foundation::Status draw() noexcept override { return in_render_pass ? pp::foundation::Status::success() : pp::foundation::Status::invalid_argument("render pass has not begun"); } [[nodiscard]] pp::foundation::Status read_texture( pp::renderer::ITexture2D& texture, ReadbackRegion region, pp::renderer::IReadbackBuffer& destination) noexcept override { const auto bytes = readback_byte_size(texture.desc(), region); if (!bytes) { return bytes.status(); } if (destination.size_bytes() < bytes.value()) { return pp::foundation::Status::out_of_range("readback buffer is too small"); } last_readback_bytes = bytes.value(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status upload_texture( pp::renderer::ITexture2D& texture, ReadbackRegion region, std::span rgba_or_channel_bytes) noexcept override { const auto bytes = readback_byte_size(texture.desc(), region); if (!bytes) { return bytes.status(); } if (rgba_or_channel_bytes.size() != bytes.value()) { return pp::foundation::Status::invalid_argument("texture upload byte size does not match the region"); } last_upload_bytes = bytes.value(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status capture_frame( IRenderTarget& target, pp::renderer::IReadbackBuffer& destination) noexcept override { const auto bytes = frame_capture_byte_size(target.color_desc()); if (!bytes) { return bytes.status(); } if (destination.size_bytes() < bytes.value()) { return pp::foundation::Status::out_of_range("frame capture buffer is too small"); } last_capture_bytes = bytes.value(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status blit_render_target( IRenderTarget& source, ReadbackRegion source_region, IRenderTarget& destination, ReadbackRegion destination_region, BlitFilter filter) noexcept override { const auto source_desc = source.color_desc(); const auto destination_desc = destination.color_desc(); const auto desc_status = validate_blit_descs(source_desc, destination_desc); if (!desc_status.ok()) { return desc_status; } const auto filter_status = validate_blit_filter(filter); if (!filter_status.ok()) { return filter_status; } const auto source_bytes = readback_byte_size(source_desc, source_region); if (!source_bytes) { return source_bytes.status(); } const auto destination_bytes = readback_byte_size(destination_desc, destination_region); if (!destination_bytes) { return destination_bytes.status(); } last_blit_source_bytes = source_bytes.value(); last_blit_destination_bytes = destination_bytes.value(); last_blit_filter = filter; return pp::foundation::Status::success(); } void end_render_pass() noexcept override { in_render_pass = false; } bool in_render_pass = false; const char* shader_name = nullptr; ScissorRect last_scissor {}; BlendState last_blend_state {}; DepthState last_depth_state {}; std::uint32_t last_texture_slot = 0; std::uint64_t last_texture_bytes = 0; std::uint64_t last_upload_bytes = 0; std::uint64_t last_readback_bytes = 0; std::uint64_t last_capture_bytes = 0; std::uint64_t last_blit_source_bytes = 0; std::uint64_t last_blit_destination_bytes = 0; BlitFilter last_blit_filter = BlitFilter::nearest; }; class FakeRenderDevice final : public IRenderDevice { public: [[nodiscard]] const char* backend_name() const noexcept override { return "fake"; } [[nodiscard]] ICommandContext& immediate_context() noexcept override { return context; } [[nodiscard]] IRenderTrace* trace() noexcept override { return &trace_recorder; } FakeCommandContext context; FakeTrace trace_recorder; }; void computes_texture_sizes(pp::tests::Harness& h) { const auto rgba = texture_byte_size(TextureDesc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, }); const auto r8 = texture_byte_size(TextureDesc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::r8, }); PP_EXPECT(h, rgba.ok()); PP_EXPECT(h, rgba.value() == 512U); PP_EXPECT(h, r8.ok()); PP_EXPECT(h, r8.value() == 128U); PP_EXPECT(h, texture_format_name(TextureFormat::depth24_stencil8) == std::string_view("depth24_stencil8")); } void rejects_invalid_or_excessive_extents(pp::tests::Harness& h) { const auto zero = validate_extent(Extent2D { .width = 0, .height = 1 }); const auto huge = validate_extent(Extent2D { .width = max_texture_dimension + 1U, .height = 1 }); const auto excessive_bytes = texture_byte_size(TextureDesc { .extent = Extent2D { .width = max_texture_dimension, .height = max_texture_dimension }, .format = TextureFormat::rgba8, }); PP_EXPECT(h, !zero.ok()); PP_EXPECT(h, zero.code == StatusCode::invalid_argument); PP_EXPECT(h, !huge.ok()); PP_EXPECT(h, huge.code == StatusCode::out_of_range); PP_EXPECT(h, !excessive_bytes.ok()); PP_EXPECT(h, excessive_bytes.status().code == StatusCode::out_of_range); } void validates_readback_bounds(pp::tests::Harness& h) { const TextureDesc desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .render_target = true, }; PP_EXPECT(h, validate_readback_region(desc, ReadbackRegion { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, validate_readback_region(desc, ReadbackRegion { .x = 63, .y = 31, .width = 1, .height = 1 }).ok()); const auto empty = validate_readback_region(desc, ReadbackRegion { .x = 0, .y = 0, .width = 0, .height = 1 }); const auto origin_outside = validate_readback_region(desc, ReadbackRegion { .x = 65, .y = 0, .width = 1, .height = 1 }); const auto overrun = validate_readback_region(desc, ReadbackRegion { .x = 63, .y = 31, .width = 2, .height = 1 }); PP_EXPECT(h, !empty.ok()); PP_EXPECT(h, empty.code == StatusCode::invalid_argument); PP_EXPECT(h, !origin_outside.ok()); PP_EXPECT(h, origin_outside.code == StatusCode::out_of_range); PP_EXPECT(h, !overrun.ok()); PP_EXPECT(h, overrun.code == StatusCode::out_of_range); } void computes_readback_byte_sizes(pp::tests::Harness& h) { const TextureDesc rgba_desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .render_target = true, }; const TextureDesc r8_desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::r8, .render_target = true, }; const auto rgba = readback_byte_size(rgba_desc, ReadbackRegion { .x = 4, .y = 2, .width = 8, .height = 3 }); const auto r8 = readback_byte_size(r8_desc, ReadbackRegion { .x = 0, .y = 0, .width = 5, .height = 7 }); const auto overrun = readback_byte_size(rgba_desc, ReadbackRegion { .x = 63, .y = 0, .width = 2, .height = 1 }); PP_EXPECT(h, rgba.ok()); PP_EXPECT(h, rgba.value() == 96U); PP_EXPECT(h, r8.ok()); PP_EXPECT(h, r8.value() == 35U); PP_EXPECT(h, !overrun.ok()); PP_EXPECT(h, overrun.status().code == StatusCode::out_of_range); } void computes_frame_capture_byte_sizes(pp::tests::Harness& h) { const TextureDesc target_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .render_target = true, }; const TextureDesc texture_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .render_target = false, }; const auto capture = frame_capture_byte_size(target_desc); const auto non_target = frame_capture_byte_size(texture_desc); PP_EXPECT(h, capture.ok()); PP_EXPECT(h, capture.value() == 512U); PP_EXPECT(h, !non_target.ok()); PP_EXPECT(h, non_target.status().code == StatusCode::invalid_argument); } void validates_blit_contract(pp::tests::Harness& h) { const TextureDesc target_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .render_target = true, }; const TextureDesc r8_target_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::r8, .render_target = true, }; const TextureDesc texture_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .render_target = false, }; PP_EXPECT(h, validate_blit_descs(target_desc, target_desc).ok()); PP_EXPECT(h, validate_blit_filter(BlitFilter::nearest).ok()); PP_EXPECT(h, validate_blit_filter(BlitFilter::linear).ok()); PP_EXPECT(h, blit_filter_name(BlitFilter::linear) == std::string_view("linear")); const auto non_target = validate_blit_descs(texture_desc, target_desc); const auto mismatched_format = validate_blit_descs(target_desc, r8_target_desc); const auto bad_filter = validate_blit_filter(static_cast(255)); PP_EXPECT(h, !non_target.ok()); PP_EXPECT(h, non_target.code == StatusCode::invalid_argument); PP_EXPECT(h, !mismatched_format.ok()); PP_EXPECT(h, mismatched_format.code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_filter.ok()); PP_EXPECT(h, bad_filter.code == StatusCode::invalid_argument); PP_EXPECT(h, blit_filter_name(static_cast(255)) == std::string_view("unknown")); } void validates_blend_contract(pp::tests::Harness& h) { const BlendState alpha_blend { .enabled = true, .source_color = BlendFactor::source_alpha, .destination_color = BlendFactor::one_minus_source_alpha, .color_op = BlendOp::add, .source_alpha = BlendFactor::one, .destination_alpha = BlendFactor::one_minus_source_alpha, .alpha_op = BlendOp::add, }; PP_EXPECT(h, validate_blend_state(alpha_blend).ok()); PP_EXPECT(h, validate_blend_factor(BlendFactor::destination_alpha).ok()); PP_EXPECT(h, validate_blend_op(BlendOp::reverse_subtract).ok()); PP_EXPECT(h, blend_factor_name(BlendFactor::one_minus_destination_alpha) == std::string_view("one_minus_destination_alpha")); PP_EXPECT(h, blend_op_name(BlendOp::subtract) == std::string_view("subtract")); auto bad_source = alpha_blend; bad_source.source_color = static_cast(255); auto bad_op = alpha_blend; bad_op.alpha_op = static_cast(255); const auto bad_source_status = validate_blend_state(bad_source); const auto bad_op_status = validate_blend_state(bad_op); PP_EXPECT(h, !bad_source_status.ok()); PP_EXPECT(h, bad_source_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_op_status.ok()); PP_EXPECT(h, bad_op_status.code == StatusCode::invalid_argument); PP_EXPECT(h, blend_factor_name(static_cast(255)) == std::string_view("unknown")); PP_EXPECT(h, blend_op_name(static_cast(255)) == std::string_view("unknown")); } void validates_depth_contract(pp::tests::Harness& h) { const DepthState read_write_depth { .test_enabled = true, .write_enabled = true, .compare = CompareOp::less_or_equal, }; PP_EXPECT(h, validate_depth_state(read_write_depth).ok()); PP_EXPECT(h, validate_compare_op(CompareOp::always).ok()); PP_EXPECT(h, compare_op_name(CompareOp::greater_or_equal) == std::string_view("greater_or_equal")); auto bad_compare = read_write_depth; bad_compare.compare = static_cast(255); const auto bad_compare_status = validate_depth_state(bad_compare); PP_EXPECT(h, !bad_compare_status.ok()); PP_EXPECT(h, bad_compare_status.code == StatusCode::invalid_argument); PP_EXPECT(h, compare_op_name(static_cast(255)) == std::string_view("unknown")); } void validates_viewports_and_mesh_descriptors(pp::tests::Harness& h) { const Extent2D target { .width = 64, .height = 32 }; PP_EXPECT(h, validate_viewport( Viewport { .x = 0, .y = 0, .width = 64, .height = 32, .min_depth = 0.0F, .max_depth = 1.0F }, target) .ok()); PP_EXPECT(h, validate_scissor(ScissorRect {}, target).ok()); PP_EXPECT(h, validate_scissor(ScissorRect { .enabled = true, .x = 0, .y = 0, .width = 64, .height = 32 }, target) .ok()); PP_EXPECT(h, validate_scissor(ScissorRect { .enabled = true, .x = 63, .y = 31, .width = 1, .height = 1 }, target) .ok()); PP_EXPECT(h, validate_mesh_desc(MeshDesc { .vertex_count = 3, .topology = PrimitiveTopology::triangles }).ok()); PP_EXPECT(h, primitive_topology_name(PrimitiveTopology::lines) == std::string_view("lines")); const auto negative_origin = validate_viewport(Viewport { .x = -1, .y = 0, .width = 1, .height = 1 }, target); const auto too_wide = validate_viewport(Viewport { .x = 63, .y = 0, .width = 2, .height = 1 }, target); const auto bad_depth = validate_viewport( Viewport { .x = 0, .y = 0, .width = 1, .height = 1, .min_depth = 0.75F, .max_depth = 0.25F }, target); const auto empty_mesh = validate_mesh_desc(MeshDesc {}); const auto negative_scissor = validate_scissor(ScissorRect { .enabled = true, .x = -1, .y = 0, .width = 1, .height = 1 }, target); const auto empty_scissor = validate_scissor(ScissorRect { .enabled = true, .x = 0, .y = 0, .width = 0, .height = 1 }, target); const auto scissor_overrun = validate_scissor(ScissorRect { .enabled = true, .x = 63, .y = 0, .width = 2, .height = 1 }, target); PP_EXPECT(h, !negative_origin.ok()); PP_EXPECT(h, negative_origin.code == StatusCode::invalid_argument); PP_EXPECT(h, !too_wide.ok()); PP_EXPECT(h, too_wide.code == StatusCode::out_of_range); PP_EXPECT(h, !bad_depth.ok()); PP_EXPECT(h, bad_depth.code == StatusCode::out_of_range); PP_EXPECT(h, !empty_mesh.ok()); PP_EXPECT(h, empty_mesh.code == StatusCode::invalid_argument); PP_EXPECT(h, !negative_scissor.ok()); PP_EXPECT(h, negative_scissor.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_scissor.ok()); PP_EXPECT(h, empty_scissor.code == StatusCode::invalid_argument); PP_EXPECT(h, !scissor_overrun.ok()); PP_EXPECT(h, scissor_overrun.code == StatusCode::out_of_range); PP_EXPECT(h, validate_texture_slot(0).ok()); PP_EXPECT(h, validate_texture_slot(max_texture_slots - 1U).ok()); const auto invalid_slot = validate_texture_slot(max_texture_slots); PP_EXPECT(h, !invalid_slot.ok()); PP_EXPECT(h, invalid_slot.code == StatusCode::out_of_range); } void validates_shader_program_descriptors(pp::tests::Harness& h) { constexpr char vertex_source[] = "#version 330 core\nvoid main(){}"; constexpr char fragment_source[] = "#version 330 core\nout vec4 color; void main(){ color = vec4(1); }"; const ShaderProgramDesc valid { .debug_name = "solid-color", .vertex = ShaderStageSource { .entry_point = "main", .source = vertex_source, .source_size = sizeof(vertex_source) - 1U, }, .fragment = ShaderStageSource { .entry_point = "main", .source = fragment_source, .source_size = sizeof(fragment_source) - 1U, }, }; PP_EXPECT(h, validate_shader_program_desc(valid).ok()); auto missing_name = valid; missing_name.debug_name = nullptr; auto missing_vertex_entry = valid; missing_vertex_entry.vertex.entry_point = ""; auto missing_fragment_source = valid; missing_fragment_source.fragment.source = nullptr; auto empty_fragment_source = valid; empty_fragment_source.fragment.source_size = 0; auto excessive_source = valid; excessive_source.vertex.source_size = max_shader_source_bytes + 1U; const auto missing_name_status = validate_shader_program_desc(missing_name); const auto missing_vertex_entry_status = validate_shader_program_desc(missing_vertex_entry); const auto missing_fragment_source_status = validate_shader_program_desc(missing_fragment_source); const auto empty_fragment_source_status = validate_shader_program_desc(empty_fragment_source); const auto excessive_source_status = validate_shader_program_desc(excessive_source); PP_EXPECT(h, !missing_name_status.ok()); PP_EXPECT(h, missing_name_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !missing_vertex_entry_status.ok()); PP_EXPECT(h, missing_vertex_entry_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !missing_fragment_source_status.ok()); PP_EXPECT(h, missing_fragment_source_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_fragment_source_status.ok()); PP_EXPECT(h, empty_fragment_source_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !excessive_source_status.ok()); PP_EXPECT(h, excessive_source_status.code == StatusCode::out_of_range); } void validates_panopainter_shader_catalog(pp::tests::Harness& h) { const auto catalog = panopainter_shader_catalog(); PP_EXPECT(h, catalog.size() == 25U); PP_EXPECT(h, validate_shader_catalog(catalog).ok()); PP_EXPECT(h, catalog.front().name == std::string_view("texture")); PP_EXPECT(h, catalog.front().path == std::string_view("data/shaders/texture.glsl")); PP_EXPECT(h, catalog.back().name == std::string_view("bakeuv")); PP_EXPECT(h, catalog.back().path == std::string_view("data/shaders/bake-uv.glsl")); bool found_stroke = false; bool found_brush_stroke = false; bool found_equirect = false; for (const auto& entry : catalog) { found_stroke = found_stroke || std::string_view(entry.name) == "stroke"; found_brush_stroke = found_brush_stroke || std::string_view(entry.name) == "brush-stroke"; found_equirect = found_equirect || std::string_view(entry.name) == "equirect"; } PP_EXPECT(h, found_stroke); PP_EXPECT(h, found_brush_stroke); PP_EXPECT(h, found_equirect); } void rejects_invalid_shader_catalogs(pp::tests::Harness& h) { const std::array duplicated { ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.glsl" }, ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture-alpha.glsl" }, }; const std::array missing_name { ShaderCatalogEntry { .name = "", .path = "data/shaders/texture.glsl" }, }; const std::array missing_path { ShaderCatalogEntry { .name = "texture", .path = "" }, }; const std::array wrong_extension { ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.txt" }, }; const auto empty = validate_shader_catalog({}); const auto duplicate = validate_shader_catalog(duplicated); const auto no_name = validate_shader_catalog(missing_name); const auto no_path = validate_shader_catalog(missing_path); const auto bad_extension = validate_shader_catalog(wrong_extension); PP_EXPECT(h, !empty.ok()); PP_EXPECT(h, empty.code == StatusCode::invalid_argument); PP_EXPECT(h, !duplicate.ok()); PP_EXPECT(h, duplicate.code == StatusCode::invalid_argument); PP_EXPECT(h, !no_name.ok()); PP_EXPECT(h, no_name.code == StatusCode::invalid_argument); PP_EXPECT(h, !no_path.ok()); PP_EXPECT(h, no_path.code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_extension.ok()); PP_EXPECT(h, bad_extension.code == StatusCode::invalid_argument); } void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) { FakeRenderDevice device; FakeRenderTarget target; FakeTexture texture; FakeReadbackBuffer readback_buffer(64U * 32U * 4U); const std::array upload_bytes {}; FakeShaderProgram shader; FakeMesh mesh; PP_EXPECT(h, device.backend_name() == std::string_view("fake")); device.trace()->marker("renderer", "begin"); PP_EXPECT(h, device.trace_recorder.last_component == std::string_view("renderer")); PP_EXPECT(h, device.trace_recorder.last_name == std::string_view("begin")); auto& context = device.immediate_context(); PP_EXPECT(h, context.begin_render_pass(target, ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }).ok()); PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, context.set_scissor(ScissorRect { .enabled = true, .x = 4, .y = 5, .width = 16, .height = 8 }).ok()); PP_EXPECT(h, context.set_blend_state(BlendState { .enabled = true, .source_color = BlendFactor::source_alpha, .destination_color = BlendFactor::one_minus_source_alpha, }) .ok()); PP_EXPECT(h, context.set_depth_state(DepthState { .test_enabled = true, .write_enabled = true, .compare = CompareOp::less_or_equal, }) .ok()); PP_EXPECT(h, context.bind_shader(shader).ok()); PP_EXPECT(h, context.bind_texture(2, texture).ok()); PP_EXPECT(h, context.bind_mesh(mesh).ok()); PP_EXPECT(h, context.draw().ok()); context.end_render_pass(); const auto draw_after_end = context.draw(); PP_EXPECT(h, !draw_after_end.ok()); PP_EXPECT(h, draw_after_end.code == StatusCode::invalid_argument); PP_EXPECT(h, context.upload_texture( texture, ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, upload_bytes) .ok()); PP_EXPECT(h, context.read_texture( texture, ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, readback_buffer) .ok()); PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok()); PP_EXPECT(h, context.blit_render_target( target, ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 8, .height = 10 }, BlitFilter::linear) .ok()); PP_EXPECT(h, device.context.shader_name == std::string_view("fake-shader")); PP_EXPECT(h, device.context.last_scissor.enabled); PP_EXPECT(h, device.context.last_scissor.x == 4); PP_EXPECT(h, device.context.last_scissor.height == 8U); PP_EXPECT(h, device.context.last_blend_state.enabled); PP_EXPECT(h, device.context.last_blend_state.source_color == BlendFactor::source_alpha); PP_EXPECT(h, device.context.last_blend_state.destination_color == BlendFactor::one_minus_source_alpha); PP_EXPECT(h, device.context.last_depth_state.test_enabled); PP_EXPECT(h, device.context.last_depth_state.write_enabled); PP_EXPECT(h, device.context.last_depth_state.compare == CompareOp::less_or_equal); PP_EXPECT(h, device.context.last_texture_slot == 2U); PP_EXPECT(h, device.context.last_texture_bytes == 8192U); PP_EXPECT(h, device.context.last_upload_bytes == 80U); PP_EXPECT(h, device.context.last_readback_bytes == 80U); PP_EXPECT(h, device.context.last_capture_bytes == 8192U); PP_EXPECT(h, device.context.last_blit_source_bytes == 80U); PP_EXPECT(h, device.context.last_blit_destination_bytes == 320U); PP_EXPECT(h, device.context.last_blit_filter == BlitFilter::linear); } void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) { RecordingRenderDevice device; RecordingTexture2D texture(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .render_target = true, }); RecordingReadbackBuffer readback_buffer(64U * 32U * 4U); const std::array upload_bytes {}; RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .render_target = true, }); RecordingRenderTarget blit_target(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .render_target = true, }); RecordingShaderProgram shader("recorded-shader"); RecordingMesh mesh(MeshDesc { .vertex_count = 3, .index_count = 0, .topology = PrimitiveTopology::triangles }); PP_EXPECT(h, device.backend_name() == std::string_view("recording")); device.trace()->marker("renderer", "frame"); auto& context = device.immediate_context(); PP_EXPECT(h, context.begin_render_pass(target, ClearColor { .r = 0.2F, .g = 0.3F, .b = 0.4F, .a = 1.0F }).ok()); PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, context.set_scissor(ScissorRect { .enabled = true, .x = 4, .y = 6, .width = 16, .height = 8 }).ok()); PP_EXPECT(h, context.set_blend_state(BlendState { .enabled = true, .source_color = BlendFactor::source_alpha, .destination_color = BlendFactor::one_minus_source_alpha, .source_alpha = BlendFactor::one, .destination_alpha = BlendFactor::one_minus_source_alpha, }) .ok()); PP_EXPECT(h, context.set_depth_state(DepthState { .test_enabled = true, .write_enabled = true, .compare = CompareOp::less_or_equal, }) .ok()); PP_EXPECT(h, context.bind_shader(shader).ok()); PP_EXPECT(h, context.bind_texture(1, texture).ok()); PP_EXPECT(h, context.bind_mesh(mesh).ok()); PP_EXPECT(h, context.draw().ok()); context.end_render_pass(); const auto commands = device.commands(); PP_EXPECT(h, commands.size() == 11U); PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::trace_marker); PP_EXPECT(h, commands[0].component == std::string_view("renderer")); PP_EXPECT(h, commands[0].name == std::string_view("frame")); PP_EXPECT(h, commands[1].kind == RecordedRenderCommandKind::begin_render_pass); PP_EXPECT(h, commands[1].target_desc.extent.width == 64U); PP_EXPECT(h, commands[1].clear_color.a == 1.0F); PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::set_viewport); PP_EXPECT(h, commands[2].viewport.height == 32U); PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::set_scissor); PP_EXPECT(h, commands[3].scissor.enabled); PP_EXPECT(h, commands[3].scissor.x == 4); PP_EXPECT(h, commands[3].scissor.height == 8U); PP_EXPECT(h, recorded_render_command_kind_name(commands[3].kind) == std::string_view("set_scissor")); PP_EXPECT(h, commands[4].kind == RecordedRenderCommandKind::set_blend_state); PP_EXPECT(h, commands[4].blend_state.enabled); PP_EXPECT(h, commands[4].blend_state.destination_color == BlendFactor::one_minus_source_alpha); PP_EXPECT(h, recorded_render_command_kind_name(commands[4].kind) == std::string_view("set_blend_state")); PP_EXPECT(h, commands[5].kind == RecordedRenderCommandKind::set_depth_state); PP_EXPECT(h, commands[5].depth_state.test_enabled); PP_EXPECT(h, commands[5].depth_state.write_enabled); PP_EXPECT(h, commands[5].depth_state.compare == CompareOp::less_or_equal); PP_EXPECT(h, recorded_render_command_kind_name(commands[5].kind) == std::string_view("set_depth_state")); PP_EXPECT(h, commands[6].kind == RecordedRenderCommandKind::bind_shader); PP_EXPECT(h, commands[6].name == std::string_view("recorded-shader")); PP_EXPECT(h, commands[7].kind == RecordedRenderCommandKind::bind_texture); PP_EXPECT(h, commands[7].texture_slot == 1U); PP_EXPECT(h, commands[7].texture_desc.extent.height == 32U); PP_EXPECT(h, recorded_render_command_kind_name(commands[7].kind) == std::string_view("bind_texture")); PP_EXPECT(h, commands[8].kind == RecordedRenderCommandKind::bind_mesh); PP_EXPECT(h, commands[8].mesh_desc.vertex_count == 3U); PP_EXPECT(h, commands[9].kind == RecordedRenderCommandKind::draw); PP_EXPECT(h, commands[10].kind == RecordedRenderCommandKind::end_render_pass); PP_EXPECT(h, recorded_render_command_kind_name(commands[9].kind) == std::string_view("draw")); PP_EXPECT(h, context.upload_texture( texture, ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, upload_bytes) .ok()); const auto commands_after_upload = device.commands(); PP_EXPECT(h, commands_after_upload.size() == 12U); PP_EXPECT(h, commands_after_upload[11].kind == RecordedRenderCommandKind::upload_texture); PP_EXPECT(h, commands_after_upload[11].texture_desc.extent.width == 64U); PP_EXPECT(h, commands_after_upload[11].readback_region.x == 4U); PP_EXPECT(h, commands_after_upload[11].upload_bytes == 96U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_upload[11].kind) == std::string_view("upload_texture")); PP_EXPECT(h, context.read_texture( texture, ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, readback_buffer) .ok()); const auto commands_after_readback = device.commands(); PP_EXPECT(h, commands_after_readback.size() == 13U); PP_EXPECT(h, commands_after_readback[12].kind == RecordedRenderCommandKind::read_texture); PP_EXPECT(h, commands_after_readback[12].texture_desc.extent.width == 64U); PP_EXPECT(h, commands_after_readback[12].readback_region.x == 4U); PP_EXPECT(h, commands_after_readback[12].readback_region.height == 3U); PP_EXPECT(h, commands_after_readback[12].readback_bytes == 96U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[12].kind) == std::string_view("read_texture")); PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok()); const auto commands_after_capture = device.commands(); PP_EXPECT(h, commands_after_capture.size() == 14U); PP_EXPECT(h, commands_after_capture[13].kind == RecordedRenderCommandKind::capture_frame); PP_EXPECT(h, commands_after_capture[13].target_desc.extent.width == 64U); PP_EXPECT(h, commands_after_capture[13].target_desc.extent.height == 32U); PP_EXPECT(h, commands_after_capture[13].capture_bytes == 8192U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[13].kind) == std::string_view("capture_frame")); PP_EXPECT(h, context.blit_render_target( target, ReadbackRegion { .x = 0, .y = 0, .width = 16, .height = 8 }, blit_target, ReadbackRegion { .x = 2, .y = 3, .width = 8, .height = 4 }, BlitFilter::linear) .ok()); const auto commands_after_blit = device.commands(); PP_EXPECT(h, commands_after_blit.size() == 15U); PP_EXPECT(h, commands_after_blit[14].kind == RecordedRenderCommandKind::blit_render_target); PP_EXPECT(h, commands_after_blit[14].source_desc.extent.width == 64U); PP_EXPECT(h, commands_after_blit[14].destination_desc.extent.height == 32U); PP_EXPECT(h, commands_after_blit[14].source_region.width == 16U); PP_EXPECT(h, commands_after_blit[14].destination_region.x == 2U); PP_EXPECT(h, commands_after_blit[14].blit_filter == BlitFilter::linear); PP_EXPECT(h, commands_after_blit[14].blit_source_bytes == 512U); PP_EXPECT(h, commands_after_blit[14].blit_destination_bytes == 128U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_blit[14].kind) == std::string_view("blit_render_target")); device.clear(); PP_EXPECT(h, device.commands().empty()); } void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Harness& h) { RecordingRenderDevice device; RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::rgba8, .render_target = true, }); RecordingRenderTarget non_render_target(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::rgba8, .render_target = false, }); RecordingRenderTarget r8_target(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::r8, .render_target = true, }); RecordingTexture2D texture(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::rgba8, .render_target = true, }); RecordingReadbackBuffer small_readback_buffer(3U); RecordingReadbackBuffer full_readback_buffer(32U * 16U * 4U); const std::array one_pixel_upload {}; const std::array undersized_upload {}; RecordingShaderProgram shader("strict-shader"); RecordingMesh mesh(MeshDesc { .vertex_count = 3, .topology = PrimitiveTopology::triangles }); RecordingMesh empty_mesh(MeshDesc {}); auto& context = device.immediate_context(); const auto draw_before_begin = context.draw(); PP_EXPECT(h, !draw_before_begin.ok()); PP_EXPECT(h, draw_before_begin.code == StatusCode::invalid_argument); const auto blend_before_begin = context.set_blend_state(BlendState {}); PP_EXPECT(h, !blend_before_begin.ok()); PP_EXPECT(h, blend_before_begin.code == StatusCode::invalid_argument); const auto scissor_before_begin = context.set_scissor(ScissorRect {}); PP_EXPECT(h, !scissor_before_begin.ok()); PP_EXPECT(h, scissor_before_begin.code == StatusCode::invalid_argument); const auto depth_before_begin = context.set_depth_state(DepthState {}); PP_EXPECT(h, !depth_before_begin.ok()); PP_EXPECT(h, depth_before_begin.code == StatusCode::invalid_argument); const auto invalid_target = context.begin_render_pass(non_render_target, ClearColor {}); PP_EXPECT(h, !invalid_target.ok()); PP_EXPECT(h, invalid_target.code == StatusCode::invalid_argument); PP_EXPECT(h, device.commands().empty()); PP_EXPECT(h, context.begin_render_pass(target, ClearColor {}).ok()); const auto upload_during_render_pass = context.upload_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, one_pixel_upload); PP_EXPECT(h, !upload_during_render_pass.ok()); PP_EXPECT(h, upload_during_render_pass.code == StatusCode::invalid_argument); const auto read_during_render_pass = context.read_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, full_readback_buffer); PP_EXPECT(h, !read_during_render_pass.ok()); PP_EXPECT(h, read_during_render_pass.code == StatusCode::invalid_argument); const auto capture_during_render_pass = context.capture_frame(target, full_readback_buffer); PP_EXPECT(h, !capture_during_render_pass.ok()); PP_EXPECT(h, capture_during_render_pass.code == StatusCode::invalid_argument); const auto blit_during_render_pass = context.blit_render_target( target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, BlitFilter::nearest); PP_EXPECT(h, !blit_during_render_pass.ok()); PP_EXPECT(h, blit_during_render_pass.code == StatusCode::invalid_argument); const auto nested_begin = context.begin_render_pass(target, ClearColor {}); PP_EXPECT(h, !nested_begin.ok()); PP_EXPECT(h, nested_begin.code == StatusCode::invalid_argument); auto invalid_blend = BlendState {}; invalid_blend.source_color = static_cast(255); const auto invalid_blend_state = context.set_blend_state(invalid_blend); PP_EXPECT(h, !invalid_blend_state.ok()); PP_EXPECT(h, invalid_blend_state.code == StatusCode::invalid_argument); PP_EXPECT(h, context.set_blend_state(BlendState { .enabled = true, .source_color = BlendFactor::source_alpha, .destination_color = BlendFactor::one_minus_source_alpha, }) .ok()); PP_EXPECT(h, context.set_scissor(ScissorRect { .enabled = true, .x = 0, .y = 0, .width = 16, .height = 8, }) .ok()); const auto invalid_scissor = context.set_scissor(ScissorRect { .enabled = true, .x = 31, .y = 15, .width = 2, .height = 1, }); PP_EXPECT(h, !invalid_scissor.ok()); PP_EXPECT(h, invalid_scissor.code == StatusCode::out_of_range); auto invalid_depth = DepthState {}; invalid_depth.compare = static_cast(255); const auto invalid_depth_state = context.set_depth_state(invalid_depth); PP_EXPECT(h, !invalid_depth_state.ok()); PP_EXPECT(h, invalid_depth_state.code == StatusCode::invalid_argument); PP_EXPECT(h, context.set_depth_state(DepthState { .test_enabled = true, .write_enabled = true, .compare = CompareOp::less_or_equal, }) .ok()); const auto draw_without_bindings = context.draw(); PP_EXPECT(h, !draw_without_bindings.ok()); PP_EXPECT(h, draw_without_bindings.code == StatusCode::invalid_argument); PP_EXPECT(h, context.bind_shader(shader).ok()); const auto bind_texture_bad_slot = context.bind_texture(max_texture_slots, texture); PP_EXPECT(h, !bind_texture_bad_slot.ok()); PP_EXPECT(h, bind_texture_bad_slot.code == StatusCode::out_of_range); PP_EXPECT(h, context.bind_texture(0, texture).ok()); const auto draw_without_mesh = context.draw(); PP_EXPECT(h, !draw_without_mesh.ok()); PP_EXPECT(h, draw_without_mesh.code == StatusCode::invalid_argument); const auto invalid_mesh = context.bind_mesh(empty_mesh); PP_EXPECT(h, !invalid_mesh.ok()); PP_EXPECT(h, invalid_mesh.code == StatusCode::invalid_argument); PP_EXPECT(h, context.bind_mesh(mesh).ok()); PP_EXPECT(h, context.draw().ok()); context.end_render_pass(); const auto viewport_after_end = context.set_viewport(Viewport { .x = 0, .y = 0, .width = 1, .height = 1 }); PP_EXPECT(h, !viewport_after_end.ok()); PP_EXPECT(h, viewport_after_end.code == StatusCode::invalid_argument); const auto blend_after_end = context.set_blend_state(BlendState {}); PP_EXPECT(h, !blend_after_end.ok()); PP_EXPECT(h, blend_after_end.code == StatusCode::invalid_argument); const auto scissor_after_end = context.set_scissor(ScissorRect {}); PP_EXPECT(h, !scissor_after_end.ok()); PP_EXPECT(h, scissor_after_end.code == StatusCode::invalid_argument); const auto depth_after_end = context.set_depth_state(DepthState {}); PP_EXPECT(h, !depth_after_end.ok()); PP_EXPECT(h, depth_after_end.code == StatusCode::invalid_argument); const auto bind_texture_after_end = context.bind_texture(0, texture); PP_EXPECT(h, !bind_texture_after_end.ok()); PP_EXPECT(h, bind_texture_after_end.code == StatusCode::invalid_argument); const auto read_outside_bounds = context.read_texture( texture, ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, full_readback_buffer); PP_EXPECT(h, !read_outside_bounds.ok()); PP_EXPECT(h, read_outside_bounds.code == StatusCode::out_of_range); const auto upload_outside_bounds = context.upload_texture( texture, ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, one_pixel_upload); PP_EXPECT(h, !upload_outside_bounds.ok()); PP_EXPECT(h, upload_outside_bounds.code == StatusCode::out_of_range); const auto upload_with_wrong_size = context.upload_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, undersized_upload); PP_EXPECT(h, !upload_with_wrong_size.ok()); PP_EXPECT(h, upload_with_wrong_size.code == StatusCode::invalid_argument); const auto read_into_small_buffer = context.read_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, small_readback_buffer); PP_EXPECT(h, !read_into_small_buffer.ok()); PP_EXPECT(h, read_into_small_buffer.code == StatusCode::out_of_range); const auto capture_into_small_buffer = context.capture_frame(target, small_readback_buffer); PP_EXPECT(h, !capture_into_small_buffer.ok()); PP_EXPECT(h, capture_into_small_buffer.code == StatusCode::out_of_range); const auto capture_non_target = context.capture_frame(non_render_target, full_readback_buffer); PP_EXPECT(h, !capture_non_target.ok()); PP_EXPECT(h, capture_non_target.code == StatusCode::invalid_argument); const auto blit_non_target = context.blit_render_target( non_render_target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, BlitFilter::nearest); PP_EXPECT(h, !blit_non_target.ok()); PP_EXPECT(h, blit_non_target.code == StatusCode::invalid_argument); const auto blit_mismatched_format = context.blit_render_target( target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, r8_target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, BlitFilter::nearest); PP_EXPECT(h, !blit_mismatched_format.ok()); PP_EXPECT(h, blit_mismatched_format.code == StatusCode::invalid_argument); const auto blit_outside_bounds = context.blit_render_target( target, ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, BlitFilter::nearest); PP_EXPECT(h, !blit_outside_bounds.ok()); PP_EXPECT(h, blit_outside_bounds.code == StatusCode::out_of_range); const auto blit_bad_filter = context.blit_render_target( target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, static_cast(255)); PP_EXPECT(h, !blit_bad_filter.ok()); PP_EXPECT(h, blit_bad_filter.code == StatusCode::invalid_argument); } } int main() { pp::tests::Harness harness; harness.run("computes_texture_sizes", computes_texture_sizes); harness.run("rejects_invalid_or_excessive_extents", rejects_invalid_or_excessive_extents); harness.run("validates_readback_bounds", validates_readback_bounds); harness.run("computes_readback_byte_sizes", computes_readback_byte_sizes); harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes); harness.run("validates_blit_contract", validates_blit_contract); harness.run("validates_blend_contract", validates_blend_contract); harness.run("validates_depth_contract", validates_depth_contract); harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); harness.run("validates_shader_program_descriptors", validates_shader_program_descriptors); harness.run("validates_panopainter_shader_catalog", validates_panopainter_shader_catalog); harness.run("rejects_invalid_shader_catalogs", rejects_invalid_shader_catalogs); harness.run("renderer_interfaces_support_backend_neutral_dispatch", renderer_interfaces_support_backend_neutral_dispatch); harness.run("recording_renderer_records_valid_command_sequences", recording_renderer_records_valid_command_sequences); harness.run("recording_renderer_rejects_invalid_command_order_and_targets", recording_renderer_rejects_invalid_command_order_and_targets); return harness.finish(); }