diff options
| -rw-r--r-- | docs/guide.md | 6 | ||||
| -rw-r--r-- | include/yasl/yasl.h | 19 | ||||
| -rw-r--r-- | src/common/backend.c | 9 | ||||
| -rw-r--r-- | src/common/backend.h | 16 | ||||
| -rw-r--r-- | src/platform/cocoa/window.m | 15 | ||||
| -rw-r--r-- | src/platform/wayland/api.h | 2 | ||||
| -rw-r--r-- | src/platform/wayland/backend.c | 7 | ||||
| -rw-r--r-- | src/platform/wayland/buffer.c | 14 | ||||
| -rw-r--r-- | src/platform/wayland/internal.h | 3 | ||||
| -rw-r--r-- | src/platform/x11/window.c | 12 | ||||
| -rw-r--r-- | tests/common/test_backends.c | 52 |
11 files changed, 155 insertions, 0 deletions
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 @@ -777,6 +777,25 @@ YASL_API bool yasl_surface_present(struct yasl_surface *s, const struct yasl_fra */ 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. * @param mode YASL_POINTER_NORMAL or YASL_POINTER_RELATIVE. 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 <string.h> + 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); |
