summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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);