Add paint renderer compositor tests
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
65
src/paint_renderer/compositor.cpp
Normal file
65
src/paint_renderer/compositor.cpp
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
src/paint_renderer/compositor.h
Normal file
23
src/paint_renderer/compositor.h
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
109
tests/paint_renderer/compositor_tests.cpp
Normal file
109
tests/paint_renderer/compositor_tests.cpp
Normal 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user