#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 #include #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::DrawDesc; 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::RenderPassDesc; using pp::renderer::SamplerAddressMode; using pp::renderer::sampler_address_mode_name; using pp::renderer::SamplerDesc; using pp::renderer::SamplerFilter; using pp::renderer::sampler_filter_name; 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_shader_uniform_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_draw_desc; using pp::renderer::validate_mesh_desc; using pp::renderer::validate_readback_region; using pp::renderer::validate_render_pass_desc; using pp::renderer::validate_sampler_address_mode; using pp::renderer::validate_sampler_desc; using pp::renderer::validate_sampler_filter; using pp::renderer::validate_scissor; using pp::renderer::validate_shader_catalog; using pp::renderer::validate_shader_program_desc; using pp::renderer::validate_shader_uniform_write; using pp::renderer::validate_texture_copy_descs; using pp::renderer::validate_texture_slot; using pp::renderer::validate_viewport; namespace { class FakeRenderTarget final : public IRenderTarget { public: explicit FakeRenderTarget(TextureDesc desc = TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .render_target = true, }) noexcept : desc_(desc) { } [[nodiscard]] TextureDesc color_desc() const noexcept override { return desc_; } private: TextureDesc desc_ {}; }; class FakeShaderProgram final : public IShaderProgram { public: explicit FakeShaderProgram(const char* debug_name = "fake-shader") noexcept : debug_name_(debug_name) { } [[nodiscard]] const char* debug_name() const noexcept override { return debug_name_; } private: const char* debug_name_ = ""; }; class FakeMesh final : public IMesh { public: explicit FakeMesh(MeshDesc desc = MeshDesc { .vertex_count = 3, .index_count = 0, .topology = PrimitiveTopology::triangles, }) noexcept : desc_(desc) { } [[nodiscard]] MeshDesc desc() const noexcept override { return desc_; } private: MeshDesc desc_ {}; }; class FakeTexture final : public pp::renderer::ITexture2D { public: explicit FakeTexture(TextureDesc desc = TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .render_target = true, }) noexcept : desc_(desc) { } [[nodiscard]] TextureDesc desc() const noexcept override { return desc_; } private: TextureDesc desc_ {}; }; 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, RenderPassDesc desc) noexcept override { const auto render_pass_status = validate_render_pass_desc(desc); if (!render_pass_status.ok()) { return render_pass_status; } in_render_pass = true; last_render_pass_desc = desc; 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_shader_uniform( const char* name, std::span bytes) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_shader_uniform_write(name, bytes); if (!status.ok()) { return status; } last_uniform_name = name; last_uniform_bytes = bytes.size(); 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_sampler( std::uint32_t slot, SamplerDesc sampler) 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 sampler_status = validate_sampler_desc(sampler); if (!sampler_status.ok()) { return sampler_status; } last_sampler_slot = slot; last_sampler_desc = sampler; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status bind_mesh(IMesh& mesh) noexcept override { const auto status = validate_mesh_desc(mesh.desc()); if (!status.ok()) { return status; } last_mesh_desc = mesh.desc(); mesh_bound = true; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status draw(DrawDesc desc) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } if (!mesh_bound) { return pp::foundation::Status::invalid_argument("mesh must be bound before draw"); } const auto status = validate_draw_desc(last_mesh_desc, desc); if (!status.ok()) { return status; } last_draw_desc = desc; return pp::foundation::Status::success(); } [[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 copy_texture( pp::renderer::ITexture2D& source, ReadbackRegion source_region, pp::renderer::ITexture2D& destination, ReadbackRegion destination_region) noexcept override { const auto status = validate_texture_copy_descs( source.desc(), source_region, destination.desc(), destination_region); if (!status.ok()) { return status; } const auto source_bytes = readback_byte_size(source.desc(), source_region); if (!source_bytes.ok()) { return source_bytes.status(); } const auto destination_bytes = readback_byte_size(destination.desc(), destination_region); if (!destination_bytes.ok()) { return destination_bytes.status(); } last_copy_source_bytes = source_bytes.value(); last_copy_destination_bytes = destination_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; mesh_bound = false; last_mesh_desc = MeshDesc {}; } bool in_render_pass = false; bool mesh_bound = false; RenderPassDesc last_render_pass_desc {}; MeshDesc last_mesh_desc {}; DrawDesc last_draw_desc {}; const char* shader_name = nullptr; const char* last_uniform_name = nullptr; std::size_t last_uniform_bytes = 0; 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::uint32_t last_sampler_slot = 0; SamplerDesc last_sampler_desc {}; std::uint64_t last_upload_bytes = 0; std::uint64_t last_copy_source_bytes = 0; std::uint64_t last_copy_destination_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]] pp::foundation::Result> create_texture( TextureDesc desc) noexcept override { const auto bytes = texture_byte_size(desc); if (!bytes.ok()) { return pp::foundation::Result>::failure(bytes.status()); } return allocate_resource(desc); } [[nodiscard]] pp::foundation::Result> create_render_target( TextureDesc color_desc) noexcept override { if (!color_desc.render_target) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("render target texture must be flagged as render_target")); } const auto bytes = texture_byte_size(color_desc); if (!bytes.ok()) { return pp::foundation::Result>::failure(bytes.status()); } return allocate_resource(color_desc); } [[nodiscard]] pp::foundation::Result> create_shader_program( ShaderProgramDesc desc) noexcept override { const auto status = validate_shader_program_desc(desc); if (!status.ok()) { return pp::foundation::Result>::failure(status); } return allocate_resource(desc.debug_name); } [[nodiscard]] pp::foundation::Result> create_mesh( MeshDesc desc) noexcept override { const auto status = validate_mesh_desc(desc); if (!status.ok()) { return pp::foundation::Result>::failure(status); } return allocate_resource(desc); } [[nodiscard]] pp::foundation::Result> create_readback_buffer( std::uint64_t size_bytes) noexcept override { if (size_bytes == 0U) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("readback buffer size must be greater than zero")); } if (size_bytes > pp::renderer::max_texture_bytes) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("readback buffer size exceeds the configured limit")); } return allocate_resource(size_bytes); } [[nodiscard]] ICommandContext& immediate_context() noexcept override { return context; } [[nodiscard]] IRenderTrace* trace() noexcept override { return &trace_recorder; } FakeCommandContext context; FakeTrace trace_recorder; private: template [[nodiscard]] static pp::foundation::Result> allocate_resource( Args&&... args) noexcept { auto resource = std::unique_ptr(new (std::nothrow) Resource(std::forward(args)...)); if (!resource) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("renderer resource allocation failed")); } std::unique_ptr erased = std::move(resource); return pp::foundation::Result>::success(std::move(erased)); } }; 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_texture_copy_contract(pp::tests::Harness& h) { const TextureDesc rgba_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, }; const TextureDesc r8_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::r8, }; PP_EXPECT(h, validate_texture_copy_descs( rgba_desc, ReadbackRegion { .x = 1, .y = 2, .width = 4, .height = 3 }, rgba_desc, ReadbackRegion { .x = 5, .y = 4, .width = 4, .height = 3 }) .ok()); const auto mismatched_format = validate_texture_copy_descs( rgba_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, r8_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); const auto mismatched_region = validate_texture_copy_descs( rgba_desc, ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }, rgba_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); const auto outside_source = validate_texture_copy_descs( rgba_desc, ReadbackRegion { .x = 15, .y = 0, .width = 2, .height = 1 }, rgba_desc, ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }); PP_EXPECT(h, !mismatched_format.ok()); PP_EXPECT(h, mismatched_format.code == StatusCode::invalid_argument); PP_EXPECT(h, !mismatched_region.ok()); PP_EXPECT(h, mismatched_region.code == StatusCode::invalid_argument); PP_EXPECT(h, !outside_source.ok()); PP_EXPECT(h, outside_source.code == StatusCode::out_of_range); } 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_sampler_contract(pp::tests::Harness& h) { const SamplerDesc sampler { .min_filter = SamplerFilter::linear, .mag_filter = SamplerFilter::nearest, .mip_filter = SamplerFilter::linear, .address_u = SamplerAddressMode::repeat, .address_v = SamplerAddressMode::mirrored_repeat, .address_w = SamplerAddressMode::clamp_to_border, }; PP_EXPECT(h, validate_sampler_desc(sampler).ok()); PP_EXPECT(h, validate_sampler_filter(SamplerFilter::nearest).ok()); PP_EXPECT(h, validate_sampler_address_mode(SamplerAddressMode::clamp_to_border).ok()); PP_EXPECT(h, sampler_filter_name(SamplerFilter::linear) == std::string_view("linear")); PP_EXPECT(h, sampler_address_mode_name(SamplerAddressMode::mirrored_repeat) == std::string_view("mirrored_repeat")); auto bad_filter = sampler; bad_filter.min_filter = static_cast(255); auto bad_address = sampler; bad_address.address_w = static_cast(255); const auto bad_filter_status = validate_sampler_desc(bad_filter); const auto bad_address_status = validate_sampler_desc(bad_address); PP_EXPECT(h, !bad_filter_status.ok()); PP_EXPECT(h, bad_filter_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_address_status.ok()); PP_EXPECT(h, bad_address_status.code == StatusCode::invalid_argument); PP_EXPECT(h, sampler_filter_name(static_cast(255)) == std::string_view("unknown")); PP_EXPECT(h, sampler_address_mode_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_draw_descriptors(pp::tests::Harness& h) { const MeshDesc vertex_mesh { .vertex_count = 8, .index_count = 0, .topology = PrimitiveTopology::triangles, }; const MeshDesc indexed_mesh { .vertex_count = 8, .index_count = 12, .topology = PrimitiveTopology::triangles, }; PP_EXPECT(h, validate_draw_desc(vertex_mesh, DrawDesc { .first_vertex = 2, .vertex_count = 3 }).ok()); PP_EXPECT(h, validate_draw_desc(indexed_mesh, DrawDesc { .first_index = 3, .index_count = 6 }).ok()); PP_EXPECT(h, validate_draw_desc(indexed_mesh, DrawDesc { .index_count = 12, .instance_count = 4 }).ok()); const auto empty_draw = validate_draw_desc(vertex_mesh, DrawDesc {}); const auto zero_instances = validate_draw_desc(vertex_mesh, DrawDesc { .vertex_count = 3, .instance_count = 0 }); const auto vertex_overrun = validate_draw_desc(vertex_mesh, DrawDesc { .first_vertex = 7, .vertex_count = 2 }); const auto indexed_without_indices = validate_draw_desc(vertex_mesh, DrawDesc { .index_count = 3 }); const auto index_overrun = validate_draw_desc(indexed_mesh, DrawDesc { .first_index = 11, .index_count = 2 }); PP_EXPECT(h, !empty_draw.ok()); PP_EXPECT(h, empty_draw.code == StatusCode::invalid_argument); PP_EXPECT(h, !zero_instances.ok()); PP_EXPECT(h, zero_instances.code == StatusCode::invalid_argument); PP_EXPECT(h, !vertex_overrun.ok()); PP_EXPECT(h, vertex_overrun.code == StatusCode::out_of_range); PP_EXPECT(h, !indexed_without_indices.ok()); PP_EXPECT(h, indexed_without_indices.code == StatusCode::invalid_argument); PP_EXPECT(h, !index_overrun.ok()); PP_EXPECT(h, index_overrun.code == StatusCode::out_of_range); } void validates_render_pass_descriptors(pp::tests::Harness& h) { const auto valid = validate_render_pass_desc(RenderPassDesc { .clear_color = ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }, .clear_depth_enabled = true, .clear_depth = 0.5F, .clear_stencil_enabled = true, .clear_stencil = 7, }); const auto no_clear = validate_render_pass_desc(RenderPassDesc { .clear_color_enabled = false, .clear_color = ClearColor {}, .clear_depth_enabled = false, .clear_stencil_enabled = false, }); const auto bad_color = validate_render_pass_desc(RenderPassDesc { .clear_color = ClearColor { .r = std::numeric_limits::infinity(), .g = 0.0F, .b = 0.0F, .a = 1.0F, }, }); const auto nan_depth = validate_render_pass_desc(RenderPassDesc { .clear_color = ClearColor {}, .clear_depth_enabled = true, .clear_depth = std::numeric_limits::quiet_NaN(), }); const auto out_of_range_depth = validate_render_pass_desc(RenderPassDesc { .clear_color = ClearColor {}, .clear_depth_enabled = true, .clear_depth = 1.5F, }); PP_EXPECT(h, valid.ok()); PP_EXPECT(h, no_clear.ok()); PP_EXPECT(h, !bad_color.ok()); PP_EXPECT(h, bad_color.code == StatusCode::invalid_argument); PP_EXPECT(h, !nan_depth.ok()); PP_EXPECT(h, nan_depth.code == StatusCode::invalid_argument); PP_EXPECT(h, !out_of_range_depth.ok()); PP_EXPECT(h, out_of_range_depth.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_shader_uniform_writes(pp::tests::Harness& h) { const std::array matrix_bytes {}; const std::array one_byte {}; static const std::array excessive_bytes {}; PP_EXPECT(h, validate_shader_uniform_write("mvp", matrix_bytes).ok()); PP_EXPECT(h, validate_shader_uniform_write("opacity", one_byte).ok()); const auto null_name = validate_shader_uniform_write(nullptr, matrix_bytes); const auto empty_name = validate_shader_uniform_write("", matrix_bytes); const auto empty_bytes = validate_shader_uniform_write("mvp", std::span {}); const auto excessive = validate_shader_uniform_write("mvp", excessive_bytes); PP_EXPECT(h, !null_name.ok()); PP_EXPECT(h, null_name.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_name.ok()); PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_bytes.ok()); PP_EXPECT(h, empty_bytes.code == StatusCode::invalid_argument); PP_EXPECT(h, !excessive.ok()); PP_EXPECT(h, excessive.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 {}; const std::array uniform_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, RenderPassDesc { .clear_color = ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }, .clear_depth_enabled = true, .clear_depth = 0.75F, }) .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.set_shader_uniform("mvp", uniform_bytes).ok()); PP_EXPECT(h, context.bind_texture(2, texture).ok()); PP_EXPECT(h, context.bind_sampler(2, SamplerDesc { .min_filter = SamplerFilter::linear, .mag_filter = SamplerFilter::nearest, .address_u = SamplerAddressMode::repeat, }) .ok()); PP_EXPECT(h, context.bind_mesh(mesh).ok()); PP_EXPECT(h, context.draw(DrawDesc { .vertex_count = 3 }).ok()); context.end_render_pass(); const auto draw_after_end = context.draw(DrawDesc { .vertex_count = 3 }); 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.copy_texture( texture, ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 4, .height = 5 }) .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_render_pass_desc.clear_color.a == 1.0F); PP_EXPECT(h, device.context.last_render_pass_desc.clear_depth_enabled); PP_EXPECT(h, device.context.last_render_pass_desc.clear_depth == 0.75F); 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_uniform_name == std::string_view("mvp")); PP_EXPECT(h, device.context.last_uniform_bytes == 64U); PP_EXPECT(h, device.context.last_draw_desc.vertex_count == 3U); PP_EXPECT(h, device.context.last_draw_desc.instance_count == 1U); PP_EXPECT(h, device.context.last_texture_slot == 2U); PP_EXPECT(h, device.context.last_texture_bytes == 8192U); PP_EXPECT(h, device.context.last_sampler_slot == 2U); PP_EXPECT(h, device.context.last_sampler_desc.mag_filter == SamplerFilter::nearest); PP_EXPECT(h, device.context.last_sampler_desc.address_u == SamplerAddressMode::repeat); PP_EXPECT(h, device.context.last_upload_bytes == 80U); PP_EXPECT(h, device.context.last_copy_source_bytes == 80U); PP_EXPECT(h, device.context.last_copy_destination_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 render_devices_create_validated_resources(pp::tests::Harness& h) { static constexpr char shader_source[] = "void main() {}"; RecordingRenderDevice device; const auto texture = device.create_texture(TextureDesc { .extent = Extent2D { .width = 8, .height = 4 }, .format = TextureFormat::rgba8, .render_target = false, }); const auto target = device.create_render_target(TextureDesc { .extent = Extent2D { .width = 8, .height = 4 }, .format = TextureFormat::rgba8, .render_target = true, }); const auto shader = device.create_shader_program(ShaderProgramDesc { .debug_name = "factory-shader", .vertex = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, .fragment = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, }); const auto mesh = device.create_mesh(MeshDesc { .vertex_count = 6, .index_count = 6, .topology = PrimitiveTopology::triangles, }); const auto readback = device.create_readback_buffer(8U * 4U * 4U); PP_EXPECT(h, texture.ok()); PP_EXPECT(h, texture.value()->desc().extent.width == 8U); PP_EXPECT(h, !texture.value()->desc().render_target); PP_EXPECT(h, target.ok()); PP_EXPECT(h, target.value()->color_desc().render_target); PP_EXPECT(h, shader.ok()); PP_EXPECT(h, shader.value()->debug_name() == std::string_view("factory-shader")); PP_EXPECT(h, mesh.ok()); PP_EXPECT(h, mesh.value()->desc().index_count == 6U); PP_EXPECT(h, readback.ok()); PP_EXPECT(h, readback.value()->size_bytes() == 128U); const auto bad_texture = device.create_texture(TextureDesc { .extent = Extent2D { .width = 0, .height = 4 }, .format = TextureFormat::rgba8, }); const auto bad_target = device.create_render_target(TextureDesc { .extent = Extent2D { .width = 8, .height = 4 }, .format = TextureFormat::rgba8, .render_target = false, }); const auto bad_shader = device.create_shader_program(ShaderProgramDesc { .debug_name = "bad-shader", .vertex = ShaderStageSource {}, .fragment = ShaderStageSource {}, }); const auto bad_mesh = device.create_mesh(MeshDesc {}); const auto bad_readback = device.create_readback_buffer(0); PP_EXPECT(h, !bad_texture.ok()); PP_EXPECT(h, bad_texture.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_target.ok()); PP_EXPECT(h, bad_target.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_shader.ok()); PP_EXPECT(h, bad_shader.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_mesh.ok()); PP_EXPECT(h, bad_mesh.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_readback.ok()); PP_EXPECT(h, bad_readback.status().code == StatusCode::invalid_argument); } void recording_renderer_records_shader_uniform_writes(pp::tests::Harness& h) { RecordingRenderDevice device; RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .render_target = true, }); RecordingShaderProgram shader("uniform-shader"); const std::array uniform_bytes {}; auto& context = device.immediate_context(); const auto before_begin = context.set_shader_uniform("mvp", uniform_bytes); PP_EXPECT(h, !before_begin.ok()); PP_EXPECT(h, before_begin.code == StatusCode::invalid_argument); PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).ok()); const auto before_shader = context.set_shader_uniform("mvp", uniform_bytes); PP_EXPECT(h, !before_shader.ok()); PP_EXPECT(h, before_shader.code == StatusCode::invalid_argument); PP_EXPECT(h, context.bind_shader(shader).ok()); PP_EXPECT(h, context.set_shader_uniform("mvp", uniform_bytes).ok()); const auto empty_name = context.set_shader_uniform("", uniform_bytes); const auto empty_bytes = context.set_shader_uniform("mvp", std::span {}); context.end_render_pass(); PP_EXPECT(h, !empty_name.ok()); PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_bytes.ok()); PP_EXPECT(h, empty_bytes.code == StatusCode::invalid_argument); const auto commands = device.commands(); PP_EXPECT(h, commands.size() == 4U); PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::begin_render_pass); PP_EXPECT(h, commands[1].kind == RecordedRenderCommandKind::bind_shader); PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::set_shader_uniform); PP_EXPECT(h, commands[2].name == std::string_view("mvp")); PP_EXPECT(h, commands[2].uniform_bytes == 64U); PP_EXPECT(h, recorded_render_command_kind_name(commands[2].kind) == std::string_view("set_shader_uniform")); PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::end_render_pass); } 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 = 3, .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, RenderPassDesc { .clear_color = ClearColor { .r = 0.2F, .g = 0.3F, .b = 0.4F, .a = 1.0F }, .clear_depth_enabled = true, .clear_depth = 1.0F, .clear_stencil_enabled = true, .clear_stencil = 7, }) .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_sampler(1, SamplerDesc { .min_filter = SamplerFilter::linear, .mag_filter = SamplerFilter::nearest, .address_u = SamplerAddressMode::repeat, .address_v = SamplerAddressMode::clamp_to_edge, .address_w = SamplerAddressMode::clamp_to_border, }) .ok()); PP_EXPECT(h, context.bind_mesh(mesh).ok()); PP_EXPECT(h, context.draw(DrawDesc { .vertex_count = 3, .index_count = 3 }).ok()); context.end_render_pass(); const auto commands = device.commands(); PP_EXPECT(h, commands.size() == 12U); 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_enabled); PP_EXPECT(h, commands[1].clear_color.a == 1.0F); PP_EXPECT(h, commands[1].clear_depth_enabled); PP_EXPECT(h, commands[1].clear_depth == 1.0F); PP_EXPECT(h, commands[1].clear_stencil_enabled); PP_EXPECT(h, commands[1].clear_stencil == 7U); 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_sampler); PP_EXPECT(h, commands[8].sampler_slot == 1U); PP_EXPECT(h, commands[8].sampler_desc.mag_filter == SamplerFilter::nearest); PP_EXPECT(h, commands[8].sampler_desc.address_w == SamplerAddressMode::clamp_to_border); PP_EXPECT(h, recorded_render_command_kind_name(commands[8].kind) == std::string_view("bind_sampler")); PP_EXPECT(h, commands[9].kind == RecordedRenderCommandKind::bind_mesh); PP_EXPECT(h, commands[9].mesh_desc.vertex_count == 3U); PP_EXPECT(h, commands[9].mesh_desc.index_count == 3U); PP_EXPECT(h, commands[10].kind == RecordedRenderCommandKind::draw); PP_EXPECT(h, commands[10].mesh_desc.vertex_count == 3U); PP_EXPECT(h, commands[10].mesh_desc.index_count == 3U); PP_EXPECT(h, commands[10].mesh_desc.topology == PrimitiveTopology::triangles); PP_EXPECT(h, commands[10].draw_desc.vertex_count == 3U); PP_EXPECT(h, commands[10].draw_desc.index_count == 3U); PP_EXPECT(h, commands[10].draw_desc.instance_count == 1U); PP_EXPECT(h, commands[11].kind == RecordedRenderCommandKind::end_render_pass); PP_EXPECT(h, recorded_render_command_kind_name(commands[10].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() == 13U); PP_EXPECT(h, commands_after_upload[12].kind == RecordedRenderCommandKind::upload_texture); PP_EXPECT(h, commands_after_upload[12].texture_desc.extent.width == 64U); PP_EXPECT(h, commands_after_upload[12].readback_region.x == 4U); PP_EXPECT(h, commands_after_upload[12].upload_bytes == 96U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_upload[12].kind) == std::string_view("upload_texture")); PP_EXPECT(h, context.copy_texture( texture, ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 8, .height = 3 }) .ok()); const auto commands_after_copy = device.commands(); PP_EXPECT(h, commands_after_copy.size() == 14U); PP_EXPECT(h, commands_after_copy[13].kind == RecordedRenderCommandKind::copy_texture); PP_EXPECT(h, commands_after_copy[13].source_region.x == 4U); PP_EXPECT(h, commands_after_copy[13].destination_region.x == 0U); PP_EXPECT(h, commands_after_copy[13].copy_source_bytes == 96U); PP_EXPECT(h, commands_after_copy[13].copy_destination_bytes == 96U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_copy[13].kind) == std::string_view("copy_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() == 15U); PP_EXPECT(h, commands_after_readback[14].kind == RecordedRenderCommandKind::read_texture); PP_EXPECT(h, commands_after_readback[14].texture_desc.extent.width == 64U); PP_EXPECT(h, commands_after_readback[14].readback_region.x == 4U); PP_EXPECT(h, commands_after_readback[14].readback_region.height == 3U); PP_EXPECT(h, commands_after_readback[14].readback_bytes == 96U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[14].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() == 16U); PP_EXPECT(h, commands_after_capture[15].kind == RecordedRenderCommandKind::capture_frame); PP_EXPECT(h, commands_after_capture[15].target_desc.extent.width == 64U); PP_EXPECT(h, commands_after_capture[15].target_desc.extent.height == 32U); PP_EXPECT(h, commands_after_capture[15].capture_bytes == 8192U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[15].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() == 17U); PP_EXPECT(h, commands_after_blit[16].kind == RecordedRenderCommandKind::blit_render_target); PP_EXPECT(h, commands_after_blit[16].source_desc.extent.width == 64U); PP_EXPECT(h, commands_after_blit[16].destination_desc.extent.height == 32U); PP_EXPECT(h, commands_after_blit[16].source_region.width == 16U); PP_EXPECT(h, commands_after_blit[16].destination_region.x == 2U); PP_EXPECT(h, commands_after_blit[16].blit_filter == BlitFilter::linear); PP_EXPECT(h, commands_after_blit[16].blit_source_bytes == 512U); PP_EXPECT(h, commands_after_blit[16].blit_destination_bytes == 128U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_blit[16].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(DrawDesc { .vertex_count = 3 }); 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 sampler_before_begin = context.bind_sampler(0, SamplerDesc {}); PP_EXPECT(h, !sampler_before_begin.ok()); PP_EXPECT(h, sampler_before_begin.code == StatusCode::invalid_argument); const auto invalid_target = context.begin_render_pass(non_render_target, RenderPassDesc {}); PP_EXPECT(h, !invalid_target.ok()); PP_EXPECT(h, invalid_target.code == StatusCode::invalid_argument); PP_EXPECT(h, device.commands().empty()); const auto invalid_clear_depth = context.begin_render_pass(target, RenderPassDesc { .clear_color = ClearColor {}, .clear_depth_enabled = true, .clear_depth = -0.25F, }); PP_EXPECT(h, !invalid_clear_depth.ok()); PP_EXPECT(h, invalid_clear_depth.code == StatusCode::out_of_range); PP_EXPECT(h, device.commands().empty()); PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).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 copy_during_render_pass = context.copy_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); PP_EXPECT(h, !copy_during_render_pass.ok()); PP_EXPECT(h, copy_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, RenderPassDesc {}); 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(DrawDesc { .vertex_count = 3 }); 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 bind_sampler_bad_slot = context.bind_sampler(max_texture_slots, SamplerDesc {}); PP_EXPECT(h, !bind_sampler_bad_slot.ok()); PP_EXPECT(h, bind_sampler_bad_slot.code == StatusCode::out_of_range); auto invalid_sampler = SamplerDesc {}; invalid_sampler.min_filter = static_cast(255); const auto bind_invalid_sampler = context.bind_sampler(0, invalid_sampler); PP_EXPECT(h, !bind_invalid_sampler.ok()); PP_EXPECT(h, bind_invalid_sampler.code == StatusCode::invalid_argument); PP_EXPECT(h, context.bind_sampler(0, SamplerDesc {}).ok()); const auto draw_without_mesh = context.draw(DrawDesc { .vertex_count = 3 }); 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()); const auto draw_outside_mesh = context.draw(DrawDesc { .first_vertex = 2, .vertex_count = 2 }); PP_EXPECT(h, !draw_outside_mesh.ok()); PP_EXPECT(h, draw_outside_mesh.code == StatusCode::out_of_range); PP_EXPECT(h, context.draw(DrawDesc { .vertex_count = 3 }).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 bind_sampler_after_end = context.bind_sampler(0, SamplerDesc {}); PP_EXPECT(h, !bind_sampler_after_end.ok()); PP_EXPECT(h, bind_sampler_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 copy_mismatched_regions = context.copy_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); PP_EXPECT(h, !copy_mismatched_regions.ok()); PP_EXPECT(h, copy_mismatched_regions.code == StatusCode::invalid_argument); const auto copy_outside_bounds = context.copy_texture( texture, ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }); PP_EXPECT(h, !copy_outside_bounds.ok()); PP_EXPECT(h, copy_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_texture_copy_contract", validates_texture_copy_contract); harness.run("validates_blend_contract", validates_blend_contract); harness.run("validates_depth_contract", validates_depth_contract); harness.run("validates_sampler_contract", validates_sampler_contract); harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); harness.run("validates_draw_descriptors", validates_draw_descriptors); harness.run("validates_render_pass_descriptors", validates_render_pass_descriptors); harness.run("validates_shader_program_descriptors", validates_shader_program_descriptors); harness.run("validates_shader_uniform_writes", validates_shader_uniform_writes); 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("render_devices_create_validated_resources", render_devices_create_validated_resources); harness.run("recording_renderer_records_shader_uniform_writes", recording_renderer_records_shader_uniform_writes); 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(); }