From 4d715afd602ec5d6aa7a25d1e89cf4d60da134d7 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 00:13:53 +0200 Subject: [PATCH] Add paint renderer compositor tests --- CMakeLists.txt | 14 +++ docs/modernization/build-inventory.md | 6 +- docs/modernization/roadmap.md | 17 ++-- scripts/automation/platform-build.ps1 | 2 +- scripts/automation/platform-build.sh | 2 +- src/paint_renderer/compositor.cpp | 65 +++++++++++++ src/paint_renderer/compositor.h | 23 +++++ tests/CMakeLists.txt | 10 ++ tests/paint_renderer/compositor_tests.cpp | 109 ++++++++++++++++++++++ 9 files changed, 236 insertions(+), 12 deletions(-) create mode 100644 src/paint_renderer/compositor.cpp create mode 100644 src/paint_renderer/compositor.h create mode 100644 tests/paint_renderer/compositor_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index df54432..d289503 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,6 +114,20 @@ target_link_libraries(pp_renderer_api PRIVATE pp_project_warnings) +add_library(pp_paint_renderer STATIC + src/paint_renderer/compositor.cpp) +target_include_directories(pp_paint_renderer + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(pp_paint_renderer + PUBLIC + pp_foundation + pp_paint + pp_renderer_api + pp_project_options + PRIVATE + pp_project_warnings) + add_library(pp_ui_core STATIC src/ui_core/layout_value.cpp) target_include_directories(pp_ui_core diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index e901c82..d1070fc 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -1,7 +1,7 @@ # Build And Platform Inventory Status: live -Last updated: 2026-05-31 +Last updated: 2026-06-01 This inventory records the known build surfaces during the CMake migration. Keep it updated as platform paths move to shared CMake targets. @@ -76,8 +76,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`, `pp_renderer_api`, `pp_ui_core`, `pano_cli`, and - their current headless test binaries. + `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, + `pp_ui_core`, `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 30b71fe..f0a1aa2 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -1,7 +1,7 @@ # PanoPainter Modernization Roadmap Status: live -Last updated: 2026-05-31 +Last updated: 2026-06-01 This is the living roadmap for modernizing PanoPainter into independently testable C++23 components while retaining all existing functionality. Keep this @@ -308,10 +308,12 @@ 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. `pp_renderer_api` has started with renderer-neutral -texture/readback descriptors and validation tests. `pp_ui_core` has started -with XML-layout-facing length parsing and invalid input tests. Continue -expanding document behavior toward legacy Canvas parity and then port OpenGL -classes behind the renderer boundary. +texture/readback descriptors and validation tests. `pp_paint_renderer` has +started with deterministic CPU layer compositing over renderer extents using +the paint blend reference. `pp_ui_core` has started with XML-layout-facing +length parsing and invalid input tests. Continue expanding document behavior +toward legacy Canvas parity and then port OpenGL classes behind the renderer +boundary. Implementation tasks: @@ -511,11 +513,11 @@ Acceptance for each phase: ## Verified Commands -Last verified on 2026-05-31: +Last verified on 2026-06-01: ```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 pp_renderer_api_tests pp_ui_core_layout_value_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 pp_paint_renderer_compositor_tests pp_ui_core_layout_value_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 @@ -533,6 +535,7 @@ Results: - `pp_paint_blend_tests` passed. - `pp_document_tests` passed. - `pp_renderer_api_tests` passed. +- `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_layout_value_tests` passed. - `pano_cli_create_document_smoke` passed. - `pano_cli_inspect_image_rejects_unsupported` passed as an expected failure diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 4f4a064..8a136d8 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", "pp_renderer_api", "pp_ui_core", "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", "pp_ui_core_layout_value_tests") + [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "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", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests") ) $ErrorActionPreference = "Stop" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index 01e7844..0d7694d 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 pp_renderer_api pp_ui_core 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 pp_ui_core_layout_value_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core 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 pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests}" start="$(date +%s)" cmake --preset "$preset" diff --git a/src/paint_renderer/compositor.cpp b/src/paint_renderer/compositor.cpp new file mode 100644 index 0000000..9ba965e --- /dev/null +++ b/src/paint_renderer/compositor.cpp @@ -0,0 +1,65 @@ +#include "paint_renderer/compositor.h" + +#include + +namespace pp::paint_renderer { + +namespace { + +[[nodiscard]] pp::foundation::Result expected_pixel_count(pp::renderer::Extent2D extent) noexcept +{ + const auto extent_status = pp::renderer::validate_extent(extent); + if (!extent_status.ok()) { + return pp::foundation::Result::failure(extent_status); + } + + const auto width = static_cast(extent.width); + const auto height = static_cast(extent.height); + if (width > std::numeric_limits::max() / height) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("pixel count overflows uint64")); + } + + const auto count = width * height; + if (count > static_cast(std::numeric_limits::max())) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("pixel count exceeds addressable memory")); + } + + return pp::foundation::Result::success(static_cast(count)); +} + +} + +pp::foundation::Status composite_layer( + std::span destination, + pp::renderer::Extent2D extent, + LayerCompositeView layer) noexcept +{ + const auto pixel_count = expected_pixel_count(extent); + if (!pixel_count) { + return pixel_count.status(); + } + + if (destination.size() != pixel_count.value() || layer.pixels.size() != pixel_count.value()) { + return pp::foundation::Status::invalid_argument("composite buffers must match the render extent"); + } + + if (layer.opacity < 0.0F || layer.opacity > 1.0F) { + return pp::foundation::Status::out_of_range("layer opacity must be between 0 and 1"); + } + + if (!layer.visible || layer.opacity == 0.0F) { + return pp::foundation::Status::success(); + } + + for (std::size_t i = 0; i < destination.size(); ++i) { + auto stroke = layer.pixels[i]; + stroke.a *= layer.opacity; + destination[i] = pp::paint::blend_pixels(destination[i], stroke, layer.blend_mode); + } + + return pp::foundation::Status::success(); +} + +} diff --git a/src/paint_renderer/compositor.h b/src/paint_renderer/compositor.h new file mode 100644 index 0000000..735b7f2 --- /dev/null +++ b/src/paint_renderer/compositor.h @@ -0,0 +1,23 @@ +#pragma once + +#include "foundation/result.h" +#include "paint/blend.h" +#include "renderer_api/renderer_api.h" + +#include + +namespace pp::paint_renderer { + +struct LayerCompositeView { + std::span pixels; + float opacity = 1.0F; + bool visible = true; + pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal; +}; + +[[nodiscard]] pp::foundation::Status composite_layer( + std::span destination, + pp::renderer::Extent2D extent, + LayerCompositeView layer) noexcept; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f24bab5..75593f9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -76,6 +76,16 @@ add_test(NAME pp_renderer_api_tests COMMAND pp_renderer_api_tests) set_tests_properties(pp_renderer_api_tests PROPERTIES LABELS "renderer;desktop-fast") +add_executable(pp_paint_renderer_compositor_tests + paint_renderer/compositor_tests.cpp) +target_link_libraries(pp_paint_renderer_compositor_tests PRIVATE + pp_paint_renderer + pp_test_harness) + +add_test(NAME pp_paint_renderer_compositor_tests COMMAND pp_paint_renderer_compositor_tests) +set_tests_properties(pp_paint_renderer_compositor_tests PROPERTIES + LABELS "renderer;paint;desktop-fast") + add_executable(pp_ui_core_layout_value_tests ui_core/layout_value_tests.cpp) target_link_libraries(pp_ui_core_layout_value_tests PRIVATE diff --git a/tests/paint_renderer/compositor_tests.cpp b/tests/paint_renderer/compositor_tests.cpp new file mode 100644 index 0000000..8ffb977 --- /dev/null +++ b/tests/paint_renderer/compositor_tests.cpp @@ -0,0 +1,109 @@ +#include "paint_renderer/compositor.h" +#include "test_harness.h" + +#include +#include + +using pp::foundation::StatusCode; +using pp::paint::BlendMode; +using pp::paint::Rgba; +using pp::paint_renderer::LayerCompositeView; +using pp::paint_renderer::composite_layer; +using pp::renderer::Extent2D; + +namespace { + +bool near(float a, float b) +{ + return std::fabs(a - b) < 0.0001F; +} + +void composites_visible_layer_with_opacity(pp::tests::Harness& h) +{ + std::vector destination { + Rgba { .r = 0.2F, .g = 0.4F, .b = 0.6F, .a = 0.5F }, + }; + const std::vector foreground { + Rgba { .r = 0.8F, .g = 0.2F, .b = 0.1F, .a = 0.5F }, + }; + + const auto status = composite_layer( + destination, + Extent2D { .width = 1, .height = 1 }, + LayerCompositeView { + .pixels = foreground, + .opacity = 0.5F, + .visible = true, + .blend_mode = BlendMode::normal, + }); + + PP_EXPECT(h, status.ok()); + PP_EXPECT(h, near(destination[0].a, 0.625F)); + PP_EXPECT(h, near(destination[0].r, 0.44F)); + PP_EXPECT(h, near(destination[0].g, 0.32F)); + PP_EXPECT(h, near(destination[0].b, 0.4F)); +} + +void invisible_and_zero_opacity_layers_are_noops(pp::tests::Harness& h) +{ + const Rgba original { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 0.4F }; + std::vector destination { original }; + const std::vector foreground { + Rgba { .r = 1.0F, .g = 1.0F, .b = 1.0F, .a = 1.0F }, + }; + + PP_EXPECT(h, composite_layer( + destination, + Extent2D { .width = 1, .height = 1 }, + LayerCompositeView { .pixels = foreground, .opacity = 1.0F, .visible = false }).ok()); + PP_EXPECT(h, near(destination[0].r, original.r)); + PP_EXPECT(h, near(destination[0].g, original.g)); + PP_EXPECT(h, near(destination[0].b, original.b)); + PP_EXPECT(h, near(destination[0].a, original.a)); + + PP_EXPECT(h, composite_layer( + destination, + Extent2D { .width = 1, .height = 1 }, + LayerCompositeView { .pixels = foreground, .opacity = 0.0F, .visible = true }).ok()); + PP_EXPECT(h, near(destination[0].r, original.r)); + PP_EXPECT(h, near(destination[0].g, original.g)); + PP_EXPECT(h, near(destination[0].b, original.b)); + PP_EXPECT(h, near(destination[0].a, original.a)); +} + +void rejects_invalid_sizes_and_opacity(pp::tests::Harness& h) +{ + std::vector destination(2); + const std::vector foreground(1); + + const auto mismatched = composite_layer( + destination, + Extent2D { .width = 2, .height = 1 }, + LayerCompositeView { .pixels = foreground }); + const auto bad_opacity = composite_layer( + destination, + Extent2D { .width = 2, .height = 1 }, + LayerCompositeView { .pixels = destination, .opacity = 1.5F }); + const auto bad_extent = composite_layer( + destination, + Extent2D { .width = 0, .height = 1 }, + LayerCompositeView { .pixels = destination }); + + PP_EXPECT(h, !mismatched.ok()); + PP_EXPECT(h, mismatched.code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_opacity.ok()); + PP_EXPECT(h, bad_opacity.code == StatusCode::out_of_range); + PP_EXPECT(h, !bad_extent.ok()); + PP_EXPECT(h, bad_extent.code == StatusCode::invalid_argument); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("composites_visible_layer_with_opacity", composites_visible_layer_with_opacity); + harness.run("invisible_and_zero_opacity_layers_are_noops", invisible_and_zero_opacity_layers_are_noops); + harness.run("rejects_invalid_sizes_and_opacity", rejects_invalid_sizes_and_opacity); + return harness.finish(); +}