From ba8bf45dbcf53c6076aacd91072673cf75223071 Mon Sep 17 00:00:00 2001 From: Emil <65846814+emil28092005@users.noreply.github.com> Date: Thu, 24 Sep 2026 02:30:41 +0300 Subject: [PATCH] Add repeatable synthetic Editor input latency probe --- cmake/EditorUI.cmake | 3 ++ tests/editor_ui_latency.cpp | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/editor_ui_latency.cpp diff --git a/cmake/EditorUI.cmake b/cmake/EditorUI.cmake index 63556c3..d53698c 100644 --- a/cmake/EditorUI.cmake +++ b/cmake/EditorUI.cmake @@ -47,5 +47,8 @@ if(TARGET faset_ui AND TARGET faset_editor_session AND TARGET faset_scene_view) set_tests_properties(editor_ui_gizmos PROPERTIES LABELS "gpu") add_test(NAME editor_ui_authoring COMMAND faset_editor_ui_tests) set_tests_properties(editor_ui_authoring PROPERTIES LABELS "gpu") + add_executable(faset_editor_ui_latency ${PROJECT_SOURCE_DIR}/tests/editor_ui_latency.cpp) + target_link_libraries(faset_editor_ui_latency PRIVATE faset_editor_ui) + target_compile_definitions(faset_editor_ui_latency PRIVATE FASET_TEST_ENGINE="${PROJECT_SOURCE_DIR}") endif() endif() diff --git a/tests/editor_ui_latency.cpp b/tests/editor_ui_latency.cpp new file mode 100644 index 0000000..2c4196c --- /dev/null +++ b/tests/editor_ui_latency.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + using namespace faset; + using Clock = std::chrono::steady_clock; + if (argc != 2) { + std::cerr << "Usage: faset_editor_ui_latency OUTPUT.json\n"; + return 2; + } + const auto root = std::filesystem::temp_directory_path() / + path_from_utf8("faset-ui-latency-" + new_id()); + try { + std::filesystem::create_directories(root); + atomic_write_json(root / "project.faset.json", + {{"format", "faset.project"}, + {"version", 1}, + {"name", "UI latency fixture"}, + {"dimension", 3}}); + editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), root}); + render::Renderer renderer({1280, 800, "Editor input latency", true, true}); + editor::EditorUI ui(session, renderer, + path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", + path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); + ui.frame({}); + Json samples = Json::array(); + constexpr int warmup = 10, measured = 100; + for (int index = 0; index < warmup + measured; ++index) { + render::Event event; + event.type = render::Event::Type::KeyDown; + event.key = index % 2 == 0 ? "E" : "W"; + const auto started = Clock::now(); + ui.frame({event}); + renderer.render(ui.snapshot()); + const auto elapsed = std::chrono::duration(Clock::now() - started) + .count(); + const auto target = index % 2 == 0 ? "gizmo-Rotate" : "gizmo-Move"; + if (!ui.widgets().find(target)->selected || + renderer.stats().validation_errors != 0 || !std::isfinite(elapsed) || + elapsed <= 0) + throw std::runtime_error("Synthetic input was not visible in a valid frame"); + if (index >= warmup) + samples.push_back({{"iteration", index - warmup + 1}, + {"milliseconds", elapsed}, + {"gizmo", event.key == "E" ? "Rotate" : "Move"}}); + } + atomic_write_json(path_from_utf8(argv[1]), + {{"format", "faset.editor-ui-latency"}, + {"version", 1}, + {"warmup_frames", warmup}, + {"measured_frames", measured}, + {"presentation_mode", "offscreen"}, + {"method", "KeyDown gizmo toggle through EditorUI::frame and " + "offscreen Vulkan render completion; no window present"}, + {"device", renderer.stats().device}, + {"validation_enabled", renderer.stats().validation_enabled}, + {"validation_errors", renderer.stats().validation_errors}, + {"samples", samples}}); + std::filesystem::remove_all(root); + std::cout << "Measured " << measured << " synthetic Editor input frames\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << "\nFixture: " << path_to_utf8(root) << '\n'; + return 1; + } +}