Add retained stroke execution helper tests

This commit is contained in:
2026-06-13 10:39:44 +02:00
parent 0a5e7302bc
commit 8acf79dbda
4 changed files with 416 additions and 0 deletions

View File

@@ -3123,6 +3123,10 @@ Results:
texture-input dispatch now shares retained stroke execution helpers, while texture-input dispatch now shares retained stroke execution helpers, while
concrete GL object mapping, framebuffer ownership, shader timing, and final concrete GL object mapping, framebuffer ownership, shader timing, and final
draw execution remain in the legacy Canvas path. draw execution remain in the legacy Canvas path.
- `pp_paint_renderer_stroke_execution_tests` now covers retained stroke texture
input binding order, sample execution destination-copy behavior, live-pass
face-framebuffer dirty tracking, and pad-face destination-copy behavior
without depending on the broader compositor test translation unit.
- `Canvas::stroke_draw` pad-pass destination bind/copy/unbind ordering now - `Canvas::stroke_draw` pad-pass destination bind/copy/unbind ordering now
shares the retained stroke execution helper callback surface, while shader shares the retained stroke execution helper callback surface, while shader
setup, pad color selection, framebuffer ownership, and final OpenGL draw setup, pad color selection, framebuffer ownership, and final OpenGL draw

View File

@@ -509,6 +509,12 @@ Done Checks:
Progress Notes: Progress Notes:
- 2026-06-13: Added `pp_paint_renderer_stroke_execution_tests` as a dedicated
retained stroke execution-helper target covering texture-input binding order,
sample destination-copy behavior, live-pass face-framebuffer dirty tracking,
and pad-face destination-copy behavior. Next test slice should extend this
target toward the newer sampler-dispatch helpers or preview-side retained
execution helpers.
- 2026-06-13: `Canvas::stroke_draw` live-pass sampler bind/unbind plus - 2026-06-13: `Canvas::stroke_draw` live-pass sampler bind/unbind plus
semantic texture-input dispatch now routes through retained stroke execution semantic texture-input dispatch now routes through retained stroke execution
helpers; concrete GL object mapping, framebuffer ownership, shader timing, helpers; concrete GL object mapping, framebuffer ownership, shader timing,

View File

