#include "renderer_api/renderer_api.h" #include "test_harness.h" #include using pp::foundation::StatusCode; using pp::renderer::ClearColor; using pp::renderer::Extent2D; 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::TextureDesc; using pp::renderer::TextureFormat; using pp::renderer::Viewport; using pp::renderer::max_texture_dimension; using pp::renderer::primitive_topology_name; using pp::renderer::texture_byte_size; using pp::renderer::texture_format_name; using pp::renderer::validate_extent; using pp::renderer::validate_mesh_desc; using pp::renderer::validate_readback_region; 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 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 bind_shader(IShaderProgram& shader) noexcept override { shader_name = shader.debug_name(); 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"); } void end_render_pass() noexcept override { in_render_pass = false; } bool in_render_pass = false; const char* shader_name = nullptr; }; 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 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_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 {}); 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); } void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) { FakeRenderDevice device; FakeRenderTarget target; 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.bind_shader(shader).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, device.context.shader_name == std::string_view("fake-shader")); } } 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("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); harness.run("renderer_interfaces_support_backend_neutral_dispatch", renderer_interfaces_support_backend_neutral_dispatch); return harness.finish(); }