summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-16 09:33:39 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-16 09:33:39 +0300
commita075a2941f8fcdae155495da634cf63368c950b7 (patch)
treea33b5636ef7b9e4879cb8ab52220e805a5e30348
parentc2832a63547e6328afde25bb3b8145267eadef47 (diff)
Pace frame requests by the frame presented after them
A frame request now only records intent. Each loop step resolves it after the callbacks that present, so a request made before the present waits for that frame. An animation that asked first got two REFRESH events per frame on X11 and with GL. With no frame in flight, the REFRESH comes one refresh interval after the last present or answer, so a request without a present cannot spin. Queued events now go before an owed REFRESH. Input reaches the application before it draws, and a queued REFRESH pays an owed one for the same surface. The loop prepare only peeks at a lost event, so deliver can report it as a final loss. A failed push from another thread wakes the loop. Signals and posts run at the end of a step, after descriptors and timers. A signal handler now sees the output that came with it. A render-thread GL swap counts as in flight on Wayland. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--include/yasl/yasl.h14
-rw-r--r--src/common/backend.c72
-rw-r--r--src/common/backend.h7
-rw-r--r--src/common/events.c11
-rw-r--r--src/common/surface.c17
-rw-r--r--src/common/types.h6
-rw-r--r--src/platform/posix/loop.c13
-rw-r--r--src/platform/wayland/backend.c4
-rw-r--r--tests/common/test_gl.c19
-rw-r--r--tests/common/test_loop.c45
-rw-r--r--tests/common/test_surface_integration.c46
11 files changed, 207 insertions, 47 deletions
diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h
index dab1091..6139bc3 100644
--- a/include/yasl/yasl.h
+++ b/include/yasl/yasl.h
@@ -114,9 +114,11 @@ YASL_API int yasl_surface_set_title(yasl_surface *surface, const char *title);
enum yasl_request { YASL_REQUEST_RESTORE, YASL_REQUEST_FULLSCREEN, YASL_REQUEST_MAXIMIZE, YASL_REQUEST_MINIMIZE };
YASL_API int yasl_surface_request(yasl_surface *surface, enum yasl_request request);
YASL_API int yasl_surface_request_size(yasl_surface *surface, int width, int height);
-/** Asks for a REFRESH when the window can show a new frame: at once when none
- * is in flight, otherwise once the system shows the current one. Requests
- * coalesce. Use it to pace animation; draw at once for input. */
+/** Asks for a REFRESH after the next frame is shown. A frame presented after
+ * the request, in the same callback, is that frame, so an animation can ask
+ * before it draws. With no frame in flight, the REFRESH comes one refresh
+ * interval after the last present or REFRESH. Requests coalesce. Use it to
+ * pace animation; draw at once for input. */
YASL_API int yasl_surface_request_frame(yasl_surface *surface);
enum yasl_cursor {
@@ -306,9 +308,9 @@ YASL_API yasl_loop *yasl_loop_create(void);
/** EBUSY while a step runs or the display runs on it. Leaves descriptors open. */
YASL_API int yasl_loop_destroy(yasl_loop *loop);
/** Waits at most timeout_ms (0 polls, -1 waits) and runs what is ready:
- * display events, then descriptors, then due timers. Returns how many
- * descriptor and timer callbacks ran. EBUSY inside a step; EPERM off the
- * loop's thread. */
+ * display events, then descriptors, then due timers, then signals. Returns
+ * how many descriptor and timer callbacks ran. EBUSY inside a step; EPERM off
+ * the loop's thread. */
YASL_API int yasl_loop_step(yasl_loop *loop, int timeout_ms);
/** Steps until yasl_loop_quit. A quit made before the run ends it at once. */
YASL_API int yasl_loop_run(yasl_loop *loop);
diff --git a/src/common/backend.c b/src/common/backend.c
index 9910e08..1a81a7b 100644
--- a/src/common/backend.c
+++ b/src/common/backend.c
@@ -22,14 +22,17 @@ static const struct yasl_private_backend_ops *backend_ops(enum yasl_private_back
}
}
+static void resolve_frame_requests(struct yasl_private_display *d);
+
/* The display drives the loop it runs on; see struct yasl_private_loop_driver. */
static bool driver_prepare(void *ud)
{
struct yasl_private_display *d = ud;
bool ready = d->ops->display_prepare(d);
+ resolve_frame_requests(d);
/* Readiness the backends cannot see: events already queued, an error to
* report, or an owed REFRESH. */
- return ready || yasl_private_events_pending(d) || yasl_private_events_error(d) ||
+ return ready || yasl_private_events_pending(d) || yasl_private_events_failed(d) ||
(d->pending && d->pending(d));
}
@@ -584,43 +587,70 @@ void yasl_private_surface_request_frame(struct yasl_surface *s)
yasl_private_loop_wake(s->display->loop);
}
+static uint64_t refresh_interval_ns(struct yasl_surface *s)
+{
+ double refresh = yasl_private_surface_timing(s).refresh_ns;
+ return refresh >= 1000000.0 && refresh <= 1000000000.0 ? (uint64_t)refresh : 16666667;
+}
+
void yasl_private_surface_notify_frame_shown(struct yasl_surface *s)
{
if (!s || !s->frame_requested)
return;
s->frame_requested = false;
+ s->frame_answered_ns = yasl_private_loop_now_ns();
s->refresh_owed = true;
}
+static void resolve_frame_request(struct yasl_surface *s);
+
static uint64_t frame_interval_done(void *ud, yasl_private_timer_id id, uint64_t interval_ns)
{
(void)id; (void)interval_ns;
- yasl_private_surface_notify_frame_shown(ud);
+ struct yasl_surface *s = ud;
+ s->frame_timer = 0;
+ /* A present since the timer was set moves the next frame later. */
+ resolve_frame_request(s);
return 0;
}
-bool yasl_private_surface_request_shown_frame(struct yasl_surface *s)
+/* The frame a request waits for is the next one the application presents,
+ * which is often after the request. So the loop decides at the start of each
+ * step, after the callbacks that draw have run. */
+static void resolve_frame_request(struct yasl_surface *s)
{
- s->frame_requested = true;
+ if (!s->frame_requested || yasl_private_loop_timer_armed(s->display->loop, s->frame_timer))
+ return;
+ /* The system reports when the frame in flight is shown. */
const struct yasl_private_backend_ops *ops = s->display->ops;
- if (ops->surface_frame_in_flight) {
- if (!ops->surface_frame_in_flight(s))
- yasl_private_surface_notify_frame_shown(s);
- return true;
- }
- /* No frame signal: one frame per refresh interval since the last present. */
- if (yasl_private_loop_timer_armed(s->display->loop, s->frame_timer))
- return true;
- double refresh = yasl_private_surface_timing(s).refresh_ns;
- uint64_t interval = refresh >= 1000000.0 && refresh <= 1000000000.0 ? (uint64_t)refresh : 16666667;
- uint64_t presented = atomic_load(&s->presented_ns);
- uint64_t since = yasl_private_loop_now_ns() - presented;
- if (!presented || since >= interval) {
+ if (ops->surface_frame_in_flight && ops->surface_frame_in_flight(s))
+ return;
+ /* Otherwise one frame per refresh interval, counted from the last
+ * present or answer, so a request with no present cannot spin. */
+ uint64_t last = atomic_load(&s->presented_ns);
+ if (s->frame_answered_ns > last)
+ last = s->frame_answered_ns;
+ uint64_t interval = refresh_interval_ns(s);
+ uint64_t since = yasl_private_loop_now_ns() - last;
+ if (!last || since >= interval) {
yasl_private_surface_notify_frame_shown(s);
- return true;
+ return;
}
s->frame_timer = yasl_private_surface_add_timer(s, interval - since, frame_interval_done, s);
- return s->frame_timer != 0;
+ /* Without a timer, an early frame is better than none. */
+ if (!s->frame_timer)
+ yasl_private_surface_notify_frame_shown(s);
+}
+
+void yasl_private_surface_request_shown_frame(struct yasl_surface *s)
+{
+ s->frame_requested = true;
+}
+
+static void resolve_frame_requests(struct yasl_private_display *d)
+{
+ for (struct yasl_surface *s = d->surfaces; s; s = s->event_next)
+ resolve_frame_request(s);
}
static uint64_t pacing_tick(void *ud, yasl_private_timer_id id, uint64_t interval)
@@ -628,9 +658,7 @@ static uint64_t pacing_tick(void *ud, yasl_private_timer_id id, uint64_t interva
(void)id; (void)interval;
struct yasl_surface *s = ud;
yasl_private_surface_request_frame(s);
- double refresh = yasl_private_surface_timing(s).refresh_ns;
- return refresh >= 1000000.0 && refresh <= 1000000000.0
- ? (uint64_t)refresh : 16666667;
+ return refresh_interval_ns(s);
}
bool yasl_private_surface_set_pacing(struct yasl_surface *s, yasl_private_pacing_mode mode)
diff --git a/src/common/backend.h b/src/common/backend.h
index 7633609..a9076e4 100644
--- a/src/common/backend.h
+++ b/src/common/backend.h
@@ -50,10 +50,13 @@ struct yasl_surface {
struct yasl_private_vector2i reported_size;
uint32_t reported_scale_120, reported_state;
bool refresh_owed;
- /* yasl_surface_request_frame waits for the frame in flight. */
+ /* yasl_surface_request_frame waits for the next frame shown. Each loop
+ * step resolves it after the callbacks that present; see backend.c. */
bool frame_requested;
/* Written by GL presents on a rendering thread. */
_Atomic uint64_t presented_ns;
+ /* When the last frame request was answered. */
+ uint64_t frame_answered_ns;
yasl_private_timer_id frame_timer;
void (*refresh)(struct yasl_surface *);
bool text_input_active;
@@ -93,6 +96,8 @@ struct yasl_surface {
bool yasl_private_events_open(struct yasl_private_display *d);
void yasl_private_events_close(struct yasl_private_display *d);
int yasl_private_events_error(struct yasl_private_display *d);
+/* Whether an event was lost, without reporting it. */
+bool yasl_private_events_failed(struct yasl_private_display *d);
bool yasl_private_events_pending(struct yasl_private_display *d);
struct yasl_surface *yasl_private_events_find(struct yasl_private_display *d, uint64_t id);
bool yasl_private_events_register(struct yasl_surface *s);
diff --git a/src/common/events.c b/src/common/events.c
index 83baf11..c310f06 100644
--- a/src/common/events.c
+++ b/src/common/events.c
@@ -71,6 +71,15 @@ int yasl_private_events_error(struct yasl_private_display *d)
return error;
}
+bool yasl_private_events_failed(struct yasl_private_display *d)
+{
+ if (!d->events) return false;
+ yasl_private_mutex_lock(&d->events->mutex);
+ bool failed = d->events->error != 0;
+ yasl_private_mutex_unlock(&d->events->mutex);
+ return failed;
+}
+
bool yasl_private_events_pending(struct yasl_private_display *d)
{
if (!d->events) return false;
@@ -207,6 +216,8 @@ failed:
yasl_private_mutex_lock(&d->events->mutex);
d->events->error = ENOMEM;
yasl_private_mutex_unlock(&d->events->mutex);
+ /* The loss is an event too; the owner reports it in its next step. */
+ yasl_private_loop_wake(d->loop);
errno = ENOMEM;
return false;
}
diff --git a/src/common/surface.c b/src/common/surface.c
index 5c29a10..6b805be 100644
--- a/src/common/surface.c
+++ b/src/common/surface.c
@@ -451,7 +451,8 @@ int yasl_surface_request_frame(yasl_surface *s)
{
if (!yasl_public_connected(s))
return -errno;
- return STATUS(yasl_private_surface_request_shown_frame(s));
+ yasl_private_surface_request_shown_frame(s);
+ return 0;
}
int yasl_surface_set_cursor(yasl_surface *s, enum yasl_cursor cursor)
{
@@ -901,14 +902,10 @@ static bool convert_event(const struct yasl_private_event *p, yasl_event *e)
return false;
}
}
+/* Queued events go first: input reaches the application before it draws, and
+ * a queued REFRESH also pays one owed to the same surface. */
static int take_event(yasl_event *out)
{
- for (yasl_surface *s = display->surfaces; s; s = s->event_next)
- if (s->refresh_owed && refresh_ready(s)) {
- s->refresh_owed = false;
- *out = (yasl_event){.type = YASL_EVENT_REFRESH, .surface = s, .time_ns = yasl_time_ns()};
- return 1;
- }
struct yasl_private_event p;
while (yasl_private_display_poll_event(display, &p)) {
/* Input method events from an older text field are stale. */
@@ -926,6 +923,12 @@ static int take_event(yasl_event *out)
if (errno)
return yasl_public_error(errno);
}
+ for (yasl_surface *s = display->surfaces; s; s = s->event_next)
+ if (s->refresh_owed && refresh_ready(s)) {
+ s->refresh_owed = false;
+ *out = (yasl_event){.type = YASL_EVENT_REFRESH, .surface = s, .time_ns = yasl_time_ns()};
+ return 1;
+ }
return 0;
}
/* Internal wakeups, such as a redraw request, keep waiting; only an event, the deadline or
diff --git a/src/common/types.h b/src/common/types.h
index 8d00d0b..67aaaa3 100644
--- a/src/common/types.h
+++ b/src/common/types.h
@@ -874,9 +874,9 @@ bool yasl_private_frame_present(struct yasl_private_frame *frame,
const struct yasl_private_rect2i *rects, size_t count);
/** Coalesce a redraw request; use on the display thread. NULL is ignored. */
void yasl_private_surface_request_frame(struct yasl_surface *s);
-/** Owe s a REFRESH once the system shows its frame in flight, or at once when
- * none is. False with errno when the fallback timer cannot start. */
-bool yasl_private_surface_request_shown_frame(struct yasl_surface *s);
+/** Owe s a REFRESH after the next frame shown. The next loop step decides
+ * which frame that is, so a present after the request counts. */
+void yasl_private_surface_request_shown_frame(struct yasl_surface *s);
/** Set drawing policy for either loop style. Returns false with errno on failure. */
bool yasl_private_surface_set_pacing(struct yasl_surface *s, yasl_private_pacing_mode mode);
diff --git a/src/platform/posix/loop.c b/src/platform/posix/loop.c
index 6624575..38f504d 100644
--- a/src/platform/posix/loop.c
+++ b/src/platform/posix/loop.c
@@ -122,15 +122,13 @@ static void run_signals(struct yasl_loop *l)
}
}
+/* Only wakes the wait; the step runs posts and signals last. */
static void loop_wake_cb(void *ud, int fd, short revents)
{
- (void)revents;
+ (void)ud; (void)revents;
char buf[128];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0 || (n < 0 && errno == EINTR)) {}
- struct yasl_loop *l = ud;
- run_posts(l);
- run_signals(l);
}
static bool open_wake_pipe(int p[2])
@@ -575,7 +573,12 @@ static int step(struct yasl_loop *l, int timeout_ms)
if (d && d->deliver) d->deliver(d->ud);
int fds = dispatch_fds(l);
if (fds < 0) return fds;
- return fds + fire_timers(l);
+ int timers = fire_timers(l);
+ /* Last, so a signal sees what came with it: a child's final output is
+ * read before its exit is handled. */
+ run_posts(l);
+ run_signals(l);
+ return fds + timers;
}
int yasl_loop_step(yasl_loop *l, int timeout_ms)
diff --git a/src/platform/wayland/backend.c b/src/platform/wayland/backend.c
index f6207ed..26d88f6 100644
--- a/src/platform/wayland/backend.c
+++ b/src/platform/wayland/backend.c
@@ -30,7 +30,9 @@ static void impl_display_check(struct yasl_private_display *d)
static bool impl_surface_frame_in_flight(const struct yasl_surface *s)
{
- return ((const struct wayland_surface *)s)->frame_cb != NULL;
+ struct wayland_surface *w = (struct wayland_surface *)s;
+ /* A swap on a rendering thread is in flight before the owner arms its callback. */
+ return w->frame_cb != NULL || atomic_load(&w->frame_deferred);
}
static void impl_display_flush(struct yasl_private_display *d)
diff --git a/tests/common/test_gl.c b/tests/common/test_gl.c
index 1bb5bec..0b91bff 100644
--- a/tests/common/test_gl.c
+++ b/tests/common/test_gl.c
@@ -146,6 +146,25 @@ int main(int argc, char **argv)
clear(GL_COLOR_BUFFER_BIT_);
REQUIRE((yasl_gl_present(primary) == 0), "swap resized GL surface");
+ /* An animation that asks for each next frame gets one REFRESH per frame
+ * shown, even though GL surfaces also get a REFRESH when a frame is shown. */
+ REQUIRE(yasl_surface_request_frame(primary) == 0, "request a frame");
+ int frames = 0;
+ double animation_end = seconds_now() + 1.0;
+ while (seconds_now() < animation_end) {
+ yasl_event event = {0};
+ int result = yasl_wait_event(&event, 50);
+ REQUIRE(result >= 0, "wait during animation");
+ if (result == 0 || event.type != YASL_EVENT_REFRESH || event.surface != primary)
+ continue;
+ frames++;
+ REQUIRE(yasl_surface_request_frame(primary) == 0, "request the next frame");
+ clear(GL_COLOR_BUFFER_BIT_);
+ REQUIRE(yasl_gl_present(primary) == 0, "present an animation frame");
+ }
+ fprintf(stderr, "%d REFRESH events in 1 s of GL animation\n", frames);
+ REQUIRE(frames >= 20 && frames <= 75, "one REFRESH per frame shown");
+
REQUIRE(yasl_surface_create(&(struct yasl_surface_options){.title="invalid attrs", .width=100, .height=100, .type=YASL_SURFACE_GL, .gl = {.major = -1}}) == NULL && errno == EINVAL,
"reject invalid GL context options");
cpu = yasl_surface_create(&(struct yasl_surface_options){.title="CPU surface", .width=100, .height=100});
diff --git a/tests/common/test_loop.c b/tests/common/test_loop.c
index bdc33b7..a77b6a5 100644
--- a/tests/common/test_loop.c
+++ b/tests/common/test_loop.c
@@ -237,6 +237,47 @@ static void test_signals(void)
sigaction(SIGUSR2, &dfl, NULL);
}
+struct order_log {
+ char order[8];
+ int count;
+};
+
+static void order_signal(void *ud, int signo)
+{
+ (void)signo;
+ struct order_log *log = ud;
+ log->order[log->count++] = 's';
+}
+
+static void order_fd(void *ud, int fd, short revents)
+{
+ (void)revents;
+ struct order_log *log = ud;
+ char b;
+ assert(read(fd, &b, 1) == 1);
+ log->order[log->count++] = 'f';
+}
+
+/* A signal runs after the descriptors of its step, so a child's last output
+ * is read before its exit is handled. */
+static void test_signal_order(void)
+{
+ yasl_loop *l = yasl_loop_create();
+ assert(l);
+ struct order_log log = {0};
+ assert(yasl_loop_watch_signals(l, (const int[]){SIGUSR1}, 1, order_signal, &log) == 0);
+ int p[2];
+ assert(pipe(p) == 0);
+ assert(yasl_loop_add_fd(l, p[0], POLLIN, order_fd, &log) > 0);
+ assert(write(p[1], "x", 1) == 1);
+ raise(SIGUSR1);
+ assert(yasl_loop_step(l, 0) >= 1);
+ assert(log.count == 2 && memcmp(log.order, "fs", 2) == 0);
+ close(p[0]);
+ close(p[1]);
+ assert(yasl_loop_destroy(l) == 0);
+}
+
struct fd_log {
int calls;
short revents;
@@ -364,7 +405,8 @@ static void test_interrupted_wait(void)
assert(yasl_loop_step(l, 100) == 0);
uint64_t elapsed_ms = (yasl_time_ns() - start) / 1000000u;
pthread_join(t, NULL);
- assert(elapsed_ms >= 90 && elapsed_ms < 140);
+ /* Lateness under load is not the bug; an early return is. */
+ assert(elapsed_ms >= 90 && elapsed_ms < 1000);
assert(yasl_loop_destroy(l) == 0);
sigaction(SIGUSR2, &old, NULL);
}
@@ -376,6 +418,7 @@ int main(void)
test_post_order();
test_cross_thread();
test_signals();
+ test_signal_order();
test_fds();
test_reentry_and_quit();
test_interrupted_wait();
diff --git a/tests/common/test_surface_integration.c b/tests/common/test_surface_integration.c
index 7862bfc..6946cf9 100644
--- a/tests/common/test_surface_integration.c
+++ b/tests/common/test_surface_integration.c
@@ -37,7 +37,8 @@ static void *wrong_thread(void *data)
struct callback_state {
yasl_loop *loop;
yasl_surface *window;
- int refreshes;
+ int refreshes, resizes, refreshes_at_resize, final_losses;
+ bool animating;
int wait_result, shutdown_result;
yasl_surface *made;
int made_destroy;
@@ -45,8 +46,18 @@ struct callback_state {
static void on_event(void *userdata, const yasl_event *e)
{
struct callback_state *st = userdata;
+ if (e->type == YASL_EVENT_DISPLAY_LOST && e->action == YASL_LOST_FINAL)
+ st->final_losses++;
+ if (e->type == YASL_EVENT_WINDOW && e->surface == st->window &&
+ e->window.width == 120 && e->window.height == 80) {
+ st->resizes++;
+ st->refreshes_at_resize = st->refreshes;
+ }
if (e->type == YASL_EVENT_REFRESH && e->surface == st->window) {
st->refreshes++;
+ /* An animation asks for its next frame before it presents this one. */
+ if (st->animating)
+ assert(yasl_surface_request_frame(e->surface) == 0);
int w = 0, h = 0;
uint32_t *pixels = yasl_cpu_pixels(e->surface, &w, &h);
assert(pixels && w > 0 && h > 0);
@@ -111,6 +122,32 @@ static void test_callback_mode(enum yasl_backend backend)
assert(yasl_loop_step(st.loop, 25) >= 0);
assert(st.refreshes == before + 1);
+ /* The frame an animation presents after its request is the frame the
+ * request waits for: one REFRESH per display refresh, not two. */
+ st.animating = true;
+ assert(yasl_surface_request_frame(st.window) == 0);
+ before = st.refreshes;
+ uint64_t start = yasl_time_ns();
+ while (yasl_time_ns() - start < 1000000000ull)
+ assert(yasl_loop_step(st.loop, 50) >= 0);
+ st.animating = false;
+ int frames = st.refreshes - before;
+ fprintf(stderr, "%d REFRESH events in 1 s of animation\n", frames);
+ assert(frames >= 20 && frames <= 75);
+
+ /* A resize owes one REFRESH, after its WINDOW event. */
+ for (int i = 0; i < 6; i++)
+ assert(yasl_loop_step(st.loop, 25) >= 0);
+ before = st.refreshes;
+ assert(yasl_surface_request_size(st.window, 120, 80) == 0);
+ step_until(&st, &st.resizes, 1);
+ for (int i = 0; i < 8; i++)
+ assert(yasl_loop_step(st.loop, 25) >= 0);
+ fprintf(stderr, "%d REFRESH events for one resize, %d before its WINDOW event\n",
+ st.refreshes - before, st.refreshes_at_resize - before);
+ assert(st.resizes == 1 && st.refreshes_at_resize == before);
+ assert(st.refreshes == before + 1);
+
/* An idle loop waits instead of spinning. */
int steps = 0;
uint64_t end = yasl_time_ns() + 300000000ull;
@@ -125,6 +162,13 @@ static void test_callback_mode(enum yasl_backend backend)
assert(yasl_loop_step(st.loop, 50) >= 0);
assert(st.made_destroy == 0);
+ /* A lost event ends the session: on_event hears it as a final loss. A
+ * string event with no text cannot be queued. */
+ struct yasl_private_event broken = {.type = YASL_PRIVATE_EVENT_PASTE, .surface = st.window,
+ .text = {NULL, 1}};
+ assert(!yasl_private_events_push(st.window->display, st.window, &broken));
+ step_until(&st, &st.final_losses, 1);
+
assert(yasl_loop_add_timer(st.loop, YASL_PRIVATE_MS_TO_NS(10), quit_in_timer, st.loop));
assert(yasl_loop_run(st.loop) == 0);
assert(yasl_shutdown() == 0);