From 6604f30ef30b4e09f487712e58760aa16d61a6b5 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 08:15:21 +0200 Subject: [PATCH] Expand renderer API interfaces --- docs/modernization/roadmap.md | 5 +- src/renderer_api/renderer_api.cpp | 71 +++++++++ src/renderer_api/renderer_api.h | 84 +++++++++++ tests/renderer_api/renderer_api_tests.cpp | 174 ++++++++++++++++++++++ 4 files changed, 332 insertions(+), 2 deletions(-) diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 998df6a..8586554 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -355,8 +355,9 @@ Goal: make OpenGL an implementation detail and establish parity tests before adding new backends. Status: started. `pp_renderer_api` exists as a headless renderer-neutral target -with texture descriptor, byte-size, and readback bounds validation. OpenGL -classes are not yet behind these interfaces. +with texture descriptor, byte-size, viewport, mesh, readback bounds, command +context, render device, shader, mesh, render target, readback, and trace +interface validation. OpenGL classes are not yet behind these interfaces. Implementation tasks: diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp index 44c90be..edb7ee4 100644 --- a/src/renderer_api/renderer_api.cpp +++ b/src/renderer_api/renderer_api.cpp @@ -1,5 +1,6 @@ #include "renderer_api/renderer_api.h" +#include #include namespace pp::renderer { @@ -66,6 +67,62 @@ pp::foundation::Result texture_byte_size(TextureDesc desc) noexce return pp::foundation::Result::success(bytes); } +pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept +{ + const auto extent_status = validate_extent(target_extent); + if (!extent_status.ok()) { + return extent_status; + } + + if (viewport.x < 0 || viewport.y < 0) { + return pp::foundation::Status::invalid_argument("viewport origin must be non-negative"); + } + + if (viewport.width == 0 || viewport.height == 0) { + return pp::foundation::Status::invalid_argument("viewport size must be greater than zero"); + } + + if (!std::isfinite(viewport.min_depth) || !std::isfinite(viewport.max_depth)) { + return pp::foundation::Status::invalid_argument("viewport depth range must be finite"); + } + + if (viewport.min_depth < 0.0F || viewport.max_depth > 1.0F || viewport.min_depth > viewport.max_depth) { + return pp::foundation::Status::out_of_range("viewport depth range must be within 0..1 and ordered"); + } + + const auto x = static_cast(viewport.x); + const auto y = static_cast(viewport.y); + if (x > target_extent.width || y > target_extent.height) { + return pp::foundation::Status::out_of_range("viewport origin is outside the render target"); + } + + if (viewport.width > target_extent.width - x || viewport.height > target_extent.height - y) { + return pp::foundation::Status::out_of_range("viewport exceeds render target bounds"); + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept +{ + if (desc.vertex_count == 0) { + return pp::foundation::Status::invalid_argument("mesh must contain at least one vertex"); + } + + if (desc.vertex_count > max_mesh_vertices || desc.index_count > max_mesh_vertices) { + return pp::foundation::Status::out_of_range("mesh vertex or index count exceeds the configured limit"); + } + + switch (desc.topology) { + case PrimitiveTopology::triangles: + case PrimitiveTopology::triangle_strip: + case PrimitiveTopology::lines: + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("mesh topology is not supported"); +} + pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept { const auto extent_status = validate_extent(desc.extent); @@ -102,4 +159,18 @@ const char* texture_format_name(TextureFormat format) noexcept return "unknown"; } +const char* primitive_topology_name(PrimitiveTopology topology) noexcept +{ + switch (topology) { + case PrimitiveTopology::triangles: + return "triangles"; + case PrimitiveTopology::triangle_strip: + return "triangle_strip"; + case PrimitiveTopology::lines: + return "lines"; + } + + return "unknown"; +} + } diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index 1420676..486f793 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -2,11 +2,13 @@ #include "foundation/result.h" +#include #include namespace pp::renderer { constexpr std::uint32_t max_texture_dimension = 32768; +constexpr std::uint32_t max_mesh_vertices = 16777216; constexpr std::uint64_t max_texture_bytes = 1024ULL * 1024ULL * 1024ULL; enum class TextureFormat : std::uint8_t { @@ -33,12 +35,70 @@ struct ReadbackRegion { std::uint32_t height = 0; }; +struct Viewport { + std::int32_t x = 0; + std::int32_t y = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + float min_depth = 0.0F; + float max_depth = 1.0F; +}; + +struct ClearColor { + float r = 0.0F; + float g = 0.0F; + float b = 0.0F; + float a = 0.0F; +}; + +enum class PrimitiveTopology : std::uint8_t { + triangles, + triangle_strip, + lines, +}; + +struct MeshDesc { + std::uint32_t vertex_count = 0; + std::uint32_t index_count = 0; + PrimitiveTopology topology = PrimitiveTopology::triangles; +}; + +struct ShaderStageSource { + const char* entry_point = "main"; + const char* source = nullptr; + std::size_t source_size = 0; +}; + +struct ShaderProgramDesc { + const char* debug_name = ""; + ShaderStageSource vertex; + ShaderStageSource fragment; +}; + class ITexture2D { public: virtual ~ITexture2D() = default; [[nodiscard]] virtual TextureDesc desc() const noexcept = 0; }; +class IRenderTarget { +public: + virtual ~IRenderTarget() = default; + [[nodiscard]] virtual TextureDesc color_desc() const noexcept = 0; +}; + +class IShaderProgram { +public: + virtual ~IShaderProgram() = default; + [[nodiscard]] virtual const char* debug_name() const noexcept = 0; +}; + +class IMesh { +public: + virtual ~IMesh() = default; + [[nodiscard]] virtual MeshDesc desc() const noexcept = 0; +}; + class IReadbackBuffer { public: virtual ~IReadbackBuffer() = default; @@ -51,10 +111,34 @@ public: virtual void marker(const char* component, const char* name) noexcept = 0; }; +class ICommandContext { +public: + virtual ~ICommandContext() = default; + [[nodiscard]] virtual pp::foundation::Status begin_render_pass( + IRenderTarget& target, + ClearColor clear_color) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status set_viewport(Viewport viewport) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status bind_mesh(IMesh& mesh) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status draw() noexcept = 0; + virtual void end_render_pass() noexcept = 0; +}; + +class IRenderDevice { +public: + virtual ~IRenderDevice() = default; + [[nodiscard]] virtual const char* backend_name() const noexcept = 0; + [[nodiscard]] virtual ICommandContext& immediate_context() noexcept = 0; + [[nodiscard]] virtual IRenderTrace* trace() noexcept = 0; +}; + [[nodiscard]] std::uint32_t bytes_per_pixel(TextureFormat format) noexcept; [[nodiscard]] pp::foundation::Status validate_extent(Extent2D extent) noexcept; +[[nodiscard]] pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept; +[[nodiscard]] pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept; [[nodiscard]] pp::foundation::Result texture_byte_size(TextureDesc desc) noexcept; [[nodiscard]] pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept; [[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept; +[[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept; } diff --git a/tests/renderer_api/renderer_api_tests.cpp b/tests/renderer_api/renderer_api_tests.cpp index 00376b7..ecc241e 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -4,18 +4,136 @@ #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 { @@ -74,6 +192,60 @@ void validates_readback_bounds(pp::tests::Harness& h) 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() @@ -82,5 +254,7 @@ int main() 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(); }