#include "pch.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 triangulate_simple(const std::vector& vertices) { std::vector ret; std::vector points(vertices.size()); std::vector 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(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(index)]); } } return ret; } namespace { bool nearly_equal(float a, float b) { return std::fabs(a - b) < 0.0001F; } std::array 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 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 make_polygon_vertices(std::initializer_list points) { std::vector 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 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> bound; std::vector> 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 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 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 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 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 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 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 events; std::vector uploaded_vertices; std::array 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 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 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) { ++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 events; std::vector uploaded_vertices; std::array 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 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 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)); } } struct StrokeFrame { int id = -1; std::array, 6> shapes {}; }; struct DummyFramebuffer { std::vector* 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 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 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 events; std::vector 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 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) { events.emplace_back("execute:" + std::to_string(frame.id) + ":" + std::to_string(face_index)); return glm::vec4( static_cast(frame.id), static_cast(face_index), static_cast(frame.id + face_index), static_cast(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 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 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 accumulated_dirty_boxes; std::array 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 include_in_committed_dirty_box { true, false, true, true, true, true }; std::array committed_dirty_faces {}; std::array pass_dirty_faces {}; std::vector events; std::vector finish_seen_accumulated; std::vector 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) { events.emplace_back("begin-face:" + std::to_string(frame.id) + ":" + std::to_string(face_index)); }, [&](StrokeFrame& frame, int face_index, std::span) { 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, 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, 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 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 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 { .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& 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 { 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 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 { .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& brush_quad, bool, glm::mat4) { return std::array { 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 frames { frame }; std::array accumulated_dirty_boxes; std::array 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 include_in_committed_dirty_box { true, true, false, true, true, true }; std::array committed_dirty_faces {}; std::array pass_dirty_faces {}; std::vector events; std::array 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) { events.push_back("prepare:" + std::to_string(face_index)); }, [&](StrokeFrame&, int face_index, std::span) { 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 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 frames { frame }; std::array accumulated_dirty_boxes; std::array 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 include_in_committed_dirty_box { true, true, true, true, true, true }; std::array committed_dirty_faces {}; std::array pass_dirty_faces {}; std::vector events; std::array 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 vertices) { events.push_back( "prepare:" + std::to_string(face_index) + ":" + std::to_string(vertices.size())); }, [&](StrokeFrame&, int face_index, std::span) { events.push_back("execute:" + std::to_string(face_index)); return glm::vec4( static_cast(face_index + 1), static_cast(face_index + 2), static_cast(face_index + 3), static_cast(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 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 dirty_faces { true, false, true }; const std::array 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 events; std::vector 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(faces), .copy_stroke_destination = true, .upload_pad_vertices = [&](std::span 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 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 events; std::array 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 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 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 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 events; std::array 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 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 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 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 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 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 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 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_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(); }