From 31322bbd837f0f21c5cef956976820044013ed73 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 00:05:41 +0200 Subject: [PATCH] Add renderer API validation tests --- CMakeLists.txt | 12 +++ docs/modernization/build-inventory.md | 4 +- docs/modernization/roadmap.md | 15 +++- scripts/automation/platform-build.ps1 | 2 +- scripts/automation/platform-build.sh | 2 +- src/renderer_api/renderer_api.cpp | 105 ++++++++++++++++++++++ src/renderer_api/renderer_api.h | 60 +++++++++++++ tests/CMakeLists.txt | 10 +++ tests/renderer_api/renderer_api_tests.cpp | 86 ++++++++++++++++++ 9 files changed, 288 insertions(+), 8 deletions(-) create mode 100644 src/renderer_api/renderer_api.cpp create mode 100644 src/renderer_api/renderer_api.h create mode 100644 tests/renderer_api/renderer_api_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0268ab1..dd6f97d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,18 @@ target_link_libraries(pp_document PRIVATE pp_project_warnings) +add_library(pp_renderer_api STATIC + src/renderer_api/renderer_api.cpp) +target_include_directories(pp_renderer_api + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(pp_renderer_api + PUBLIC + pp_foundation + pp_project_options + PRIVATE + pp_project_warnings) + if(PP_BUILD_TOOLS) add_subdirectory(tools/pano_cli) endif() diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 37b9e5b..02824c1 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -75,8 +75,8 @@ Known local toolchain state: - Android NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865` - Android arm64 headless configure/build passes through root CMake and the `platform-build` automation wrapper for `pp_foundation`, `pp_assets`, - `pp_paint`, `pp_document`, `pano_cli`, and their current headless test - binaries. + `pp_paint`, `pp_document`, `pp_renderer_api`, `pano_cli`, and their current + headless test binaries. - `vcpkg` is not on PATH yet; see DEBT-0007. Known warnings after the current CMake app build: diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 41c38dc..3562467 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -46,7 +46,7 @@ or temporary adapters live only in chat history. | 2 | Toolchain, Diagnostics, And Dependencies | In progress | Strict desktop library builds compile cleanly | | 3 | Test Harness And Agent-Ready Automation | In progress | `ctest --preset desktop-fast` runs headlessly | | 4 | Component Split Without Behavior Change | Started | Each extracted target builds and tests | -| 5 | Renderer Boundary And OpenGL Parity | Not started | OpenGL output matches golden readbacks | +| 5 | Renderer Boundary And OpenGL Parity | Started | OpenGL output matches golden readbacks | | 6 | Platform Alignment | Not started | Every supported platform has named validation | | 7 | Hardening, Coverage, And Breaking-Point Tests | Not started | Each component has edge/failure tests | | 8 | Future Backend Readiness | Not started | Vulkan/Metal lab targets remain non-default | @@ -306,8 +306,10 @@ and stroke timing spans with invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection and corrupt/truncated/unsupported tests. `pp_paint` has started with CPU reference math for the five current shader blend modes. `pp_document` has started with a pure canvas/layer model and -layer invariant tests. Continue expanding document behavior toward legacy -Canvas parity. +layer invariant tests. `pp_renderer_api` has started with renderer-neutral +texture/readback descriptors and validation tests. Continue expanding document +behavior toward legacy Canvas parity and then port OpenGL classes behind the +renderer boundary. Implementation tasks: @@ -343,6 +345,10 @@ Gate: 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. + Implementation tasks: - Introduce renderer interfaces: @@ -507,7 +513,7 @@ Last verified on 2026-05-31: ```powershell cmake --preset windows-msvc-default -cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests pano_cli PanoPainter +cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests pano_cli PanoPainter ctest --preset desktop-fast --build-config Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli @@ -523,6 +529,7 @@ Results: - `pp_assets_image_format_tests` passed. - `pp_paint_blend_tests` passed. - `pp_document_tests` passed. +- `pp_renderer_api_tests` passed. - `pano_cli_create_document_smoke` passed. - `pano_cli_inspect_image_rejects_unsupported` passed as an expected failure test. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index ca36dd5..7e6d76a 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param( [string[]]$Presets = @("android-arm64"), - [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_paint_blend_tests", "pp_document_tests") + [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_paint_blend_tests", "pp_document_tests", "pp_renderer_api_tests") ) $ErrorActionPreference = "Stop" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index b5ad5b0..b061ccf 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u preset="${1:-android-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests}" start="$(date +%s)" cmake --preset "$preset" diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp new file mode 100644 index 0000000..44c90be --- /dev/null +++ b/src/renderer_api/renderer_api.cpp @@ -0,0 +1,105 @@ +#include "renderer_api/renderer_api.h" + +#include + +namespace pp::renderer { + +std::uint32_t bytes_per_pixel(TextureFormat format) noexcept +{ + switch (format) { + case TextureFormat::rgba8: + return 4; + case TextureFormat::r8: + return 1; + case TextureFormat::depth24_stencil8: + return 4; + } + + return 0; +} + +pp::foundation::Status validate_extent(Extent2D extent) noexcept +{ + if (extent.width == 0 || extent.height == 0) { + return pp::foundation::Status::invalid_argument("texture extent must be greater than zero"); + } + + if (extent.width > max_texture_dimension || extent.height > max_texture_dimension) { + return pp::foundation::Status::out_of_range("texture extent exceeds the configured limit"); + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Result texture_byte_size(TextureDesc desc) noexcept +{ + const auto extent_status = validate_extent(desc.extent); + if (!extent_status.ok()) { + return pp::foundation::Result::failure(extent_status); + } + + const auto bpp = static_cast(bytes_per_pixel(desc.format)); + if (bpp == 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("texture format is not supported")); + } + + const auto width = static_cast(desc.extent.width); + const auto height = static_cast(desc.extent.height); + if (width > std::numeric_limits::max() / height) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("texture size overflows uint64")); + } + + const auto pixels = width * height; + if (pixels > std::numeric_limits::max() / bpp) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("texture byte size overflows uint64")); + } + + const auto bytes = pixels * bpp; + if (bytes > max_texture_bytes) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("texture byte size exceeds the configured limit")); + } + + return pp::foundation::Result::success(bytes); +} + +pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept +{ + const auto extent_status = validate_extent(desc.extent); + if (!extent_status.ok()) { + return extent_status; + } + + if (region.width == 0 || region.height == 0) { + return pp::foundation::Status::invalid_argument("readback region must be greater than zero"); + } + + if (region.x > desc.extent.width || region.y > desc.extent.height) { + return pp::foundation::Status::out_of_range("readback origin is outside the texture"); + } + + if (region.width > desc.extent.width - region.x || region.height > desc.extent.height - region.y) { + return pp::foundation::Status::out_of_range("readback region exceeds texture bounds"); + } + + return pp::foundation::Status::success(); +} + +const char* texture_format_name(TextureFormat format) noexcept +{ + switch (format) { + case TextureFormat::rgba8: + return "rgba8"; + case TextureFormat::r8: + return "r8"; + case TextureFormat::depth24_stencil8: + return "depth24_stencil8"; + } + + return "unknown"; +} + +} diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h new file mode 100644 index 0000000..1420676 --- /dev/null +++ b/src/renderer_api/renderer_api.h @@ -0,0 +1,60 @@ +#pragma once + +#include "foundation/result.h" + +#include + +namespace pp::renderer { + +constexpr std::uint32_t max_texture_dimension = 32768; +constexpr std::uint64_t max_texture_bytes = 1024ULL * 1024ULL * 1024ULL; + +enum class TextureFormat : std::uint8_t { + rgba8, + r8, + depth24_stencil8, +}; + +struct Extent2D { + std::uint32_t width = 0; + std::uint32_t height = 0; +}; + +struct TextureDesc { + Extent2D extent; + TextureFormat format = TextureFormat::rgba8; + bool render_target = false; +}; + +struct ReadbackRegion { + std::uint32_t x = 0; + std::uint32_t y = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; +}; + +class ITexture2D { +public: + virtual ~ITexture2D() = default; + [[nodiscard]] virtual TextureDesc desc() const noexcept = 0; +}; + +class IReadbackBuffer { +public: + virtual ~IReadbackBuffer() = default; + [[nodiscard]] virtual std::uint64_t size_bytes() const noexcept = 0; +}; + +class IRenderTrace { +public: + virtual ~IRenderTrace() = default; + virtual void marker(const char* component, const char* name) noexcept = 0; +}; + +[[nodiscard]] std::uint32_t bytes_per_pixel(TextureFormat format) noexcept; +[[nodiscard]] pp::foundation::Status validate_extent(Extent2D extent) 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; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cc0ea09..a59942b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -66,6 +66,16 @@ add_test(NAME pp_document_tests COMMAND pp_document_tests) set_tests_properties(pp_document_tests PROPERTIES LABELS "document;desktop-fast") +add_executable(pp_renderer_api_tests + renderer_api/renderer_api_tests.cpp) +target_link_libraries(pp_renderer_api_tests PRIVATE + pp_renderer_api + pp_test_harness) + +add_test(NAME pp_renderer_api_tests COMMAND pp_renderer_api_tests) +set_tests_properties(pp_renderer_api_tests PROPERTIES + LABELS "renderer;desktop-fast") + if(TARGET pano_cli) add_test(NAME pano_cli_create_document_smoke COMMAND pano_cli create-document --width 64 --height 32 --layers 2) diff --git a/tests/renderer_api/renderer_api_tests.cpp b/tests/renderer_api/renderer_api_tests.cpp new file mode 100644 index 0000000..00376b7 --- /dev/null +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -0,0 +1,86 @@ +#include "renderer_api/renderer_api.h" +#include "test_harness.h" + +#include + +using pp::foundation::StatusCode; +using pp::renderer::Extent2D; +using pp::renderer::ReadbackRegion; +using pp::renderer::TextureDesc; +using pp::renderer::TextureFormat; +using pp::renderer::max_texture_dimension; +using pp::renderer::texture_byte_size; +using pp::renderer::texture_format_name; +using pp::renderer::validate_extent; +using pp::renderer::validate_readback_region; + +namespace { + +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); +} + +} + +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); + return harness.finish(); +}