summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/yasl/yasl.h40
-rw-r--r--src/cocoa/window.m26
-rw-r--r--src/common/backend.c106
-rw-r--r--src/common/backend.h6
-rw-r--r--src/common/loop.c267
-rw-r--r--src/wayland/surface.c2
-rw-r--r--src/x11/window.c35
-rw-r--r--tests/common/test_timers.c297
-rw-r--r--tests/meson.build8
9 files changed, 742 insertions, 45 deletions
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 <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+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'],