@@ -277,6 +277,30 @@ add_test(NAME pp_paint_renderer_compositor_tests COMMAND pp_paint_renderer_compo
set_tests_properties(pp_paint_renderer_compositor_tests PROPERTIES set_tests_properties(pp_paint_renderer_compositor_tests PROPERTIES
LABELS "renderer;paint;desktop-fast") LABELS "renderer;paint;desktop-fast")
add_executable(pp_paint_renderer_stroke_execution_tests
paint_renderer/stroke_execution_tests.cpp)
target_link_libraries(pp_paint_renderer_stroke_execution_tests PRIVATE
pp_paint_renderer
pp_test_harness)
target_include_directories(pp_paint_renderer_stroke_execution_tests PRIVATE
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_paint_renderer_stroke_execution_tests PRIVATE
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
target_precompile_headers(pp_paint_renderer_stroke_execution_tests PRIVATE
"${PROJECT_SOURCE_DIR}/src/pch.h")
add_test(NAME pp_paint_renderer_stroke_execution_tests COMMAND pp_paint_renderer_stroke_execution_tests)
set_tests_properties(pp_paint_renderer_stroke_execution_tests PROPERTIES
LABELS "renderer;paint;desktop-fast")
add_executable(pp_platform_api_tests add_executable(pp_platform_api_tests
platform_api/platform_services_tests.cpp) platform_api/platform_services_tests.cpp)
target_link_libraries(pp_platform_api_tests PRIVATE target_link_libraries(pp_platform_api_tests PRIVATE

View File

@@ -0,0 +1,382 @@
#include "pch.h"
#include <algorithm>
#include <array>
#include <cfloat>
#include <condition_variable>
#include <cstdio>
#include <cmath>
#include <cstdint>
#include <deque>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <random>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "legacy_canvas_stroke_execution_services.h"
#include "test_harness.h"
using pp::panopainter::LegacyCanvasStrokePadExecutionRequest;
using pp::panopainter::LegacyCanvasStrokePadFace;
using pp::panopainter::LegacyCanvasStrokeTextureBinding;
using pp::panopainter::LegacyCanvasStrokeTextureInput;
using pp::panopainter::LegacyStrokeSampleExecutionRequest;
namespace {
bool nearly_equal(float a, float b)
{
return std::fabs(a - b) < 0.0001F;
}
std::array<vertex_t, 4> make_quad_vertices()
{
return {
vertex_t(glm::vec2(10.0F, 20.0F)),
vertex_t(glm::vec2(10.0F, 30.0F)),
vertex_t(glm::vec2(30.0F, 30.0F)),
vertex_t(glm::vec2(30.0F, 20.0F)),
};
}
std::array<pp::paint_renderer::CanvasStrokePoint, 4> make_sample_points()
{
return {
pp::paint_renderer::CanvasStrokePoint { .x = 10.0F, .y = 20.0F },
pp::paint_renderer::CanvasStrokePoint { .x = 10.0F, .y = 30.0F },
pp::paint_renderer::CanvasStrokePoint { .x = 30.0F, .y = 30.0F },
pp::paint_renderer::CanvasStrokePoint { .x = 30.0F, .y = 20.0F },
};
}
void retained_stroke_texture_inputs_bind_and_unbind_in_declared_order(pp::tests::Harness& h)
{
const std::array<LegacyCanvasStrokeTextureBinding, 4> bindings {
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::brush_tip, .slot = 1 },
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::stroke_destination, .slot = 0 },
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::mixer, .slot = 3 },
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::pattern, .slot = 4 },
};
std::vector<std::pair<LegacyCanvasStrokeTextureInput, int>> bound;
std::vector<std::pair<LegacyCanvasStrokeTextureInput, int>> unbound;
pp::panopainter::bind_legacy_canvas_stroke_texture_inputs(
bindings,
[&](LegacyCanvasStrokeTextureInput input, int slot) {
bound.emplace_back(input, slot);
});
pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs(
bindings,
[&](LegacyCanvasStrokeTextureInput input, int slot) {
unbound.emplace_back(input, slot);
});
PP_EXPECT(h, bound.size() == bindings.size());
PP_EXPECT(h, unbound.size() == bindings.size());
for (std::size_t index = 0; index < bindings.size(); ++index) {
PP_EXPECT(h, bound[index].first == bindings[index].input);
PP_EXPECT(h, bound[index].second == bindings[index].slot);
PP_EXPECT(h, unbound[index] == bound[index]);
}
}
void retained_stroke_sample_executor_copies_destination_and_expands_quads(pp::tests::Harness& h)
{
const auto vertices = make_quad_vertices();
const auto sample_points = make_sample_points();
std::vector<std::string> events;
std::vector<vertex_t> uploaded_vertices;
std::array<int, 6> copy_args {};
const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample(
LegacyStrokeSampleExecutionRequest {
.context = "test",
.target_size = glm::vec2(64.0F, 64.0F),
.vertices = vertices,
.sample_points = sample_points,
.copy_stroke_destination = true,
.bind_destination_texture = [&]() { events.emplace_back("bind"); },
.copy_framebuffer_to_destination_texture =
[&](int dst_x, int dst_y, int src_x, int src_y, int width, int height) {
events.emplace_back("copy");
copy_args = { dst_x, dst_y, src_x, src_y, width, height };
},
.unbind_destination_texture = [&]() { events.emplace_back("unbind"); },
.upload_brush_vertices = [&](std::span<const vertex_t> uploaded) {
events.emplace_back("upload");
uploaded_vertices.assign(uploaded.begin(), uploaded.end());
},
.draw_brush_shape = [&]() { events.emplace_back("draw"); },
});
PP_EXPECT(h, result.ok);
PP_EXPECT(h, result.copy_position == glm::ivec2(9, 19));
PP_EXPECT(h, result.copy_size == glm::ivec2(22, 12));
const std::vector<std::string> expected_events { "bind", "copy", "upload", "draw", "unbind" };
PP_EXPECT(h, nearly_equal(result.dirty_bounds.x, 9.0F));
PP_EXPECT(h, nearly_equal(result.dirty_bounds.y, 19.0F));
PP_EXPECT(h, nearly_equal(result.dirty_bounds.z, 31.0F));
PP_EXPECT(h, nearly_equal(result.dirty_bounds.w, 31.0F));
PP_EXPECT(h, events == expected_events);
PP_EXPECT(h, copy_args[0] == 9);
PP_EXPECT(h, copy_args[1] == 19);
PP_EXPECT(h, copy_args[2] == 9);
PP_EXPECT(h, copy_args[3] == 19);
PP_EXPECT(h, copy_args[4] == 22);
PP_EXPECT(h, copy_args[5] == 12);
PP_EXPECT(h, uploaded_vertices.size() == 6U);
PP_EXPECT(h, nearly_equal(uploaded_vertices[0].pos.x, vertices[0].pos.x));
PP_EXPECT(h, nearly_equal(uploaded_vertices[1].pos.y, vertices[1].pos.y));
PP_EXPECT(h, nearly_equal(uploaded_vertices[2].pos.x, vertices[2].pos.x));
PP_EXPECT(h, nearly_equal(uploaded_vertices[3].pos.x, vertices[0].pos.x));
PP_EXPECT(h, nearly_equal(uploaded_vertices[4].pos.x, vertices[2].pos.x));
PP_EXPECT(h, nearly_equal(uploaded_vertices[5].pos.y, vertices[3].pos.y));
}
void retained_stroke_sample_executor_unbinds_and_skips_draw_when_bounds_are_empty(pp::tests::Harness& h)
{
const auto vertices = make_quad_vertices();
int bind_calls = 0;
int copy_calls = 0;
int unbind_calls = 0;
int upload_calls = 0;
int draw_calls = 0;
const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample(
LegacyStrokeSampleExecutionRequest {
.context = "test",
.target_size = glm::vec2(64.0F, 64.0F),
.vertices = vertices,
.sample_points = {},
.copy_stroke_destination = true,
.bind_destination_texture = [&]() { ++bind_calls; },
.copy_framebuffer_to_destination_texture =
[&](int, int, int, int, int, int) { ++copy_calls; },
.unbind_destination_texture = [&]() { ++unbind_calls; },
.upload_brush_vertices = [&](std::span<const vertex_t>) { ++upload_calls; },
.draw_brush_shape = [&]() { ++draw_calls; },
});
PP_EXPECT(h, !result.ok);
PP_EXPECT(h, bind_calls == 1);
PP_EXPECT(h, copy_calls == 0);
PP_EXPECT(h, unbind_calls == 1);
PP_EXPECT(h, upload_calls == 0);
PP_EXPECT(h, draw_calls == 0);
}
struct StrokeFrame {
int id = -1;
std::array<std::vector<vertex_t>, 6> shapes {};
};
struct DummyFramebuffer {
std::vector<std::string>* events = nullptr;
int face_index = -1;
void bindFramebuffer()
{
events->push_back("bind:" + std::to_string(face_index));
}
void unbindFramebuffer()
{
events->push_back("unbind:" + std::to_string(face_index));
}
};
void retained_stroke_live_pass_with_face_framebuffers_preserves_order_and_dirty_tracking(pp::tests::Harness& h)
{
StrokeFrame frame;
frame.id = 7;
frame.shapes[0] = {
vertex_t(glm::vec2(0.0F, 0.0F)),
vertex_t(glm::vec2(1.0F, 0.0F)),
vertex_t(glm::vec2(1.0F, 1.0F)),
};
frame.shapes[2] = {
vertex_t(glm::vec2(2.0F, 2.0F)),
vertex_t(glm::vec2(3.0F, 2.0F)),
vertex_t(glm::vec2(3.0F, 3.0F)),
};
std::array<StrokeFrame, 1> frames { frame };
std::array<glm::vec4, 6> accumulated_dirty_boxes;
std::array<glm::vec4, 6> pass_dirty_boxes;
accumulated_dirty_boxes.fill(glm::vec4(64.0F, 64.0F, 0.0F, 0.0F));
pass_dirty_boxes.fill(glm::vec4(64.0F, 64.0F, 0.0F, 0.0F));
accumulated_dirty_boxes[2] = glm::vec4(7.0F, 8.0F, 9.0F, 10.0F);
pass_dirty_boxes[2] = glm::vec4(20.0F, 21.0F, 22.0F, 23.0F);
std::array<bool, 6> include_in_committed_dirty_box { true, true, false, true, true, true };
std::array<bool, 6> committed_dirty_faces {};
std::array<bool, 6> pass_dirty_faces {};
std::vector<std::string> events;
std::array<DummyFramebuffer, 6> face_framebuffers {};
for (int face_index = 0; face_index < 6; ++face_index) {
face_framebuffers[face_index].events = &events;
face_framebuffers[face_index].face_index = face_index;
}
const auto executed_faces = pp::panopainter::execute_legacy_canvas_stroke_live_pass_with_face_framebuffers(
frames,
pp::renderer::Extent2D { .width = 64, .height = 64 },
accumulated_dirty_boxes,
pass_dirty_boxes,
include_in_committed_dirty_box,
[&](StrokeFrame& current_frame) {
events.push_back("begin-frame:" + std::to_string(current_frame.id));
},
[&](StrokeFrame&, int face_index, std::span<const vertex_t>) {
events.push_back("prepare:" + std::to_string(face_index));
},
[&](StrokeFrame&, int face_index, std::span<const vertex_t>) {
events.push_back("execute:" + std::to_string(face_index));
if (face_index == 0) {
return glm::vec4(1.0F, 2.0F, 3.0F, 4.0F);
}
return glm::vec4(10.0F, 11.0F, 12.0F, 13.0F);
},
face_framebuffers,
true,
committed_dirty_faces,
pass_dirty_faces);
PP_EXPECT(h, executed_faces == 2U);
const std::vector<std::string> expected_events {
"begin-frame:7",
"prepare:0",
"bind:0",
"execute:0",
"unbind:0",
"prepare:2",
"bind:2",
"execute:2",
"unbind:2",
};
PP_EXPECT(h, events == expected_events);
PP_EXPECT(h, accumulated_dirty_boxes[0].x <= 1.0F);
PP_EXPECT(h, accumulated_dirty_boxes[0].y <= 2.0F);
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[0].z, 3.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[0].w, 4.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[0].x, 1.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[0].w, 4.0F));
PP_EXPECT(h, committed_dirty_faces[0]);
PP_EXPECT(h, pass_dirty_faces[0]);
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[2].x, 7.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[2].y, 8.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[2].z, 9.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[2].w, 10.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[2].x, 10.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[2].y, 11.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[2].z, 12.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[2].w, 13.0F));
PP_EXPECT(h, !committed_dirty_faces[2]);
PP_EXPECT(h, pass_dirty_faces[2]);
}
void retained_stroke_pad_executor_copies_destination_for_dirty_faces_only(pp::tests::Harness& h)
{
const std::array<bool, 3> dirty_faces { true, false, true };
const std::array<glm::vec4, 3> pass_dirty_boxes {
glm::vec4(5.0F, 10.0F, 20.0F, 30.0F),
glm::vec4(0.0F, 0.0F, 0.0F, 0.0F),
glm::vec4(60.0F, 15.0F, 70.0F, 25.0F),
};
const auto faces = pp::panopainter::make_legacy_canvas_stroke_pad_faces(dirty_faces, pass_dirty_boxes);
std::vector<std::string> events;
std::vector<pp::paint_renderer::CanvasStrokeCopyRegion> copy_regions;
const auto result = pp::panopainter::execute_legacy_canvas_stroke_pad_faces(
LegacyCanvasStrokePadExecutionRequest {
.context = "test",
.extent = pp::renderer::Extent2D { .width = 100, .height = 80 },
.faces = std::span<const LegacyCanvasStrokePadFace>(faces),
.copy_stroke_destination = true,
.upload_pad_vertices = [&](std::span<const vertex_t> vertices) {
events.emplace_back("upload:" + std::to_string(vertices.size()));
},
.begin_face = [&](int face_index) {
events.emplace_back("begin:" + std::to_string(face_index));
},
.bind_destination_texture = [&](int face_index) {
events.emplace_back("bind:" + std::to_string(face_index));
},
.copy_framebuffer_to_destination_texture =
[&](const pp::paint_renderer::CanvasStrokeCopyRegion& copy_region) {
events.emplace_back("copy");
copy_regions.push_back(copy_region);
},
.unbind_destination_texture = [&](int face_index) {
events.emplace_back("unbind:" + std::to_string(face_index));
},
.draw_pad = [&]() { events.emplace_back("draw"); },
.finish_face = [&](int face_index) {
events.emplace_back("finish:" + std::to_string(face_index));
},
});
PP_EXPECT(h, result.ok);
PP_EXPECT(h, result.padded_faces == 2U);
PP_EXPECT(h, faces[0].index == 0);
PP_EXPECT(h, faces[1].index == 1);
PP_EXPECT(h, faces[2].index == 2);
PP_EXPECT(h, faces[0].dirty);
PP_EXPECT(h, !faces[1].dirty);
PP_EXPECT(h, faces[2].dirty);
const std::vector<std::string> expected_events {
"upload:6",
"begin:0",
"bind:0",
"copy",
"draw",
"unbind:0",
"finish:0",
"upload:6",
"begin:2",
"bind:2",
"copy",
"draw",
"unbind:2",
"finish:2",
};
PP_EXPECT(h, events == expected_events);
PP_EXPECT(h, copy_regions.size() == 2U);
PP_EXPECT(h, copy_regions[0].x == 0);
PP_EXPECT(h, copy_regions[0].y == 0);
PP_EXPECT(h, copy_regions[0].width == 40);
PP_EXPECT(h, copy_regions[0].height == 50);
}
} // namespace
int main()
{
pp::tests::Harness harness;
harness.run(
"retained_stroke_texture_inputs_bind_and_unbind_in_declared_order",
retained_stroke_texture_inputs_bind_and_unbind_in_declared_order);
harness.run(
"retained_stroke_sample_executor_copies_destination_and_expands_quads",
retained_stroke_sample_executor_copies_destination_and_expands_quads);
harness.run(
"retained_stroke_sample_executor_unbinds_and_skips_draw_when_bounds_are_empty",
retained_stroke_sample_executor_unbinds_and_skips_draw_when_bounds_are_empty);
harness.run(
"retained_stroke_live_pass_with_face_framebuffers_preserves_order_and_dirty_tracking",
retained_stroke_live_pass_with_face_framebuffers_preserves_order_and_dirty_tracking);
harness.run(
"retained_stroke_pad_executor_copies_destination_for_dirty_faces_only",
retained_stroke_pad_executor_copies_destination_for_dirty_faces_only);
return harness.finish();
}