Add paint renderer compositor tests

This commit is contained in:
2026-06-01 00:13:53 +02:00
parent ac0d0ab49c
commit 4d715afd60
9 changed files with 236 additions and 12 deletions

View File

@@ -114,6 +114,20 @@ target_link_libraries(pp_renderer_api
PRIVATE PRIVATE
pp_project_warnings) 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 add_library(pp_ui_core STATIC
src/ui_core/layout_value.cpp) src/ui_core/layout_value.cpp)
target_include_directories(pp_ui_core target_include_directories(pp_ui_core

View File

@@ -1,7 +1,7 @@
# Build And Platform Inventory # Build And Platform Inventory
Status: live Status: live
Last updated: 2026-05-31 Last updated: 2026-06-01
This inventory records the known build surfaces during the CMake migration. This inventory records the known build surfaces during the CMake migration.
Keep it updated as platform paths move to shared CMake targets. 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 NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865`
- Android arm64 headless configure/build passes through root CMake and the - Android arm64 headless configure/build passes through root CMake and the
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`, `platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_ui_core`, `pano_cli`, and `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
their current headless test binaries. `pp_ui_core`, `pano_cli`, and their current headless test binaries.
- `vcpkg` is not on PATH yet; see DEBT-0007. - `vcpkg` is not on PATH yet; see DEBT-0007.
Known warnings after the current CMake app build: Known warnings after the current CMake app build:

View File

@@ -1,7 +1,7 @@
# PanoPainter Modernization Roadmap # PanoPainter Modernization Roadmap
Status: live Status: live
Last updated: 2026-05-31 Last updated: 2026-06-01
This is the living roadmap for modernizing PanoPainter into independently This is the living roadmap for modernizing PanoPainter into independently
testable C++23 components while retaining all existing functionality. Keep this 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 `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 blend modes. `pp_document` has started with a pure canvas/layer model and
layer invariant tests. `pp_renderer_api` has started with renderer-neutral layer invariant tests. `pp_renderer_api` has started with renderer-neutral
texture/readback descriptors and validation tests. `pp_ui_core` has started texture/readback descriptors and validation tests. `pp_paint_renderer` has
with XML-layout-facing length parsing and invalid input tests. Continue started with deterministic CPU layer compositing over renderer extents using
expanding document behavior toward legacy Canvas parity and then port OpenGL the paint blend reference. `pp_ui_core` has started with XML-layout-facing
classes behind the renderer boundary. 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: Implementation tasks:
@@ -511,11 +513,11 @@ Acceptance for each phase:
## Verified Commands ## Verified Commands
Last verified on 2026-05-31: Last verified on 2026-06-01:
```powershell ```powershell
cmake --preset windows-msvc-default 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 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\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli 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_paint_blend_tests` passed.
- `pp_document_tests` passed. - `pp_document_tests` passed.
- `pp_renderer_api_tests` passed. - `pp_renderer_api_tests` passed.
- `pp_paint_renderer_compositor_tests` passed.
- `pp_ui_core_layout_value_tests` passed. - `pp_ui_core_layout_value_tests` passed.
- `pano_cli_create_document_smoke` passed. - `pano_cli_create_document_smoke` passed.
- `pano_cli_inspect_image_rejects_unsupported` passed as an expected failure - `pano_cli_inspect_image_rejects_unsupported` passed as an expected failure

View File

@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string[]]$Presets = @("android-arm64"), [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" $ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}" preset="${1:-android-arm64}"
shift || true 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)" start="$(date +%s)"
cmake --preset "$preset" cmake --preset "$preset"

View File

@@ -0,0 +1,65 @@
#include "paint_renderer/compositor.h"
#include <limits>
namespace pp::paint_renderer {
namespace {
[[nodiscard]] pp::foundation::Result<std::size_t> 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<std::size_t>::failure(extent_status);
}
const auto width = static_cast<std::uint64_t>(extent.width);
const auto height = static_cast<std::uint64_t>(extent.height);
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("pixel count overflows uint64"));
}
const auto count = width * height;
if (count > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("pixel count exceeds addressable memory"));
}
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(count));
}
}
pp::foundation::Status composite_layer(
std::span<pp::paint::Rgba> 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();
}
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include "foundation/result.h"
#include "paint/blend.h"
#include "renderer_api/renderer_api.h"
#include <span>
namespace pp::paint_renderer {
struct LayerCompositeView {
std::span<const pp::paint::Rgba> 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<pp::paint::Rgba> destination,
pp::renderer::Extent2D extent,
LayerCompositeView layer) noexcept;
}

View File

@@ -76,6 +76,16 @@ add_test(NAME pp_renderer_api_tests COMMAND pp_renderer_api_tests)
set_tests_properties(pp_renderer_api_tests PROPERTIES set_tests_properties(pp_renderer_api_tests PROPERTIES
LABELS "renderer;desktop-fast") 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 add_executable(pp_ui_core_layout_value_tests
ui_core/layout_value_tests.cpp) ui_core/layout_value_tests.cpp)
target_link_libraries(pp_ui_core_layout_value_tests PRIVATE target_link_libraries(pp_ui_core_layout_value_tests PRIVATE

View File

@@ -0,0 +1,109 @@
#include "paint_renderer/compositor.h"
#include "test_harness.h"
#include <cmath>
#include <vector>
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<Rgba> destination {
Rgba { .r = 0.2F, .g = 0.4F, .b = 0.6F, .a = 0.5F },
};
const std::vector<Rgba> 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<Rgba> destination { original };
const std::vector<Rgba> 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<Rgba> destination(2);
const std::vector<Rgba> 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();
}