Files
panopainter/tests/paint_renderer/stroke_execution_tests.cpp

1291 lines
52 KiB
C++

#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 <poly2tri.h>
#include "legacy_canvas_stroke_execution_services.h"
#include "legacy_canvas_stroke_preview_services.h"
#include "test_harness.h"
using pp::panopainter::LegacyCanvasStrokePadExecutionRequest;
using pp::panopainter::LegacyCanvasStrokePadFace;
using pp::panopainter::LegacyCanvasStrokeMixPassPlane;
using pp::panopainter::LegacyCanvasStrokeMixPassRequest;
using pp::panopainter::LegacyCanvasStrokeSamplerDispatch;
using pp::panopainter::LegacyCanvasStrokeTextureBinding;
using pp::panopainter::LegacyCanvasStrokeTextureInputDispatch;
using pp::panopainter::LegacyCanvasStrokeTextureInput;
using pp::panopainter::LegacyStrokePreviewCopySize;
using pp::panopainter::LegacyStrokeSampleExecutionRequest;
std::vector<vertex_t> triangulate_simple(const std::vector<vertex_t>& vertices)
{
std::vector<vertex_t> ret;
std::vector<p2t::Point> points(vertices.size());
std::vector<p2t::Point*> points_ptr(vertices.size());
for (std::size_t i = 0; i < vertices.size(); ++i)
{
points[i] = { vertices[i].pos.x, vertices[i].pos.y };
points_ptr[i] = &points[i];
}
auto cdt = std::make_unique<p2t::CDT>(points_ptr);
cdt->Triangulate();
const auto triangles = cdt->GetTriangles();
for (auto* triangle : triangles)
{
for (int i = 0; i < 3; ++i)
{
const auto index = std::distance(points.data(), triangle->GetPoint(i));
ret.push_back(vertices[static_cast<std::size_t>(index)]);
}
}
return ret;
}
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 },
};
}
std::vector<vertex_t> make_polygon_vertices(std::initializer_list<glm::vec2> points)
{
std::vector<vertex_t> vertices;
vertices.reserve(points.size());
for (const auto& point : points) {
vertices.emplace_back(point);
}
return vertices;
}
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_texture_dispatch_activates_units_and_routes_per_input(pp::tests::Harness& h)
{
const std::array<LegacyCanvasStrokeTextureBinding, 4> bindings {
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::brush_tip, .slot = 2 },
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::stroke_destination, .slot = 0 },
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::pattern, .slot = 4 },
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::mixer, .slot = 1 },
};
std::vector<std::string> events;
const LegacyCanvasStrokeTextureInputDispatch dispatch {
.activate_texture_unit = [&](int slot) { events.emplace_back("activate:" + std::to_string(slot)); },
.bind_brush_tip = [&]() { events.emplace_back("bind:brush_tip"); },
.unbind_brush_tip = [&]() { events.emplace_back("unbind:brush_tip"); },
.bind_stroke_destination = [&]() { events.emplace_back("bind:stroke_destination"); },
.unbind_stroke_destination = [&]() { events.emplace_back("unbind:stroke_destination"); },
.bind_pattern = [&]() { events.emplace_back("bind:pattern"); },
.unbind_pattern = [&]() { events.emplace_back("unbind:pattern"); },
.bind_mixer = [&]() { events.emplace_back("bind:mixer"); },
.unbind_mixer = [&]() { events.emplace_back("unbind:mixer"); },
};
pp::panopainter::bind_legacy_canvas_stroke_texture_input(
LegacyCanvasStrokeTextureInput::brush_tip,
dispatch);
pp::panopainter::unbind_legacy_canvas_stroke_texture_input(
LegacyCanvasStrokeTextureInput::brush_tip,
dispatch);
pp::panopainter::bind_legacy_canvas_stroke_texture_inputs(bindings, dispatch);
pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs(bindings, dispatch);
const std::vector<std::string> expected_events {
"bind:brush_tip",
"unbind:brush_tip",
"activate:2",
"bind:brush_tip",
"activate:0",
"bind:stroke_destination",
"activate:4",
"bind:pattern",
"activate:1",
"bind:mixer",
"activate:2",
"unbind:brush_tip",
"activate:0",
"unbind:stroke_destination",
"activate:4",
"unbind:pattern",
"activate:1",
"unbind:mixer",
};
PP_EXPECT(h, events == expected_events);
}
void retained_stroke_sampler_dispatch_routes_bind_and_unbind_per_input(pp::tests::Harness& h)
{
const std::array<LegacyCanvasStrokeTextureBinding, 4> bindings {
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::brush_tip, .slot = 3 },
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::stroke_destination, .slot = 1 },
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::pattern, .slot = 7 },
LegacyCanvasStrokeTextureBinding { .input = LegacyCanvasStrokeTextureInput::mixer, .slot = 5 },
};
std::vector<std::string> events;
const LegacyCanvasStrokeSamplerDispatch dispatch {
.bind_brush_tip_sampler = [&](int slot) { events.emplace_back("bind:brush_tip:" + std::to_string(slot)); },
.unbind_brush_tip_sampler = [&]() { events.emplace_back("unbind:brush_tip"); },
.bind_stroke_destination_sampler =
[&](int slot) { events.emplace_back("bind:stroke_destination:" + std::to_string(slot)); },
.unbind_stroke_destination_sampler = [&]() { events.emplace_back("unbind:stroke_destination"); },
.bind_pattern_sampler = [&](int slot) { events.emplace_back("bind:pattern:" + std::to_string(slot)); },
.unbind_pattern_sampler = [&]() { events.emplace_back("unbind:pattern"); },
.bind_mixer_sampler = [&](int slot) { events.emplace_back("bind:mixer:" + std::to_string(slot)); },
.unbind_mixer_sampler = [&]() { events.emplace_back("unbind:mixer"); },
};
pp::panopainter::bind_legacy_canvas_stroke_sampler_input(
LegacyCanvasStrokeTextureInput::mixer,
9,
dispatch);
pp::panopainter::unbind_legacy_canvas_stroke_sampler_input(
LegacyCanvasStrokeTextureInput::mixer,
dispatch);
pp::panopainter::bind_legacy_canvas_stroke_sampler_inputs(bindings, dispatch);
pp::panopainter::unbind_legacy_canvas_stroke_sampler_inputs(bindings, dispatch);
const std::vector<std::string> expected_events {
"bind:mixer:9",
"unbind:mixer",
"bind:brush_tip:3",
"bind:stroke_destination:1",
"bind:pattern:7",
"bind:mixer:5",
"unbind:brush_tip",
"unbind:stroke_destination",
"unbind:pattern",
"unbind:mixer",
};
PP_EXPECT(h, events == expected_events);
}
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);
}
void retained_stroke_face_sample_polygon_triangulates_and_forwards_face_callbacks(pp::tests::Harness& h)
{
const auto polygon_vertices = make_polygon_vertices({
glm::vec2(10.0F, 10.0F),
glm::vec2(30.0F, 10.0F),
glm::vec2(35.0F, 20.0F),
glm::vec2(20.0F, 35.0F),
glm::vec2(5.0F, 20.0F),
});
std::vector<std::string> events;
std::vector<vertex_t> uploaded_vertices;
std::array<int, 7> copy_args {};
int upload_face_index = -1;
int draw_face_index = -1;
const auto result = pp::panopainter::execute_legacy_canvas_stroke_face_sample_polygon(
pp::panopainter::LegacyStrokeFaceSamplePolygonExecutionRequest {
.context = "test",
.target_size = glm::vec2(64.0F, 64.0F),
.polygon_vertices = polygon_vertices,
.face_index = 4,
.copy_stroke_destination = true,
.bind_destination_texture = [&](int face_index) {
events.emplace_back("bind:" + std::to_string(face_index));
},
.copy_framebuffer_to_destination_texture =
[&](int face_index, int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
events.emplace_back("copy:" + std::to_string(face_index));
copy_args = { face_index, src_x, src_y, dst_x, dst_y, width, height };
},
.unbind_destination_texture = [&](int face_index) {
events.emplace_back("unbind:" + std::to_string(face_index));
},
.upload_brush_vertices = [&](int face_index, std::span<const vertex_t> vertices) {
events.emplace_back("upload:" + std::to_string(face_index) + ":" + std::to_string(vertices.size()));
upload_face_index = face_index;
uploaded_vertices.assign(vertices.begin(), vertices.end());
},
.draw_brush_shape = [&](int face_index) {
events.emplace_back("draw:" + std::to_string(face_index));
draw_face_index = face_index;
},
});
PP_EXPECT(h, result.ok);
PP_EXPECT(h, result.copy_position == glm::ivec2(4, 9));
PP_EXPECT(h, result.copy_size == glm::ivec2(32, 27));
PP_EXPECT(h, nearly_equal(result.dirty_bounds.x, 4.0F));
PP_EXPECT(h, nearly_equal(result.dirty_bounds.y, 9.0F));
PP_EXPECT(h, nearly_equal(result.dirty_bounds.z, 36.0F));
PP_EXPECT(h, nearly_equal(result.dirty_bounds.w, 36.0F));
PP_EXPECT(h, upload_face_index == 4);
PP_EXPECT(h, draw_face_index == 4);
const std::vector<std::string> expected_events {
"bind:4",
"copy:4",
"upload:4:9",
"draw:4",
"unbind:4",
};
PP_EXPECT(h, events == expected_events);
PP_EXPECT(h, copy_args[0] == 4);
PP_EXPECT(h, copy_args[1] == 4);
PP_EXPECT(h, copy_args[2] == 9);
PP_EXPECT(h, copy_args[3] == 4);
PP_EXPECT(h, copy_args[4] == 9);
PP_EXPECT(h, copy_args[5] == 32);
PP_EXPECT(h, copy_args[6] == 27);
PP_EXPECT(h, uploaded_vertices.size() == 9U);
const auto expected_triangulated_vertices = make_polygon_vertices({
glm::vec2(5.0F, 20.0F),
glm::vec2(10.0F, 10.0F),
glm::vec2(20.0F, 35.0F),
glm::vec2(20.0F, 35.0F),
glm::vec2(10.0F, 10.0F),
glm::vec2(30.0F, 10.0F),
glm::vec2(20.0F, 35.0F),
glm::vec2(30.0F, 10.0F),
glm::vec2(35.0F, 20.0F),
});
PP_EXPECT(h, uploaded_vertices.size() == expected_triangulated_vertices.size());
for (std::size_t index = 0; index < uploaded_vertices.size(); ++index) {
PP_EXPECT(h, nearly_equal(uploaded_vertices[index].pos.x, expected_triangulated_vertices[index].pos.x));
PP_EXPECT(h, nearly_equal(uploaded_vertices[index].pos.y, expected_triangulated_vertices[index].pos.y));
}
}
void retained_stroke_face_sample_polygon_skips_destination_callbacks_when_copy_is_disabled(pp::tests::Harness& h)
{
const auto polygon_vertices = make_polygon_vertices({
glm::vec2(8.0F, 9.0F),
glm::vec2(18.0F, 9.0F),
glm::vec2(13.0F, 21.0F),
});
int bind_face_index = -1;
int copy_face_index = -1;
int unbind_face_index = -1;
int upload_face_index = -1;
int draw_face_index = -1;
int bind_calls = 0;
int copy_calls = 0;
int unbind_calls = 0;
std::size_t uploaded_vertex_count = 0;
const auto result = pp::panopainter::execute_legacy_canvas_stroke_face_sample_polygon(
pp::panopainter::LegacyStrokeFaceSamplePolygonExecutionRequest {
.context = "test",
.target_size = glm::vec2(64.0F, 64.0F),
.polygon_vertices = polygon_vertices,
.face_index = 3,
.copy_stroke_destination = false,
.bind_destination_texture = [&](int face_index) {
++bind_calls;
bind_face_index = face_index;
},
.copy_framebuffer_to_destination_texture =
[&](int face_index, int, int, int, int, int, int) {
++copy_calls;
copy_face_index = face_index;
},
.unbind_destination_texture = [&](int face_index) {
++unbind_calls;
unbind_face_index = face_index;
},
.upload_brush_vertices = [&](int face_index, std::span<const vertex_t> vertices) {
upload_face_index = face_index;
uploaded_vertex_count = vertices.size();
},
.draw_brush_shape = [&](int face_index) {
draw_face_index = face_index;
},
});
PP_EXPECT(h, result.ok);
PP_EXPECT(h, bind_calls == 0);
PP_EXPECT(h, copy_calls == 0);
PP_EXPECT(h, unbind_calls == 0);
PP_EXPECT(h, upload_face_index == 3);
PP_EXPECT(h, draw_face_index == 3);
PP_EXPECT(h, uploaded_vertex_count == 3U);
PP_EXPECT(h, bind_face_index == -1);
PP_EXPECT(h, copy_face_index == -1);
PP_EXPECT(h, unbind_face_index == -1);
}
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));
}
};
std::vector<vertex_t> make_face_triangle(float origin_x, float origin_y)
{
return {
vertex_t(glm::vec2(origin_x + 0.0F, origin_y + 0.0F)),
vertex_t(glm::vec2(origin_x + 1.0F, origin_y + 0.0F)),
vertex_t(glm::vec2(origin_x + 0.5F, origin_y + 1.0F)),
};
}
void retained_stroke_frame_samples_preserve_frame_face_callback_order(pp::tests::Harness& h)
{
std::array<StrokeFrame, 2> frames {};
frames[0].id = 11;
frames[0].shapes[1] = make_face_triangle(1.0F, 10.0F);
frames[0].shapes[4] = make_face_triangle(4.0F, 40.0F);
frames[1].id = 12;
frames[1].shapes[0] = make_face_triangle(2.0F, 20.0F);
std::vector<std::string> events;
std::vector<glm::vec4> finish_boxes;
const auto executed_faces = pp::panopainter::execute_legacy_canvas_stroke_frame_samples(
frames,
[&](StrokeFrame& frame) {
events.emplace_back("begin-frame:" + std::to_string(frame.id));
},
[&](StrokeFrame& frame, int face_index, std::span<const vertex_t> vertices) {
events.emplace_back(
"begin-face:" + std::to_string(frame.id) + ":" + std::to_string(face_index) + ":" +
std::to_string(vertices.size()));
},
[&](StrokeFrame& frame, int face_index, std::span<const vertex_t>) {
events.emplace_back("execute:" + std::to_string(frame.id) + ":" + std::to_string(face_index));
return glm::vec4(
static_cast<float>(frame.id),
static_cast<float>(face_index),
static_cast<float>(frame.id + face_index),
static_cast<float>(frame.id + face_index + 1));
},
[&](StrokeFrame& frame, int face_index, glm::vec4 sample_dirty_box) {
events.emplace_back("finish:" + std::to_string(frame.id) + ":" + std::to_string(face_index));
finish_boxes.push_back(sample_dirty_box);
});
PP_EXPECT(h, executed_faces == 3U);
const std::vector<std::string> expected_events {
"begin-frame:11",
"begin-face:11:1:3",
"execute:11:1",
"finish:11:1",
"begin-face:11:4:3",
"execute:11:4",
"finish:11:4",
"begin-frame:12",
"begin-face:12:0:3",
"execute:12:0",
"finish:12:0",
};
PP_EXPECT(h, events == expected_events);
PP_EXPECT(h, finish_boxes.size() == 3U);
PP_EXPECT(h, nearly_equal(finish_boxes[0].x, 11.0F));
PP_EXPECT(h, nearly_equal(finish_boxes[0].y, 1.0F));
PP_EXPECT(h, nearly_equal(finish_boxes[1].x, 11.0F));
PP_EXPECT(h, nearly_equal(finish_boxes[1].y, 4.0F));
PP_EXPECT(h, nearly_equal(finish_boxes[2].x, 12.0F));
PP_EXPECT(h, nearly_equal(finish_boxes[2].y, 0.0F));
}
void retained_stroke_frame_samples_with_dirty_tracking_updates_after_finish(pp::tests::Harness& h)
{
std::array<StrokeFrame, 2> frames {};
frames[0].id = 21;
frames[0].shapes[1] = make_face_triangle(2.0F, 6.0F);
frames[1].id = 22;
frames[1].shapes[4] = make_face_triangle(8.0F, 12.0F);
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[1] = glm::vec4(10.0F, 10.0F, 14.0F, 14.0F);
pass_dirty_boxes[1] = glm::vec4(20.0F, 20.0F, 21.0F, 21.0F);
accumulated_dirty_boxes[4] = glm::vec4(40.0F, 40.0F, 44.0F, 44.0F);
pass_dirty_boxes[4] = glm::vec4(50.0F, 50.0F, 51.0F, 51.0F);
std::array<bool, 6> include_in_committed_dirty_box { true, false, true, true, true, true };
std::array<bool, 6> committed_dirty_faces {};
std::array<bool, 6> pass_dirty_faces {};
std::vector<std::string> events;
std::vector<glm::vec4> finish_seen_accumulated;
std::vector<glm::vec4> finish_seen_pass;
const auto executed_faces = pp::panopainter::execute_legacy_canvas_stroke_frame_samples_with_dirty_tracking(
frames,
pp::renderer::Extent2D { .width = 64, .height = 64 },
accumulated_dirty_boxes,
pass_dirty_boxes,
include_in_committed_dirty_box,
[&](StrokeFrame& frame) {
events.emplace_back("begin-frame:" + std::to_string(frame.id));
},
[&](StrokeFrame& frame, int face_index, std::span<const vertex_t>) {
events.emplace_back("begin-face:" + std::to_string(frame.id) + ":" + std::to_string(face_index));
},
[&](StrokeFrame& frame, int face_index, std::span<const vertex_t>) {
events.emplace_back("execute:" + std::to_string(frame.id) + ":" + std::to_string(face_index));
if (face_index == 1) {
return glm::vec4(1.0F, 2.0F, 5.0F, 6.0F);
}
return glm::vec4(7.0F, 8.0F, 9.0F, 10.0F);
},
[&](StrokeFrame& frame, int face_index, std::span<const vertex_t>, auto& request) {
events.emplace_back("prepare:" + std::to_string(frame.id) + ":" + std::to_string(face_index));
if (face_index == 4) {
request.previous_pass_dirty_box = request.sample_dirty_box;
}
},
[&](StrokeFrame& frame, int face_index, std::span<const vertex_t>, glm::vec4 sample_dirty_box) {
events.emplace_back("finish:" + std::to_string(frame.id) + ":" + std::to_string(face_index));
finish_seen_accumulated.push_back(accumulated_dirty_boxes[face_index]);
finish_seen_pass.push_back(pass_dirty_boxes[face_index]);
if (face_index == 1) {
PP_EXPECT(h, nearly_equal(sample_dirty_box.x, 1.0F));
PP_EXPECT(h, nearly_equal(sample_dirty_box.w, 6.0F));
} else {
PP_EXPECT(h, nearly_equal(sample_dirty_box.x, 7.0F));
PP_EXPECT(h, nearly_equal(sample_dirty_box.w, 10.0F));
}
},
committed_dirty_faces,
pass_dirty_faces);
PP_EXPECT(h, executed_faces == 2U);
const std::vector<std::string> expected_events {
"begin-frame:21",
"begin-face:21:1",
"execute:21:1",
"prepare:21:1",
"finish:21:1",
"begin-frame:22",
"begin-face:22:4",
"execute:22:4",
"prepare:22:4",
"finish:22:4",
};
PP_EXPECT(h, events == expected_events);
PP_EXPECT(h, finish_seen_accumulated.size() == 2U);
PP_EXPECT(h, nearly_equal(finish_seen_accumulated[0].x, 10.0F));
PP_EXPECT(h, nearly_equal(finish_seen_accumulated[0].w, 14.0F));
PP_EXPECT(h, nearly_equal(finish_seen_pass[0].x, 20.0F));
PP_EXPECT(h, nearly_equal(finish_seen_pass[0].w, 21.0F));
PP_EXPECT(h, nearly_equal(finish_seen_accumulated[1].x, 40.0F));
PP_EXPECT(h, nearly_equal(finish_seen_accumulated[1].w, 44.0F));
PP_EXPECT(h, nearly_equal(finish_seen_pass[1].x, 50.0F));
PP_EXPECT(h, nearly_equal(finish_seen_pass[1].w, 51.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[1].x, 10.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[1].y, 10.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[1].z, 14.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[1].w, 14.0F));
PP_EXPECT(h, !committed_dirty_faces[1]);
PP_EXPECT(h, pass_dirty_faces[1]);
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[1].x, 1.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[1].y, 2.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[1].z, 21.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[1].w, 21.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[4].x, 7.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[4].y, 8.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[4].z, 44.0F));
PP_EXPECT(h, nearly_equal(accumulated_dirty_boxes[4].w, 44.0F));
PP_EXPECT(h, committed_dirty_faces[4]);
PP_EXPECT(h, pass_dirty_faces[4]);
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[4].x, 7.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[4].y, 8.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[4].z, 9.0F));
PP_EXPECT(h, nearly_equal(pass_dirty_boxes[4].w, 10.0F));
}
void retained_stroke_frame_planner_uses_previous_sample_and_projection_mode(pp::tests::Harness& h)
{
struct FramePlan {
glm::vec4 col;
float flow;
float opacity;
std::array<vertex_t, 4> shapes;
glm::vec4 m_mixer_rect;
};
const auto frames = pp::panopainter::plan_legacy_canvas_stroke_frames(
pp::panopainter::LegacyCanvasStrokeComputeRequest {
.previous_sample = StrokeSample {
.col = { 0.25F, 0.5F, 0.75F },
.pos = { 10.0F, 20.0F, 0.0F },
.origin = { 0.0F, 0.0F, 0.0F },
.scale = { 1.0F, 1.0F },
.size = 4.0F,
.flow = 0.5F,
.opacity = 0.75F,
.angle = 0.0F,
},
.samples = std::array<StrokeSample, 2> {
StrokeSample {
.col = { 1.0F, 0.0F, 0.0F },
.pos = { 14.0F, 26.0F, 0.0F },
.origin = { 0.0F, 0.0F, 0.0F },
.scale = { 1.0F, 1.0F },
.size = 2.0F,
.flow = 0.6F,
.opacity = 0.7F,
.angle = 0.0F,
},
StrokeSample {
.col = { 0.0F, 1.0F, 0.0F },
.pos = { 18.0F, 30.0F, 2.0F },
.origin = { 0.0F, 0.0F, 0.0F },
.scale = { 1.0F, 1.0F },
.size = 6.0F,
.flow = 0.8F,
.opacity = 0.9F,
.angle = 0.0F,
},
},
.zoom = 1.0F,
.mixer_size = glm::vec2(64.0F, 64.0F),
.model_view = glm::mat4(1.0F),
},
[&](std::array<vertex_t, 4>& brush_quad, bool project_3d, glm::mat4 model_view) {
PP_EXPECT(h, !glm::any(glm::isnan(model_view[0])));
for (const auto& vertex : brush_quad) {
PP_EXPECT(h, !glm::any(glm::isnan(vertex.pos)));
}
return std::array<vertex_t, 4> { brush_quad };
},
[](glm::vec4 mixer_rect, glm::vec4 color, float flow, float opacity, auto&& shapes) {
return FramePlan {
.col = color,
.flow = flow,
.opacity = opacity,
.shapes = std::move(shapes),
.m_mixer_rect = mixer_rect,
};
});
PP_EXPECT(h, frames.size() == 2U);
PP_EXPECT(h, nearly_equal(frames[0].col.r, 1.0F));
PP_EXPECT(h, nearly_equal(frames[0].flow, 0.6F));
PP_EXPECT(h, nearly_equal(frames[0].opacity, 0.7F));
PP_EXPECT(h, nearly_equal(frames[1].col.g, 1.0F));
PP_EXPECT(h, nearly_equal(frames[1].flow, 0.8F));
PP_EXPECT(h, nearly_equal(frames[1].opacity, 0.9F));
PP_EXPECT(h, frames[0].m_mixer_rect.z > 0.0F);
PP_EXPECT(h, frames[0].m_mixer_rect.w > 0.0F);
PP_EXPECT(h, frames[1].m_mixer_rect.z > 0.0F);
PP_EXPECT(h, frames[1].m_mixer_rect.w > 0.0F);
}
void retained_stroke_frame_planner_scales_mixer_bounds_with_zoom(pp::tests::Harness& h)
{
struct FramePlan {
glm::vec4 col;
float flow;
float opacity;
std::array<vertex_t, 4> shapes;
glm::vec4 m_mixer_rect;
};
const auto frames = pp::panopainter::plan_legacy_canvas_stroke_frames(
pp::panopainter::LegacyCanvasStrokeComputeRequest {
.previous_sample = StrokeSample {
.col = { 0.0F, 0.0F, 1.0F },
.pos = { 5.0F, 6.0F, 0.0F },
.origin = { 0.0F, 0.0F, 0.0F },
.scale = { 1.0F, 1.0F },
.size = 3.0F,
.flow = 1.0F,
.opacity = 1.0F,
.angle = 0.0F,
},
.samples = std::array<StrokeSample, 1> {
StrokeSample {
.col = { 1.0F, 1.0F, 0.0F },
.pos = { 9.0F, 10.0F, 0.0F },
.origin = { 0.0F, 0.0F, 0.0F },
.scale = { 1.0F, 1.0F },
.size = 4.0F,
.flow = 0.25F,
.opacity = 0.5F,
.angle = 0.0F,
},
},
.zoom = 2.0F,
.mixer_size = glm::vec2(64.0F, 64.0F),
.model_view = glm::mat4(1.0F),
},
[&](std::array<vertex_t, 4>& brush_quad, bool, glm::mat4) {
return std::array<vertex_t, 4> { brush_quad };
},
[](glm::vec4 mixer_rect, glm::vec4 color, float flow, float opacity, auto&& shapes) {
return FramePlan {
.col = color,
.flow = flow,
.opacity = opacity,
.shapes = std::move(shapes),
.m_mixer_rect = mixer_rect,
};
});
PP_EXPECT(h, frames.size() == 1U);
PP_EXPECT(h, nearly_equal(frames[0].col.b, 0.0F));
PP_EXPECT(h, nearly_equal(frames[0].flow, 0.25F));
PP_EXPECT(h, nearly_equal(frames[0].opacity, 0.5F));
PP_EXPECT(h, frames[0].m_mixer_rect.z > 0.0F);
PP_EXPECT(h, frames[0].m_mixer_rect.w > 0.0F);
}
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_live_pass_clears_before_traversal_and_copies_afterwards(pp::tests::Harness& h)
{
StrokeFrame frame;
frame.id = 9;
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));
std::array<bool, 6> include_in_committed_dirty_box { true, true, true, 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;
}
events.emplace_back("clear");
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> vertices) {
events.push_back(
"prepare:" + std::to_string(face_index) + ":" + std::to_string(vertices.size()));
},
[&](StrokeFrame&, int face_index, std::span<const vertex_t>) {
events.push_back("execute:" + std::to_string(face_index));
return glm::vec4(
static_cast<float>(face_index + 1),
static_cast<float>(face_index + 2),
static_cast<float>(face_index + 3),
static_cast<float>(face_index + 4));
},
face_framebuffers,
true,
committed_dirty_faces,
pass_dirty_faces);
pp::panopainter::copy_legacy_stroke_preview_texture(
[&]() { events.emplace_back("bind-preview"); },
[&](int dst_x, int dst_y, int src_x, int src_y, int width, int height) {
events.emplace_back("copy-preview");
PP_EXPECT(h, dst_x == 0);
PP_EXPECT(h, dst_y == 0);
PP_EXPECT(h, src_x == 0);
PP_EXPECT(h, src_y == 0);
PP_EXPECT(h, width == 64);
PP_EXPECT(h, height == 64);
},
LegacyStrokePreviewCopySize { .width = 64, .height = 64 });
PP_EXPECT(h, executed_faces == 2U);
const std::vector<std::string> expected_events {
"clear",
"begin-frame:9",
"prepare:0:3",
"bind:0",
"execute:0",
"unbind:0",
"prepare:2:3",
"bind:2",
"execute:2",
"unbind:2",
"bind-preview",
"copy-preview",
};
PP_EXPECT(h, events == expected_events);
PP_EXPECT(h, committed_dirty_faces[0]);
PP_EXPECT(h, pass_dirty_faces[0]);
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);
}
void retained_stroke_preview_background_capture_preserves_retained_call_order(pp::tests::Harness& h)
{
std::vector<std::string> events;
std::array<int, 6> copy_args {};
pp::panopainter::execute_legacy_stroke_preview_background_capture(
[&]() { events.emplace_back("setup"); },
[&]() { events.emplace_back("draw"); },
[&]() { events.emplace_back("bind"); },
[&](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 };
},
LegacyStrokePreviewCopySize { .width = 48, .height = 32 });
const std::vector<std::string> expected_events { "setup", "draw", "bind", "copy" };
PP_EXPECT(h, events == expected_events);
PP_EXPECT(h, copy_args[0] == 0);
PP_EXPECT(h, copy_args[1] == 0);
PP_EXPECT(h, copy_args[2] == 0);
PP_EXPECT(h, copy_args[3] == 0);
PP_EXPECT(h, copy_args[4] == 48);
PP_EXPECT(h, copy_args[5] == 32);
}
void retained_stroke_preview_final_composite_preserves_retained_call_order(pp::tests::Harness& h)
{
std::vector<std::string> events;
pp::panopainter::execute_legacy_stroke_preview_final_composite(
[&]() { events.emplace_back("setup"); },
[&]() { events.emplace_back("bind-samplers"); },
[&]() { events.emplace_back("bind-inputs"); },
[&]() { events.emplace_back("draw"); });
const std::vector<std::string> expected_events {
"setup",
"bind-samplers",
"bind-inputs",
"draw",
};
PP_EXPECT(h, events == expected_events);
}
void retained_stroke_preview_texture_copy_binds_before_copy(pp::tests::Harness& h)
{
std::vector<std::string> events;
std::array<int, 6> copy_args {};
pp::panopainter::copy_legacy_stroke_preview_texture(
[&]() { events.emplace_back("bind"); },
[&](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 };
},
LegacyStrokePreviewCopySize { .width = 96, .height = 64 });
const std::vector<std::string> expected_events { "bind", "copy" };
PP_EXPECT(h, events == expected_events);
PP_EXPECT(h, copy_args[0] == 0);
PP_EXPECT(h, copy_args[1] == 0);
PP_EXPECT(h, copy_args[2] == 0);
PP_EXPECT(h, copy_args[3] == 0);
PP_EXPECT(h, copy_args[4] == 96);
PP_EXPECT(h, copy_args[5] == 64);
}
void retained_stroke_temporary_composite_preserves_retained_call_order(pp::tests::Harness& h)
{
std::vector<std::string> events;
pp::panopainter::execute_legacy_canvas_stroke_temporary_composite(
[&]() { events.emplace_back("setup"); },
[&]() { events.emplace_back("bind-samplers"); },
[&]() { events.emplace_back("bind-textures"); },
[&]() { events.emplace_back("draw"); },
[&]() { events.emplace_back("unbind-textures"); });
const std::vector<std::string> expected_events {
"setup",
"bind-samplers",
"bind-textures",
"draw",
"unbind-textures",
};
PP_EXPECT(h, events == expected_events);
}
void retained_stroke_mix_pass_skips_inactive_planes_and_preserves_texture_order(pp::tests::Harness& h)
{
const std::array<LegacyCanvasStrokeMixPassPlane, 4> planes {
LegacyCanvasStrokeMixPassPlane { .index = 0, .visible = true, .has_target = true, .opacity = 1.0F },
LegacyCanvasStrokeMixPassPlane { .index = 1, .visible = false, .has_target = true, .opacity = 1.0F },
LegacyCanvasStrokeMixPassPlane { .index = 2, .visible = true, .has_target = false, .opacity = 1.0F },
LegacyCanvasStrokeMixPassPlane { .index = 3, .visible = true, .has_target = true, .opacity = 0.0F },
};
std::vector<std::string> events;
const auto result = pp::panopainter::execute_legacy_canvas_stroke_mix_pass(
LegacyCanvasStrokeMixPassRequest {
.context = "test",
.resolution = glm::vec2(128.0F, 64.0F),
.planes = planes,
.bind_mix_samplers = [&]() { events.emplace_back("bind-samplers"); },
.unbind_mix_samplers = [&]() { events.emplace_back("unbind-samplers"); },
.setup_plane_shader = [&](int plane_index, const glm::mat4&) {
events.emplace_back("setup:" + std::to_string(plane_index));
},
.bind_layer_texture = [&](int plane_index) {
events.emplace_back("bind-layer:" + std::to_string(plane_index));
},
.bind_stroke_texture = [&](int plane_index) {
events.emplace_back("bind-stroke:" + std::to_string(plane_index));
},
.bind_mask_texture = [&](int plane_index) {
events.emplace_back("bind-mask:" + std::to_string(plane_index));
},
.draw_plane = [&]() {
events.emplace_back("draw");
},
.unbind_mask_texture = [&](int plane_index) {
events.emplace_back("unbind-mask:" + std::to_string(plane_index));
},
.unbind_stroke_texture = [&](int plane_index) {
events.emplace_back("unbind-stroke:" + std::to_string(plane_index));
},
.unbind_layer_texture = [&](int plane_index) {
events.emplace_back("unbind-layer:" + std::to_string(plane_index));
},
});
PP_EXPECT(h, result.ok);
PP_EXPECT(h, result.composed_planes == 1U);
const std::vector<std::string> expected_events {
"bind-samplers",
"setup:0",
"bind-layer:0",
"bind-stroke:0",
"bind-mask:0",
"draw",
"unbind-mask:0",
"unbind-stroke:0",
"unbind-layer:0",
"unbind-samplers",
};
PP_EXPECT(h, events == expected_events);
}
void retained_stroke_mix_pass_rejects_incomplete_requests(pp::tests::Harness& h)
{
std::vector<std::string> events;
const auto result = pp::panopainter::execute_legacy_canvas_stroke_mix_pass(
LegacyCanvasStrokeMixPassRequest {
.context = "test",
.resolution = glm::vec2(128.0F, 64.0F),
.bind_mix_samplers = [&]() { events.emplace_back("bind-samplers"); },
});
PP_EXPECT(h, !result.ok);
PP_EXPECT(h, result.composed_planes == 0U);
PP_EXPECT(h, events.empty());
}
} // 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_texture_dispatch_activates_units_and_routes_per_input",
retained_stroke_texture_dispatch_activates_units_and_routes_per_input);
harness.run(
"retained_stroke_sampler_dispatch_routes_bind_and_unbind_per_input",
retained_stroke_sampler_dispatch_routes_bind_and_unbind_per_input);
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_face_sample_polygon_triangulates_and_forwards_face_callbacks",
retained_stroke_face_sample_polygon_triangulates_and_forwards_face_callbacks);
harness.run(
"retained_stroke_face_sample_polygon_skips_destination_callbacks_when_copy_is_disabled",
retained_stroke_face_sample_polygon_skips_destination_callbacks_when_copy_is_disabled);
harness.run(
"retained_stroke_frame_samples_preserve_frame_face_callback_order",
retained_stroke_frame_samples_preserve_frame_face_callback_order);
harness.run(
"retained_stroke_frame_samples_with_dirty_tracking_updates_after_finish",
retained_stroke_frame_samples_with_dirty_tracking_updates_after_finish);
harness.run(
"retained_stroke_frame_planner_uses_previous_sample_and_projection_mode",
retained_stroke_frame_planner_uses_previous_sample_and_projection_mode);
harness.run(
"retained_stroke_frame_planner_scales_mixer_bounds_with_zoom",
retained_stroke_frame_planner_scales_mixer_bounds_with_zoom);
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_live_pass_clears_before_traversal_and_copies_afterwards",
retained_stroke_live_pass_clears_before_traversal_and_copies_afterwards);
harness.run(
"retained_stroke_pad_executor_copies_destination_for_dirty_faces_only",
retained_stroke_pad_executor_copies_destination_for_dirty_faces_only);
harness.run(
"retained_stroke_preview_background_capture_preserves_retained_call_order",
retained_stroke_preview_background_capture_preserves_retained_call_order);
harness.run(
"retained_stroke_preview_final_composite_preserves_retained_call_order",
retained_stroke_preview_final_composite_preserves_retained_call_order);
harness.run(
"retained_stroke_preview_texture_copy_binds_before_copy",
retained_stroke_preview_texture_copy_binds_before_copy);
harness.run(
"retained_stroke_temporary_composite_preserves_retained_call_order",
retained_stroke_temporary_composite_preserves_retained_call_order);
harness.run(
"retained_stroke_mix_pass_skips_inactive_planes_and_preserves_texture_order",
retained_stroke_mix_pass_skips_inactive_planes_and_preserves_texture_order);
harness.run(
"retained_stroke_mix_pass_rejects_incomplete_requests",
retained_stroke_mix_pass_rejects_incomplete_requests);
return harness.finish();
}