diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 959dd93..aada2f1 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -3145,6 +3145,10 @@ Results: background capture ordering, final composite ordering, and preview texture-copy bind-before-copy behavior through `legacy_canvas_stroke_preview_services.h`. +- `pp_paint_renderer_stroke_execution_tests` now also covers direct retained + frame-sample callback ordering and dirty-tracking wrapper timing/state + semantics, including pre-update dirty visibility and + `previous_pass_dirty_box` override behavior. - `pp_paint_renderer_stroke_execution_tests` now also covers retained texture dispatch activation order and sampler-dispatch routing across brush tip, destination, pattern, and mixer helper inputs. diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 2dd9e03..f359ed8 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -509,6 +509,13 @@ Done Checks: Progress Notes: +- 2026-06-13: `pp_paint_renderer_stroke_execution_tests` now also covers + direct retained frame-sample callback ordering plus + `execute_legacy_canvas_stroke_frame_samples_with_dirty_tracking(...)` + timing/state semantics, including pre-update dirty visibility and + `previous_pass_dirty_box` override behavior. Next test slice should target + the next header-level preview live sample ordering surface without reopening + production files. - 2026-06-13: `Canvas::stroke_draw_mix()` now routes visible-plane filtering, retained sampler/texture-slot binding, and final plane draw ordering through `execute_legacy_canvas_stroke_mix_pass(...)`; mixer framebuffer/state setup diff --git a/tests/paint_renderer/stroke_execution_tests.cpp b/tests/paint_renderer/stroke_execution_tests.cpp index b34a5a6..950ef44 100644 --- a/tests/paint_renderer/stroke_execution_tests.cpp +++ b/tests/paint_renderer/stroke_execution_tests.cpp @@ -301,6 +301,183 @@ struct DummyFramebuffer { } }; +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_live_pass_with_face_framebuffers_preserves_order_and_dirty_tracking(pp::tests::Harness& h) { StrokeFrame frame; @@ -627,6 +804,12 @@ int main() 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_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_live_pass_with_face_framebuffers_preserves_order_and_dirty_tracking", retained_stroke_live_pass_with_face_framebuffers_preserves_order_and_dirty_tracking);