Add renderer API validation tests
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
105
src/renderer_api/renderer_api.cpp
Normal file
105
src/renderer_api/renderer_api.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "renderer_api/renderer_api.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
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<std::uint64_t> texture_byte_size(TextureDesc desc) noexcept
|
||||
{
|
||||
const auto extent_status = validate_extent(desc.extent);
|
||||
if (!extent_status.ok()) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(extent_status);
|
||||
}
|
||||
|
||||
const auto bpp = static_cast<std::uint64_t>(bytes_per_pixel(desc.format));
|
||||
if (bpp == 0) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("texture format is not supported"));
|
||||
}
|
||||
|
||||
const auto width = static_cast<std::uint64_t>(desc.extent.width);
|
||||
const auto height = static_cast<std::uint64_t>(desc.extent.height);
|
||||
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::out_of_range("texture size overflows uint64"));
|
||||
}
|
||||
|
||||
const auto pixels = width * height;
|
||||
if (pixels > std::numeric_limits<std::uint64_t>::max() / bpp) {
|
||||
return pp::foundation::Result<std::uint64_t>::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<std::uint64_t>::failure(
|
||||
pp::foundation::Status::out_of_range("texture byte size exceeds the configured limit"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::uint64_t>::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";
|
||||
}
|
||||
|
||||
}
|
||||
60
src/renderer_api/renderer_api.h
Normal file
60
src/renderer_api/renderer_api.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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<std::uint64_t> 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;
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
86
tests/renderer_api/renderer_api_tests.cpp
Normal file
86
tests/renderer_api/renderer_api_tests.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include "renderer_api/renderer_api.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user