#include "pch.h" #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 "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 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 }, }; } 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_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); } 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)); } }; 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_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); } } // 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(); }