From b7944e4bbfa64a6bb1a1d9e1f172827dde53aed2 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Tue, 13 Oct 2026 17:12:10 +0300 Subject: Read back the last presented CPU frame wsap_term writes PNG screenshots for its debug server and keeps its own copy of the frame because YASL had no readback. yasl_surface_read_pixels() copies the frame from the last successful present, or reports its extent when the buffer is too small. X11 and Cocoa keep it in their single CPU target until the next draw starts; X11 now also forgets it when a resize replaces the target, and Cocoa records the presented extent because a resize can change the size before the next draw. Wayland remembers which buffer it committed and forgets it when a draw reuses that buffer. The backend test checks the copy on Wayland and X11, and that draws that never present do not show through. The Cocoa path is not built or run here. Co-Authored-By: Claude Opus 5.5 --- docs/guide.md | 6 +++++ include/yasl/yasl.h | 19 +++++++++++++++ src/common/backend.c | 9 +++++++ src/common/backend.h | 16 +++++++++++++ src/platform/cocoa/window.m | 15 ++++++++++++ src/platform/wayland/api.h | 2 ++ src/platform/wayland/backend.c | 7 ++++++ src/platform/wayland/buffer.c | 14 +++++++++++ src/platform/wayland/internal.h | 3 +++ src/platform/x11/window.c | 12 ++++++++++ tests/common/test_backends.c | 52 +++++++++++++++++++++++++++++++++++++++++ 11 files changed, 155 insertions(+) diff --git a/docs/guide.md b/docs/guide.md index 2926011..4949f88 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -154,6 +154,12 @@ is unpaced: scheduling continuously will use CPU continuously. Use an applicatio timer to limit redraws there. Suspended/hidden windows need not draw at each requested interval. +`yasl_surface_read_pixels()` copies the last presented CPU frame, for example +for a screenshot or a debugging tool. Call it outside draw callbacks. A frame +is available from a successful present until the next draw starts, and again +once that draw presents. Call it with no buffer first to learn the extent. The +copy is YASL's frame, not the composited screen, and native rendering has none. + `yasl_surface_timing()` reports estimates and their provenance. All timestamps are monotonic nanoseconds. `YASL_TIMING_SUBMISSION` is CPU submission time, `YASL_TIMING_FRAME_CALLBACK` is a compositor pacing signal, and only diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h index 7c9bfbd..5946816 100644 --- a/include/yasl/yasl.h +++ b/include/yasl/yasl.h @@ -776,6 +776,25 @@ YASL_API bool yasl_surface_present(struct yasl_surface *s, const struct yasl_fra * ev->surface to identify the receiving surface. */ YASL_API bool yasl_surface_poll_event(struct yasl_surface *s, struct yasl_event *ev); +/** + * Copy the last frame presented from the surface's CPU target. + * @param s Surface. + * @param out Output for size->x * size->y premultiplied ARGB8888 pixels with + * tightly packed rows, or NULL when cap is zero. + * @param cap Capacity of out in pixels. + * @param size Required output: the frame extent in device pixels, or zero when + * no frame is available. + * @return true if copied. false leaves out unchanged; when *size is nonzero, + * out was too small, so allocate size->x * size->y pixels and call again. + * A frame is available from a successful yasl_surface_present() until the + * next draw callback starts, and again once that draw presents. After a draw + * that does not present or a resize that replaces the target, no frame may be + * available; native rendering never has one. Call outside draw callbacks. This copies YASL's frame, not + * the composited screen: decorations, other windows, and color conversion are + * not included. + */ +YASL_API bool yasl_surface_read_pixels(struct yasl_surface *s, uint32_t *out, size_t cap, + struct yasl_vector2i *size); /** * Select absolute or relative (locked) pointer input for a surface. * @param s Surface. diff --git a/src/common/backend.c b/src/common/backend.c index b15233f..bb5ae33 100644 --- a/src/common/backend.c +++ b/src/common/backend.c @@ -322,6 +322,15 @@ bool yasl_surface_poll_event(struct yasl_surface *s, struct yasl_event *ev) return input_queue_pop(&s->input, ev); } +bool yasl_surface_read_pixels(struct yasl_surface *s, uint32_t *out, size_t cap, + struct yasl_vector2i *size) +{ + if (!size) return false; + *size = (struct yasl_vector2i){0}; + if (!s || (!out && cap) || !s->display->ops->surface_read_pixels) return false; + return s->display->ops->surface_read_pixels(s, out, cap, size); +} + bool yasl_surface_set_pointer_mode(struct yasl_surface *s, enum yasl_pointer_mode mode) { if (!s || (mode != YASL_POINTER_NORMAL && mode != YASL_POINTER_RELATIVE)) diff --git a/src/common/backend.h b/src/common/backend.h index 96ce9e7..13ce9b5 100644 --- a/src/common/backend.h +++ b/src/common/backend.h @@ -7,6 +7,8 @@ #include "common/input.h" #include "common/timing.h" +#include + struct yasl_backend_ops; struct yasl_display { @@ -43,6 +45,17 @@ void yasl_surface_clear_timers(struct yasl_surface *s); struct yasl_surface *yasl_create_toplevel(struct yasl_display *d, const struct yasl_window_spec *spec, void *graphics_device); +/* Report a presented CPU frame's extent, and copy it when out can hold it. */ +static inline bool yasl_copy_frame(const uint32_t *pixels, struct yasl_vector2i extent, + uint32_t *out, size_t cap, struct yasl_vector2i *size) +{ + *size = extent; + size_t count = (size_t)extent.x * (size_t)extent.y; + if (!count || count > cap) return false; + memcpy(out, pixels, count * sizeof(*pixels)); + return true; +} + static inline uint64_t yasl_frame_begin(struct yasl_surface *s) { if (++s->frame_generation == 0) ++s->frame_generation; @@ -95,6 +108,9 @@ struct yasl_backend_ops { bool (*surface_place_under_pointer)(struct yasl_surface *s, struct yasl_vector2i at); /* Optional; NULL means the bell is unsupported. */ bool (*surface_bell)(struct yasl_surface *s); + /* Optional; NULL means no readback. Fills size only for an available frame. */ + bool (*surface_read_pixels)(struct yasl_surface *s, uint32_t *out, size_t cap, + struct yasl_vector2i *size); void *(*display_get_native_handle)(struct yasl_display *d); void *(*surface_get_native_handle)(struct yasl_surface *s); bool (*surface_get_native_frame)(struct yasl_surface *s, diff --git a/src/platform/cocoa/window.m b/src/platform/cocoa/window.m index 1efff59..2f9f8e5 100644 --- a/src/platform/cocoa/window.m +++ b/src/platform/cocoa/window.m @@ -89,6 +89,8 @@ struct cocoa_surface { struct yasl_frame frame; struct yasl_damage_list damage; bool did_present; + /* Extent of the frame in pixels, valid while did_present is set. */ + struct yasl_vector2i shown_size; unsigned idle_frames; bool want_draw; bool frame_due; @@ -1073,6 +1075,7 @@ bool cocoa_surface_present(struct cocoa_surface *w, const struct yasl_framebuffe } yasl_clock_presented(&w->clock, yasl_loop_now_ns(), false); w->did_present = true; + w->shown_size = w->size; return true; } @@ -2198,6 +2201,17 @@ static bool impl_surface_place_under_pointer(struct yasl_surface *s, struct yasl return cocoa_surface_place_under_pointer((struct cocoa_surface *)s, at); } +static bool impl_surface_read_pixels(struct yasl_surface *s, uint32_t *out, size_t cap, + struct yasl_vector2i *size) +{ + struct cocoa_surface *w = (struct cocoa_surface *)s; + /* did_present stays set from the last draw until the next one starts, and + * pixels are reallocated only when a draw starts. */ + if (w->closed || w->rendering || !w->did_present || !w->software_queue || !w->pixels) + return false; + return yasl_copy_frame(w->pixels, w->shown_size, out, cap, size); +} + static bool impl_surface_bell(struct yasl_surface *s) { assert([NSThread isMainThread]); @@ -2266,6 +2280,7 @@ const struct yasl_backend_ops yasl_cocoa_backend = { .surface_drag_carry = impl_surface_drag_carry, .surface_place_under_pointer = impl_surface_place_under_pointer, .surface_bell = impl_surface_bell, + .surface_read_pixels = impl_surface_read_pixels, .display_get_native_handle = impl_display_get_native_handle, .surface_get_native_handle = impl_surface_get_native_handle, .surface_get_native_frame = impl_surface_get_native_frame, diff --git a/src/platform/wayland/api.h b/src/platform/wayland/api.h index 6c3154c..9f290f5 100644 --- a/src/platform/wayland/api.h +++ b/src/platform/wayland/api.h @@ -39,5 +39,7 @@ bool wayland_surface_show_window_menu(struct wayland_surface *s, struct yasl_vec bool wayland_surface_drag_carry(struct wayland_surface *s, struct yasl_vector2i offset); bool wayland_surface_place_under_pointer(struct wayland_surface *s, struct yasl_vector2i at); bool wayland_surface_bell(struct wayland_surface *s); +bool wayland_surface_read_pixels(struct wayland_surface *s, uint32_t *out, size_t cap, + struct yasl_vector2i *size); #endif diff --git a/src/platform/wayland/backend.c b/src/platform/wayland/backend.c index ad9ed4c..4585cd6 100644 --- a/src/platform/wayland/backend.c +++ b/src/platform/wayland/backend.c @@ -185,6 +185,12 @@ static bool impl_surface_place_under_pointer(struct yasl_surface *s, struct yasl return wayland_surface_place_under_pointer((struct wayland_surface *)s, at); } +static bool impl_surface_read_pixels(struct yasl_surface *s, uint32_t *out, size_t cap, + struct yasl_vector2i *size) +{ + return wayland_surface_read_pixels((struct wayland_surface *)s, out, cap, size); +} + static bool impl_surface_bell(struct yasl_surface *s) { return wayland_surface_bell((struct wayland_surface *)s); @@ -240,6 +246,7 @@ const struct yasl_backend_ops yasl_wayland_backend = { .surface_drag_carry = impl_surface_drag_carry, .surface_place_under_pointer = impl_surface_place_under_pointer, .surface_bell = impl_surface_bell, + .surface_read_pixels = impl_surface_read_pixels, .display_get_native_handle = impl_display_get_native_handle, .surface_get_native_handle = impl_surface_get_native_handle, }; diff --git a/src/platform/wayland/buffer.c b/src/platform/wayland/buffer.c index f5a2c0f..b906f4f 100644 --- a/src/platform/wayland/buffer.c +++ b/src/platform/wayland/buffer.c @@ -472,9 +472,20 @@ bool wayland_surface_present(struct wayland_surface *w, const struct yasl_frameb return false; commit_buffer(w, w->cur); w->frame_presented = true; + w->shown = w->cur; + w->has_shown = true; return true; } +bool wayland_surface_read_pixels(struct wayland_surface *w, uint32_t *out, size_t cap, + struct yasl_vector2i *size) +{ + if (!w || w->closed || w->frame_active || !w->has_shown) return false; + const struct buffer *b = &w->buffers[w->shown]; + if (!b->pixels) return false; + return yasl_copy_frame(b->pixels, (struct yasl_vector2i){b->width, b->height}, out, cap, size); +} + void surface_draw_frame(struct wayland_surface *w) { if (w->closed || !w->configured) @@ -491,6 +502,9 @@ void surface_draw_frame(struct wayland_surface *w) w->frames_skipped++; return; } + /* Drawing reuses the presented buffer only after the compositor released it. */ + if (w->has_shown && idx == w->shown) + w->has_shown = false; if (buffer_prepare(w, &w->buffers[idx], w->width, w->height, &buffer_listener) < 0) { w->frames_skipped++; return; diff --git a/src/platform/wayland/internal.h b/src/platform/wayland/internal.h index 84a603d..34d4133 100644 --- a/src/platform/wayland/internal.h +++ b/src/platform/wayland/internal.h @@ -307,6 +307,8 @@ struct wayland_surface { int ime_shown_begin, ime_shown_end; int floating_width, floating_height; int cur; + /* Buffer holding the last presented frame, valid while has_shown is set. */ + int shown; int width, height; int logical_width, logical_height; int req_width, req_height; @@ -318,6 +320,7 @@ struct wayland_surface { /* 1-byte booleans packed tightly */ bool frame_active; bool frame_presented; + bool has_shown; bool size_pending; bool sample_refresh; bool has_pending_serial; diff --git a/src/platform/x11/window.c b/src/platform/x11/window.c index 724987f..765d83c 100644 --- a/src/platform/x11/window.c +++ b/src/platform/x11/window.c @@ -473,6 +473,8 @@ static bool resize(struct x11_surface *s, struct yasl_vector2i size) return true; } uint64_t start = yasl_loop_now_ns(); + /* The presented frame goes with the old pixels. */ + s->presented = false; if (s->shm_active) { XShmDetach(d->native, &s->shm_info); @@ -1883,6 +1885,15 @@ static int double_click_ms(const struct yasl_display *base) return portal_double_click_ms(); } +static bool read_pixels(struct yasl_surface *base, uint32_t *out, size_t cap, + struct yasl_vector2i *size) +{ + struct x11_surface *s = (struct x11_surface *)base; + /* presented stays set from the last draw until the next one starts. */ + if (s->destroyed || s->drawing || !s->presented || !s->pixels) return false; + return yasl_copy_frame(s->pixels, s->size, out, cap, size); +} + static bool bell(struct yasl_surface *base) { struct x11_surface *s = (struct x11_surface *)base; @@ -2138,6 +2149,7 @@ const struct yasl_backend_ops yasl_x11_backend = { .surface_drag_carry = drag_carry, .surface_place_under_pointer = place, .surface_bell = bell, + .surface_read_pixels = read_pixels, .display_double_click_ms = double_click_ms, .surface_start_interactive_move = interactive_move, .surface_start_interactive_resize = interactive_resize, diff --git a/tests/common/test_backends.c b/tests/common/test_backends.c index 74860c8..30f9b67 100644 --- a/tests/common/test_backends.c +++ b/tests/common/test_backends.c @@ -112,6 +112,15 @@ static void configure(struct yasl_surface *s, struct yasl_geometry geometry, voi if (w->destroy_on_configure) { yasl_surface_destroy(s); w->surface = NULL; } } +static void draw_without_present(struct yasl_surface *s, struct yasl_frame *frame, void *ud) +{ + (void)s; + struct window *w = ud; + size_t count = (size_t)frame->target.size.x * (size_t)frame->target.size.y; + for (size_t i = 0; i < count; i++) frame->target.pixels[i] = 0xff000000; + w->draws++; +} + static void close_window(struct yasl_surface *s, void *ud) { struct window *w = ud; @@ -146,6 +155,48 @@ static struct window create(struct yasl_display *d) return (struct window){.surface = s, .display = d}; } +/* The last presented frame reads back until a later draw replaces it. */ +static void test_read_pixels(struct yasl_loop *loop, struct yasl_display *d, struct window *w) +{ + struct yasl_vector2i size = {-1, -1}; + assert(!yasl_surface_read_pixels(w->surface, NULL, 0, NULL)); + assert(!yasl_surface_read_pixels(NULL, NULL, 0, &size) && !size.x && !size.y); + assert(!yasl_surface_read_pixels(w->surface, NULL, 0, &size)); + struct yasl_vector2i extent = yasl_surface_geometry(w->surface).size; + assert(size.x == extent.x && size.y == extent.y); + size_t count = (size_t)size.x * (size_t)size.y; + uint32_t *pixels = calloc(count, sizeof(*pixels)); + assert(pixels); + assert(!yasl_surface_read_pixels(w->surface, pixels, count - 1, &size)); + assert(!pixels[0]); + assert(yasl_surface_read_pixels(w->surface, pixels, count, &size)); + for (size_t i = 0; i < count; i++) assert(pixels[i] == 0xff4080c0); + + /* Draws that never present must not show through. X11 reuses its one + * target; Wayland keeps the presented buffer while the compositor holds it. */ + struct yasl_window_cbs skip = callbacks; + skip.draw = draw_without_present; + yasl_surface_set_callbacks(w->surface, &skip, w); + for (int i = 0; i < 4; i++) { + yasl_surface_schedule_redraw(w->surface); + pump(loop, d, NULL); + if (yasl_surface_read_pixels(w->surface, pixels, count, &size)) { + assert(yasl_display_backend(d) != YASL_BACKEND_X11); + for (size_t j = 0; j < count; j++) assert(pixels[j] == 0xff4080c0); + } else { + assert(!size.x && !size.y); + } + } + + attach(w); + yasl_surface_schedule_redraw(w->surface); + pump(loop, d, NULL); + memset(pixels, 0, count * sizeof(*pixels)); + assert(yasl_surface_read_pixels(w->surface, pixels, count, &size)); + assert(pixels[0] == 0xff4080c0 && pixels[count - 1] == 0xff4080c0); + free(pixels); +} + static void test_window_spec(struct yasl_loop *loop, struct yasl_display *d) { struct window w = {.display = d, .destroy_on_draw = true}; @@ -427,6 +478,7 @@ int main(int argc, char **argv) yasl_surface_schedule_redraw(w.surface); pump(loop, d, NULL); assert(w.draws > 0); + test_read_pixels(loop, d, &w); if (yasl_display_backend(d) == YASL_BACKEND_X11) x11_behavior(loop, d, &w); if (w.surface) yasl_surface_destroy(w.surface); -- cgit v1.2.3