diff options
| -rw-r--r-- | README.md | 47 | ||||
| -rw-r--r-- | include/yasl/timing.h | 10 | ||||
| -rw-r--r-- | include/yasl/yasl.h | 64 | ||||
| -rw-r--r-- | src/cocoa/window.m | 67 | ||||
| -rw-r--r-- | src/common/backend.c | 57 | ||||
| -rw-r--r-- | src/common/backend.h | 9 | ||||
| -rw-r--r-- | src/common/input.c | 103 | ||||
| -rw-r--r-- | src/common/input.h | 19 | ||||
| -rw-r--r-- | src/common/meson.build | 2 | ||||
| -rw-r--r-- | src/common/timing.c | 1 | ||||
| -rw-r--r-- | src/wayland/buffer.c | 9 | ||||
| -rw-r--r-- | src/wayland/clipboard.c | 32 | ||||
| -rw-r--r-- | src/wayland/clipboard.h | 5 | ||||
| -rw-r--r-- | src/wayland/drag.c | 12 | ||||
| -rw-r--r-- | src/wayland/internal.h | 34 | ||||
| -rw-r--r-- | src/wayland/keyboard.c | 6 | ||||
| -rw-r--r-- | src/wayland/pointer.c | 8 | ||||
| -rw-r--r-- | src/wayland/surface.c | 13 | ||||
| -rw-r--r-- | src/x11/window.c | 109 | ||||
| -rw-r--r-- | tests/cocoa/test_native.c | 14 | ||||
| -rw-r--r-- | tests/cocoa/test_ownership.m | 45 | ||||
| -rw-r--r-- | tests/common/test_backends.c | 92 | ||||
| -rw-r--r-- | tests/common/test_contracts.c | 175 | ||||
| -rw-r--r-- | tests/common/test_input_alloc.c | 48 | ||||
| -rw-r--r-- | tests/meson.build | 28 | ||||
| -rw-r--r-- | tests/wayland/test_input.c | 26 |
26 files changed, 855 insertions, 180 deletions
@@ -76,6 +76,47 @@ The native surface handle is a `wl_surface *` on Wayland. On X11, convert it to `Window` through `uintptr_t`. On Cocoa, the native surface handle is the content `NSView *`. +## Ownership and backend contracts + +- Calls and callbacks use the thread that opened the display (the main thread + on Cocoa). Supplied event loops are borrowed. Destroy popups before parents, + surfaces before displays, and displays before supplied loops. Surface + destruction inside surface callbacks is supported; display or loop destruction + during callbacks/dispatch is not. +- Callback tables, drag listeners, input strings, titles, clipboard text, and + drag pixels are copied. Userdata is borrowed. Cocoa retains a supplied Metal + device. Native handles are borrowed, not transferred to the caller. +- Text, paste, and file-drop event payloads remain valid until the next + **successful** input poll on that surface or surface destruction. Dispatch, + flush, selection replacement, and failed polls do not invalidate them. + Copy a payload if it must live longer. +- Input queues grow rather than silently dropping events at a fixed limit. + If allocation fails, pending input is discarded and `YASL_INPUT_RESET` is + delivered before later events. Apps must clear held keys/buttons and input + composition state on this event. +- Clipboard requests retain their original target. Destroying that target + cancels delivery, rather than redirecting the paste to a focused window. + Unsupported primary selections are no-ops, never aliases for the clipboard. +- CPU pixels are tightly packed premultiplied `0xAARRGGBB`. Creation sizes are + logical pixels; framebuffer sizes, pointer coordinates, caret and damage are + device pixels. Every draw target needs complete valid pixel contents; do not + assume a recycled framebuffer contains the previous frame. +- Present only the current draw callback's target, at most once. Targets expire + when the callback returns; preserve the opaque `generation` when copying a + target. Damage is required, clipped to the target, and validated uniformly. Backends may upload the whole frame despite damage hints. +- A non-NULL `graphics_device` requires `YASL_CAP_NATIVE_GRAPHICS` (currently + Cocoa); other backends reject it with `ENOTSUP`. In native Metal mode the app + submits and presents the drawable, then calls `yasl_surface_present()` to + record that submission. +- `YASL_CAP_FRAME_PACING` distinguishes compositor/display-driven draws from + X11's unpaced flush-driven draws. `yasl_timing.source` distinguishes submission + estimates, frame callbacks, and confirmed presentation feedback. Refresh and + next-presentation times are estimates, not universal vsync guarantees. + +These are pre-release API changes. Applications must handle the new reset input +kind and should inspect timing provenance instead of assuming all timestamps +confirm scanout. See `include/yasl/yasl.h` for the complete contracts. + ## Building Use a C23 compiler and Meson 1.6 or later. @@ -104,7 +145,11 @@ meson test -C build-test --print-errorlogs The tests start isolated display servers. They test explicit requests, automatic selection, and fallback. -They also test both backends in one process. +They also test both backends in one process, callback-table copying, input +payload ownership, queue growth, concurrent clipboard targets, and consistent +frame/damage validation. Wayland IME ownership tests need no display server. +For memory and undefined-behavior checks, configure a separate build with +`-Db_sanitize=address,undefined`. On macOS, the same option runs native Cocoa window tests. These tests need a desktop session. diff --git a/include/yasl/timing.h b/include/yasl/timing.h index 7c66967..85f305d 100644 --- a/include/yasl/timing.h +++ b/include/yasl/timing.h @@ -15,11 +15,20 @@ struct yasl_resize_stats { long minflt; }; +/* Describes last_present_ns, not merely availability of an extension. */ +enum yasl_timing_source { + YASL_TIMING_UNKNOWN = 0, + YASL_TIMING_SUBMISSION, /* CPU submission time; not scanout confirmation */ + YASL_TIMING_FRAME_CALLBACK, /* compositor pacing callback, not scanout */ + YASL_TIMING_PRESENTATION, /* compositor-confirmed presentation */ +}; + struct yasl_timing { double refresh_ns; uint64_t last_present_ns; uint64_t next_present_ns; uint64_t frames_skipped; + enum yasl_timing_source source; struct yasl_resize_stats resize; }; @@ -33,6 +42,7 @@ struct yasl_frame_clock { long resize_minflt; uint64_t frame_time_ns; double refresh_ns; + enum yasl_timing_source source; }; uint64_t yasl_clock_extend_ms(uint32_t ms, uint64_t now_ns); diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h index 6fea487..2852112 100644 --- a/include/yasl/yasl.h +++ b/include/yasl/yasl.h @@ -4,6 +4,8 @@ #include <stdbool.h> #include <stddef.h> #include <stdint.h> +#include <limits.h> +#include <float.h> #include "yasl/timing.h" @@ -45,14 +47,25 @@ struct yasl_rect2i { #define YASL_SCALE_UNIT 120 #define YASL_MIN_LOGICAL_PX 64 +/* Nearest integer, ties away from zero. Zero scale returns zero; finite + * out-of-range results saturate. Non-finite logical coordinates return zero. */ static inline int yasl_scale_to_device(double logical, uint32_t scale_120) { - return (int)((logical * (double)scale_120 + YASL_SCALE_UNIT / 2) / YASL_SCALE_UNIT); + if (!scale_120 || !(logical >= -DBL_MAX && logical <= DBL_MAX)) return 0; + double px = logical * ((double)scale_120 / YASL_SCALE_UNIT); + if (px >= INT_MAX) return INT_MAX; + if (px <= INT_MIN) return INT_MIN; + return (int)(px < 0 ? px - 0.5 : px + 0.5); } static inline int yasl_scale_to_logical(int device, uint32_t scale_120) { - return (int)(((int64_t)device * YASL_SCALE_UNIT + scale_120 / 2) / scale_120); + if (!scale_120) return 0; + int64_t px = (int64_t)device * YASL_SCALE_UNIT; + px = (px + (px < 0 ? -(int64_t)(scale_120 / 2) : scale_120 / 2)) / scale_120; + if (px > INT_MAX) return INT_MAX; + if (px < INT_MIN) return INT_MIN; + return (int)px; } /* Capabilities */ @@ -63,6 +76,8 @@ enum yasl_display_cap : uint32_t { YASL_CAP_PLACE_WINDOW = 1 << 3, YASL_CAP_PRIMARY_SELECTION = 1 << 4, YASL_CAP_CSD = 1 << 5, + YASL_CAP_NATIVE_GRAPHICS = 1 << 6, + YASL_CAP_FRAME_PACING = 1 << 7, /* compositor/display-driven draws */ }; enum yasl_selection : uint8_t { @@ -103,7 +118,12 @@ enum yasl_resize_edge : uint32_t { YASL_EDGE_BOTTOM_RIGHT = 10, }; -/* Damage & Framebuffer */ +/* Damage & Framebuffer + * CPU pixels are tightly packed premultiplied ARGB8888 (0xAARRGGBB). + * Size, pointer positions, IME caret and damage use device pixels; surface + * creation sizes use logical pixels. Damage is a hint: backends may upload + * the whole frame. Every pixel must contain valid content on each draw. + */ #define YASL_DAMAGE_MAX 64 struct yasl_damage_list { @@ -113,6 +133,7 @@ struct yasl_damage_list { }; struct yasl_framebuffer { + uint64_t generation; /* Opaque draw identity; preserve when copying target. */ uint32_t *pixels; struct yasl_vector2i size; int buffer; @@ -224,6 +245,9 @@ enum yasl_input_kind { YASL_INPUT_PASTE, YASL_INPUT_DROP_FILES, YASL_INPUT_TEXT, + /* Allocation failure broke the input stream. Clear all held keys/buttons + * and composition state before processing subsequent events. */ + YASL_INPUT_RESET, }; struct yasl_input_event { @@ -240,6 +264,17 @@ struct yasl_input_event { }; }; +/* Ownership and threading + * All calls and callbacks run on the display's opening thread (main thread on + * Cocoa). Displays borrow supplied loops; destroy surfaces before displays, + * popups before parents, and displays before their supplied loop. + * Specs, strings, callback tables and drag listeners are copied during calls. + * Userdata is borrowed until callbacks are removed or the object/drag ends. + * Native handles are borrowed and expire when their owning object is destroyed. + * Surface destruction is allowed from its callbacks; display/loop destruction + * during dispatch or callbacks is not supported. + */ + /* Callbacks */ struct yasl_display_cbs { void (*quit)(void *ud); @@ -284,9 +319,12 @@ YASL_API void yasl_display_dispatch(struct yasl_display *d); YASL_API int yasl_display_dispatch_timeout(struct yasl_display *d, int timeout_ms); YASL_API void yasl_display_flush(struct yasl_display *d); YASL_API int yasl_display_modifiers(const struct yasl_display *d); +/* Unsupported/invalid selections are no-ops, never clipboard aliases. */ YASL_API void yasl_display_copy(struct yasl_display *d, enum yasl_selection sel, const char *text); YASL_API void yasl_display_paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *target); +/* A successful drag invokes the copied listener's non-NULL end once, including + * cancel and display close. Failed starts do not invoke it. */ YASL_API bool yasl_display_drag_start(struct yasl_display *d, struct yasl_surface *origin, const struct yasl_drag_image *image, const struct yasl_drag_listener *l, void *ud); @@ -298,7 +336,9 @@ struct yasl_surface_spec { struct yasl_vector2i size; bool fullscreen; bool tearing; - /* Optional native graphics device. NULL selects CPU pixel presentation. */ + /* Optional native graphics device (retained on Cocoa). Requires + * YASL_CAP_NATIVE_GRAPHICS; other backends reject it with ENOTSUP. + * NULL selects CPU pixel presentation. */ void *graphics_device; }; @@ -323,19 +363,33 @@ YASL_API void yasl_surface_set_callbacks(struct yasl_surface *s, const struct yasl_surface_cbs *cbs, void *ud); YASL_API void yasl_surface_set_title(struct yasl_surface *s, const char *title); YASL_API void yasl_surface_schedule_redraw(struct yasl_surface *s); +/* Present exactly the target supplied to the current draw callback, at most + * once. Targets and native drawables expire when that callback returns or + * the surface is destroyed. + * damage is required: n must be -1 or 0..YASL_DAMAGE_MAX. Rectangles must have + * nonnegative extents; they are clipped to the target. Invalid input returns + * false without consuming the frame. Native renderers submit/present their + * drawable themselves, then call this function to record the submission. */ YASL_API bool yasl_surface_present(struct yasl_surface *s, const struct yasl_framebuffer *fb, const struct yasl_damage_list *damage); +/* Event payload strings are library-owned, NUL-terminated, and valid until + * the next SUCCESSFUL poll on this surface or surface destruction. Dispatch, + * flush and unsuccessful polls do not invalidate them. Copy to keep longer. */ YASL_API bool yasl_surface_input_poll(struct yasl_surface *s, struct yasl_input_event *ev); YASL_API struct yasl_vector2i yasl_surface_size(const struct yasl_surface *s); YASL_API uint32_t yasl_surface_scale_120(const struct yasl_surface *s); YASL_API uint32_t yasl_surface_state(const struct yasl_surface *s); +/* Timing uses monotonic nanoseconds. Check source before treating timestamps + * as presentation confirmation. refresh/next_present are estimates. Without + * YASL_CAP_FRAME_PACING, redraws run on flush without display synchronization. + * Tearing may bypass pacing even when the capability is present. */ YASL_API struct yasl_timing yasl_surface_timing(const struct yasl_surface *s); YASL_API uint64_t yasl_surface_frame_time_ns(struct yasl_surface *s, uint64_t now); YASL_API void yasl_surface_set_cursor(struct yasl_surface *s, enum yasl_cursor cursor); YASL_API void yasl_surface_set_background_color(struct yasl_surface *s, uint32_t bg); -/* IME Support */ +/* IME Support: preedit is borrowed until the next dispatch or destruction. */ YASL_API const char *yasl_surface_ime_preedit(const struct yasl_surface *s); YASL_API bool yasl_surface_ime_preedit_cursor(const struct yasl_surface *s, int *begin, int *end); YASL_API void yasl_surface_set_ime_caret(struct yasl_surface *s, struct yasl_rect2i caret); diff --git a/src/cocoa/window.m b/src/cocoa/window.m index 8712ce8..4454be3 100644 --- a/src/cocoa/window.m +++ b/src/cocoa/window.m @@ -42,35 +42,7 @@ enum { @property (nonatomic, assign) BOOL fullscreenTransition; @end -#define YASL_QUEUE_CAP 256 -struct input_queue { - struct yasl_input_event events[YASL_QUEUE_CAP]; - int head; - int tail; - int count; -}; - -static inline void input_queue_push(struct input_queue *q, const struct yasl_input_event *ev) -{ - if (q->count >= YASL_QUEUE_CAP) return; - q->events[q->tail] = *ev; - q->tail = (q->tail + 1) % YASL_QUEUE_CAP; - q->count++; -} - -static inline bool input_queue_pop(struct input_queue *q, struct yasl_input_event *ev) -{ - if (q->count <= 0) return false; - *ev = q->events[q->head]; - q->head = (q->head + 1) % YASL_QUEUE_CAP; - q->count--; - return true; -} - -static inline void input_queue_clear(struct input_queue *q) -{ - q->head = q->tail = q->count = 0; -} +#include "common/input.h" struct cocoa_surface { struct yasl_surface base; @@ -145,15 +117,6 @@ static struct cocoa_display darwin_ctx; static void render_and_commit(struct cocoa_surface *w); static void reap_dead(struct cocoa_display *d); -static uint64_t media_to_loop_ns(CFTimeInterval t) -{ - double delta = (t - CACurrentMediaTime()) * 1e9; - uint64_t now = yasl_loop_now_ns(); - if (delta < 0.0 && -delta >= (double)now) - return 0; - return (uint64_t)((double)now + delta); -} - static void set_state(struct cocoa_surface *w, uint32_t bit, bool on) { if (!w || w->closed) @@ -502,7 +465,6 @@ static NSCursor *cocoa_cursor(enum yasl_cursor cursor) CFTimeInterval period = link.targetTimestamp - link.timestamp; if (period > 0.0 && period < 0.25) w->clock.refresh_ns = period * 1e9; - w->clock.last_present_ns = media_to_loop_ns(link.timestamp); w->frame_due = true; if (!w->want_draw || (w->state & YASL_STATE_SUSPENDED)) { if (++w->idle_frames >= LINK_IDLE_FRAMES) @@ -810,8 +772,10 @@ static NSString *plain_string(id string) bool cocoa_surface_present(struct cocoa_surface *w, const struct yasl_framebuffer *fb, const struct yasl_damage_list *damage) { - (void)damage; - if (!w || w->closed || !w->rendering || w->did_present || !fb || + if (!w || w->closed || !w->rendering || w->did_present || !fb || !damage || + fb->pixels != w->frame.target.pixels || fb->size.x != w->size.x || + fb->size.y != w->size.y || fb->buffer != w->frame.target.buffer || + fb->transaction != w->frame.target.transaction || !fb->native_drawable || fb->native_drawable != w->frame.target.native_drawable) return false; if (w->software_queue) { @@ -850,7 +814,7 @@ static void render_and_commit(struct cocoa_surface *w) return; size_t count = (size_t)w->size.x * (size_t)w->size.y; - if (count > w->pixel_capacity) { + if (w->software_queue && count > w->pixel_capacity) { uint32_t *pixels = realloc(w->pixels, count * sizeof(*pixels)); if (!pixels) return; w->pixels = pixels; @@ -866,7 +830,8 @@ static void render_and_commit(struct cocoa_surface *w) w->frame = (struct yasl_frame){ .surface = &w->base, .target = { - .pixels = w->pixels, + .generation = yasl_frame_begin(&w->base), + .pixels = w->software_queue ? w->pixels : NULL, .size = w->size, .damage = &w->damage, .native_drawable = drawable, @@ -943,7 +908,8 @@ void cocoa_display_dispatch(struct cocoa_display *d) bool immediate = false; for (int i = 0; i < d->count; i++) { struct cocoa_surface *w = d->windows[i]; - if (w->input.count > 0 || (w->want_draw && (!w->link || w->frame_due || w->unpaced))) { + if (w->input.count > 0 || w->input.reset_pending || + (w->want_draw && (!w->link || w->frame_due || w->unpaced))) { immediate = true; break; } @@ -1039,7 +1005,8 @@ struct cocoa_display *cocoa_display_open(struct yasl_loop *loop, const struct ya darwin_ctx.open = true; darwin_ctx.loop = loop; if (spec) { - darwin_ctx.cbs = spec->cbs; + darwin_ctx.base.callbacks = spec->cbs ? *spec->cbs : (struct yasl_display_cbs){0}; + darwin_ctx.cbs = &darwin_ctx.base.callbacks; darwin_ctx.ud = spec->ud; if (spec->app_id) darwin_ctx.app_id = strdup(spec->app_id); } @@ -1065,7 +1032,8 @@ void cocoa_display_close(struct cocoa_display *d) uint32_t cocoa_display_caps(const struct cocoa_display *d) { - return d ? (YASL_CAP_PLACE_WINDOW | YASL_CAP_CSD) : 0; + return d ? (YASL_CAP_PLACE_WINDOW | YASL_CAP_CSD | YASL_CAP_NATIVE_GRAPHICS | + YASL_CAP_FRAME_PACING) : 0; } struct cocoa_surface *cocoa_surface_create_toplevel(struct cocoa_display *d, @@ -1302,7 +1270,7 @@ void cocoa_display_paste(struct cocoa_display *d, enum yasl_selection sel, struc (void)d; (void)sel; if (!target) return; NSString *s = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]; - const char *utf8 = s.length ? s.UTF8String : NULL; + const char *utf8 = s ? s.UTF8String : NULL; if (utf8) { struct yasl_input_event ev = { .kind = YASL_INPUT_PASTE, @@ -1388,9 +1356,9 @@ void *cocoa_surface_get_native_handle(struct cocoa_surface *s) void cocoa_surface_set_callbacks(struct cocoa_surface *w, const struct yasl_surface_cbs *cbs, void *ud) { - static const struct yasl_surface_cbs empty = {0}; if (!w || w->closed) return; - w->cbs = cbs ? cbs : ∅ + w->base.callbacks = cbs ? *cbs : (struct yasl_surface_cbs){0}; + w->cbs = &w->base.callbacks; w->ud = ud; } @@ -1412,6 +1380,7 @@ struct yasl_timing cocoa_surface_timing(const struct cocoa_surface *w) timing.last_present_ns = w->clock.last_present_ns; timing.next_present_ns = yasl_clock_next_present_ns(&w->clock); timing.frames_skipped = w->frames_skipped; + timing.source = w->clock.source; yasl_clock_resize_stats(&w->clock, &timing.resize); } return timing; diff --git a/src/common/backend.c b/src/common/backend.c index 2220f1f..214854b 100644 --- a/src/common/backend.c +++ b/src/common/backend.c @@ -113,14 +113,18 @@ int yasl_display_modifiers(const struct yasl_display *d) void yasl_display_copy(struct yasl_display *d, enum yasl_selection sel, const char *text) { - if (!d || !d->ops->display_copy) + if (!d || !d->ops->display_copy || !text || + (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY) || + (sel == YASL_PRIMARY && !(yasl_display_caps(d) & YASL_CAP_PRIMARY_SELECTION))) return; d->ops->display_copy(d, sel, text); } void yasl_display_paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *target) { - if (!d || !d->ops->display_paste) + if (!d || !d->ops->display_paste || + (sel != YASL_CLIPBOARD && sel != YASL_PRIMARY) || + (sel == YASL_PRIMARY && !(yasl_display_caps(d) & YASL_CAP_PRIMARY_SELECTION))) return; if (!target || target->display != d) return; @@ -145,8 +149,17 @@ void yasl_display_drag_cancel(struct yasl_display *d) struct yasl_surface *yasl_surface_create_toplevel(struct yasl_display *d, const struct yasl_surface_spec *spec) { - if (!d || !d->ops->surface_create_toplevel) - return 0; + if (!d || !d->ops->surface_create_toplevel || !spec || + spec->size.x <= 0 || spec->size.y <= 0 || + spec->size.x > 32767 || spec->size.y > 32767 || + (int64_t)spec->size.x * spec->size.y * 4 > INT32_MAX) { + errno = EINVAL; + return NULL; + } + if (spec->graphics_device && !(yasl_display_caps(d) & YASL_CAP_NATIVE_GRAPHICS)) { + errno = ENOTSUP; + return NULL; + } return d->ops->surface_create_toplevel(d, spec); } @@ -201,14 +214,34 @@ void yasl_surface_schedule_redraw(struct yasl_surface *s) bool yasl_surface_present(struct yasl_surface *s, const struct yasl_framebuffer *fb, const struct yasl_damage_list *damage) { - if (!s || !s->display->ops->surface_present) - return 0; - return s->display->ops->surface_present(s, fb, damage); + if (!s || !s->display->ops->surface_present || !fb || !damage || + !fb->generation || fb->generation != s->frame_generation || + fb->size.x <= 0 || fb->size.y <= 0 || + damage->n < -1 || damage->n > YASL_DAMAGE_MAX) + return false; + struct yasl_damage_list clipped = {.n = damage->n}; + if (damage->n >= 0) { + clipped.n = 0; + for (int i = 0; i < damage->n; i++) { + struct yasl_rect2i r = damage->rects[i]; + if (r.width < 0 || r.height < 0) return false; + int64_t right = (int64_t)r.x + r.width; + int64_t bottom = (int64_t)r.y + r.height; + int64_t x = r.x > 0 ? r.x : 0, y = r.y > 0 ? r.y : 0; + if (right > fb->size.x) right = fb->size.x; + if (bottom > fb->size.y) bottom = fb->size.y; + if (right > x && bottom > y) + clipped.rects[clipped.n++] = (struct yasl_rect2i){ + (int)x, (int)y, (int)(right - x), (int)(bottom - y), + }; + } + } + return s->display->ops->surface_present(s, fb, &clipped); } bool yasl_surface_input_poll(struct yasl_surface *s, struct yasl_input_event *ev) { - if (!s || !s->display->ops->surface_input_poll) + if (!s || !ev || !s->display->ops->surface_input_poll) return 0; return s->display->ops->surface_input_poll(s, ev); } @@ -271,7 +304,7 @@ const char *yasl_surface_ime_preedit(const struct yasl_surface *s) bool yasl_surface_ime_preedit_cursor(const struct yasl_surface *s, int *begin, int *end) { - if (!s || !s->display->ops->surface_ime_preedit_cursor) + if (!s || !begin || !end || !s->display->ops->surface_ime_preedit_cursor) return 0; return s->display->ops->surface_ime_preedit_cursor(s, begin, end); } @@ -292,7 +325,11 @@ bool yasl_surface_start_interactive_move(struct yasl_surface *s) bool yasl_surface_start_interactive_resize(struct yasl_surface *s, enum yasl_resize_edge edge) { - if (!s || !s->display->ops->surface_start_interactive_resize) + if (!s || !s->display->ops->surface_start_interactive_resize || + (edge != YASL_EDGE_TOP && edge != YASL_EDGE_BOTTOM && + edge != YASL_EDGE_LEFT && edge != YASL_EDGE_RIGHT && + edge != YASL_EDGE_TOP_LEFT && edge != YASL_EDGE_TOP_RIGHT && + edge != YASL_EDGE_BOTTOM_LEFT && edge != YASL_EDGE_BOTTOM_RIGHT)) return 0; return s->display->ops->surface_start_interactive_resize(s, edge); } diff --git a/src/common/backend.h b/src/common/backend.h index c1d878b..c6691eb 100644 --- a/src/common/backend.h +++ b/src/common/backend.h @@ -7,12 +7,21 @@ struct yasl_backend_ops; struct yasl_display { const struct yasl_backend_ops *ops; + struct yasl_display_cbs callbacks; }; struct yasl_surface { struct yasl_display *display; + struct yasl_surface_cbs callbacks; + uint64_t frame_generation; }; +static inline uint64_t yasl_frame_begin(struct yasl_surface *s) +{ + if (++s->frame_generation == 0) ++s->frame_generation; + return s->frame_generation; +} + struct yasl_backend_ops { enum yasl_backend type; struct yasl_display *(*display_open)(struct yasl_loop *loop, const struct yasl_display_spec *spec); diff --git a/src/common/input.c b/src/common/input.c new file mode 100644 index 0000000..eac4d1a --- /dev/null +++ b/src/common/input.c @@ -0,0 +1,103 @@ +#include "input.h" + +#include <stdlib.h> +#include <string.h> + +static const char *event_text(const struct yasl_input_event *ev, size_t *len) +{ + switch (ev->kind) { + case YASL_INPUT_TEXT: *len = ev->text.len; return ev->text.text; + case YASL_INPUT_PASTE: *len = ev->paste.len; return ev->paste.text; + case YASL_INPUT_DROP_FILES: *len = ev->drop_files.len; return ev->drop_files.files; + default: *len = 0; return NULL; + } +} + +static void set_text(struct yasl_input_event *ev, const char *text) +{ + switch (ev->kind) { + case YASL_INPUT_TEXT: ev->text.text = text; break; + case YASL_INPUT_PASTE: ev->paste.text = text; break; + case YASL_INPUT_DROP_FILES: ev->drop_files.files = text; break; + default: break; + } +} + +static void discard_pending(struct input_queue *q) +{ + for (size_t i = 0; i < q->count; i++) { + size_t len; + free((void *)event_text(&q->events[(q->head + i) % q->capacity], &len)); + } + q->head = q->count = 0; +} + +bool input_queue_push(struct input_queue *q, const struct yasl_input_event *ev) +{ + struct yasl_input_event copy = *ev; + size_t len; + const char *text = event_text(ev, &len); + char *owned = NULL; + if (ev->kind == YASL_INPUT_TEXT || ev->kind == YASL_INPUT_PASTE || + ev->kind == YASL_INPUT_DROP_FILES) { + if ((!text && len) || len == SIZE_MAX) goto failed; + owned = malloc(len + 1); + if (!owned) goto failed; + if (len) memcpy(owned, text, len); + owned[len] = '\0'; + set_text(©, owned); + } + if (q->count == q->capacity) { + size_t capacity = q->capacity ? q->capacity * 2 : 32; + if (capacity < q->capacity || capacity > SIZE_MAX / sizeof(*q->events)) + goto failed; + struct yasl_input_event *events = malloc(capacity * sizeof(*events)); + if (!events) goto failed; + for (size_t i = 0; i < q->count; i++) + events[i] = q->events[(q->head + i) % q->capacity]; + free(q->events); + q->events = events; + q->capacity = capacity; + q->head = 0; + } + q->events[(q->head + q->count) % q->capacity] = copy; + q->count++; + return true; + +failed: + free(owned); + /* A missing release must never silently leave app input state stuck. + * Discard the old stream and deliver an allocation-free reset first. */ + discard_pending(q); + q->reset_surface = ev->surface; + q->reset_pending = true; + return false; +} + +bool input_queue_pop(struct input_queue *q, struct yasl_input_event *ev) +{ + if (!ev || (!q->count && !q->reset_pending)) return false; + free(q->last_text); + q->last_text = NULL; + if (q->reset_pending) { + *ev = (struct yasl_input_event){ + .surface = q->reset_surface, .kind = YASL_INPUT_RESET, + }; + q->reset_pending = false; + return true; + } + *ev = q->events[q->head]; + size_t len; + q->last_text = (char *)event_text(ev, &len); + q->head = (q->head + 1) % q->capacity; + q->count--; + return true; +} + +void input_queue_clear(struct input_queue *q) +{ + discard_pending(q); + free(q->last_text); + free(q->events); + *q = (struct input_queue){0}; +} diff --git a/src/common/input.h b/src/common/input.h new file mode 100644 index 0000000..d7264f6 --- /dev/null +++ b/src/common/input.h @@ -0,0 +1,19 @@ +#ifndef YASL_INPUT_H +#define YASL_INPUT_H + +#include "yasl/yasl.h" + +/* Each queued string and the last successfully polled string are owned here. */ +struct input_queue { + struct yasl_input_event *events; + size_t head, count, capacity; + char *last_text; + struct yasl_surface *reset_surface; + bool reset_pending; +}; + +bool input_queue_push(struct input_queue *q, const struct yasl_input_event *ev); +bool input_queue_pop(struct input_queue *q, struct yasl_input_event *ev); +void input_queue_clear(struct input_queue *q); + +#endif diff --git a/src/common/meson.build b/src/common/meson.build index 61653df..5cfb187 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -1 +1 @@ -sources += files('backend.c', 'loop.c', 'timing.c', 'utf8.c') +sources += files('backend.c', 'input.c', 'loop.c', 'timing.c', 'utf8.c') diff --git a/src/common/timing.c b/src/common/timing.c index 3e3a705..17f71b5 100644 --- a/src/common/timing.c +++ b/src/common/timing.c @@ -17,6 +17,7 @@ void yasl_clock_presented(struct yasl_frame_clock *c, uint64_t now, bool sample) c->refresh_ns += 0.125 * (dt - c->refresh_ns); } c->last_present_ns = now; + c->source = YASL_TIMING_SUBMISSION; } void yasl_clock_note_resize(struct yasl_frame_clock *c, uint64_t dt_ns) diff --git a/src/wayland/buffer.c b/src/wayland/buffer.c index 76f6c0e..3f9cf84 100644 --- a/src/wayland/buffer.c +++ b/src/wayland/buffer.c @@ -278,8 +278,10 @@ static void frame_done(void *data, struct wl_callback *cb, uint32_t ms) w->frame_cb = NULL; uint64_t now = yasl_loop_now_ns(); - if (!w->conn->pres_clock_known) + if (!w->conn->pres_clock_known) { yasl_clock_presented(&w->clock, yasl_clock_extend_ms(ms, now), w->sample_refresh); + w->clock.source = YASL_TIMING_FRAME_CALLBACK; + } if (w->want_draw) wayland_surface_schedule_redraw(w); @@ -358,6 +360,7 @@ static void presented(void *data, struct wp_presentation_feedback *fb, (((uint64_t)tv_sec_hi << 32) | tv_sec_lo) * 1000000000ull + tv_nsec); if (refresh) w->clock.refresh_ns = refresh; w->clock.last_present_ns = ns; + w->clock.source = YASL_TIMING_PRESENTATION; } (void)fb; feedback_destroy(feedback); @@ -444,7 +447,7 @@ bool wayland_surface_present(struct wayland_surface *w, const struct yasl_frameb if (!w || w->closed || !w->frame_active || w->frame_presented || !fb || !damage || fb->pixels != w->frame.target.pixels || fb->size.x != w->width || fb->size.y != w->height || - fb->buffer != w->cur) + fb->buffer != w->cur || fb->native_drawable || fb->transaction) return false; w->damage = *damage; @@ -485,6 +488,7 @@ void surface_draw_frame(struct wayland_surface *w) w->frame = (struct yasl_frame){ .surface = &w->base, .target = { + .generation = yasl_frame_begin(&w->base), .pixels = w->buffers[idx].pixels, .size = { .x = w->width, .y = w->height }, .buffer = idx, @@ -544,6 +548,7 @@ struct yasl_timing wayland_surface_timing(const struct wayland_surface *w) .last_present_ns = w->clock.last_present_ns, .next_present_ns = yasl_clock_next_present_ns(&w->clock), .frames_skipped = w->frames_skipped, + .source = w->clock.source, }; yasl_clock_resize_stats(&w->clock, &t.resize); return t; diff --git a/src/wayland/clipboard.c b/src/wayland/clipboard.c index 0e6ce33..7df1a70 100644 --- a/src/wayland/clipboard.c +++ b/src/wayland/clipboard.c @@ -134,10 +134,8 @@ static int transfer_io(int fd, short events, void *ud) memcpy(t->text + t->len, buf, (size_t)n); t->len += n; } else if (n == 0) { - if (t->len) { - t->owner->receive(t->text, t->len, t->uri_list, - t->drop_surface, t->owner->ud); - } + t->owner->receive(t->text ? t->text : "", t->len, t->uri_list, + t->drop_surface, t->owner->ud); transfer_end(t, true); } else if (errno != EAGAIN && errno != EINTR) transfer_destroy(t); } @@ -556,24 +554,22 @@ void clipboard_copy(struct clipboard *c, const char *text, uint32_t serial) wl_data_device_set_selection(c->device, c->source, serial); } -void clipboard_paste(struct clipboard *c) +void clipboard_paste(struct clipboard *c, struct wl_surface *target) { if (!c || !c->device || !c->selection || !c->selection->mime) return; int fds[2]; if (pipe2(fds, O_CLOEXEC) < 0) return; wl_data_offer_receive(c->selection->proxy, c->selection->mime, fds[1]); close(fds[1]); - start_transfer(c, fds[0], false, false); + struct transfer *t = start_transfer(c, fds[0], false, false); + if (t) t->drop_surface = target; wl_display_flush(c->display); } void clipboard_copy_primary(struct clipboard *c, const char *text, uint32_t serial) { if (!c || !text || !serial) return; - if (!c->primary_device || !c->primary_manager) { - clipboard_copy(c, text, serial); - return; - } + if (!c->primary_device || !c->primary_manager) return; char *copy = strdup(text); if (!copy) return; if (c->primary_source) zwp_primary_selection_source_v1_destroy(c->primary_source); @@ -589,7 +585,7 @@ void clipboard_copy_primary(struct clipboard *c, const char *text, uint32_t seri zwp_primary_selection_device_v1_set_selection(c->primary_device, c->primary_source, serial); } -void clipboard_paste_primary(struct clipboard *c) +void clipboard_paste_primary(struct clipboard *c, struct wl_surface *target) { if (!c) return; if (c->primary_device && c->primary_selection && c->primary_selection->mime) { @@ -598,11 +594,21 @@ void clipboard_paste_primary(struct clipboard *c) zwp_primary_selection_offer_v1_receive(c->primary_selection->proxy, c->primary_selection->mime, fds[1]); close(fds[1]); - start_transfer(c, fds[0], false, false); + struct transfer *t = start_transfer(c, fds[0], false, false); + if (t) t->drop_surface = target; wl_display_flush(c->display); return; } - clipboard_paste(c); +} + +void clipboard_forget_surface(struct clipboard *c, struct wl_surface *surface) +{ + if (!c || !surface) return; + for (struct transfer *t = c->transfers, *next; t; t = next) { + next = t->next; + if (t->drop_surface == surface) transfer_destroy(t); + } + if (c->drag_surface == surface) c->drag_surface = NULL; } void clipboard_destroy(struct clipboard *c) diff --git a/src/wayland/clipboard.h b/src/wayland/clipboard.h index 6ea75c6..9fefa8e 100644 --- a/src/wayland/clipboard.h +++ b/src/wayland/clipboard.h @@ -24,9 +24,10 @@ void clipboard_destroy(struct clipboard *); bool clipboard_set_seat(struct clipboard *, struct wl_seat *); void clipboard_set_primary_manager(struct clipboard *, struct zwp_primary_selection_device_manager_v1 *); void clipboard_copy(struct clipboard *, const char *, uint32_t); -void clipboard_paste(struct clipboard *); +void clipboard_paste(struct clipboard *, struct wl_surface *target); +void clipboard_forget_surface(struct clipboard *, struct wl_surface *); void clipboard_copy_primary(struct clipboard *, const char *, uint32_t); -void clipboard_paste_primary(struct clipboard *); +void clipboard_paste_primary(struct clipboard *, struct wl_surface *target); typedef void (*clipboard_drag_fn)(enum yasl_drag_event, struct wl_surface *on, double x, double y, void *); diff --git a/src/wayland/drag.c b/src/wayland/drag.c index 7c15002..989dbb6 100644 --- a/src/wayland/drag.c +++ b/src/wayland/drag.c @@ -90,11 +90,11 @@ static void drag_clear(struct wayland_display *c, bool ended) static void drag_finish(struct wayland_display *c, enum yasl_drag_end how, bool ended) { - const struct yasl_drag_listener *l = c->drag.listener; + struct yasl_drag_listener l = c->drag.listener; void *ud = c->drag.ud; drag_clear(c, ended); - if (l && l->end) - l->end(ud, how); + if (l.end) + l.end(ud, how); } void conn_drag_end(struct wayland_display *c) @@ -111,7 +111,7 @@ static void source_cancelled(void *ud, struct wl_data_source *s) { (void)s; struct wayland_display *c = ud; - bool released = c->drag.dropped || !c->drag.carrying; + bool released = c->drag.dropped; drag_finish(c, released ? YASL_DRAG_DROPPED : YASL_DRAG_CANCELLED, true); } @@ -171,7 +171,7 @@ bool wayland_display_drag_start(struct wayland_display *c, struct wayland_surfac } c->drag.source = s; c->drag.seat = seat; - c->drag.listener = l; + c->drag.listener = *l; c->drag.ud = ud; wl_data_source_add_listener(s, &source_listener, c); wl_data_source_offer(s, clipboard_drag_mime()); @@ -190,7 +190,7 @@ bool wayland_display_drag_start(struct wayland_display *c, struct wayland_surfac void wayland_display_drag_cancel(struct wayland_display *c) { if (c && c->drag.source) - drag_clear(c, false); + drag_finish(c, YASL_DRAG_CANCELLED, false); } bool wayland_surface_drag_carry(struct wayland_surface *w, struct yasl_vector2i offset) diff --git a/src/wayland/internal.h b/src/wayland/internal.h index cba5096..ccfd2dc 100644 --- a/src/wayland/internal.h +++ b/src/wayland/internal.h @@ -100,7 +100,7 @@ struct conn_drag { struct wl_data_source *source; struct xdg_toplevel_drag_v1 *toplevel; struct drag_icon icon; - const struct yasl_drag_listener *listener; + struct yasl_drag_listener listener; void *ud; struct seat *seat; uint32_t scale_120; @@ -192,36 +192,7 @@ struct seat { bool touch_scrolling; }; -#define YASL_QUEUE_CAP 256 -#define YASL_QUEUE_MASK (YASL_QUEUE_CAP - 1) -struct input_queue { - struct yasl_input_event events[YASL_QUEUE_CAP]; - int head; - int tail; - int count; -}; - -static inline void input_queue_push(struct input_queue *q, const struct yasl_input_event *ev) -{ - if (unlikely(q->count >= YASL_QUEUE_CAP)) return; - q->events[q->tail] = *ev; - q->tail = (q->tail + 1) & YASL_QUEUE_MASK; - q->count++; -} - -static inline bool input_queue_pop(struct input_queue *q, struct yasl_input_event *ev) -{ - if (unlikely(q->count <= 0)) return false; - *ev = q->events[q->head]; - q->head = (q->head + 1) & YASL_QUEUE_MASK; - q->count--; - return true; -} - -static inline void input_queue_clear(struct input_queue *q) -{ - q->head = q->tail = q->count = 0; -} +#include "common/input.h" struct wayland_display { struct yasl_display base; @@ -257,7 +228,6 @@ struct wayland_display { struct xdg_toplevel_drag_manager_v1 *toplevel_drag_mgr; struct conn_drag drag; struct wayland_surface *surfaces; - struct wayland_surface *paste_target; struct wayland_surface *dead; /* 4-byte integers */ diff --git a/src/wayland/keyboard.c b/src/wayland/keyboard.c index 6bdd58c..f7e2691 100644 --- a/src/wayland/keyboard.c +++ b/src/wayland/keyboard.c @@ -374,11 +374,11 @@ void wayland_display_paste(struct wayland_display *c, enum yasl_selection sel, s struct seat *s = c ? conn_active_seat(c) : NULL; if (!s || !s->clipboard) return; - c->paste_target = target; + if (!target || target->closed) return; if (sel == YASL_PRIMARY) - clipboard_paste_primary(s->clipboard); + clipboard_paste_primary(s->clipboard, target->surface); else - clipboard_paste(s->clipboard); + clipboard_paste(s->clipboard, target->surface); } bool wayland_surface_input_poll(struct wayland_surface *w, struct yasl_input_event *ev) diff --git a/src/wayland/pointer.c b/src/wayland/pointer.c index d434047..3b3ad73 100644 --- a/src/wayland/pointer.c +++ b/src/wayland/pointer.c @@ -1032,10 +1032,9 @@ static void clipboard_received(const char *text, size_t len, bool uri_list, { struct seat *s = ud; struct wayland_display *c = s->conn; - struct wayland_surface *w = on ? surface_root(conn_surface_for(c, on)) : NULL; - if (!w) - w = c->paste_target ? c->paste_target : s->keyboard_focus; - if (!len || !w) return; + struct wayland_surface *w = conn_surface_for(c, on); + if (uri_list) w = surface_root(w); + if (!w || w->closed) return; struct yasl_input_event ev = { .surface = &w->base, .kind = uri_list ? YASL_INPUT_DROP_FILES : YASL_INPUT_PASTE, @@ -1216,6 +1215,7 @@ void seat_bind(struct wayland_display *c, struct wl_registry *registry, void seat_forget_surface(struct seat *s, struct wayland_surface *w) { + clipboard_forget_surface(s->clipboard, w->surface); struct wayland_surface **refs[] = { &s->pointer_focus, &s->keyboard_focus, &s->tablet_focus, &s->ime_focus, &s->focus_leaving, &s->ptr_left, diff --git a/src/wayland/surface.c b/src/wayland/surface.c index 8de4992..ef355ab 100644 --- a/src/wayland/surface.c +++ b/src/wayland/surface.c @@ -128,7 +128,7 @@ static inline bool display_has_immediate_work(const struct wayland_display *c) if (unlikely(!c || c->lost)) return false; for (struct wayland_surface *w = c->surfaces; w; w = w->conn_next) { bool ready_draw = w->want_draw & (w->frame_cb == NULL) & w->configured; - bool has_input = (w->input.count > 0); + bool has_input = (w->input.count > 0 || w->input.reset_pending); if (unlikely((ready_draw | has_input) & !w->closed)) return true; } @@ -250,7 +250,8 @@ struct wayland_display *wayland_display_open(struct yasl_loop *loop, const struc } if (!c->loop) goto err; if (spec) { - c->cbs = spec->cbs; + c->base.callbacks = spec->cbs ? *spec->cbs : (struct yasl_display_cbs){0}; + c->cbs = &c->base.callbacks; c->ud = spec->ud; if (spec->app_id) c->app_id = strdup(spec->app_id); } @@ -288,7 +289,7 @@ void wayland_display_close(struct wayland_display *c) uint32_t wayland_display_caps(const struct wayland_display *c) { if (!c) return 0; - uint32_t caps = YASL_CAP_POPUPS | YASL_CAP_CSD; + uint32_t caps = YASL_CAP_POPUPS | YASL_CAP_CSD | YASL_CAP_FRAME_PACING; struct seat *s = conn_active_seat(c); const struct wl_data_device *device = s ? clipboard_data_device(s->clipboard) : NULL; if (device && wl_data_device_get_version((struct wl_data_device *)device) >= 3) @@ -318,8 +319,6 @@ static void conn_unlink(struct wayland_surface *w) break; } } - if (c->paste_target == w) - c->paste_target = NULL; for (struct seat *s = c->seats; s; s = s->next) seat_forget_surface(s, w); w->conn = NULL; @@ -397,8 +396,8 @@ err: void wayland_surface_set_callbacks(struct wayland_surface *w, const struct yasl_surface_cbs *cbs, void *ud) { if (w && !w->closed) { - static const struct yasl_surface_cbs no_callbacks = {0}; - w->cbs = cbs ? cbs : &no_callbacks; + w->base.callbacks = cbs ? *cbs : (struct yasl_surface_cbs){0}; + w->cbs = &w->base.callbacks; w->ud = ud; } } diff --git a/src/x11/window.c b/src/x11/window.c index 4680d3f..e40db6e 100644 --- a/src/x11/window.c +++ b/src/x11/window.c @@ -1,5 +1,6 @@ #define _POSIX_C_SOURCE 200809L #include "common/backend.h" +#include "common/input.h" #include <X11/Xatom.h> #include <X11/XKBlib.h> @@ -16,7 +17,6 @@ #include <stdlib.h> #include <string.h> -#define INPUT_CAP 256 #define TEXT_LIMIT (16 * 1024 * 1024) #ifndef likely @@ -38,10 +38,7 @@ struct x11_surface { struct yasl_vector2i size; const struct yasl_surface_cbs *cbs; void *ud; - struct yasl_input_event events[INPUT_CAP]; - char *event_text[INPUT_CAP]; - char *last_text; - unsigned head, count; + struct input_queue input; struct yasl_frame frame; struct yasl_damage_list damage; struct yasl_frame_clock clock; @@ -49,6 +46,12 @@ struct x11_surface { bool want_draw, drawing, presented, destroyed; }; +struct paste_request { + struct paste_request *next; + struct x11_surface *target; + Window window; +}; + struct x11_display { struct yasl_display base; Display *native; @@ -62,7 +65,7 @@ struct x11_display { Atom protocols, delete_window, utf8, clipboard, targets, paste_property; Atom net_name, net_state, fullscreen, maximized_h, maximized_v; char *selection[2]; - struct x11_surface *paste_target; + struct paste_request *pastes; uint32_t mods; char *app_id; bool owns_loop; @@ -86,25 +89,18 @@ static void leave(struct x11_display *d) static void push(struct x11_surface *s, struct yasl_input_event event, const char *text, size_t len) { - if (s->destroyed || s->count == INPUT_CAP) return; - unsigned slot = (s->head + s->count) % INPUT_CAP; + if (s->destroyed) return; if (text) { - char *copy = malloc(len + 1); - if (!copy) return; - memcpy(copy, text, len); - copy[len] = 0; - s->event_text[slot] = copy; if (event.kind == YASL_INPUT_TEXT) { - event.text.text = copy; + event.text.text = text; event.text.len = len; } else { - event.paste.text = copy; + event.paste.text = text; event.paste.len = len; } } event.surface = &s->base; - s->events[slot] = event; - s->count++; + input_queue_push(&s->input, &event); } static uint32_t modifiers(unsigned state) @@ -149,6 +145,23 @@ static bool resize(struct x11_surface *s, struct yasl_vector2i size) return true; } +static void cancel_pastes(struct x11_display *d, struct x11_surface *target) +{ + for (struct paste_request **p = &d->pastes; *p;) { + struct paste_request *r = *p; + if (!target) { + *p = r->next; + XDestroyWindow(d->native, r->window); + free(r); + } else { + /* Keep the requestor alive until SelectionNotify: the owner may + * still be writing to its property. Never retain a dead target. */ + if (r->target == target) r->target = NULL; + p = &r->next; + } + } +} + static void surface_destroy(struct yasl_surface *base) { struct x11_surface *s = (struct x11_surface *)base; @@ -158,14 +171,13 @@ static void surface_destroy(struct yasl_surface *base) for (struct x11_surface **p = &d->surfaces; *p; p = &(*p)->next) { if (*p == s) { *p = s->next; break; } } - if (d->paste_target == s) d->paste_target = NULL; + cancel_pastes(d, s); if (s->ic) XDestroyIC(s->ic); if (s->image) XDestroyImage(s->image); free(s->pixels); if (s->gc) XFreeGC(d->native, s->gc); if (s->window) XDestroyWindow(d->native, s->window); - for (unsigned i = 0; i < INPUT_CAP; i++) free(s->event_text[i]); - free(s->last_text); + input_queue_clear(&s->input); if (d->depth) { s->dead_next = d->dead; d->dead = s; @@ -177,7 +189,8 @@ static void set_callbacks(struct yasl_surface *base, { struct x11_surface *s = (struct x11_surface *)base; if (s->destroyed) return; - s->cbs = cbs; + s->base.callbacks = cbs ? *cbs : (struct yasl_surface_cbs){0}; + s->cbs = &s->base.callbacks; s->ud = ud; } @@ -295,14 +308,22 @@ static void selection_request(struct x11_display *d, XSelectionRequestEvent *req static void selection_notify(struct x11_display *d, XSelectionEvent *event) { - struct x11_surface *s = d->paste_target; - d->paste_target = NULL; - if (!s || event->property == None) return; + struct paste_request **p = &d->pastes; + while (*p && (*p)->window != event->requestor) p = &(*p)->next; + if (!*p) return; + struct paste_request *r = *p; + *p = r->next; + struct x11_surface *s = r->target; + if (!s || event->property == None) { + XDestroyWindow(d->native, r->window); + free(r); + return; + } Atom actual; int format; unsigned long count, left; unsigned char *data = NULL; - if (XGetWindowProperty(d->native, d->selection_window, event->property, + if (XGetWindowProperty(d->native, r->window, event->property, 0, TEXT_LIMIT / 4, True, AnyPropertyType, &actual, &format, &count, &left, &data) == Success && (actual == d->utf8 || actual == XA_STRING) && format == 8 && !left) { @@ -310,6 +331,8 @@ static void selection_notify(struct x11_display *d, XSelectionEvent *event) (const char *)data, count); } if (data) XFree(data); + XDestroyWindow(d->native, r->window); + free(r); } static void key_event(struct x11_surface *s, XKeyEvent *key, bool repeat) @@ -458,6 +481,7 @@ static bool present(struct yasl_surface *base, const struct yasl_framebuffer *fb struct x11_surface *s = (struct x11_surface *)base; if (s->destroyed || !s->drawing || s->presented || !fb || !damage || fb->pixels != s->pixels || fb->size.x != s->size.x || fb->size.y != s->size.y || + fb->buffer != s->frame.target.buffer || fb->native_drawable || fb->transaction || damage->n < -1 || damage->n > YASL_DAMAGE_MAX) return false; struct x11_display *d = display_of(base); XImage *im = s->image; @@ -502,7 +526,8 @@ static void flush(struct yasl_display *base) s->damage = (struct yasl_damage_list){0}; s->frame = (struct yasl_frame){ .surface = &s->base, - .target = {.pixels = s->pixels, .size = s->size, .damage = &s->damage}, + .target = {.generation = yasl_frame_begin(&s->base), + .pixels = s->pixels, .size = s->size, .damage = &s->damage}, }; s->cbs->draw(&s->base, &s->frame, s->ud); s->drawing = false; @@ -537,6 +562,7 @@ static struct yasl_display *display_open(struct yasl_loop *loop, struct x11_display *d = calloc(1, sizeof(*d)); if (!d) return NULL; d->base.ops = &yasl_x11_backend; + d->base.callbacks = spec && spec->cbs ? *spec->cbs : (struct yasl_display_cbs){0}; d->native = XOpenDisplay(NULL); if (!d->native) { free(d); errno = ENODEV; return NULL; } d->screen = DefaultScreen(d->native); @@ -547,7 +573,13 @@ static struct yasl_display *display_open(struct yasl_loop *loop, d->loop = loop; } d->app_id = strdup(spec && spec->app_id ? spec->app_id : "YASL"); - if (!d->app_id) { XCloseDisplay(d->native); free(d); return NULL; } + if (!d->loop || !d->app_id) { + if (d->owns_loop) yasl_loop_destroy(d->loop); + free(d->app_id); + XCloseDisplay(d->native); + free(d); + return NULL; + } d->im = XOpenIM(d->native, NULL, NULL, NULL); d->selection_window = XCreateSimpleWindow(d->native, RootWindow(d->native, d->screen), 0, 0, 1, 1, 0, 0, 0); @@ -583,6 +615,7 @@ static void display_close(struct yasl_display *base) { struct x11_display *d = (struct x11_display *)base; assert(!d->surfaces && "destroy every surface before its display"); + cancel_pastes(d, NULL); if (d->loop && d->watch > 0) yasl_loop_del_fd(d->loop, d->watch); if (d->owns_loop && d->loop) yasl_loop_destroy(d->loop); if (d->im) XCloseIM(d->im); @@ -623,9 +656,15 @@ static void paste(struct yasl_display *base, enum yasl_selection sel, struct yas const char *text = d->selection[sel]; if (text) push(s, (struct yasl_input_event){.kind = YASL_INPUT_PASTE}, text, strlen(text)); } else { - d->paste_target = s; + struct paste_request *r = calloc(1, sizeof(*r)); + if (!r) return; + r->window = XCreateSimpleWindow(d->native, RootWindow(d->native, d->screen), + 0, 0, 1, 1, 0, 0, 0); + r->target = s; + r->next = d->pastes; + d->pastes = r; XConvertSelection(d->native, selection, d->utf8, d->paste_property, - d->selection_window, CurrentTime); + r->window, CurrentTime); XFlush(d->native); } } @@ -633,14 +672,7 @@ static void paste(struct yasl_display *base, enum yasl_selection sel, struct yas static bool input_poll(struct yasl_surface *base, struct yasl_input_event *event) { struct x11_surface *s = (struct x11_surface *)base; - if (!event || s->destroyed || !s->count) return false; - free(s->last_text); - s->last_text = s->event_text[s->head]; - s->event_text[s->head] = NULL; - *event = s->events[s->head]; - s->head = (s->head + 1) % INPUT_CAP; - s->count--; - return true; + return !s->destroyed && input_queue_pop(&s->input, event); } static void schedule_redraw(struct yasl_surface *base) @@ -667,7 +699,8 @@ static struct yasl_timing timing(const struct yasl_surface *base) const struct x11_surface *s = (const struct x11_surface *)base; struct yasl_timing t = {.refresh_ns = s->clock.refresh_ns, .last_present_ns = s->clock.last_present_ns, - .next_present_ns = yasl_clock_next_present_ns(&s->clock)}; + .next_present_ns = yasl_clock_next_present_ns(&s->clock), + .source = s->clock.source}; yasl_clock_resize_stats(&s->clock, &t.resize); return t; } diff --git a/tests/cocoa/test_native.c b/tests/cocoa/test_native.c index 03fde88..41df79a 100644 --- a/tests/cocoa/test_native.c +++ b/tests/cocoa/test_native.c @@ -19,7 +19,16 @@ static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *u struct yasl_vector2i size = yasl_surface_size(surface); assert(size.x == frame->target.size.x && size.y == frame->target.size.y); for (int i = 0; i < size.x * size.y; i++) frame->target.pixels[i] = 0xff4080c0; - struct yasl_damage_list damage = {.n = -1}; + struct yasl_damage_list damage = {.n = YASL_DAMAGE_MAX + 1}; + assert(!yasl_surface_present(surface, &frame->target, &damage)); + damage.n = -1; + struct yasl_framebuffer invalid = frame->target; + invalid.buffer++; + assert(!yasl_surface_present(surface, &invalid, &damage)); + invalid = frame->target; + invalid.generation--; + assert(!yasl_surface_present(surface, &invalid, &damage)); + assert(!yasl_surface_present(surface, &frame->target, NULL)); assert(yasl_surface_present(surface, &frame->target, &damage)); assert(!yasl_surface_present(surface, &frame->target, &damage)); state->draws++; @@ -36,8 +45,9 @@ static void test_window(enum yasl_backend request, bool null_spec) struct yasl_surface_spec surface = {.title = "Native backend test", .size = {160, 100}}; state.surface = yasl_surface_create_toplevel(state.display, &surface); assert(state.surface); - static const struct yasl_surface_cbs callbacks = {.draw = draw}; + struct yasl_surface_cbs callbacks = {.draw = draw}; yasl_surface_set_callbacks(state.surface, &callbacks, &state); + callbacks.draw = NULL; yasl_surface_schedule_redraw(state.surface); uint64_t deadline = yasl_loop_now_ns() + 3000000000ull; while (!state.draws && yasl_loop_now_ns() < deadline) { diff --git a/tests/cocoa/test_ownership.m b/tests/cocoa/test_ownership.m new file mode 100644 index 0000000..aa75b1c --- /dev/null +++ b/tests/cocoa/test_ownership.m @@ -0,0 +1,45 @@ +#import <AppKit/AppKit.h> +#include "yasl/yasl.h" +#include <assert.h> +#include <stdio.h> +#include <string.h> + +int main(void) +{ + struct yasl_display *d = yasl_display_open(NULL, NULL); + assert(d); + struct yasl_surface_spec spec = {.title = "Ownership test", .size = {160, 100}}; + struct yasl_surface *s = yasl_surface_create_toplevel(d, &spec); + assert(s); + struct yasl_input_event event; + while (yasl_surface_input_poll(s, &event)) {} + @autoreleasepool { + for (int i = 0; i < 600; i++) { + char text[32]; + snprintf(text, sizeof(text), "paste-%d", i); + yasl_display_copy(d, YASL_CLIPBOARD, text); + yasl_display_paste(d, YASL_CLIPBOARD, s); + memset(text, 'x', sizeof(text)); + } + } + /* All NSString/UTF8String temporaries have now expired. */ + for (int i = 0; i < 600; i++) { + char text[32]; + snprintf(text, sizeof(text), "paste-%d", i); + assert(yasl_surface_input_poll(s, &event)); + assert(event.kind == YASL_INPUT_PASTE && strcmp(event.paste.text, text) == 0); + } + const char *last = event.paste.text; + assert(!yasl_surface_input_poll(s, &event)); + @autoreleasepool { yasl_display_copy(d, YASL_CLIPBOARD, "new selection"); } + assert(strcmp(last, "paste-599") == 0); + @autoreleasepool { + yasl_display_copy(d, YASL_CLIPBOARD, ""); + yasl_display_paste(d, YASL_CLIPBOARD, s); + } + assert(yasl_surface_input_poll(s, &event)); + assert(event.kind == YASL_INPUT_PASTE && !event.paste.len && !event.paste.text[0]); + yasl_surface_destroy(s); + yasl_display_close(d); + return 0; +} diff --git a/tests/common/test_backends.c b/tests/common/test_backends.c index 48a6b68..3372290 100644 --- a/tests/common/test_backends.c +++ b/tests/common/test_backends.c @@ -67,7 +67,24 @@ static void draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud) assert(size.x == frame->target.size.x && size.y == frame->target.size.y); assert(frame->target.pixels); for (int i = 0; i < size.x * size.y; i++) frame->target.pixels[i] = 0xff4080c0; - struct yasl_damage_list damage = {.n = -1}; + struct yasl_damage_list damage = {.n = YASL_DAMAGE_MAX + 1}; + assert(!yasl_surface_present(s, &frame->target, &damage)); + damage.n = -2; + assert(!yasl_surface_present(s, &frame->target, &damage)); + damage.n = 1; + damage.rects[0].width = -1; + assert(!yasl_surface_present(s, &frame->target, &damage)); + damage.n = -1; + struct yasl_framebuffer invalid = frame->target; + invalid.buffer++; + assert(!yasl_surface_present(s, &invalid, &damage)); + invalid = frame->target; + invalid.generation--; + assert(!yasl_surface_present(s, &invalid, &damage)); + invalid = frame->target; + invalid.native_drawable = (void *)(uintptr_t)1; + assert(!yasl_surface_present(s, &invalid, &damage)); + assert(!yasl_surface_present(s, &frame->target, NULL)); assert(yasl_surface_present(s, &frame->target, &damage)); assert(!yasl_surface_present(s, &frame->target, &damage)); w->draws++; @@ -98,12 +115,22 @@ static const struct yasl_surface_cbs callbacks = { .draw = draw, .configure = configure, .close = close_window, }; +static void attach(struct window *w) +{ + struct yasl_surface_cbs temporary = callbacks; + yasl_surface_set_callbacks(w->surface, &temporary, w); + temporary.draw = NULL; + temporary.configure = NULL; + temporary.close = NULL; +} + static struct window create(struct yasl_display *d) { struct yasl_surface_spec spec = {.title = "Backend test", .size = {160, 100}}; struct yasl_surface *s = yasl_surface_create_toplevel(d, &spec); assert(s); assert(yasl_surface_display(s) == d); + assert(yasl_surface_timing(s).source == YASL_TIMING_UNKNOWN); return (struct window){.surface = s, .display = d}; } @@ -153,6 +180,61 @@ static void x11_behavior(struct yasl_loop *loop, struct yasl_display *d, struct } } assert(saw_key && saw_paste); + assert(yasl_surface_timing(w->surface).source == YASL_TIMING_SUBMISSION); + assert(!(yasl_display_caps(d) & YASL_CAP_FRAME_PACING)); + + /* Reuse caller storage, replace the selection, and exceed the old queue + * cap before polling: each paste must retain its own original contents. */ + for (int i = 0; i < 300; i++) { + char text[32]; + snprintf(text, sizeof(text), "owned-%d", i); + yasl_display_copy(d, YASL_CLIPBOARD, text); + yasl_display_paste(d, YASL_CLIPBOARD, w->surface); + memset(text, 'x', sizeof(text)); + } + for (int i = 0; i < 300; i++) { + char text[32]; + snprintf(text, sizeof(text), "owned-%d", i); + assert(yasl_surface_input_poll(w->surface, &event)); + assert(event.kind == YASL_INPUT_PASTE && strcmp(event.paste.text, text) == 0); + } + const char *last = event.paste.text; + assert(!yasl_surface_input_poll(w->surface, &event)); + yasl_display_copy(d, YASL_CLIPBOARD, "changed"); + pump(loop, d, NULL); + assert(strcmp(last, "owned-299") == 0); + yasl_display_copy(d, YASL_CLIPBOARD, ""); + yasl_display_paste(d, YASL_CLIPBOARD, w->surface); + assert(yasl_surface_input_poll(w->surface, &event)); + assert(event.kind == YASL_INPUT_PASTE && !event.paste.len && !event.paste.text[0]); + + /* Concurrent external conversions must not share a mutable target slot. */ + struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11}; + struct yasl_display *owner = yasl_display_open(loop, &ds); + assert(owner); + yasl_display_copy(owner, YASL_CLIPBOARD, "external"); + struct window other = create(d), cancelled = create(d); + yasl_display_paste(d, YASL_CLIPBOARD, w->surface); + yasl_display_paste(d, YASL_CLIPBOARD, other.surface); + yasl_display_paste(d, YASL_CLIPBOARD, cancelled.surface); + yasl_surface_destroy(cancelled.surface); + pump(loop, d, owner); + int first_pastes = 0, second_pastes = 0; + while (yasl_surface_input_poll(w->surface, &event)) { + if (event.kind == YASL_INPUT_PASTE) { + assert(strcmp(event.paste.text, "external") == 0); + first_pastes++; + } + } + while (yasl_surface_input_poll(other.surface, &event)) { + if (event.kind == YASL_INPUT_PASTE) { + assert(strcmp(event.paste.text, "external") == 0); + second_pastes++; + } + } + assert(first_pastes == 1 && second_pastes == 1); + yasl_surface_destroy(other.surface); + yasl_display_close(owner); key.xclient = (XClientMessageEvent){.type = ClientMessage, .display = native, .window = window, .message_type = XInternAtom(native, "WM_PROTOCOLS", False), .format = 32, .data.l = {(long)XInternAtom(native, "WM_DELETE_WINDOW", False)}}; @@ -177,8 +259,8 @@ int main(int argc, char **argv) struct yasl_display *b = yasl_display_open(loop, &spec); assert(a && b); struct window wa = create(a), wb = create(b); - yasl_surface_set_callbacks(wa.surface, &callbacks, &wa); - yasl_surface_set_callbacks(wb.surface, &callbacks, &wb); + attach(&wa); + attach(&wb); pump(loop, a, b); assert(wa.draws && wb.draws); yasl_surface_destroy(wa.surface); @@ -195,7 +277,7 @@ int main(int argc, char **argv) struct window w = create(d); pump(loop, d, NULL); assert(w.draws == 0); - yasl_surface_set_callbacks(w.surface, &callbacks, &w); + attach(&w); yasl_surface_schedule_redraw(w.surface); pump(loop, d, NULL); assert(w.draws > 0); @@ -204,7 +286,7 @@ int main(int argc, char **argv) if (w.surface) yasl_surface_destroy(w.surface); w = create(d); w.destroy_on_draw = true; - yasl_surface_set_callbacks(w.surface, &callbacks, &w); + attach(&w); pump(loop, d, NULL); assert(w.draws && !w.surface); yasl_display_close(d); diff --git a/tests/common/test_contracts.c b/tests/common/test_contracts.c new file mode 100644 index 0000000..65eebcd --- /dev/null +++ b/tests/common/test_contracts.c @@ -0,0 +1,175 @@ +#include "common/backend.h" +#include "common/input.h" + +#include <assert.h> +#include <errno.h> +#include <math.h> +#include <stdlib.h> +#include <string.h> + +static void test_queue(void) +{ + struct input_queue q = {0}; + struct yasl_surface surface = {0}; + const enum yasl_input_kind kinds[] = { + YASL_INPUT_TEXT, YASL_INPUT_PASTE, YASL_INPUT_DROP_FILES, + }; + for (size_t k = 0; k < sizeof(kinds) / sizeof(kinds[0]); k++) { + char *source = malloc(4); + assert(source); + memcpy(source, "a\0b", 4); + struct yasl_input_event in = {.surface = &surface, .kind = kinds[k]}; + if (in.kind == YASL_INPUT_TEXT) { in.text.text = source; in.text.len = 3; } + if (in.kind == YASL_INPUT_PASTE) { in.paste.text = source; in.paste.len = 3; } + if (in.kind == YASL_INPUT_DROP_FILES) { in.drop_files.files = source; in.drop_files.len = 3; } + assert(input_queue_push(&q, &in)); + memset(source, 'x', 4); + free(source); + struct yasl_input_event out; + assert(input_queue_pop(&q, &out)); + const char *payload = out.kind == YASL_INPUT_TEXT ? out.text.text : + out.kind == YASL_INPUT_PASTE ? out.paste.text : out.drop_files.files; + assert(out.surface == &surface && out.kind == kinds[k]); + assert(memcmp(payload, "a\0b", 4) == 0); + assert(!input_queue_pop(&q, &out)); + assert(memcmp(payload, "a\0b", 4) == 0); + /* Growth/wraparound must not invalidate a previously polled payload. */ + for (unsigned i = 0; i < 4096; i++) { + in = (struct yasl_input_event){.kind = YASL_INPUT_KEY, .key.key = i}; + assert(input_queue_push(&q, &in)); + } + assert(memcmp(payload, "a\0b", 4) == 0); + for (unsigned i = 0; i < 2048; i++) { + assert(input_queue_pop(&q, &out)); + assert(out.key.key == i); + } + for (unsigned i = 4096; i < 6144; i++) { + in.key.key = i; + assert(input_queue_push(&q, &in)); + } + for (unsigned i = 2048; i < 6144; i++) { + assert(input_queue_pop(&q, &out)); + assert(out.key.key == i); + } + assert(!input_queue_pop(&q, &out)); + } + struct yasl_input_event empty = {.surface = &surface, .kind = YASL_INPUT_PASTE}; + struct yasl_input_event out; + assert(input_queue_push(&q, &empty)); + assert(input_queue_pop(&q, &out)); + assert(out.paste.text && !out.paste.len && out.paste.text[0] == '\0'); + struct yasl_input_event in = {.surface = &surface, .kind = YASL_INPUT_TEXT, + .text = {"owned", 5}}; + assert(input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out)); + const char *last = out.text.text; + assert(input_queue_push(&q, &in)); + /* Exercise the allocation-failure reset path without allocating huge RAM. */ + in.text.len = SIZE_MAX; + assert(!input_queue_push(&q, &in)); + assert(strcmp(last, "owned") == 0); + in = (struct yasl_input_event){.surface = &surface, .kind = YASL_INPUT_KEY}; + assert(input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_RESET); + assert(out.surface == &surface); + assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_KEY); + assert(!input_queue_pop(&q, &out)); + in = (struct yasl_input_event){.kind = YASL_INPUT_PASTE, .paste = {"pending", 7}}; + assert(input_queue_push(&q, &in)); + input_queue_clear(&q); + input_queue_clear(&q); + assert(!q.events && !q.last_text && !q.count && !q.reset_pending); +} + +static int presents, copies, pastes, creates; +static struct yasl_damage_list received; +static bool present(struct yasl_surface *s, const struct yasl_framebuffer *fb, + const struct yasl_damage_list *damage) +{ + (void)s; (void)fb; + presents++; + received = *damage; + return true; +} +static uint32_t caps(const struct yasl_display *d) { (void)d; return 0; } +static void copy(struct yasl_display *d, enum yasl_selection sel, const char *text) +{ (void)d; (void)sel; (void)text; copies++; } +static void paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *s) +{ (void)d; (void)sel; (void)s; pastes++; } +static struct yasl_surface *create(struct yasl_display *d, const struct yasl_surface_spec *spec) +{ (void)d; (void)spec; creates++; return NULL; } + +static void test_validation(void) +{ + const struct yasl_backend_ops ops = {.surface_present = present, + .display_caps = caps, .display_copy = copy, .display_paste = paste, + .surface_create_toplevel = create}; + struct yasl_display d = {.ops = &ops}; + struct yasl_surface s = {.display = &d, .frame_generation = 1}; + struct yasl_framebuffer fb = {.generation = 1, .size = {100, 80}}; + struct yasl_damage_list damage = {.n = -2}; + assert(!yasl_surface_present(&s, &fb, &damage)); + damage.n = YASL_DAMAGE_MAX + 1; + assert(!yasl_surface_present(&s, &fb, &damage)); + damage.n = 1; + damage.rects[0] = (struct yasl_rect2i){0, 0, -1, 1}; + assert(!yasl_surface_present(&s, &fb, &damage)); + assert(!yasl_surface_present(&s, &fb, NULL)); + assert(!presents); + damage.n = -1; + fb.generation = 0; + assert(!yasl_surface_present(&s, &fb, &damage)); + fb.generation = 2; + assert(!yasl_surface_present(&s, &fb, &damage)); + fb.generation = 1; + damage.n = 4; + damage.rects[0] = (struct yasl_rect2i){-10, -10, 30, 30}; + damage.rects[1] = (struct yasl_rect2i){90, 70, INT_MAX, INT_MAX}; + damage.rects[2] = (struct yasl_rect2i){INT_MAX, INT_MAX, INT_MAX, INT_MAX}; + damage.rects[3] = (struct yasl_rect2i){INT_MIN, INT_MIN, INT_MAX, INT_MAX}; + assert(yasl_surface_present(&s, &fb, &damage)); + assert(received.n == 2 && received.rects[0].x == 0 && received.rects[0].width == 20); + assert(received.rects[1].width == 10 && received.rects[1].height == 10); + assert(damage.n == 4 && damage.rects[0].x == -10); + damage.n = -1; + assert(yasl_surface_present(&s, &fb, &damage) && received.n == -1); + damage.n = 0; + assert(yasl_surface_present(&s, &fb, &damage) && received.n == 0); + yasl_display_copy(&d, YASL_PRIMARY, "no alias"); + yasl_display_copy(&d, (enum yasl_selection)255, "invalid"); + yasl_display_paste(&d, YASL_PRIMARY, &s); + yasl_display_paste(&d, (enum yasl_selection)255, &s); + assert(!copies && !pastes); + yasl_display_copy(&d, YASL_CLIPBOARD, "yes"); + yasl_display_paste(&d, YASL_CLIPBOARD, &s); + assert(copies == 1 && pastes == 1); + struct yasl_surface_spec spec = {.size = {100, 80}, .graphics_device = &d}; + assert(!yasl_surface_create_toplevel(&d, &spec) && errno == ENOTSUP); + spec.graphics_device = NULL; + spec.size.x = 0; + assert(!yasl_surface_create_toplevel(&d, &spec) && errno == EINVAL); + assert(!creates); +} + +static void test_scale(void) +{ + assert(yasl_scale_to_device(-10, 120) == -10); + assert(yasl_scale_to_device(-0.5, 120) == -1); + assert(yasl_scale_to_device(0.5, 120) == 1); + assert(yasl_scale_to_device(NAN, 120) == 0); + assert(yasl_scale_to_device(INFINITY, 120) == 0); + assert(yasl_scale_to_device(DBL_MAX, 240) == INT_MAX); + assert(yasl_scale_to_logical(-1, 240) == -1); + assert(yasl_scale_to_logical(1, 240) == 1); + assert(yasl_scale_to_logical(INT_MAX, 1) == INT_MAX); + assert(yasl_scale_to_logical(INT_MIN, 1) == INT_MIN); + assert(yasl_scale_to_logical(1, 0) == 0); +} + +int main(void) +{ + test_queue(); + test_validation(); + test_scale(); + return 0; +} diff --git a/tests/common/test_input_alloc.c b/tests/common/test_input_alloc.c new file mode 100644 index 0000000..e311dc0 --- /dev/null +++ b/tests/common/test_input_alloc.c @@ -0,0 +1,48 @@ +#include "common/input.h" +#include <assert.h> +#include <stdlib.h> +#include <string.h> + +/* This executable compiles input.c with malloc redirected here. */ +static int allocations = -1; +void *yasl_test_malloc(size_t bytes) +{ + if (allocations == 0) return NULL; + if (allocations > 0) allocations--; + return calloc(1, bytes); +} + +int main(void) +{ + struct yasl_surface *surface = (struct yasl_surface *)(uintptr_t)1; + for (int failure = 0; failure < 2; failure++) { + struct input_queue q = {0}; + struct yasl_input_event in = {.surface = surface, .kind = YASL_INPUT_PASTE, + .paste = {"payload", 7}}, out; + /* Fail either payload allocation or the initial event-array allocation. */ + allocations = failure; + assert(!input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_RESET); + assert(out.surface == surface); + assert(!input_queue_pop(&q, &out)); + input_queue_clear(&q); + + allocations = -1; + assert(input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out)); + const char *last = out.paste.text; + for (size_t i = 0; i < q.capacity; i++) assert(input_queue_push(&q, &in)); + allocations = failure; + assert(!input_queue_push(&q, &in)); + assert(strcmp(last, "payload") == 0); + assert(!q.count && q.reset_pending); + allocations = -1; + in = (struct yasl_input_event){.surface = surface, .kind = YASL_INPUT_KEY, + .key.action = YASL_KEY_RELEASE}; + assert(input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_RESET); + assert(input_queue_pop(&q, &out) && out.key.action == YASL_KEY_RELEASE); + input_queue_clear(&q); + } + return 0; +} diff --git a/tests/meson.build b/tests/meson.build index 950f5f5..1fcac3e 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -12,6 +12,22 @@ add_test_setup('msan') test('basic', test_exe) +contracts_test = executable( + 'yasl-contracts-test', + 'common/test_contracts.c', + include_directories: inc, + dependencies: yasl_dep, +) +test('contracts', contracts_test) + +input_alloc_test = executable( + 'yasl-input-alloc-test', + ['common/test_input_alloc.c', '../src/common/input.c'], + include_directories: inc, + c_args: ['-Dmalloc=yasl_test_malloc'], +) +test('input-allocation-failure', input_alloc_test) + selection_test = executable( 'yasl-selection-test', 'common/test_selection.c', @@ -19,12 +35,24 @@ selection_test = executable( ) test('selection', selection_test) +if has_wayland + ime_test = executable('yasl-ime-test', 'wayland/test_input.c', + include_directories: inc, + dependencies: yasl_dep, + ) + test('ime-ownership', ime_test) +endif + if get_option('integration_tests') if has_cocoa native_test = executable('yasl-native-test', 'cocoa/test_native.c', dependencies: yasl_dep, ) test('native', native_test, timeout: 20) + ownership_test = executable('yasl-ownership-test', 'cocoa/test_ownership.m', + dependencies: yasl_dep, + ) + test('native-ownership', ownership_test, timeout: 20) else if not has_wayland or not has_x11 error('Linux integration tests require Wayland and X11.') diff --git a/tests/wayland/test_input.c b/tests/wayland/test_input.c new file mode 100644 index 0000000..f421e6c --- /dev/null +++ b/tests/wayland/test_input.c @@ -0,0 +1,26 @@ +#include "wayland/internal.h" +#include <assert.h> + +int main(void) +{ + struct wayland_surface w = {0}; + struct seat seat = {.ime_focus = &w}; + wayland_surface_set_callbacks(&w, NULL, NULL); + for (int i = 0; i < 600; i++) { + char text[32]; + snprintf(text, sizeof(text), "commit-%d", i); + text_input_listener.commit_string(&seat, NULL, text); + text_input_listener.done(&seat, NULL, (uint32_t)i); + assert(!w.ime_commit); + } + for (int i = 0; i < 600; i++) { + char text[32]; + snprintf(text, sizeof(text), "commit-%d", i); + struct yasl_input_event event; + assert(wayland_surface_input_poll(&w, &event)); + assert(event.kind == YASL_INPUT_TEXT && event.surface == &w.base); + assert(strcmp(event.text.text, text) == 0); + } + input_queue_clear(&w.input); + return 0; +} |
