From a12abedd6089361813d8ad683c1757207bce7d68 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 12 Aug 2026 04:15:49 +1000 Subject: [PATCH] [client] wayland: test presentation feedback lifecycle --- client/tests/CMakeLists.txt | 33 ++ client/tests/wayland_presentation_test.c | 497 +++++++++++++++++++++++ 2 files changed, 530 insertions(+) create mode 100644 client/tests/wayland_presentation_test.c diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 6c80061a..529a204b 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -102,6 +102,39 @@ set_tests_properties(wayland-motion-tests PROPERTIES TIMEOUT 10 ) +if(ENABLE_WAYLAND) + add_executable(wayland-presentation-tests + wayland_presentation_test.c + ../displayservers/Wayland/presentation.c + ) + add_dependencies(wayland-presentation-tests wayland_protocol) + target_include_directories(wayland-presentation-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../displayservers/Wayland" + "${CMAKE_BINARY_DIR}/wayland" + ) + target_link_libraries(wayland-presentation-tests + ${EXE_FLAGS} + lg_common + ) + set(WAYLAND_PRESENTATION_CASES + feedback-first + swap-first + discard + swap-fail + token-zero + invalid-time + free-pending + ) + foreach(name IN LISTS WAYLAND_PRESENTATION_CASES) + add_test(NAME wayland-presentation-${name} + COMMAND wayland-presentation-tests ${name} + ) + set_tests_properties(wayland-presentation-${name} PROPERTIES + TIMEOUT 10 + ) + endforeach() +endif() + add_executable(displayserver-input-tests displayserver_input_test.c ../displayservers/Wayland/input_event.c diff --git a/client/tests/wayland_presentation_test.c b/client/tests/wayland_presentation_test.c new file mode 100644 index 00000000..3470b74d --- /dev/null +++ b/client/tests/wayland_presentation_test.c @@ -0,0 +1,497 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "wayland.h" +#include "test.h" + +#include "common/debug.h" + +#include +#include +#include +#include +#include + +#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0])) +#define MAX_FEEDBACK 8U +#define MAX_NOTICE 8U +#define SEC UINT64_C(1000000000) + +enum ProxyKind +{ + PROXY_PRESENTATION, + PROXY_FEEDBACK, +}; + +struct Proxy +{ + enum ProxyKind kind; + void (**listener)(void); + void * data; + bool dead; +}; + +struct Proto +{ + struct Proxy presentation; + struct Proxy feedback[MAX_FEEDBACK]; + unsigned int feedbackN; + unsigned int feedbackDestroyN; + unsigned int presentationDestroyN; +}; + +struct Notice +{ + uint64_t token; + uint64_t time; + bool valid; +}; + +struct Log +{ + struct Notice notice[MAX_NOTICE]; + unsigned int noticeN; + unsigned int graphRegisterN; + unsigned int graphCompactN; + unsigned int graphUnregisterN; + uint64_t period; + bool havePeriod; +}; + +struct WaylandDSState wlWm; + +static struct Proto proto; +static struct Log rec; +static uint64_t now; + +const struct wl_interface wp_presentation_feedback_interface = +{ + .name = "wp_presentation_feedback", + .version = 1, +}; + +int clock_gettime(clockid_t id, struct timespec * ts) +{ + CHECK(id == CLOCK_MONOTONIC); + ts->tv_sec = now / SEC; + ts->tv_nsec = now % SEC; + return 0; +} + +void wl_list_init(struct wl_list * list) +{ + list->next = list; + list->prev = list; +} + +void wl_list_insert(struct wl_list * list, struct wl_list * elm) +{ + elm->prev = list; + elm->next = list->next; + list->next->prev = elm; + list->next = elm; +} + +void wl_list_remove(struct wl_list * elm) +{ + elm->prev->next = elm->next; + elm->next->prev = elm->prev; + elm->next = NULL; + elm->prev = NULL; +} + +int wl_list_empty(const struct wl_list * list) +{ + return list->next == list; +} + +uint32_t wl_proxy_get_version(struct wl_proxy * proxy) +{ + CHECK(proxy); + return 1; +} + +int wl_proxy_add_listener(struct wl_proxy * proxy, + void (**listener)(void), void * data) +{ + struct Proxy * p = (struct Proxy *)proxy; + CHECK(p); + CHECK(!p->dead); + CHECK(!p->listener); + p->listener = listener; + p->data = data; + return 0; +} + +static void destroy(struct Proxy * p) +{ + CHECK(p); + CHECK(!p->dead); + p->dead = true; + + if (p->kind == PROXY_PRESENTATION) + ++proto.presentationDestroyN; + else + ++proto.feedbackDestroyN; +} + +struct wl_proxy * wl_proxy_marshal_flags(struct wl_proxy * proxy, + uint32_t opcode, const struct wl_interface * interface, + uint32_t version, uint32_t flags, ...) +{ + struct Proxy * p = (struct Proxy *)proxy; + (void)opcode; + (void)version; + + CHECK(p == &proto.presentation); + if (flags & WL_MARSHAL_FLAG_DESTROY) + { + CHECK(!interface); + destroy(p); + return NULL; + } + + CHECK(interface == &wp_presentation_feedback_interface); + CHECK(proto.feedbackN < ARRAY_LENGTH(proto.feedback)); + p = &proto.feedback[proto.feedbackN++]; + p->kind = PROXY_FEEDBACK; + return (struct wl_proxy *)p; +} + +void wl_proxy_destroy(struct wl_proxy * proxy) +{ + destroy((struct Proxy *)proxy); +} + +void app_handleFramePresented(uint64_t token, uint64_t time, bool valid) +{ + CHECK(rec.noticeN < ARRAY_LENGTH(rec.notice)); + struct Notice * notice = &rec.notice[rec.noticeN++]; + notice->token = token; + notice->time = time; + notice->valid = valid; +} + +GraphHandle app_registerGraph(const char * name, RingBuffer buffer, + float min, float max, GraphFormatFn formatFn) +{ + CHECK(strcmp(name, "PHOTON") == 0); + CHECK(buffer); + CHECK(min == 0.0f); + CHECK(max == 30.0f); + CHECK(!formatFn); + ++rec.graphRegisterN; + return (GraphHandle)&rec; +} + +void app_setGraphCompact(GraphHandle handle, bool compact) +{ + CHECK(handle == (GraphHandle)&rec); + CHECK(compact); + ++rec.graphCompactN; +} + +void app_unregisterGraph(GraphHandle handle) +{ + CHECK(handle == (GraphHandle)&rec); + ++rec.graphUnregisterN; +} + +bool waylandOutputGetFramePeriod(uint64_t * period) +{ + if (!rec.havePeriod) + return false; + *period = rec.period; + return true; +} + +static struct Proxy * feedback(unsigned int no) +{ + CHECK(no < proto.feedbackN); + struct Proxy * p = &proto.feedback[no]; + CHECK(p->kind == PROXY_FEEDBACK); + CHECK(p->listener); + CHECK(!p->dead); + return p; +} + +static void clockReady(void) +{ + struct Proxy * p = &proto.presentation; + CHECK(p->listener); + const struct wp_presentation_listener * listener = + (const struct wp_presentation_listener *)p->listener; + listener->clock_id(p->data, (struct wp_presentation *)p, CLOCK_MONOTONIC); +} + +static void present(unsigned int no, uint64_t time, uint32_t nsec) +{ + struct Proxy * p = feedback(no); + const struct wp_presentation_feedback_listener * listener = + (const struct wp_presentation_feedback_listener *)p->listener; + const uint64_t sec = time / SEC; + listener->presented(p->data, (struct wp_presentation_feedback *)p, + sec >> 32, sec, nsec, 0, 0, 0, 0); +} + +static void discard(unsigned int no) +{ + struct Proxy * p = feedback(no); + const struct wp_presentation_feedback_listener * listener = + (const struct wp_presentation_feedback_listener *)p->listener; + listener->discarded(p->data, (struct wp_presentation_feedback *)p); +} + +static void checkNotice(unsigned int no, uint64_t token, + uint64_t time, bool valid) +{ + CHECK(no < rec.noticeN); + CHECK(rec.notice[no].token == token); + CHECK(rec.notice[no].time == time); + CHECK(rec.notice[no].valid == valid); +} + +static void start(void) +{ + memset(&wlWm, 0, sizeof(wlWm)); + memset(&proto, 0, sizeof(proto)); + memset(&rec, 0, sizeof(rec)); + now = 10 * SEC; + proto.presentation.kind = PROXY_PRESENTATION; + wlWm.presentation = (struct wp_presentation *)&proto.presentation; + wlWm.surface = (struct wl_surface *)(uintptr_t)1; + + CHECK(waylandPresentationInit()); + CHECK(rec.graphRegisterN == 1); + CHECK(rec.graphCompactN == 1); + CHECK(rec.graphUnregisterN == 0); + CHECK(!waylandPresentationFrame(1)); + CHECK(proto.feedbackN == 0); + clockReady(); +} + +static void finish(void) +{ + waylandPresentationFree(); + CHECK(!wlWm.presentation); + CHECK(proto.presentation.dead); + CHECK(proto.presentationDestroyN == 1); + CHECK(rec.graphUnregisterN == 1); +} + +static void testFeedbackFirst(void) +{ + start(); + struct WaylandPresentationFrame * frame = waylandPresentationFrame(11); + CHECK(frame); + + present(0, 10 * SEC, 200000000); + CHECK(rec.noticeN == 0); + CHECK(proto.feedbackDestroyN == 1); + + now = 10 * SEC + 100000000; + bool tracked; + waylandPresentationSwapDone(frame, true, &tracked); + CHECK(tracked); + CHECK(rec.noticeN == 1); + checkNotice(0, 11, 100000000, true); + + finish(); + CHECK(rec.noticeN == 1); + CHECK(proto.feedbackDestroyN == 1); +} + +static void testSwapFirst(void) +{ + start(); + struct WaylandPresentationFrame * frame = waylandPresentationFrame(12); + CHECK(frame); + + now = 10 * SEC + 100000000; + bool tracked; + waylandPresentationSwapDone(frame, true, &tracked); + CHECK(tracked); + CHECK(rec.noticeN == 0); + + present(0, 10 * SEC, 200000000); + CHECK(rec.noticeN == 1); + checkNotice(0, 12, 100000000, true); + + finish(); + CHECK(rec.noticeN == 1); + CHECK(proto.feedbackDestroyN == 1); +} + +static void testDiscard(void) +{ + start(); + struct WaylandPresentationFrame * first = waylandPresentationFrame(21); + CHECK(first); + now = 10 * SEC + 100000000; + bool tracked; + waylandPresentationSwapDone(first, true, &tracked); + CHECK(tracked); + discard(0); + CHECK(rec.noticeN == 1); + checkNotice(0, 21, 0, false); + + struct WaylandPresentationFrame * second = waylandPresentationFrame(22); + CHECK(second); + discard(1); + CHECK(rec.noticeN == 1); + waylandPresentationSwapDone(second, true, &tracked); + CHECK(tracked); + CHECK(rec.noticeN == 2); + checkNotice(1, 22, 0, false); + + finish(); + CHECK(rec.noticeN == 2); + CHECK(proto.feedbackDestroyN == 2); +} + +static void testSwapFail(void) +{ + start(); + struct WaylandPresentationFrame * frame = waylandPresentationFrame(31); + CHECK(frame); + + bool tracked = true; + waylandPresentationSwapDone(frame, false, &tracked); + CHECK(!tracked); + CHECK(rec.noticeN == 1); + checkNotice(0, 31, 0, false); + + present(0, 10 * SEC, 200000000); + CHECK(rec.noticeN == 1); + CHECK(proto.feedbackDestroyN == 1); + + tracked = true; + waylandPresentationSwapDone(NULL, true, &tracked); + CHECK(!tracked); + CHECK(rec.noticeN == 1); + + finish(); + CHECK(rec.noticeN == 1); +} + +static void testTokenZero(void) +{ + start(); + struct WaylandPresentationFrame * frame = waylandPresentationFrame(0); + CHECK(frame); + + now = 10 * SEC + 100000000; + bool tracked = true; + waylandPresentationSwapDone(frame, true, &tracked); + CHECK(!tracked); + present(0, 10 * SEC, 200000000); + CHECK(rec.noticeN == 0); + + finish(); + CHECK(rec.noticeN == 0); + CHECK(proto.feedbackDestroyN == 1); +} + +static void testInvalidTime(void) +{ + start(); + struct WaylandPresentationFrame * frame = waylandPresentationFrame(41); + CHECK(frame); + now = 10 * SEC + 100000000; + bool tracked; + waylandPresentationSwapDone(frame, true, &tracked); + CHECK(tracked); + present(0, 10 * SEC, 1000000000); + CHECK(rec.noticeN == 1); + checkNotice(0, 41, 0, false); + + frame = waylandPresentationFrame(42); + CHECK(frame); + now = 10 * SEC + 200000000; + waylandPresentationSwapDone(frame, true, &tracked); + CHECK(tracked); + present(1, 10 * SEC, 100000000); + CHECK(rec.noticeN == 2); + checkNotice(1, 42, 0, false); + + finish(); + CHECK(rec.noticeN == 2); + CHECK(proto.feedbackDestroyN == 2); +} + +static void testFreePending(void) +{ + start(); + struct WaylandPresentationFrame * frame = waylandPresentationFrame(51); + CHECK(frame); + now = 10 * SEC + 100000000; + bool tracked; + waylandPresentationSwapDone(frame, true, &tracked); + CHECK(tracked); + CHECK(rec.noticeN == 0); + CHECK(proto.feedbackDestroyN == 0); + + finish(); + CHECK(rec.noticeN == 1); + checkNotice(0, 51, 0, false); + CHECK(proto.feedbackDestroyN == 1); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "feedback-first", testFeedbackFirst }, + { "swap-first" , testSwapFirst }, + { "discard" , testDiscard }, + { "swap-fail" , testSwapFail }, + { "token-zero" , testTokenZero }, + { "invalid-time" , testInvalidTime }, + { "free-pending" , testFreePending }, +}; + +int main(int argc, char ** argv) +{ + debug_init(); + + if (argc == 2) + { + for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i) + if (strcmp(argv[1], tests[i].name) == 0) + { + tests[i].run(); + return 0; + } + + fprintf(stderr, "unknown test: %s\n", argv[1]); + return EXIT_FAILURE; + } + + if (argc != 1) + return EXIT_FAILURE; + + for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i) + tests[i].run(); + return 0; +}