diff options
| -rw-r--r-- | include/yasl/yasl.h | 14 | ||||
| -rw-r--r-- | src/common/backend.c | 72 | ||||
| -rw-r--r-- | src/common/backend.h | 7 | ||||
| -rw-r--r-- | src/common/events.c | 11 | ||||
| -rw-r--r-- | src/common/surface.c | 17 | ||||
| -rw-r--r-- | src/common/types.h | 6 | ||||
| -rw-r--r-- | src/platform/posix/loop.c | 13 | ||||
| -rw-r--r-- | src/platform/wayland/backend.c | 4 | ||||
| -rw-r--r-- | tests/common/test_gl.c | 19 | ||||
| -rw-r--r-- | tests/common/test_loop.c | 45 | ||||
| -rw-r--r-- | tests/common/test_surface_integration.c | 46 |
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); |
