diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 0986966a..fe1cc336 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -203,6 +203,40 @@ foreach(name IN LISTS TRANSPORT_FALLBACK_CASES) ) endforeach() +add_executable(frame-scheduler-tests + frame_scheduler_test.c + ../src/frame_scheduler.c +) +target_compile_definitions(frame-scheduler-tests PRIVATE + CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 +) +target_include_directories(frame-scheduler-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + "${PROJECT_TOP}/repos/gui/cimgui" + "${PROJECT_TOP}/repos/gui/cimgui/imgui" +) +target_link_libraries(frame-scheduler-tests + ${EXE_FLAGS} + lg_common +) +set(FRAME_SCHEDULER_CASES + absent + start + enqueue + ack + period + cadence + feedback +) +foreach(name IN LISTS FRAME_SCHEDULER_CASES) + add_test(NAME frame-scheduler-${name} + COMMAND frame-scheduler-tests ${name} + ) + set_tests_properties(frame-scheduler-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + if(ENABLE_AUDIO) pkg_check_modules(AUDIO_TEST_SAMPLERATE REQUIRED IMPORTED_TARGET samplerate) diff --git a/client/tests/frame_scheduler_test.c b/client/tests/frame_scheduler_test.c new file mode 100644 index 00000000..db9b5757 --- /dev/null +++ b/client/tests/frame_scheduler_test.c @@ -0,0 +1,418 @@ +/** + * 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 "frame_scheduler.h" +#include "main.h" +#include "test.h" + +#include "common/debug.h" + +#include +#include +#include +#include +#include + +#define MAX_CTL 16U +#define NS_MS(x) ((uint64_t)(x) * UINT64_C(1000000)) + +#define PERIOD_10MS NS_MS(10) +#define PERIOD_104MS UINT64_C(10400000) +#define PERIOD_11MS NS_MS(11) +#define PERIOD_60HZ UINT64_C(16666667) + +struct LG_Transport +{ + int unused; +}; + +struct Trace +{ + uint64_t now; + uint64_t period; + bool havePeriod; + unsigned int periodN; + unsigned int sendN; + unsigned int ackN; + unsigned int sendRetN; + unsigned int ackRetN; + LG_TransportControl ctl[MAX_CTL]; + LG_TransportControlToken tok[MAX_CTL]; + uint64_t sendAt[MAX_CTL]; + LG_TransportStatus sendRet[MAX_CTL]; + LG_TransportStatus ackRet[MAX_CTL]; + LG_TransportControlToken lastTok; +}; + +static struct Trace t; +static struct LG_Transport tr; + +struct AppState g_state; + +int clock_gettime(clockid_t id, struct timespec * ts) +{ + ts->tv_sec = t.now / UINT64_C(1000000000); + ts->tv_nsec = t.now % UINT64_C(1000000000); + return 0; +} + +static bool getPeriod(uint64_t * period) +{ + ++t.periodN; + if (!t.havePeriod) + return false; + *period = t.period; + return true; +} + +static LG_TransportStatus sendCtl(LG_Transport * transport, + const LG_TransportControl * control, LG_TransportControlToken * token) +{ + CHECK(transport == &tr); + CHECK(control); + CHECK(token); + CHECK(t.sendN < MAX_CTL); + + const unsigned int no = t.sendN++; + t.ctl[no] = *control; + t.sendAt[no] = t.now; + const LG_TransportStatus status = no < t.sendRetN ? + t.sendRet[no] : LG_TRANSPORT_OK; + if (status == LG_TRANSPORT_OK) + { + t.lastTok = no + 1; + t.tok[no] = t.lastTok; + *token = t.lastTok; + } + return status; +} + +static LG_TransportStatus ackCtl( + LG_Transport * transport, LG_TransportControlToken token) +{ + CHECK(transport == &tr); + CHECK(token == t.lastTok); + + const unsigned int no = t.ackN++; + return no < t.ackRetN ? t.ackRet[no] : LG_TRANSPORT_OK; +} + +static struct LG_DisplayServerOps dsOps = +{ + .getFramePeriod = getPeriod, +}; + +static const LG_TransportOps trOps = +{ + .name = "fake", + .sendControl = sendCtl, + .controlStatus = ackCtl, +}; + +static void init(LG_TransportFeatureFlags features, uint64_t period) +{ + t.now = UINT64_C(1000000000); + t.period = period; + t.havePeriod = true; + g_state.ds = &dsOps; + g_state.transport.handle = &tr; + g_state.transport.ops = &trOps; + frameScheduler_init(); + frameScheduler_start(features); +} + +static void step(uint64_t ns) +{ + t.now += ns; +} + +static const LG_TransportControl * control(unsigned int no) +{ + CHECK(no < t.sendN); + CHECK(t.ctl[no].type == LG_TRANSPORT_CONTROL_FRAME_SCHEDULE); + return &t.ctl[no]; +} + +static void checkCtl(unsigned int no, LG_TransportFrameScheduleFlags flags, + uint64_t period, uint32_t generation) +{ + const LG_TransportControl * ctl = control(no); + CHECK(ctl->frameSchedule.flags == flags); + CHECK(ctl->frameSchedule.period == period); + CHECK(ctl->frameSchedule.generation == generation); + CHECK(ctl->frameSchedule.targetSlack == UINT64_C(500000)); + CHECK(ctl->frameSchedule.lease == 1000); +} + +static void testAbsent(void) +{ + init(0, PERIOD_60HZ); + frameScheduler_observeCadence(); + frameScheduler_update(); + frameScheduler_feedback(1, 1, 1, 1, 1); + frameScheduler_stop(); + + CHECK(t.periodN == 0); + CHECK(t.sendN == 0); + CHECK(t.ackN == 0); + frameScheduler_free(); +} + +static void testStart(void) +{ + init(LG_TRANSPORT_FEATURE_FRAME_SCHEDULE, PERIOD_60HZ); + frameScheduler_observeCadence(); + frameScheduler_update(); + + checkCtl(0, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE | + LG_TRANSPORT_FRAME_SCHEDULE_RESET | + LG_TRANSPORT_FRAME_SCHEDULE_IMMEDIATE, PERIOD_60HZ, 2); + CHECK(t.sendN == 1); + CHECK(t.ackN == 0); + + step(NS_MS(250)); + frameScheduler_update(); + checkCtl(1, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE, PERIOD_60HZ, 2); + CHECK(t.ackN == 1); + + frameScheduler_stop(); + checkCtl(2, LG_TRANSPORT_FRAME_SCHEDULE_RELEASE, 0, 2); + CHECK(t.ackN == 2); + frameScheduler_free(); +} + +static void testEnqueue(void) +{ + init(LG_TRANSPORT_FEATURE_FRAME_SCHEDULE, PERIOD_10MS); + t.sendRet[0] = LG_TRANSPORT_UNAVAILABLE; + t.sendRet[1] = LG_TRANSPORT_ERROR; + t.sendRetN = 2; + + frameScheduler_observeCadence(); + frameScheduler_update(); + CHECK(t.sendN == 1); + + step(NS_MS(9)); + frameScheduler_update(); + CHECK(t.sendN == 1); + step(NS_MS(1)); + frameScheduler_update(); + CHECK(t.sendN == 2); + + step(NS_MS(19)); + frameScheduler_update(); + CHECK(t.sendN == 2); + step(NS_MS(1)); + frameScheduler_update(); + CHECK(t.sendN == 3); + CHECK(t.sendAt[1] - t.sendAt[0] == NS_MS(10)); + CHECK(t.sendAt[2] - t.sendAt[1] == NS_MS(20)); + + frameScheduler_stop(); + checkCtl(3, LG_TRANSPORT_FRAME_SCHEDULE_RELEASE, 0, 2); + CHECK(t.ackN == 1); + + /* A failed initial enqueue must not discard the reset request. */ + checkCtl(2, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE | + LG_TRANSPORT_FRAME_SCHEDULE_RESET | + LG_TRANSPORT_FRAME_SCHEDULE_IMMEDIATE, PERIOD_10MS, 2); + frameScheduler_free(); +} + +static void testAck(void) +{ + init(LG_TRANSPORT_FEATURE_FRAME_SCHEDULE, PERIOD_10MS); + t.ackRet[0] = LG_TRANSPORT_UNAVAILABLE; + t.ackRet[1] = LG_TRANSPORT_ERROR; + t.ackRet[2] = LG_TRANSPORT_OK; + t.ackRetN = 3; + + frameScheduler_observeCadence(); + frameScheduler_update(); + CHECK(t.sendN == 1); + + step(NS_MS(250)); + frameScheduler_update(); + CHECK(t.ackN == 1); + CHECK(t.sendN == 1); + step(NS_MS(9)); + frameScheduler_update(); + CHECK(t.ackN == 1); + step(NS_MS(1)); + frameScheduler_update(); + CHECK(t.ackN == 2); + + step(NS_MS(19)); + frameScheduler_update(); + CHECK(t.ackN == 2); + step(NS_MS(1)); + frameScheduler_update(); + CHECK(t.ackN == 3); + CHECK(t.sendN == 2); + + frameScheduler_stop(); + checkCtl(2, LG_TRANSPORT_FRAME_SCHEDULE_RELEASE, 0, 2); + CHECK(t.ackN == 4); + + /* Recovery from an acknowledgement fault requests an immediate frame. */ + checkCtl(1, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE | + LG_TRANSPORT_FRAME_SCHEDULE_IMMEDIATE, PERIOD_10MS, 2); + frameScheduler_free(); +} + +static void testPeriod(void) +{ + init(LG_TRANSPORT_FEATURE_FRAME_SCHEDULE, NS_MS(1)); + frameScheduler_observeCadence(); + frameScheduler_update(); + CHECK(t.sendN == 0); + + t.period = PERIOD_10MS; + frameScheduler_update(); + checkCtl(0, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE | + LG_TRANSPORT_FRAME_SCHEDULE_RESET | + LG_TRANSPORT_FRAME_SCHEDULE_IMMEDIATE, PERIOD_10MS, 2); + + step(NS_MS(250)); + t.period = PERIOD_104MS; + frameScheduler_update(); + checkCtl(1, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE, + UINT64_C(10050000), 2); + + step(NS_MS(250)); + t.period = PERIOD_11MS; + frameScheduler_update(); + checkCtl(2, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE | + LG_TRANSPORT_FRAME_SCHEDULE_RESET | + LG_TRANSPORT_FRAME_SCHEDULE_IMMEDIATE, PERIOD_11MS, 3); + + t.period = UINT64_C(1000000001); + frameScheduler_update(); + CHECK(t.sendN == 3); + + frameScheduler_stop(); + checkCtl(3, LG_TRANSPORT_FRAME_SCHEDULE_RELEASE, 0, 3); + frameScheduler_free(); +} + +static void testCadence(void) +{ + init(LG_TRANSPORT_FEATURE_FRAME_SCHEDULE, PERIOD_60HZ); + frameScheduler_observeCadence(); + frameScheduler_update(); + frameScheduler_feedback(20, 2, 7, 9, UINT64_C(100000000)); + + step(UINT64_C(500000001)); + frameScheduler_update(); + checkCtl(1, LG_TRANSPORT_FRAME_SCHEDULE_RELEASE, 0, 2); + + step(NS_MS(10)); + frameScheduler_observeCadence(); + frameScheduler_update(); + checkCtl(2, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE | + LG_TRANSPORT_FRAME_SCHEDULE_RESET | + LG_TRANSPORT_FRAME_SCHEDULE_IMMEDIATE, PERIOD_60HZ, 3); + CHECK(control(2)->frameSchedule.phaseError == 0); + CHECK(control(2)->frameSchedule.feedbackFrameSerial == 0); + CHECK(control(2)->frameSchedule.feedbackScheduleEpoch == 0); + CHECK(control(2)->frameSchedule.feedbackDeadlineSerial == 0); + + frameScheduler_stop(); + checkCtl(3, LG_TRANSPORT_FRAME_SCHEDULE_RELEASE, 0, 3); + frameScheduler_free(); +} + +static void testFeedback(void) +{ + init(LG_TRANSPORT_FEATURE_FRAME_SCHEDULE, PERIOD_10MS); + frameScheduler_observeCadence(); + frameScheduler_update(); + + frameScheduler_feedback(1, 0, 1, 1, UINT64_C(100000000)); + frameScheduler_feedback(1, 3, 1, 1, UINT64_C(100000000)); + frameScheduler_feedback(20, 2, 7, 9, UINT64_C(100000000)); + step(NS_MS(49)); + frameScheduler_update(); + CHECK(t.sendN == 1); + step(NS_MS(1)); + frameScheduler_update(); + CHECK(t.sendN == 2); + checkCtl(1, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE, PERIOD_10MS, 2); + CHECK(control(1)->frameSchedule.phaseError == INT64_C(5000000)); + CHECK(control(1)->frameSchedule.feedbackFrameSerial == 20); + CHECK(control(1)->frameSchedule.feedbackScheduleEpoch == 7); + CHECK(control(1)->frameSchedule.feedbackDeadlineSerial == 9); + + frameScheduler_feedback(20, 2, 7, 9, 0); + frameScheduler_feedback(21, 3, 7, 10, 0); + step(NS_MS(50)); + frameScheduler_update(); + CHECK(t.sendN == 2); + + frameScheduler_feedback(21, 2, 8, 10, 0); + frameScheduler_update(); + CHECK(t.sendN == 3); + checkCtl(2, LG_TRANSPORT_FRAME_SCHEDULE_ACTIVE, PERIOD_10MS, 2); + CHECK(control(2)->frameSchedule.phaseError == -INT64_C(500000)); + CHECK(control(2)->frameSchedule.feedbackFrameSerial == 21); + CHECK(control(2)->frameSchedule.feedbackScheduleEpoch == 8); + CHECK(control(2)->frameSchedule.feedbackDeadlineSerial == 10); + + frameScheduler_stop(); + checkCtl(3, LG_TRANSPORT_FRAME_SCHEDULE_RELEASE, 0, 2); + frameScheduler_free(); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "absent" , testAbsent }, + { "start" , testStart }, + { "enqueue" , testEnqueue }, + { "ack" , testAck }, + { "period" , testPeriod }, + { "cadence" , testCadence }, + { "feedback", testFeedback }, +}; + +int main(int argc, char ** argv) +{ + if (argc != 2) + { + fprintf(stderr, "usage: %s \n", argv[0]); + return EXIT_FAILURE; + } + + debug_init(); + for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++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; +}