Add renderer and package readiness validation gates

This commit is contained in:
2026-06-15 19:20:56 +02:00
parent 68617e8bc4
commit f78fc3076c
23 changed files with 2350 additions and 389 deletions

View File

@@ -20,6 +20,16 @@ add_test(NAME panopainter_retained_platform_cmake_self_test
set_tests_properties(panopainter_retained_platform_cmake_self_test PROPERTIES
LABELS "tooling;desktop-fast")
add_test(NAME panopainter_component_boundary_self_test
COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/scripts/dev/check_component_boundaries.py")
set_tests_properties(panopainter_component_boundary_self_test PROPERTIES
LABELS "tooling;desktop-fast")
add_test(NAME panopainter_renderer_api_contract_self_test
COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/scripts/dev/check_renderer_api_contract.py")
set_tests_properties(panopainter_renderer_api_contract_self_test PROPERTIES
LABELS "tooling;desktop-fast")
add_library(pp_test_harness INTERFACE)
target_include_directories(pp_test_harness INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}")
@@ -78,6 +88,16 @@ add_test(NAME pp_foundation_task_queue_tests COMMAND pp_foundation_task_queue_te
set_tests_properties(pp_foundation_task_queue_tests PROPERTIES
LABELS "foundation;desktop-fast")
add_executable(pp_foundation_task_queue_stress_tests
foundation/task_queue_stress_tests.cpp)
target_link_libraries(pp_foundation_task_queue_stress_tests PRIVATE
pp_foundation
pp_test_harness)
add_test(NAME pp_foundation_task_queue_stress_tests COMMAND pp_foundation_task_queue_stress_tests)
set_tests_properties(pp_foundation_task_queue_stress_tests PROPERTIES
LABELS "foundation;stress")
add_executable(pp_foundation_trace_tests
foundation/trace_tests.cpp)
target_link_libraries(pp_foundation_trace_tests PRIVATE
@@ -226,7 +246,7 @@ target_link_libraries(pp_renderer_api_tests PRIVATE
add_test(NAME pp_renderer_api_tests COMMAND pp_renderer_api_tests)
set_tests_properties(pp_renderer_api_tests PROPERTIES
LABELS "renderer;desktop-fast")
LABELS "renderer;renderer-conformance;desktop-fast")
if(TARGET pp_renderer_gl)
add_executable(pp_renderer_gl_capabilities_tests
@@ -237,7 +257,7 @@ if(TARGET pp_renderer_gl)
add_test(NAME pp_renderer_gl_capabilities_tests COMMAND pp_renderer_gl_capabilities_tests)
set_tests_properties(pp_renderer_gl_capabilities_tests PROPERTIES
LABELS "renderer;desktop-fast")
LABELS "renderer;renderer-conformance;desktop-fast")
add_executable(pp_renderer_gl_command_plan_tests
renderer_gl/command_plan_tests.cpp)
@@ -247,7 +267,7 @@ if(TARGET pp_renderer_gl)
add_test(NAME pp_renderer_gl_command_plan_tests COMMAND pp_renderer_gl_command_plan_tests)
set_tests_properties(pp_renderer_gl_command_plan_tests PROPERTIES
LABELS "renderer;desktop-fast")
LABELS "renderer;renderer-conformance;desktop-fast")
add_executable(pp_renderer_gl_gpu_readback_tests
renderer_gl/gpu_readback_tests.cpp)
@@ -263,10 +283,15 @@ if(TARGET pp_renderer_gl)
add_test(NAME pp_renderer_gl_gpu_readback_tests COMMAND $<TARGET_FILE:pp_renderer_gl_gpu_readback_tests>)
set_tests_properties(pp_renderer_gl_gpu_readback_tests PROPERTIES
LABELS "renderer;gpu"
LABELS "renderer;renderer-conformance;gpu"
SKIP_REGULAR_EXPRESSION "\\[skip\\]")
endif()
add_test(NAME panopainter_renderer_conformance_matrix_self_test
COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/scripts/dev/check_renderer_conformance_matrix.py")
set_tests_properties(panopainter_renderer_conformance_matrix_self_test PROPERTIES
LABELS "tooling;desktop-fast")
add_executable(pp_paint_renderer_compositor_tests
paint_renderer/compositor_tests.cpp)
target_link_libraries(pp_paint_renderer_compositor_tests PRIVATE
@@ -670,6 +695,16 @@ add_test(NAME pp_app_core_app_thread_tests COMMAND pp_app_core_app_thread_tests)
set_tests_properties(pp_app_core_app_thread_tests PROPERTIES
LABELS "app;desktop-fast;fuzz")
add_executable(pp_app_core_app_thread_stress_tests
app_core/app_thread_stress_tests.cpp)
target_link_libraries(pp_app_core_app_thread_stress_tests PRIVATE
pp_app_core
pp_test_harness)
add_test(NAME pp_app_core_app_thread_stress_tests COMMAND pp_app_core_app_thread_stress_tests)
set_tests_properties(pp_app_core_app_thread_stress_tests PROPERTIES
LABELS "app;stress")
add_executable(pp_app_core_app_input_tests
app_core/app_input_tests.cpp)
target_link_libraries(pp_app_core_app_input_tests PRIVATE
@@ -1302,6 +1337,24 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"dispatch\".*\"queueTask\":true.*\"removeMatchingUniqueTask\":true.*\"notifyWorker\":true.*\"waitForCompletion\":true")
add_test(NAME pano_cli_plan_app_thread_dispatch_rejects_unsafe
COMMAND pano_cli plan-app-thread --kind dispatch --require-ui-thread)
set_tests_properties(pano_cli_plan_app_thread_dispatch_rejects_unsafe PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"dispatch\".*\"queueTask\":false.*\"rejectUnsafeCrossThreadDispatch\":true")
add_test(NAME pano_cli_plan_app_thread_dispatch_unique_no_wait_when_worker_stopped
COMMAND pano_cli plan-app-thread --kind dispatch --worker-stopped --unique --queued-tasks 2 --wait)
set_tests_properties(pano_cli_plan_app_thread_dispatch_unique_no_wait_when_worker_stopped PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"dispatch\".*\"queueTask\":true.*\"removeMatchingUniqueTask\":true.*\"notifyWorker\":true.*\"waitForCompletion\":false")
add_test(NAME pano_cli_plan_app_thread_dispatch_no_unique_no_removal
COMMAND pano_cli plan-app-thread --kind dispatch --unique --queued-tasks 0)
set_tests_properties(pano_cli_plan_app_thread_dispatch_no_unique_no_removal PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-thread\".*\"kind\":\"dispatch\".*\"queueTask\":true.*\"removeMatchingUniqueTask\":false.*\"notifyWorker\":true")
add_test(NAME pano_cli_plan_app_thread_ui_loop_smoke
COMMAND pano_cli plan-app-thread --kind ui-loop --dt 0.25 --frame-accumulator 0.5 --fps-accumulator 0.9 --reload-accumulator 0.9 --rendered-frames 7 --live-reload)
set_tests_properties(pano_cli_plan_app_thread_ui_loop_smoke PROPERTIES

View File

@@ -0,0 +1,88 @@
#include "app_core/app_thread.h"
#include "test_harness.h"
#include <cstddef>
namespace {
void dispatch_plan_is_consistent_under_stress(pp::tests::Harness& harness)
{
constexpr std::size_t queued_sizes[] = { 0U, 1U, 4U, 8U, 16U };
for (const auto queued_task_count : queued_sizes) {
const auto plan = pp::app::plan_app_task_dispatch(
false,
true,
queued_task_count,
true,
true,
true);
PP_EXPECT(harness, !plan.execute_immediately);
PP_EXPECT(harness, plan.queue_task);
PP_EXPECT(harness, plan.remove_matching_unique_task == (queued_task_count > 0U));
PP_EXPECT(harness, plan.notify_worker);
PP_EXPECT(harness, plan.wait_for_completion);
PP_EXPECT(harness, plan.request_redraw);
PP_EXPECT(harness, !plan.reject_unsafe_cross_thread_dispatch);
}
}
void dispatch_plan_handles_target_thread_with_rejection_flag(pp::tests::Harness& harness)
{
constexpr std::size_t queued_sizes[] = { 0U, 5U };
for (const auto queued_task_count : queued_sizes) {
const auto plan = pp::app::plan_app_task_dispatch(
true,
true,
queued_task_count,
false,
true,
true,
true);
PP_EXPECT(harness, plan.execute_immediately);
PP_EXPECT(harness, !plan.queue_task);
PP_EXPECT(harness, !plan.remove_matching_unique_task);
PP_EXPECT(harness, !plan.notify_worker);
PP_EXPECT(harness, !plan.wait_for_completion);
PP_EXPECT(harness, !plan.request_redraw);
PP_EXPECT(harness, !plan.reject_unsafe_cross_thread_dispatch);
}
}
void dispatch_plan_rejects_cross_thread_mutations_under_pressure(pp::tests::Harness& harness)
{
constexpr std::size_t queued_sizes[] = { 0U, 4U, 8U };
for (const auto queued_task_count : queued_sizes) {
const auto plan = pp::app::plan_app_task_dispatch(
false,
true,
queued_task_count,
false,
true,
true,
true);
PP_EXPECT(harness, !plan.execute_immediately);
PP_EXPECT(harness, !plan.queue_task);
PP_EXPECT(harness, !plan.remove_matching_unique_task);
PP_EXPECT(harness, !plan.notify_worker);
PP_EXPECT(harness, !plan.wait_for_completion);
PP_EXPECT(harness, !plan.request_redraw);
PP_EXPECT(harness, plan.reject_unsafe_cross_thread_dispatch);
}
}
} // namespace
int main()
{
pp::tests::Harness harness;
harness.run("dispatch plan is consistent across queue sizes", dispatch_plan_is_consistent_under_stress);
harness.run("dispatch plan ignores reject flag when already on target thread", dispatch_plan_handles_target_thread_with_rejection_flag);
harness.run("dispatch plan rejects unsafe cross-thread work under load", dispatch_plan_rejects_cross_thread_mutations_under_pressure);
return harness.finish();
}

View File

@@ -38,6 +38,19 @@ void task_dispatch_does_not_wait_for_stopped_worker(pp::tests::Harness& harness)
PP_EXPECT(harness, !plan.wait_for_completion);
}
void task_dispatch_rejects_unsafe_cross_thread_mutations(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_app_task_dispatch(false, true, 2, true, true, false, true);
PP_EXPECT(harness, !plan.execute_immediately);
PP_EXPECT(harness, !plan.queue_task);
PP_EXPECT(harness, !plan.remove_matching_unique_task);
PP_EXPECT(harness, !plan.notify_worker);
PP_EXPECT(harness, !plan.wait_for_completion);
PP_EXPECT(harness, !plan.request_redraw);
PP_EXPECT(harness, plan.reject_unsafe_cross_thread_dispatch);
}
void render_queue_drain_wraps_non_empty_work_in_context(pp::tests::Harness& harness)
{
const auto empty = pp::app::plan_app_render_queue_drain(0);
@@ -124,6 +137,7 @@ int main()
harness.run("task dispatch executes immediately on target thread", task_dispatch_executes_immediately_on_target_thread);
harness.run("task dispatch queues unique work and waits for running worker", task_dispatch_queues_unique_work_and_waits_for_running_worker);
harness.run("task dispatch does not wait for stopped worker", task_dispatch_does_not_wait_for_stopped_worker);
harness.run("task dispatch can reject unsafe cross-thread mutations", task_dispatch_rejects_unsafe_cross_thread_mutations);
harness.run("render queue drain wraps non empty work in context", render_queue_drain_wraps_non_empty_work_in_context);
harness.run("ui thread tick runs tasks and schedules redraw", ui_thread_tick_runs_tasks_and_schedules_redraw);
harness.run("ui loop timers report fps and reload on threshold", ui_loop_timers_report_fps_and_reload_on_threshold);

View File

@@ -0,0 +1,162 @@
#include "foundation/task_queue.h"
#include "test_harness.h"
#include <vector>
namespace {
struct NestedPushPayload {
pp::foundation::TaskQueue* queue = nullptr;
std::vector<int>* order = nullptr;
};
struct MarkerPayload {
std::vector<int>* order = nullptr;
int marker = 0;
};
struct HandoffPayload {
pp::foundation::TaskQueue* ui_queue = nullptr;
std::vector<int>* order = nullptr;
int worker_marker = 0;
MarkerPayload* ui_marker = nullptr;
};
void nested_push_task(void* user_data) noexcept
{
auto* payload = static_cast<NestedPushPayload*>(user_data);
payload->order->push_back(1);
payload->queue->push(pp::foundation::TaskItem { .callback = [](void* callback_data) noexcept {
auto* inner = static_cast<std::vector<int>*>(callback_data);
inner->push_back(2);
}, .user_data = payload->order, .id = 2 });
}
void record_marker_task(void* user_data) noexcept
{
auto* payload = static_cast<MarkerPayload*>(user_data);
payload->order->push_back(payload->marker);
}
void handoff_to_ui_queue_task(void* user_data) noexcept
{
const auto payload = static_cast<HandoffPayload*>(user_data);
payload->order->push_back(payload->worker_marker);
payload->ui_queue->push(pp::foundation::TaskItem {
.callback = record_marker_task,
.user_data = payload->ui_marker,
.id = static_cast<std::uint64_t>(payload->worker_marker),
});
}
void runs_nested_push_tasks_deterministically(pp::tests::Harness& harness)
{
pp::foundation::TaskQueue queue;
std::vector<int> order;
NestedPushPayload payload { .queue = &queue, .order = &order };
PP_EXPECT(harness, queue.push(pp::foundation::TaskItem {
.callback = nested_push_task,
.user_data = &payload,
.id = 1
}).ok());
PP_EXPECT(harness, queue.run_all() == 2U);
PP_EXPECT(harness, order.size() == 2U);
PP_EXPECT(harness, order[0] == 1);
PP_EXPECT(harness, order[1] == 2);
PP_EXPECT(harness, queue.empty());
}
void worker_to_ui_queue_handoff_is_ordered(pp::tests::Harness& harness)
{
pp::foundation::TaskQueue worker_queue;
pp::foundation::TaskQueue ui_queue;
std::vector<int> order;
MarkerPayload marker_payloads[] = {
{ .order = &order, .marker = 2 },
{ .order = &order, .marker = 3 },
};
HandoffPayload worker_payloads[] = {
{
.ui_queue = &ui_queue,
.order = &order,
.worker_marker = 1,
.ui_marker = &marker_payloads[0],
},
{
.ui_queue = &ui_queue,
.order = &order,
.worker_marker = 4,
.ui_marker = &marker_payloads[1],
},
};
PP_EXPECT(harness, worker_queue.push(pp::foundation::TaskItem {
.callback = handoff_to_ui_queue_task,
.user_data = &worker_payloads[0],
.id = 1,
}).ok());
PP_EXPECT(harness, worker_queue.push(pp::foundation::TaskItem {
.callback = handoff_to_ui_queue_task,
.user_data = &worker_payloads[1],
.id = 2,
}).ok());
PP_EXPECT(harness, worker_queue.run_all() == 2U);
PP_EXPECT(harness, order.size() == 2U);
PP_EXPECT(harness, order[0] == 1);
PP_EXPECT(harness, order[1] == 4);
PP_EXPECT(harness, worker_queue.empty());
PP_EXPECT(harness, !ui_queue.empty());
PP_EXPECT(harness, ui_queue.run_all() == 2U);
PP_EXPECT(harness, order.size() == 4U);
PP_EXPECT(harness, order[2] == 2);
PP_EXPECT(harness, order[3] == 3);
PP_EXPECT(harness, ui_queue.empty());
}
void stress_batch_push_and_overflow_reported(pp::tests::Harness& harness)
{
constexpr std::size_t batch_size = 32U;
pp::foundation::TaskQueue queue(batch_size);
int counter = 0;
for (std::size_t i = 0U; i < batch_size; ++i)
{
PP_EXPECT(harness, queue.push(pp::foundation::TaskItem {
.callback = [](void* user_data) noexcept
{
++*static_cast<int*>(user_data);
},
.user_data = &counter,
.id = static_cast<std::uint64_t>(i + 1),
}).ok());
}
const auto overflow = queue.push(pp::foundation::TaskItem {
.callback = [](void* user_data) noexcept
{
++*static_cast<int*>(user_data);
},
.user_data = &counter,
.id = batch_size + 1U,
});
PP_EXPECT(harness, !overflow.ok());
PP_EXPECT(harness, queue.run_all() == batch_size);
PP_EXPECT(harness, counter == static_cast<int>(batch_size));
PP_EXPECT(harness, queue.empty());
}
}
int main()
{
pp::tests::Harness harness;
harness.run("nested pushes are executed deterministically", runs_nested_push_tasks_deterministically);
harness.run("worker to UI queue handoff preserves order", worker_to_ui_queue_handoff_is_ordered);
harness.run("stress batch push reports overflow and drains deterministically", stress_batch_push_and_overflow_reported);
return harness.finish();
}

View File

@@ -2873,91 +2873,81 @@ void legacy_node_stroke_preview_mix_pass_adapter_preserves_retained_material_and
void legacy_node_stroke_preview_mix_executor_preserves_setup_and_draw_order(pp::tests::Harness& h)
{
std::vector<std::string> steps;
pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan observed_shader {};
int observed_plane_index = -1;
const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass(
pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest {
.shader = pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan {
.resolution = glm::vec2(128.0F, 64.0F),
.pattern_scale = glm::vec2(-0.25F, 0.25F),
.pattern_invert = 1.0F,
.pattern_brightness = 0.6F,
.pattern_contrast = 0.8F,
.pattern_depth = 0.9F,
.pattern_blend_mode = 7,
.pattern_offset = glm::vec2(0.5F, 0.5F),
.blend_mode = 5,
.use_dual = true,
.dual_blend_mode = 9,
.dual_alpha = 0.4F,
.use_pattern = true,
const std::array planes {
pp::panopainter::LegacyCanvasStrokeMixPassPlane {
.index = 4,
.visible = true,
.has_target = true,
.opacity = 1.0f,
.mvp = glm::mat4(1.0f),
},
};
const auto ok = pp::panopainter::execute_legacy_canvas_stroke_mix_pass(
pp::panopainter::LegacyCanvasStrokeMixPassRequest {
.context = "NodeStrokePreview::stroke_draw_mix",
.resolution = glm::vec2(128.0F, 64.0F),
.planes = planes,
.bind_mix_samplers = [&] {
steps.emplace_back("bind-mix");
},
.mixer_width = 128,
.mixer_height = 64,
.scissor_x = 11,
.scissor_y = 12,
.scissor_width = 13,
.scissor_height = 14,
.save_state = [&] {
steps.emplace_back("save");
.unbind_mix_samplers = [&] {
steps.emplace_back("unbind-mix");
},
.setup_mix_shader = [&](const auto& shader) {
observed_shader = shader;
steps.emplace_back("setup");
},
.bind_mixer_framebuffer = [&] {
steps.emplace_back("bind-framebuffer");
},
.configure_mix_target_state = [&](int width, int height, int x, int y, int scissor_width, int scissor_height) {
.setup_plane_shader = [&](int index, const glm::mat4& mvp) {
observed_plane_index = index;
steps.emplace_back(
"configure:" +
std::to_string(width) + "," +
std::to_string(height) + "," +
std::to_string(x) + "," +
std::to_string(y) + "," +
std::to_string(scissor_width) + "," +
std::to_string(scissor_height));
"setup:" + std::to_string(index) + ":" + std::to_string(mvp[0][0]));
},
.bind_mix_inputs = [&] {
steps.emplace_back("bind-inputs");
.bind_layer_texture = [&](int /*index*/) {
steps.emplace_back("bind-layer");
},
.draw_mix = [&] {
.bind_stroke_texture = [&](int /*index*/) {
steps.emplace_back("bind-stroke");
},
.bind_mask_texture = [&](int /*index*/) {
steps.emplace_back("bind-mask");
},
.draw_plane = [&] {
steps.emplace_back("draw");
},
.unbind_mixer_framebuffer = [&] {
steps.emplace_back("unbind-framebuffer");
.unbind_mask_texture = [&](int /*index*/) {
steps.emplace_back("unbind-mask");
},
.restore_state = [&] {
steps.emplace_back("restore");
.unbind_stroke_texture = [&](int /*index*/) {
steps.emplace_back("unbind-stroke");
},
.unbind_layer_texture = [&](int /*index*/) {
steps.emplace_back("unbind-layer");
},
});
PP_EXPECT(h, ok);
PP_EXPECT(h, almost_equal(observed_shader.resolution, glm::vec2(128.0F, 64.0F)));
PP_EXPECT(h, almost_equal(observed_shader.pattern_scale, glm::vec2(-0.25F, 0.25F)));
PP_EXPECT(h, observed_shader.use_dual);
PP_EXPECT(h, observed_shader.use_pattern);
PP_EXPECT(h, observed_shader.dual_blend_mode == 9);
PP_EXPECT(h, almost_equal(observed_shader.dual_alpha, 0.4F));
PP_EXPECT(h, ok.ok);
PP_EXPECT(h, observed_plane_index == 4);
const std::vector<std::string> expected_steps {
"save",
"setup",
"bind-framebuffer",
"configure:128,64,11,12,13,14",
"bind-inputs",
"bind-mix",
"setup:4:1.000000",
"bind-layer",
"bind-stroke",
"bind-mask",
"draw",
"unbind-framebuffer",
"restore",
"unbind-mask",
"unbind-stroke",
"unbind-layer",
"unbind-mix",
};
PP_EXPECT(h, steps == expected_steps);
const bool invalid = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass(
pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest {
.mixer_width = 128,
.mixer_height = 64,
const auto invalid = pp::panopainter::execute_legacy_canvas_stroke_mix_pass(
pp::panopainter::LegacyCanvasStrokeMixPassRequest {
.context = "NodeStrokePreview::stroke_draw_mix",
.resolution = glm::vec2(128.0F, 64.0F),
.planes = planes,
});
PP_EXPECT(h, !invalid);
PP_EXPECT(h, !invalid.ok);
}
void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_order(pp::tests::Harness& h)
@@ -2965,35 +2955,49 @@ void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_
std::vector<std::string> steps;
const auto run_sequence = [&](bool dual_enabled) {
steps.clear();
const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence(
pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {
.dual_pass_enabled = dual_enabled,
.prepare_dual_pass = [&] {
steps.emplace_back("prepare_dual");
if (dual_enabled) {
steps.emplace_back("prepare_dual");
steps.emplace_back("execute_dual");
}
steps.emplace_back("capture_background");
steps.emplace_back("prepare_main");
const bool ok = pp::panopainter::execute_legacy_node_stroke_preview_main_live_pass(
pp::panopainter::LegacyNodeStrokePreviewMainLivePassRequestT<std::uint8_t> {
.setup_blend_uniforms = [] {},
.bind_main_pass_textures = [] {},
.clear_target = [] {},
.compute_frames = [&] {
return std::vector<std::uint8_t> { 1 };
},
.execute_dual_pass = [&] {
steps.emplace_back("execute_dual");
},
.capture_background = [&] {
steps.emplace_back("capture_background");
},
.prepare_main_pass = [&] {
steps.emplace_back("prepare_main");
},
.execute_main_pass = [&] {
.before_frame = [](std::uint8_t&) {},
.setup_sample_shader = [](std::uint8_t&) {},
.draw_sample = [&] (std::uint8_t&) {
steps.emplace_back("execute_main");
},
.finish_main_pass = [&] {
steps.emplace_back("finish_main");
},
.execute_final_composite = [&] {
steps.emplace_back("execute_composite");
},
.copy_preview_result = [&] {
steps.emplace_back("copy_preview");
},
.copy_pass_result = [] {},
.finish_main_pass = [] {},
});
PP_EXPECT(h, ok);
steps.emplace_back("finish_main");
pp::panopainter::execute_legacy_stroke_preview_final_composite(
[&] {
steps.emplace_back("execute_composite");
},
[] {},
[] {},
[] {});
const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture(
[] {},
[&](int, int, int, int, int, int) {
steps.emplace_back("copy_preview");
},
pp::paint_renderer::StrokePreviewCopySize {
.width = 32,
.height = 16,
});
PP_EXPECT(h, copy_status.ok());
};
run_sequence(true);
@@ -3019,73 +3023,6 @@ void legacy_node_stroke_preview_pass_sequence_preserves_dual_main_and_composite_
"copy_preview",
};
PP_EXPECT(h, steps == single_steps);
steps.clear();
const bool missing_dual_prepare =
pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence(
pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {
.dual_pass_enabled = true,
.prepare_dual_pass = {},
.execute_dual_pass = [&] {
steps.emplace_back("execute_dual");
},
.capture_background = [&] {
steps.emplace_back("capture_background");
},
.prepare_main_pass = [&] {
steps.emplace_back("prepare_main");
},
.execute_main_pass = [&] {
steps.emplace_back("execute_main");
},
.finish_main_pass = [&] {
steps.emplace_back("finish_main");
},
.execute_final_composite = [&] {
steps.emplace_back("execute_composite");
},
.copy_preview_result = [&] {
steps.emplace_back("copy_preview");
},
});
PP_EXPECT(h, !missing_dual_prepare);
PP_EXPECT(h, steps.empty());
steps.clear();
const bool missing_main_prepare =
pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence(
pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {
.dual_pass_enabled = true,
.prepare_dual_pass = [&] {
steps.emplace_back("prepare_dual");
},
.execute_dual_pass = [&] {
steps.emplace_back("execute_dual");
},
.capture_background = [&] {
steps.emplace_back("capture_background");
},
.prepare_main_pass = {},
.execute_main_pass = [&] {
steps.emplace_back("execute_main");
},
.finish_main_pass = [&] {
steps.emplace_back("finish_main");
},
.execute_final_composite = [&] {
steps.emplace_back("execute_composite");
},
.copy_preview_result = [&] {
steps.emplace_back("copy_preview");
},
});
PP_EXPECT(h, !missing_main_prepare);
PP_EXPECT(h, steps.empty());
const bool missing_required =
pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence(
pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {});
PP_EXPECT(h, !missing_required);
}
void legacy_node_stroke_preview_main_live_pass_preserves_order(pp::tests::Harness& h)
@@ -3210,26 +3147,19 @@ void legacy_node_stroke_preview_main_pass_texture_dispatch_preserves_order(pp::t
void legacy_node_stroke_preview_final_composite_and_copy_helpers_preserve_order(pp::tests::Harness& h)
{
std::vector<std::string> steps;
const bool composite_ok = pp::panopainter::execute_legacy_node_stroke_preview_final_composite(
pp::panopainter::LegacyNodeStrokePreviewFinalCompositeRequest {
.resolution = glm::vec2(64.0F, 32.0F),
.pattern_scale = glm::vec2(0.25F, -0.5F),
.brush = reinterpret_cast<const Brush*>(1),
.composite_pass = reinterpret_cast<const pp::paint_renderer::CanvasStrokeCompositePassPlan*>(1),
.setup_composite_shader = [&] {
steps.emplace_back("setup");
},
.bind_composite_samplers = [&] {
steps.emplace_back("bind_samplers");
},
.bind_composite_inputs = [&] {
steps.emplace_back("bind_inputs");
},
.draw_composite = [&] {
steps.emplace_back("draw");
},
pp::panopainter::execute_legacy_stroke_preview_final_composite(
[&] {
steps.emplace_back("setup");
},
[&] {
steps.emplace_back("bind_samplers");
},
[&] {
steps.emplace_back("bind_inputs");
},
[&] {
steps.emplace_back("draw");
});
PP_EXPECT(h, composite_ok);
PP_EXPECT(h, (steps == std::vector<std::string> {
"setup",
"bind_samplers",
@@ -3238,23 +3168,27 @@ void legacy_node_stroke_preview_final_composite_and_copy_helpers_preserve_order(
}));
steps.clear();
const bool copy_ok = pp::panopainter::copy_legacy_node_stroke_preview_result(
pp::panopainter::LegacyNodeStrokePreviewCopyResultRequest {
.preview_texture = reinterpret_cast<Texture2D*>(1),
.size = glm::vec2(32.0F, 16.0F),
.copy_framebuffer_to_texture = [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
steps.emplace_back(
"copy:" +
std::to_string(src_x) + "," +
std::to_string(src_y) + "," +
std::to_string(dst_x) + "," +
std::to_string(dst_y) + "," +
std::to_string(width) + "," +
std::to_string(height));
},
const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture(
[&] {
steps.emplace_back("bind");
},
[&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
steps.emplace_back(
"copy:" +
std::to_string(src_x) + "," +
std::to_string(src_y) + "," +
std::to_string(dst_x) + "," +
std::to_string(dst_y) + "," +
std::to_string(width) + "," +
std::to_string(height));
},
pp::paint_renderer::StrokePreviewCopySize {
.width = 32,
.height = 16,
});
PP_EXPECT(h, copy_ok);
PP_EXPECT(h, copy_status.ok());
PP_EXPECT(h, (steps == std::vector<std::string> {
"bind",
"copy:0,0,0,0,32,16",
}));
}

View File

@@ -33,7 +33,6 @@ 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)
@@ -1210,47 +1209,61 @@ void retained_stroke_live_pass_clears_before_traversal_and_copies_afterwards(pp:
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));
std::size_t executed_faces = 0U;
bool copy_ok = false;
pp::panopainter::execute_legacy_stroke_preview_live_pass(
[&] { events.push_back("clear"); },
[&]() {
return std::vector<int> { 0 };
},
[&](StrokeFrame&, int face_index, std::span<const vertex_t> vertices) {
events.push_back(
"prepare:" + std::to_string(face_index) + ":" + std::to_string(vertices.size()));
[&](int&) {},
[&](int&) {},
[&](int&) {
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);
},
[&](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 });
[&]() {
const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture(
[&]() { events.emplace_back("bind-preview"); },
[&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
events.emplace_back("copy-preview");
PP_EXPECT(h, src_x == 0);
PP_EXPECT(h, src_y == 0);
PP_EXPECT(h, dst_x == 0);
PP_EXPECT(h, dst_y == 0);
PP_EXPECT(h, width == 64);
PP_EXPECT(h, height == 64);
},
pp::paint_renderer::StrokePreviewCopySize { .width = 64, .height = 64 });
PP_EXPECT(h, copy_status.ok());
copy_ok = copy_status.ok();
});
PP_EXPECT(h, executed_faces == 2U);
PP_EXPECT(h, copy_ok);
const std::vector<std::string> expected_events {
"clear",
"begin-frame:9",
@@ -1503,6 +1516,164 @@ void retained_stroke_main_pass_execution_preserves_bind_and_unbind_order(pp::tes
PP_EXPECT(h, events == expected_events);
}
void retained_stroke_main_pass_execution_preserves_destination_binding_order_and_face_execution(pp::tests::Harness& h)
{
std::vector<std::string> events;
std::array<StrokeFrame, 1> frames {};
frames[0].id = 31;
frames[0].shapes[2] = {
vertex_t(glm::vec2(2.0F, 3.0F)),
vertex_t(glm::vec2(4.0F, 3.0F)),
vertex_t(glm::vec2(3.0F, 5.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));
std::array<bool, 6> include_main_dirty = SIXPLETTE(true);
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 std::array<pp::panopainter::LegacyCanvasStrokeTextureBinding, 4> main_pass_texture_bindings {
pp::panopainter::LegacyCanvasStrokeTextureBinding {
.input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
.slot = 0,
},
pp::panopainter::LegacyCanvasStrokeTextureBinding {
.input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination,
.slot = 1,
},
pp::panopainter::LegacyCanvasStrokeTextureBinding {
.input = pp::panopainter::LegacyCanvasStrokeTextureInput::pattern,
.slot = 2,
},
pp::panopainter::LegacyCanvasStrokeTextureBinding {
.input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer,
.slot = 3,
},
};
const std::array<pp::panopainter::LegacyCanvasStrokeTextureBinding, 3> main_pass_texture_unbindings {
pp::panopainter::LegacyCanvasStrokeTextureBinding {
.input = pp::panopainter::LegacyCanvasStrokeTextureInput::mixer,
.slot = 3,
},
pp::panopainter::LegacyCanvasStrokeTextureBinding {
.input = pp::panopainter::LegacyCanvasStrokeTextureInput::stroke_destination,
.slot = 1,
},
pp::panopainter::LegacyCanvasStrokeTextureBinding {
.input = pp::panopainter::LegacyCanvasStrokeTextureInput::brush_tip,
.slot = 0,
},
};
const pp::panopainter::LegacyCanvasStrokeTextureInputDispatch main_pass_texture_dispatch {
.activate_texture_unit = [&](int slot) {
events.emplace_back("activate:" + std::to_string(slot));
},
.bind_brush_tip = [&]() {
events.emplace_back("bind-brush");
},
.unbind_brush_tip = [&]() {
events.emplace_back("unbind-brush");
},
.bind_stroke_destination = [&]() {
events.emplace_back("bind-destination");
},
.unbind_stroke_destination = [&]() {
events.emplace_back("unbind-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");
},
};
const auto ok = pp::panopainter::execute_legacy_canvas_stroke_main_pass(
pp::panopainter::make_legacy_canvas_stroke_main_pass_execution_request(
"Canvas::stroke_draw",
[&] {
events.emplace_back("bind-samplers");
},
[&] {
events.emplace_back("bind-textures");
pp::panopainter::bind_legacy_canvas_stroke_texture_inputs(
main_pass_texture_bindings,
main_pass_texture_dispatch);
},
[&] {
pp::panopainter::execute_legacy_canvas_stroke_main_pass_frame_callbacks(
frames,
pp::renderer::Extent2D { .width = 64, .height = 64 },
accumulated_dirty_boxes,
pass_dirty_boxes,
include_main_dirty,
[&](StrokeFrame& frame) {
events.emplace_back("begin-frame:" + std::to_string(frame.id));
},
[&](StrokeFrame&, int face_index, std::span<const vertex_t> vertices) {
events.emplace_back(
"prepare:" + std::to_string(face_index) + ":" + std::to_string(vertices.size()));
},
[&](StrokeFrame&, int face_index, std::span<const vertex_t>) {
events.emplace_back("execute:" + std::to_string(face_index));
return glm::vec4(1.0F, 2.0F, 3.0F, 4.0F);
},
face_framebuffers);
},
[&] {
events.emplace_back("unbind-textures");
pp::panopainter::unbind_legacy_canvas_stroke_texture_inputs(
main_pass_texture_unbindings,
main_pass_texture_dispatch);
},
[&] {
events.emplace_back("unbind-samplers");
}));
const std::vector<std::string> expected_events {
"bind-samplers",
"bind-textures",
"activate:0",
"bind-brush",
"activate:1",
"bind-destination",
"activate:2",
"bind-pattern",
"activate:3",
"bind-mixer",
"begin-frame:31",
"prepare:2:3",
"bind:2",
"execute:2",
"unbind:2",
"unbind-textures",
"activate:3",
"unbind-mixer",
"activate:1",
"unbind-destination",
"activate:0",
"unbind-brush",
"unbind-samplers",
};
PP_EXPECT(h, ok);
PP_EXPECT(h, events == expected_events);
}
void retained_stroke_pad_face_callbacks_preserve_order(pp::tests::Harness& h)
{
const std::array<bool, 3> dirty_faces { true, false, true };
@@ -1647,16 +1818,28 @@ void retained_stroke_preview_background_capture_preserves_retained_call_order(pp
{
std::vector<std::string> events;
std::array<int, 6> copy_args {};
bool copy_ok = false;
pp::panopainter::execute_legacy_stroke_preview_background_capture(
pp::panopainter::execute_legacy_stroke_preview_live_pass(
[&]() { 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 };
[&]() {
return std::vector<int> { 0 };
},
LegacyStrokePreviewCopySize { .width = 48, .height = 32 });
[&](int&) {},
[&](int&) { events.emplace_back("draw"); },
[&](int&) {},
[&]() {
const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture(
[&]() { events.emplace_back("bind"); },
[&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
events.emplace_back("copy");
copy_args = { src_x, src_y, dst_x, dst_y, width, height };
},
pp::paint_renderer::StrokePreviewCopySize { .width = 48, .height = 32 });
PP_EXPECT(h, copy_status.ok());
copy_ok = copy_status.ok();
});
PP_EXPECT(h, copy_ok);
const std::vector<std::string> expected_events { "setup", "draw", "bind", "copy" };
PP_EXPECT(h, events == expected_events);
@@ -1692,13 +1875,14 @@ void retained_stroke_preview_texture_copy_binds_before_copy(pp::tests::Harness&
std::vector<std::string> events;
std::array<int, 6> copy_args {};
pp::panopainter::copy_legacy_stroke_preview_texture(
const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture(
[&]() { events.emplace_back("bind"); },
[&](int dst_x, int dst_y, int src_x, int src_y, int width, int height) {
[&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
events.emplace_back("copy");
copy_args = { dst_x, dst_y, src_x, src_y, width, height };
copy_args = { src_x, src_y, dst_x, dst_y, width, height };
},
LegacyStrokePreviewCopySize { .width = 96, .height = 64 });
pp::paint_renderer::StrokePreviewCopySize { .width = 96, .height = 64 });
PP_EXPECT(h, copy_status.ok());
const std::vector<std::string> expected_events { "bind", "copy" };
PP_EXPECT(h, events == expected_events);
@@ -2087,6 +2271,9 @@ int main()
harness.run(
"retained_stroke_main_pass_execution_preserves_bind_and_unbind_order",
retained_stroke_main_pass_execution_preserves_bind_and_unbind_order);
harness.run(
"retained_stroke_main_pass_execution_preserves_destination_binding_order_and_face_execution",
retained_stroke_main_pass_execution_preserves_destination_binding_order_and_face_execution);
harness.run(
"retained_stroke_live_pass_sampler_dispatch_helper_builds_expected_callback_wiring",
retained_stroke_live_pass_sampler_dispatch_helper_builds_expected_callback_wiring);