From f7fa0930cb46046f995fa1ab927aef62466e995e Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 00:29:34 +0300 Subject: feat: add SDL3-style surface, display, and loop timer API for X11, Wayland, and Cocoa --- include/yasl/yasl.h | 40 ++++++ src/cocoa/window.m | 26 +++- src/common/backend.c | 106 ++++++++++++++++ src/common/backend.h | 6 + src/common/loop.c | 267 ++++++++++++++++++++++++++++++++++------ src/wayland/surface.c | 2 + src/x11/window.c | 35 +++++- tests/common/test_timers.c | 297 +++++++++++++++++++++++++++++++++++++++++++++ tests/meson.build | 8 ++ 9 files changed, 742 insertions(+), 45 deletions(-) create mode 100644 tests/common/test_timers.c diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h index 2852112..cde7f34 100644 --- a/include/yasl/yasl.h +++ b/include/yasl/yasl.h @@ -420,6 +420,46 @@ YASL_API int yasl_loop_add_fd(struct yasl_loop *l, int fd, short events, int (*fn)(int fd, short revents, void *ud), void *ud); YASL_API int yasl_loop_del_fd(struct yasl_loop *l, int id); YASL_API int yasl_loop_set_fd_events(struct yasl_loop *l, int id, short events); +YASL_API int yasl_loop_next_timeout_ms(const struct yasl_loop *l); + +/* Timers (SDL3-compatible callback model) + * Timer callbacks return the next interval in nanoseconds (or milliseconds for _ms), + * or 0 to disarm and remove the timer. Returning the same interval repeats the timer; + * returning a different non-zero interval reschedules it with the new interval. + * + * All timers fire on the display's opening thread during event dispatch/step. + * Surface timers are bound to a specific surface and are automatically canceled + * if that surface is destroyed. */ +typedef uint32_t yasl_timer_id; + +typedef uint64_t (*yasl_timer_cb)(void *ud, yasl_timer_id id, uint64_t interval_ns); +typedef uint32_t (*yasl_timer_ms_cb)(void *ud, yasl_timer_id id, uint32_t interval_ms); + +#define YASL_MS_TO_NS(ms) ((uint64_t)(ms) * 1000000ull) +#define YASL_NS_TO_MS(ns) ((uint64_t)(ns) / 1000000ull) + +/* Surface-bound Timers */ +YASL_API yasl_timer_id yasl_surface_add_timer(struct yasl_surface *s, uint64_t interval_ns, + yasl_timer_cb cb, void *ud); +YASL_API yasl_timer_id yasl_surface_add_timer_ms(struct yasl_surface *s, uint32_t interval_ms, + yasl_timer_ms_cb cb, void *ud); +YASL_API bool yasl_surface_del_timer(struct yasl_surface *s, yasl_timer_id id); + +/* Display-bound Timers */ +YASL_API yasl_timer_id yasl_display_add_timer(struct yasl_display *d, uint64_t interval_ns, + yasl_timer_cb cb, void *ud); +YASL_API yasl_timer_id yasl_display_add_timer_ms(struct yasl_display *d, uint32_t interval_ms, + yasl_timer_ms_cb cb, void *ud); +YASL_API bool yasl_display_del_timer(struct yasl_display *d, yasl_timer_id id); +YASL_API struct yasl_loop *yasl_display_loop(struct yasl_display *d); +YASL_API struct yasl_loop *yasl_surface_loop(struct yasl_surface *s); + +/* Loop Timers */ +YASL_API yasl_timer_id yasl_loop_add_timer(struct yasl_loop *l, uint64_t interval_ns, + yasl_timer_cb cb, void *ud); +YASL_API yasl_timer_id yasl_loop_add_timer_ms(struct yasl_loop *l, uint32_t interval_ms, + yasl_timer_ms_cb cb, void *ud); +YASL_API bool yasl_loop_del_timer(struct yasl_loop *l, yasl_timer_id id); #ifdef __cplusplus } diff --git a/src/cocoa/window.m b/src/cocoa/window.m index 4454be3..fc69412 100644 --- a/src/cocoa/window.m +++ b/src/cocoa/window.m @@ -110,6 +110,7 @@ struct cocoa_display { bool open; bool launched; bool pumping; + bool owns_loop; }; static struct cocoa_display darwin_ctx; @@ -867,6 +868,13 @@ int cocoa_display_dispatch_timeout(struct cocoa_display *d, int timeout_ms) d->depth++; int count = 0; @try { + if (d->loop) { + int timer_to = yasl_loop_next_timeout_ms(d->loop); + if (timer_to >= 0 && (timeout_ms < 0 || timer_to < timeout_ms)) { + timeout_ms = timer_to; + } + } + NSDate *limitDate; if (timeout_ms == 0) limitDate = [NSDate distantPast]; @@ -895,6 +903,10 @@ int cocoa_display_dispatch_timeout(struct cocoa_display *d, int timeout_ms) } } } + + if (d->loop) { + yasl_loop_step(d->loop, 0); + } } @finally { d->depth--; d->pumping = false; @@ -1003,7 +1015,14 @@ struct cocoa_display *cocoa_display_open(struct yasl_loop *loop, const struct ya @autoreleasepool { app_init(name); } darwin_ctx.base.ops = &yasl_cocoa_backend; darwin_ctx.open = true; - darwin_ctx.loop = loop; + if (!loop) { + darwin_ctx.loop = yasl_loop_create(); + darwin_ctx.owns_loop = true; + } else { + darwin_ctx.loop = loop; + darwin_ctx.owns_loop = false; + } + darwin_ctx.base.loop = darwin_ctx.loop; if (spec) { darwin_ctx.base.callbacks = spec->cbs ? *spec->cbs : (struct yasl_display_cbs){0}; darwin_ctx.cbs = &darwin_ctx.base.callbacks; @@ -1022,6 +1041,11 @@ void cocoa_display_close(struct cocoa_display *d) [NSEvent removeMonitor:d->key_up_monitor]; d->key_up_monitor = nil; } + if (d->owns_loop && d->loop) { + yasl_loop_destroy(d->loop); + d->loop = NULL; + d->base.loop = NULL; + } NSApp.delegate = nil; [d->delegate release]; d->delegate = nil; diff --git a/src/common/backend.c b/src/common/backend.c index 214854b..27ab297 100644 --- a/src/common/backend.c +++ b/src/common/backend.c @@ -181,6 +181,7 @@ void yasl_surface_destroy(struct yasl_surface *s) { if (!s || !s->display->ops->surface_destroy) return; + yasl_surface_clear_timers(s); s->display->ops->surface_destroy(s); } @@ -368,3 +369,108 @@ void *yasl_surface_get_native_handle(struct yasl_surface *s) return 0; return s->display->ops->surface_get_native_handle(s); } + +void yasl_surface_clear_timers(struct yasl_surface *s) +{ + if (!s || !s->timers) return; + struct yasl_loop *loop = s->display ? s->display->loop : NULL; + for (int i = 0; i < s->timer_count; i++) { + if (loop && s->timers[i]) { + yasl_loop_del_timer(loop, s->timers[i]); + } + } + free(s->timers); + s->timers = NULL; + s->timer_count = 0; + s->timer_capacity = 0; +} + +yasl_timer_id yasl_surface_add_timer(struct yasl_surface *s, uint64_t interval_ns, + yasl_timer_cb cb, void *ud) +{ + if (!s || !s->display || !s->display->loop || !cb || interval_ns == 0) + return 0; + + yasl_timer_id id = yasl_loop_add_timer(s->display->loop, interval_ns, cb, ud); + if (!id) return 0; + + if (s->timer_count >= s->timer_capacity) { + int new_cap = s->timer_capacity ? s->timer_capacity * 2 : 4; + yasl_timer_id *nt = realloc(s->timers, (size_t)new_cap * sizeof(*nt)); + if (!nt) { + yasl_loop_del_timer(s->display->loop, id); + return 0; + } + s->timers = nt; + s->timer_capacity = new_cap; + } + s->timers[s->timer_count++] = id; + return id; +} + +yasl_timer_id yasl_surface_add_timer_ms(struct yasl_surface *s, uint32_t interval_ms, + yasl_timer_ms_cb cb, void *ud) +{ + if (!s || !s->display || !s->display->loop || !cb || interval_ms == 0) + return 0; + + yasl_timer_id id = yasl_loop_add_timer_ms(s->display->loop, interval_ms, cb, ud); + if (!id) return 0; + + if (s->timer_count >= s->timer_capacity) { + int new_cap = s->timer_capacity ? s->timer_capacity * 2 : 4; + yasl_timer_id *nt = realloc(s->timers, (size_t)new_cap * sizeof(*nt)); + if (!nt) { + yasl_loop_del_timer(s->display->loop, id); + return 0; + } + s->timers = nt; + s->timer_capacity = new_cap; + } + s->timers[s->timer_count++] = id; + return id; +} + +bool yasl_surface_del_timer(struct yasl_surface *s, yasl_timer_id id) +{ + if (!s || id == 0) return false; + for (int i = 0; i < s->timer_count; i++) { + if (s->timers[i] == id) { + s->timers[i] = 0; + if (s->display && s->display->loop) + yasl_loop_del_timer(s->display->loop, id); + return true; + } + } + return false; +} + +yasl_timer_id yasl_display_add_timer(struct yasl_display *d, uint64_t interval_ns, + yasl_timer_cb cb, void *ud) +{ + if (!d || !d->loop) return 0; + return yasl_loop_add_timer(d->loop, interval_ns, cb, ud); +} + +yasl_timer_id yasl_display_add_timer_ms(struct yasl_display *d, uint32_t interval_ms, + yasl_timer_ms_cb cb, void *ud) +{ + if (!d || !d->loop) return 0; + return yasl_loop_add_timer_ms(d->loop, interval_ms, cb, ud); +} + +bool yasl_display_del_timer(struct yasl_display *d, yasl_timer_id id) +{ + if (!d || !d->loop) return false; + return yasl_loop_del_timer(d->loop, id); +} + +struct yasl_loop *yasl_display_loop(struct yasl_display *d) +{ + return d ? d->loop : NULL; +} + +struct yasl_loop *yasl_surface_loop(struct yasl_surface *s) +{ + return (s && s->display) ? s->display->loop : NULL; +} diff --git a/src/common/backend.h b/src/common/backend.h index c6691eb..b52a147 100644 --- a/src/common/backend.h +++ b/src/common/backend.h @@ -8,14 +8,20 @@ struct yasl_backend_ops; struct yasl_display { const struct yasl_backend_ops *ops; struct yasl_display_cbs callbacks; + struct yasl_loop *loop; }; struct yasl_surface { struct yasl_display *display; struct yasl_surface_cbs callbacks; uint64_t frame_generation; + yasl_timer_id *timers; + int timer_count; + int timer_capacity; }; +void yasl_surface_clear_timers(struct yasl_surface *s); + static inline uint64_t yasl_frame_begin(struct yasl_surface *s) { if (++s->frame_generation == 0) ++s->frame_generation; diff --git a/src/common/loop.c b/src/common/loop.c index ea9bee3..a1f2434 100644 --- a/src/common/loop.c +++ b/src/common/loop.c @@ -22,6 +22,20 @@ struct yasl_fd_source { short events; }; +struct yasl_timer_source { + union { + yasl_timer_cb ns; + yasl_timer_ms_cb ms; + void *raw; + } fn; + void *ud; + yasl_timer_id id; + uint64_t interval_ns; + uint64_t deadline_ns; + bool ms_mode; + bool deleted; +}; + struct yasl_loop { struct yasl_fd_source *fds; struct pollfd *pollfds; @@ -32,6 +46,12 @@ struct yasl_loop { int wake_rfd; int wake_wfd; int wake_watch; + + struct yasl_timer_source *timers; + int timer_count; + int timer_capacity; + uint32_t next_timer_id; + int timer_firing_depth; }; uint64_t yasl_loop_now_ns(void) @@ -67,6 +87,7 @@ struct yasl_loop *yasl_loop_create(void) } l->next_id = 1; + l->next_timer_id = 1; l->wake_rfd = -1; l->wake_wfd = -1; @@ -106,6 +127,7 @@ void yasl_loop_destroy(struct yasl_loop *l) close(l->wake_wfd); free(l->fds); free(l->pollfds); + free(l->timers); free(l); } @@ -177,65 +199,232 @@ void yasl_loop_kick(struct yasl_loop *l) } while (n < 0 && errno == EINTR); } +static void timer_cleanup_deleted(struct yasl_loop *l) +{ + if (!l || l->timer_firing_depth > 0) return; + int dest = 0; + for (int src = 0; src < l->timer_count; src++) { + if (!l->timers[src].deleted) { + if (dest != src) + l->timers[dest] = l->timers[src]; + dest++; + } + } + l->timer_count = dest; +} + +int yasl_loop_next_timeout_ms(const struct yasl_loop *l) +{ + if (!l || l->timer_count <= 0) return -1; + uint64_t now = yasl_loop_now_ns(); + uint64_t min_deadline = UINT64_MAX; + bool has_active = false; + for (int i = 0; i < l->timer_count; i++) { + if (!l->timers[i].deleted) { + has_active = true; + if (l->timers[i].deadline_ns < min_deadline) + min_deadline = l->timers[i].deadline_ns; + } + } + if (!has_active) return -1; + if (min_deadline <= now) return 0; + uint64_t diff = min_deadline - now; + uint64_t ms = (diff + 999999ull) / 1000000ull; + return (ms > (uint64_t)INT_MAX) ? INT_MAX : (int)ms; +} + +yasl_timer_id yasl_loop_add_timer(struct yasl_loop *l, uint64_t interval_ns, + yasl_timer_cb cb, void *ud) +{ + if (!l || !cb || interval_ns == 0) return 0; + + if (l->timer_count >= l->timer_capacity) { + int new_cap = l->timer_capacity ? l->timer_capacity * 2 : 16; + struct yasl_timer_source *new_timers = realloc(l->timers, (size_t)new_cap * sizeof(*new_timers)); + if (!new_timers) return 0; + l->timers = new_timers; + l->timer_capacity = new_cap; + } + + uint32_t id = l->next_timer_id++; + if (l->next_timer_id == 0) l->next_timer_id = 1; + + uint64_t now = yasl_loop_now_ns(); + l->timers[l->timer_count++] = (struct yasl_timer_source){ + .fn.ns = cb, + .ud = ud, + .id = id, + .interval_ns = interval_ns, + .deadline_ns = now + interval_ns, + .ms_mode = false, + .deleted = false, + }; + + yasl_loop_kick(l); + return id; +} + +yasl_timer_id yasl_loop_add_timer_ms(struct yasl_loop *l, uint32_t interval_ms, + yasl_timer_ms_cb cb, void *ud) +{ + if (!l || !cb || interval_ms == 0) return 0; + + if (l->timer_count >= l->timer_capacity) { + int new_cap = l->timer_capacity ? l->timer_capacity * 2 : 16; + struct yasl_timer_source *new_timers = realloc(l->timers, (size_t)new_cap * sizeof(*new_timers)); + if (!new_timers) return 0; + l->timers = new_timers; + l->timer_capacity = new_cap; + } + + uint32_t id = l->next_timer_id++; + if (l->next_timer_id == 0) l->next_timer_id = 1; + + uint64_t now = yasl_loop_now_ns(); + uint64_t interval_ns = (uint64_t)interval_ms * 1000000ull; + l->timers[l->timer_count++] = (struct yasl_timer_source){ + .fn.ms = cb, + .ud = ud, + .id = id, + .interval_ns = interval_ns, + .deadline_ns = now + interval_ns, + .ms_mode = true, + .deleted = false, + }; + + yasl_loop_kick(l); + return id; +} + +bool yasl_loop_del_timer(struct yasl_loop *l, yasl_timer_id id) +{ + if (!l || id == 0) return false; + for (int i = 0; i < l->timer_count; i++) { + if (l->timers[i].id == id && !l->timers[i].deleted) { + l->timers[i].deleted = true; + if (l->timer_firing_depth == 0) { + timer_cleanup_deleted(l); + } + return true; + } + } + return false; +} + int yasl_loop_step(struct yasl_loop *l, int timeout_ms) { if (!l) return -1; + + int timer_to = yasl_loop_next_timeout_ms(l); + int effective_timeout = timeout_ms; + if (timer_to >= 0) { + if (effective_timeout < 0 || timer_to < effective_timeout) + effective_timeout = timer_to; + } + if (l->count <= 0) { - if (timeout_ms > 0) { + if (effective_timeout > 0) { struct timespec req = { - .tv_sec = timeout_ms / 1000, - .tv_nsec = (long)(timeout_ms % 1000) * 1000000L, + .tv_sec = effective_timeout / 1000, + .tv_nsec = (long)(effective_timeout % 1000) * 1000000L, }; nanosleep(&req, NULL); } - return 0; + } else { + int n; + do { + n = poll(l->pollfds, (nfds_t)l->count, effective_timeout); + } while (n < 0 && errno == EINTR); + + if (n < 0) return -1; } - int n; - do { - n = poll(l->pollfds, (nfds_t)l->count, timeout_ms); - } while (n < 0 && errno == EINTR); + int handled = 0; - if (n < 0) return -1; - if (n == 0) return 0; + if (l->count > 0) { + int count = l->count; + struct yasl_fd_source sstack[32]; + struct pollfd pstack[32]; + struct yasl_fd_source *scopy = (count <= 32) ? sstack : malloc((size_t)count * sizeof(*scopy)); + struct pollfd *pcopy = (count <= 32) ? pstack : malloc((size_t)count * sizeof(*pcopy)); + if (!scopy || !pcopy) { + if (scopy && scopy != sstack) free(scopy); + if (pcopy && pcopy != pstack) free(pcopy); + return -1; + } - int handled = 0; - int count = l->count; - - struct yasl_fd_source sstack[32]; - struct pollfd pstack[32]; - struct yasl_fd_source *scopy = (count <= 32) ? sstack : malloc((size_t)count * sizeof(*scopy)); - struct pollfd *pcopy = (count <= 32) ? pstack : malloc((size_t)count * sizeof(*pcopy)); - if (!scopy || !pcopy) { - if (scopy && scopy != sstack) free(scopy); - if (pcopy && pcopy != pstack) free(pcopy); - return -1; + memcpy(scopy, l->fds, (size_t)count * sizeof(*scopy)); + memcpy(pcopy, l->pollfds, (size_t)count * sizeof(*pcopy)); + + for (int i = 0; i < count; i++) { + if (pcopy[i].revents) { + short revents = pcopy[i].revents; + // Verify that the source was not deleted by an earlier callback in this step + bool valid = false; + for (int j = 0; j < l->count; j++) { + if (l->fds[j].id == scopy[i].id) { + l->pollfds[j].revents = 0; + valid = true; + break; + } + } + if (valid && scopy[i].fn) { + scopy[i].fn(scopy[i].fd, revents, scopy[i].ud); + handled++; + } + } + } + + if (scopy != sstack) free(scopy); + if (pcopy != pstack) free(pcopy); } - memcpy(scopy, l->fds, (size_t)count * sizeof(*scopy)); - memcpy(pcopy, l->pollfds, (size_t)count * sizeof(*pcopy)); - - for (int i = 0; i < count; i++) { - if (pcopy[i].revents) { - short revents = pcopy[i].revents; - // Verify that the source was not deleted by an earlier callback in this step - bool valid = false; - for (int j = 0; j < l->count; j++) { - if (l->fds[j].id == scopy[i].id) { - l->pollfds[j].revents = 0; - valid = true; + if (l->timer_count > 0) { + uint64_t now = yasl_loop_now_ns(); + l->timer_firing_depth++; + int initial_count = l->timer_count; + for (int i = 0; i < initial_count; i++) { + if (i >= l->timer_count) break; + if (l->timers[i].deleted || l->timers[i].deadline_ns > now) + continue; + + yasl_timer_id tid = l->timers[i].id; + uint64_t interval = l->timers[i].interval_ns; + bool is_ms = l->timers[i].ms_mode; + void *ud = l->timers[i].ud; + struct yasl_timer_source timer_src = l->timers[i]; + + uint64_t next_interval = 0; + if (is_ms) { + uint32_t old_ms = (uint32_t)(interval / 1000000ull); + uint32_t res_ms = timer_src.fn.ms(ud, tid, old_ms); + next_interval = (uint64_t)res_ms * 1000000ull; + } else { + next_interval = timer_src.fn.ns(ud, tid, interval); + } + handled++; + + for (int j = 0; j < l->timer_count; j++) { + if (l->timers[j].id == tid) { + if (next_interval == 0 || l->timers[j].deleted) { + l->timers[j].deleted = true; + } else { + l->timers[j].interval_ns = next_interval; + if (l->timers[j].deadline_ns + next_interval > now) + l->timers[j].deadline_ns += next_interval; + else + l->timers[j].deadline_ns = now + next_interval; + } break; } } - if (valid && scopy[i].fn) { - scopy[i].fn(scopy[i].fd, revents, scopy[i].ud); - handled++; - } + } + l->timer_firing_depth--; + if (l->timer_firing_depth == 0) { + timer_cleanup_deleted(l); } } - if (scopy != sstack) free(scopy); - if (pcopy != pstack) free(pcopy); return handled; } diff --git a/src/wayland/surface.c b/src/wayland/surface.c index ef355ab..4f064e2 100644 --- a/src/wayland/surface.c +++ b/src/wayland/surface.c @@ -232,6 +232,7 @@ static void display_free(struct wayland_display *c) if (c->owns_loop && c->loop) { yasl_loop_destroy(c->loop); c->loop = NULL; + c->base.loop = NULL; } free(c->app_id); free(c); @@ -248,6 +249,7 @@ struct wayland_display *wayland_display_open(struct yasl_loop *loop, const struc } else { c->loop = loop; } + c->base.loop = c->loop; if (!c->loop) goto err; if (spec) { c->base.callbacks = spec->cbs ? *spec->cbs : (struct yasl_display_cbs){0}; diff --git a/src/x11/window.c b/src/x11/window.c index e40db6e..2d9aff3 100644 --- a/src/x11/window.c +++ b/src/x11/window.c @@ -549,9 +549,13 @@ static int dispatch_timeout(struct yasl_display *base, int timeout_ms) struct x11_display *d = (struct x11_display *)base; if (unlikely(!d || !d->native)) return -1; dispatch(base); - if (!XPending(d->native) && d->loop && timeout_ms != 0) { - yasl_loop_step(d->loop, timeout_ms); - dispatch(base); + if (d->loop) { + if (!XPending(d->native) && timeout_ms != 0) { + yasl_loop_step(d->loop, timeout_ms); + dispatch(base); + } else { + yasl_loop_step(d->loop, 0); + } } return 0; } @@ -572,6 +576,7 @@ static struct yasl_display *display_open(struct yasl_loop *loop, } else { d->loop = loop; } + d->base.loop = d->loop; d->app_id = strdup(spec && spec->app_id ? spec->app_id : "YASL"); if (!d->loop || !d->app_id) { if (d->owns_loop) yasl_loop_destroy(d->loop); @@ -617,7 +622,11 @@ static void display_close(struct yasl_display *base) assert(!d->surfaces && "destroy every surface before its display"); cancel_pastes(d, NULL); if (d->loop && d->watch > 0) yasl_loop_del_fd(d->loop, d->watch); - if (d->owns_loop && d->loop) yasl_loop_destroy(d->loop); + if (d->owns_loop && d->loop) { + yasl_loop_destroy(d->loop); + d->loop = NULL; + d->base.loop = NULL; + } if (d->im) XCloseIM(d->im); XDestroyWindow(d->native, d->selection_window); XCloseDisplay(d->native); @@ -757,12 +766,28 @@ static bool interactive_move(struct yasl_surface *base) return true; } +static void display_dispatch(struct yasl_display *base) +{ + struct x11_display *d = (struct x11_display *)base; + if (unlikely(!d || !d->native)) return; + bool immediate = false; + for (struct x11_surface *s = d->surfaces; s; s = s->next) { + if (s->want_draw || s->input.count > 0 || s->input.reset_pending) { + immediate = true; + break; + } + } + if (d->loop && yasl_loop_is_running(d->loop)) + immediate = true; + dispatch_timeout(base, immediate ? 0 : -1); +} + const struct yasl_backend_ops yasl_x11_backend = { .type = YASL_BACKEND_X11, .display_open = display_open, .display_close = display_close, .display_caps = caps, - .display_dispatch = dispatch, + .display_dispatch = display_dispatch, .display_dispatch_timeout = dispatch_timeout, .display_flush = flush, .display_modifiers = mods, diff --git a/tests/common/test_timers.c b/tests/common/test_timers.c new file mode 100644 index 0000000..9b6499d --- /dev/null +++ b/tests/common/test_timers.c @@ -0,0 +1,297 @@ +#define _POSIX_C_SOURCE 200809L +#include "yasl/yasl.h" + +#include +#include +#include +#include + +struct test_ctx { + int count; + uint64_t last_time; + yasl_timer_id id; + yasl_timer_id other_id; + struct yasl_loop *loop; + struct yasl_surface *surface; + bool done; +}; + +/* 1. Single-shot timer: returns 0, fires exactly once. */ +static uint64_t oneshot_cb(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + struct test_ctx *ctx = ud; + assert(id == ctx->id); + assert(interval_ns > 0); + ctx->count++; + return 0; /* Disarm */ +} + +/* 2. Repeating timer: ticks N times, then returns 0. */ +static uint64_t repeat_cb(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + struct test_ctx *ctx = ud; + assert(id == ctx->id); + (void)interval_ns; + ctx->count++; + if (ctx->count >= 5) { + ctx->done = true; + return 0; /* Disarm after 5 ticks */ + } + return interval_ns; /* Repeat with same interval */ +} + +/* 3. Rescheduling timer: changes interval dynamically. */ +static uint64_t reschedule_cb(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + struct test_ctx *ctx = ud; + assert(id == ctx->id); + ctx->count++; + if (ctx->count == 1) { + assert(interval_ns == 2000000ull); + return 5000000ull; /* Reschedule to 5ms */ + } else if (ctx->count == 2) { + assert(interval_ns == 5000000ull); + ctx->done = true; + return 0; /* Done */ + } + return 0; +} + +/* 4. Timer adding another timer from inside callback. */ +static uint64_t child_timer_cb(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + (void)interval_ns; + struct test_ctx *ctx = ud; + assert(id == ctx->other_id); + ctx->count += 10; + return 0; +} + +static uint64_t parent_timer_cb(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + (void)interval_ns; + struct test_ctx *ctx = ud; + assert(id == ctx->id); + ctx->count += 1; + ctx->other_id = yasl_loop_add_timer(ctx->loop, 1000000ull, child_timer_cb, ctx); + assert(ctx->other_id > 0); + return 0; +} + +/* 5. Timer deleting another timer from inside callback. */ +static uint64_t victim_timer_cb(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + (void)ud; (void)id; (void)interval_ns; + assert(false && "Victim timer should have been cancelled before firing!"); + return 0; +} + +static uint64_t killer_timer_cb(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + (void)id; (void)interval_ns; + struct test_ctx *ctx = ud; + ctx->count++; + assert(yasl_loop_del_timer(ctx->loop, ctx->other_id)); + return 0; +} + +/* 6. Millisecond variant timer. */ +static uint32_t ms_timer_cb(void *ud, yasl_timer_id id, uint32_t interval_ms) +{ + struct test_ctx *ctx = ud; + assert(id == ctx->id); + assert(interval_ms == 5); + ctx->count++; + if (ctx->count >= 3) { + ctx->done = true; + return 0; + } + return interval_ms; +} + +static void test_loop_timers(void) +{ + struct yasl_loop *l = yasl_loop_create(); + assert(l); + + /* Invalid arguments */ + assert(yasl_loop_add_timer(NULL, 1000, oneshot_cb, NULL) == 0); + assert(yasl_loop_add_timer(l, 0, oneshot_cb, NULL) == 0); + assert(yasl_loop_add_timer(l, 1000, NULL, NULL) == 0); + assert(!yasl_loop_del_timer(NULL, 1)); + assert(!yasl_loop_del_timer(l, 0)); + assert(!yasl_loop_del_timer(l, 99999)); + assert(yasl_loop_next_timeout_ms(l) == -1); + + /* Test 1: Single-shot */ + struct test_ctx ctx1 = { .loop = l }; + ctx1.id = yasl_loop_add_timer(l, 5000000ull, oneshot_cb, &ctx1); + assert(ctx1.id > 0); + assert(yasl_loop_next_timeout_ms(l) >= 0); + + while (ctx1.count == 0) { + int handled = yasl_loop_step(l, 50); + assert(handled >= 0); + } + assert(ctx1.count == 1); + /* Should be disarmed and removed */ + assert(!yasl_loop_del_timer(l, ctx1.id)); + assert(yasl_loop_next_timeout_ms(l) == -1); + + /* Test 2: Repeating */ + struct test_ctx ctx2 = { .loop = l }; + ctx2.id = yasl_loop_add_timer(l, 2000000ull, repeat_cb, &ctx2); + assert(ctx2.id > 0); + while (!ctx2.done) { + yasl_loop_step(l, 50); + } + assert(ctx2.count == 5); + assert(!yasl_loop_del_timer(l, ctx2.id)); + + /* Test 3: Reschedule dynamically */ + struct test_ctx ctx3 = { .loop = l }; + ctx3.id = yasl_loop_add_timer(l, 2000000ull, reschedule_cb, &ctx3); + assert(ctx3.id > 0); + while (!ctx3.done) { + yasl_loop_step(l, 50); + } + assert(ctx3.count == 2); + + /* Test 4: Timer cancellation before firing */ + struct test_ctx ctx4 = { .loop = l }; + yasl_timer_id cancel_id = yasl_loop_add_timer(l, 5000000ull, oneshot_cb, &ctx4); + assert(cancel_id > 0); + assert(yasl_loop_del_timer(l, cancel_id)); + assert(!yasl_loop_del_timer(l, cancel_id)); + /* Step for 10ms; timer should not fire */ + yasl_loop_step(l, 10); + assert(ctx4.count == 0); + + /* Test 5: Child timer added inside callback */ + struct test_ctx ctx5 = { .loop = l }; + ctx5.id = yasl_loop_add_timer(l, 2000000ull, parent_timer_cb, &ctx5); + while (ctx5.count < 11) { + yasl_loop_step(l, 50); + } + assert(ctx5.count == 11); + + /* Test 6: Killer timer deletes victim timer before victim fires */ + struct test_ctx ctx6 = { .loop = l }; + ctx6.id = yasl_loop_add_timer(l, 1000000ull, killer_timer_cb, &ctx6); + ctx6.other_id = yasl_loop_add_timer(l, 10000000ull, victim_timer_cb, &ctx6); + while (ctx6.count == 0) { + yasl_loop_step(l, 50); + } + assert(ctx6.count == 1); + yasl_loop_step(l, 20); + + /* Test 7: Millisecond API */ + struct test_ctx ctx7 = { .loop = l }; + ctx7.id = yasl_loop_add_timer_ms(l, 5, ms_timer_cb, &ctx7); + assert(ctx7.id > 0); + while (!ctx7.done) { + yasl_loop_step(l, 50); + } + assert(ctx7.count == 3); + + yasl_loop_destroy(l); +} + +/* Surface timer tests */ +static uint64_t surface_timer_repeat_cb(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + (void)interval_ns; + struct test_ctx *ctx = ud; + assert(id == ctx->id); + ctx->count++; + return interval_ns; +} + +static uint64_t surface_suicide_cb(void *ud, yasl_timer_id id, uint64_t interval_ns) +{ + (void)id; (void)interval_ns; + struct test_ctx *ctx = ud; + ctx->count++; + /* Destroy surface from within the timer callback */ + yasl_surface_destroy(ctx->surface); + ctx->surface = NULL; + return 1000000ull; /* Even if it returns > 0, surface is destroyed */ +} + +static void test_surface_timers(void) +{ + struct yasl_loop *loop = yasl_loop_create(); + assert(loop); + + struct yasl_display_spec spec = { .app_id = "org.yasl.timer_test" }; + struct yasl_display *d = yasl_display_open(loop, &spec); + if (!d) { + /* Headless without display server */ + yasl_loop_destroy(loop); + return; + } + + assert(yasl_display_loop(d) == loop); + + struct yasl_surface_spec sspec = { + .title = "Timer Surface", + .size = { .x = 160, .y = 100 }, + }; + struct yasl_surface *s = yasl_surface_create_toplevel(d, &sspec); + assert(s); + assert(yasl_surface_loop(s) == loop); + + /* Display timers */ + struct test_ctx dctx = {0}; + dctx.id = yasl_display_add_timer(d, 2000000ull, oneshot_cb, &dctx); + assert(dctx.id > 0); + while (dctx.count == 0) { + yasl_display_dispatch_timeout(d, 50); + } + assert(dctx.count == 1); + + /* Surface timer basic repeat & manual delete */ + struct test_ctx sctx = {0}; + sctx.id = yasl_surface_add_timer(s, 2000000ull, surface_timer_repeat_cb, &sctx); + assert(sctx.id > 0); + while (sctx.count < 3) { + yasl_display_dispatch_timeout(d, 50); + } + assert(sctx.count >= 3); + assert(yasl_surface_del_timer(s, sctx.id)); + int count_before = sctx.count; + yasl_display_dispatch_timeout(d, 10); + assert(sctx.count == count_before); + + /* Surface timer auto-cleanup on surface destruction */ + struct test_ctx sctx2 = {0}; + sctx2.id = yasl_surface_add_timer(s, 2000000ull, surface_timer_repeat_cb, &sctx2); + assert(sctx2.id > 0); + yasl_surface_destroy(s); + /* Step display/loop; timer must not fire on dead surface */ + yasl_display_dispatch_timeout(d, 10); + assert(sctx2.count == 0); + + /* Surface suicide from timer callback */ + s = yasl_surface_create_toplevel(d, &sspec); + assert(s); + struct test_ctx sctx3 = { .surface = s }; + sctx3.id = yasl_surface_add_timer(s, 2000000ull, surface_suicide_cb, &sctx3); + assert(sctx3.id > 0); + while (sctx3.count == 0) { + yasl_display_dispatch_timeout(d, 50); + } + assert(sctx3.count == 1); + assert(sctx3.surface == NULL); + + yasl_display_close(d); + yasl_loop_destroy(loop); +} + +int main(void) +{ + test_loop_timers(); + test_surface_timers(); + printf("Timer tests passed successfully.\n"); + return 0; +} diff --git a/tests/meson.build b/tests/meson.build index 1fcac3e..f2d5e79 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -20,6 +20,14 @@ contracts_test = executable( ) test('contracts', contracts_test) +timer_test = executable( + 'yasl-timer-test', + 'common/test_timers.c', + include_directories: inc, + dependencies: yasl_dep, +) +test('timers', timer_test) + input_alloc_test = executable( 'yasl-input-alloc-test', ['common/test_input_alloc.c', '../src/common/input.c'], -- cgit v1.2.3