summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/api-policy.md48
-rw-r--r--docs/api-redesign.md147
-rw-r--r--docs/getting-started.md138
-rw-r--r--docs/guide.md617
-rw-r--r--docs/migrating.md132
-rw-r--r--docs/testing.md166
6 files changed, 226 insertions, 1022 deletions
diff --git a/docs/api-policy.md b/docs/api-policy.md
index 3e510f3..f2f4d92 100644
--- a/docs/api-policy.md
+++ b/docs/api-policy.md
@@ -1,38 +1,18 @@
# Public API policy
-YASL 0.1 is pre-release. The headers under `include/yasl/` are the public API; implementation
-headers under `src/` are private even when their symbols appear in a static
-archive. The shared library exports only declarations from public headers.
-Native backend accessors are exported only for built backends. Installation
-copies both libraries, public headers, enabled native headers, and pkg-config metadata.
+YASL is pre-release 0.1. Source and ABI compatibility are not guaranteed. Rebuild
+consumers against matching installed headers/library; do not serialize public events
+or assume their layout is a stable file/network format.
-There is currently no stable ABI promise and no versioned shared-library SONAME.
-Rebuild consumers against the exact headers/library pair they ship. Source and
-binary compatibility can change before 1.0. Do not assume struct layout is stable
-across releases, mix headers and binaries from different revisions, or persist
-public structs as a file/network format. The release version is declared in
-`meson.build`.
+Only installed include/yasl headers are public. The surface library exposes an
+opaque surface and one tagged event record, with direct C arguments and ordinary
+pointer/integer results. Native backend headers, geometry records, callbacks, loops,
+and drawing targets under src are private even when a static archive contains them.
+The shared library exports exactly the installed public declarations.
-See [migration notes](migrating.md) for window creation, portable keyboard, and
-fractional scroll API changes.
-
-API changes should keep ordinary CPU drawing usable with only `<yasl/yasl.h>`
-and the C standard library. Prefer an explicit capability and an ordinary
-unsupported result over backend names in portable application logic. Native
-rendering and handles belong in the extension headers. Avoid adding engine,
-widget, renderer, or application policy to the surface layer.
-
-Every public addition or behavior change needs:
-
-- A header contract describing arguments, result, units, lifetime, threading,
- callback timing, and unsupported behavior where applicable.
-- An update to the guide and its backend limits when observable behavior changes.
-- A runnable example when the new workflow cannot be understood from an isolated
- call, and meaningful regression coverage for ownership or behavior changes.
-- A check that shared exports and consumers of the installed library still work.
-
-Functions documenting errno set it on failure. Other booleans report acceptance
-or availability without a guaranteed errno; void setters are best effort.
-Callbacks and successful returns do not imply confirmed screen presentation.
-See `yasl_timing.source` for provenance. Future changes that strengthen error
-reporting must preserve these distinctions in documentation.
+Do not restore removed runner/frame/display APIs or compatibility aliases. Keep
+rendering bridges separate from the portable surface/input core and preserve the
+application's ownership of GPU resources and its loop. Public additions need clear
+units, ownership, thread/callback timing, errors and unsupported behavior, useful
+examples, and behavior coverage. Update the guide and test installed shared/static
+consumers and exports. Changes affecting Cocoa need a native macOS build/test.
diff --git a/docs/api-redesign.md b/docs/api-redesign.md
deleted file mode 100644
index a111a92..0000000
--- a/docs/api-redesign.md
+++ /dev/null
@@ -1,147 +0,0 @@
-# Public API redesign
-
-This is the implementation contract for the pre-release API revision. The
-portable layer owns event ordering, queued payloads, surface identities, and
-frame validation. Backends translate native behavior into that contract.
-
-## One event stream
-
-`yasl_display_poll_event(display, &event)` consumes one display-wide FIFO. Input,
-window resize/close/state, drag hover/drop, composition, clipboard results, and
-display quit/reopen/loss use the same `yasl_event`. Events have an increasing
-`sequence` assigned when YASL queues them, and a monotonic `time_ns`. Sequence
-orders capture by YASL, not unknowable ordering between OS threads or devices.
-The runner consumes this same queue; it does not drain separate queues by window.
-
-Event strings are copied on enqueue and borrowed until the next successful
-display poll or display close, even if the recipient is destroyed. Destroying a
-surface removes its undelivered events. A callback may destroy its recipient;
-it must stop using that surface. Draws remain synchronous to satisfy native
-live-resize rules; the runner drains queued events before invoking a frame.
-
-Legacy surface polling consumes input from the same queue and discards pending
-non-input notifications, which were already delivered through legacy callbacks.
-Applications must choose one polling style; discarding prevents idle spin and
-unbounded retention in old manual loops. Legacy window
-callbacks remain for migration, but applications should choose queue delivery
-and a draw callback. Backend notifications must queue before calling a legacy
-callback, and must not access a surface after a callback that can destroy it.
-
-## One frame contract
-
-Both manual drawing and `yasl_app_desc.frame` receive `struct yasl_frame`.
-Its `target` describes CPU pixels (NULL for native rendering), device-pixel size,
-and row stride. The frame also carries frame index, scale, and an estimated
-animation timestamp. Native drawables are queried through `<yasl/native.h>`;
-there is no untyped GPU layer or public internal pointer in the portable frame.
-
-`yasl_frame_present(frame, rects, count)` submits the exact active frame once.
-NULL with zero count means full damage; a non-NULL array with zero count means
-no damage. NULL with nonzero count and negative extents fail with EINVAL.
-More than the backend rectangle limit falls back to full damage after validating
-every rectangle; damage must never be silently lost. Targets expire at callback
-return. Pixels must always describe a complete frame.
-
-`yasl_surface_request_frame` is the normal coalescing redraw operation.
-`yasl_surface_set_pacing` returns success/failure and works with either loop
-style. Continuous drawing uses native pacing where available and a refresh-based
-timer otherwise. On-demand surfaces require explicit invalidation.
-
-## Identities, posting, and shutdown
-
-`yasl_surface_id(surface)` returns a nonzero identity unique within its display.
-`yasl_display_post(display, &message)` is thread-safe; messages target an identity
-(zero means display-wide), never a borrowed surface pointer. The display must
-remain alive during posting, and producer threads must stop before display close.
-After a successful post the optional release callback runs exactly once on the
-display thread, after consumption or cancellation (including surface destruction
-and shutdown). A failed post leaves payload ownership with the caller. Posting
-to a retired identity fails. The display queue owns only its copy of the message;
-the release callback defines payload cleanup. Release may post, but must not
-poll, dispatch, or destroy YASL objects. Legacy `yasl_post_event` adapts to
-this mechanism, with the caller responsible for its borrowed target lifetime.
-
-Application callbacks may return CONTINUE, QUIT, or ERROR. ERROR makes
-`yasl_run` return failure after cleanup. `yasl_app_exit(display, result)` permits
-timers, fd callbacks, and frames to request the same orderly termination.
-The optional initial window is available through `yasl_app_window(display)`.
-Surface userdata is independent of legacy callback userdata and accessible
-through `yasl_surface_set_userdata` / `yasl_surface_userdata`.
-
-## Window requests and units
-
-New code spells toplevel creation size `size_logical`; `size` remains an alias
-for migration. Framebuffer geometry, pointer coordinates, popup placement,
-damage, IME carets, and drag-image coordinates are device pixels as documented
-on their structures. Conversion helpers remain explicit.
-
-`yasl_surface_set_size(surface, logical_size)`,
-`yasl_surface_set_fullscreen(surface, enabled)`,
-`yasl_surface_set_maximized(surface, enabled)`, and
-`yasl_surface_minimize(surface)` return whether the native request was issued.
-They set errno on failure: EINVAL for invalid input/non-toplevel surfaces,
-ENOTSUP for unsupported requests, or an OS error. Success is acceptance of a
-request, not a promise that the window manager honored it. Resulting size/state
-is reported through events. No portable absolute window-position setter exists.
-
-## Clipboard and composition
-
-`yasl_clipboard_set(display, selection, text)` returns whether a copied UTF-8
-offer was issued, with errno on failure. `yasl_clipboard_request(surface,
-selection)` returns a nonzero request ID, or zero with errno on failure.
-The backend operation `display_request_paste(d, selection, surface, id)` returns
-true only after accepting responsibility for one completion. It completes via
-`yasl_surface_complete_paste(surface, id, status, text, len)`; status is
-YASL_TRANSFER_OK, UNAVAILABLE, or FAILED. Results use YASL_EVENT_PASTE and its
-`.paste` payload (data/len/request_id/status); legacy `.text` aliases data/len.
-Surface destruction cancels delivery and native transfers. A backend must
-complete errors as well as success, including empty text. Request IDs are
-monotonic per display. Legacy paste uses ID zero. External Linux transfers time out after 30 seconds.
-
-YASL_EVENT_COMPOSITION carries copied UTF-8 preedit text, byte selection offsets
-(both -1 if unavailable), and an active flag. Each native change queues a
-snapshot, including composition ending; committed text remains YASL_EVENT_TEXT.
-The query functions remain migration helpers.
-
-## Backend boundary
-
-Common helpers declared in `common/backend.h`:
-
-* `yasl_surface_notify_configure(s, geometry)`
-* `yasl_surface_notify_close(s)`
-* `yasl_surface_notify_state(s, state)`
-* `yasl_surface_notify_drag(s, event, position_px)`
-* `yasl_surface_notify_drop(s, event, position_px)`
-* `yasl_surface_notify_composition(s, text, begin, end)` (NULL/empty ends it)
-* `yasl_surface_draw(s, frame)` (fills frame metadata and invokes draw)
-* `yasl_display_notify_event(d, YASL_EVENT_DISPLAY_QUIT/REOPEN/LOST)`
-* `yasl_surface_complete_paste(s, request_id, status, text, length)`
-
-Backends keep using their existing input_queue; a new `surface_input_queue`
-operation exposes that queue to common code for attaching its event publisher.
-Backends must replace direct lifecycle/draw/drag callbacks with these helpers,
-without weakening their native destruction guards. They implement optional
-surface_set_size/fullscreen/maximized/minimize and display_copy_text /
-display_request_paste operations. Common wrappers validate arguments and
-report unsupported operations uniformly.
-
-Linux backends are compiled and tested independently and together. Cocoa changes
-can be inspected here but require macOS compilation/runtime verification.
-
-## Validation
-
-The combined Linux configuration passes all 26 Meson tests, including isolated
-Weston/Xwayland integration, runner error/cleanup paths, event ordering,
-clipboard outcomes, composition, and destruction during callbacks. Separate
-Wayland-only and X11-only builds pass their unit suites and the final public
-export, C++11, and event-contract checks.
-
-AddressSanitizer and UndefinedBehaviorSanitizer pass the common contract tests
-and the isolated backend integration suite. Leak detection was disabled because
-LeakSanitizer is unavailable in this execution environment. Doxygen builds with
-warnings treated as errors. A staged install links both shared and static
-consumers using only the installed headers and pkg-config metadata.
-
-Cocoa code and native regression tests were updated and reviewed, but were not
-compiled or executed: macOS validation remains required. Existing X11 INCR and
-Wayland arbitrary-size limitations are reported explicitly rather than hidden.
diff --git a/docs/getting-started.md b/docs/getting-started.md
index d1f284d..bb7f095 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -1,113 +1,71 @@
# Getting started
-YASL gives a C application desktop windows with CPU pixel targets, scale-aware
-geometry, queued input, clipboard text, and optional frame pacing. It fits
-software-rendered tools such as editors, terminals, drawing applications, and
-custom interfaces that already have their own renderer. A window needs neither
-an OpenGL context nor a graphics device supplied by the application.
+YASL creates desktop surfaces and delivers queued input. You own the event loop,
+renderer, graphics device/context use, and application lifetime.
-The core headers expose no window system types. Native handles and Metal drawing
-are separate opt-in extensions. YASL currently supports Linux (Wayland and X11)
-and macOS (Cocoa); it does not provide widgets, font shaping, audio, an OpenGL
-context API, or a Vulkan surface API. It is pre-release software; see the
-[API policy](api-policy.md) before distributing a library that depends on its ABI.
-
-## Build and run
-
-Use a C23 compiler, Meson 1.6 or newer, and Ninja. GCC and Clang can build the library. On Linux install either backend's development dependencies:
-
-| Backend | pkg-config dependencies |
-| --- | --- |
-| Wayland | `wayland-client`, `wayland-cursor`, `xkbcommon` |
-| X11 | `x11`, `xi`, `xkbcommon`, `xext`; optional `xrandr` |
-| Linux desktop services | Optional `dbus-1` for notifications and appearance |
-| Cocoa | macOS 14+ SDK with AppKit, Metal, QuartzCore, and UserNotifications; Metal-capable device |
+Build with a C23 compiler, Meson 1.6+, and Ninja. Linux backend dependencies are
+wayland-client, wayland-cursor, xkbcommon, or x11, xi, xext, xkbcommon (optional
+xrandr). Optional egl and wayland-egl enable GL contexts; Vulkan SDK development
+files enable the Vulkan bridge. Optional dbus-1 enables Linux desktop services.
+Cocoa requires macOS 14+ and the AppKit, Metal, QuartzCore, OpenGL, and
+UserNotifications system frameworks. Optional Vulkan SDK enables MoltenVK WSI.
```sh
meson setup build
meson compile -C build
./build/yasl-hello
+./build/yasl-game
meson test -C build --print-errorlogs
```
-Each Linux backend defaults to `auto`, meaning build it when its dependencies
-are available. Require one with `-Dwayland=enabled` or `-Dx11=enabled`; omit one
-with `-Dwayland=disabled` or `-Dx11=disabled`. At least one backend is required.
-`-Ddbus` works the same way for the optional D-Bus desktop services.
-Cocoa is selected on macOS and requires no extra third-party dependencies.
-Its minimum deployment target is macOS 14. Set `MACOSX_DEPLOYMENT_TARGET=14.0`
-(or newer) before configuring if your toolchain defaults to an older target.
-CPU pixel targets are presented through the system's Metal device; applications
-do not need to create or manage that device themselves.
+Backend options `-Dwayland=enabled|disabled|auto`, `-Dx11=...`, and `-Ddbus=...`
+control dependency selection. At least one window backend must be built. Runtime
+AUTO selects the native session and falls back to another built Linux backend.
+For an explicit choice call `yasl_set_backend` before creating any surface.
-The complete beginner example is [hello.c](../examples/hello.c). It lets
-`yasl_run()` create its window from `yasl_app_desc.window`, fills every pixel
-with an RGB gradient, and presents inside a frame callback.
-`yasl_run()` handles input delivery, display notifications, and cleanup. The
-[interactive window](../examples/simple_window.c) adds pointer input; the
-[tablet example](../examples/tablet.c) demonstrates retained application content,
-pressure, erasers, resizing, and scale changes.
+## A window
-## Link an application
+```c
+#include <yasl/yasl.h>
+
+int main(void)
+{
+ yasl_surface *s = yasl_surface_create("Example", 800, 600, 0);
+ if (!s) return 1;
+ /* Attach a GPU renderer here and draw on each REFRESH. */
+ yasl_event event;
+ while (yasl_wait_event(&event, -1) >= 0 &&
+ event.type != YASL_EVENT_CLOSE && event.type != YASL_EVENT_QUIT) {
+ }
+ /* Destroy GPU objects before shutting down. */
+ yasl_shutdown();
+ return 0;
+}
+```
-Install the libraries, public headers, and pkg-config metadata into your prefix:
+The first creation initializes a shared backend. More windows use that backend;
+there is no public display/context object. Closing the final window does not
+implicitly terminate the application or reset configuration. `yasl_shutdown`
+releases shared resources and restores defaults.
+
+[hello.c](../examples/hello.c) draws into a CPU surface's pixel buffer on every REFRESH. [game.c](../examples/game.c) creates a GL context, binds it,
+loads procedures, draws, and swaps. [tablet.c](../examples/tablet.c) retains a canvas
+and draws pressure-sensitive pen/eraser strokes. [notepad.c](../examples/notepad.c)
+uses a worker queue, wakeups, clipboard, IME, and its own blink deadline.
+
+## Link and install
```sh
meson setup build --prefix=/usr/local --libdir=lib
meson compile -C build
meson install -C build
cc -std=c23 examples/hello.c -o hello $(pkg-config --cflags --libs yasl)
-./hello
-```
-
-The prefix must be writable. For a custom prefix, add its pkg-config directory
-to `PKG_CONFIG_PATH` and its library directory to the runtime loader path.
-Use `pkg-config --static --libs yasl` for static consumers so backend dependencies
-are included. Installed native accessor headers match the built backends.
-
-A Meson application can instead vendor the source under `subprojects/yasl`:
-
-```meson
-project('my-app', 'c', default_options: ['c_std=c23'])
-yasl = subproject('yasl').get_variable('yasl_dep')
-executable('my-app', 'main.c', dependencies: yasl)
-```
-
-The subproject dependency includes headers and backend link dependencies.
-Public headers can be included from C++11 or newer; the library itself builds
-as C23. The libraries, public headers, and pkg-config metadata are installed together.
-
-## The application cycle
-
-1. Include `<yasl/app.h>` and initialize a `yasl_app_desc` with your userdata and
- `init`, `event`, and `frame` callbacks. Call `yasl_run(&desc)`.
-2. In `init`, create a toplevel with a positive **logical-pixel** size and call
- `yasl_surface_request_frame(surface)` for the first redraw.
-3. In `event`, handle the union member selected by `event->type`. Return
- `YASL_APP_QUIT` for a close request, `YASL_APP_ERROR` for failure, or
- `YASL_APP_CONTINUE` to keep running.
-4. In `frame`, fill every pixel in `frame->target.pixels` with premultiplied
- `0xAARRGGBB` and present once with `yasl_frame_present(frame, NULL, 0)`.
-5. Let the runner close remaining surfaces and the display. Release your own
- resources in the optional `quit` callback or after `yasl_run()` returns.
-
-```c
-struct yasl_app_desc desc = {
- .app_id = "org.example.app",
- .userdata = &app,
- .init = init,
- .event = event,
- .frame = draw,
-};
-return yasl_run(&desc);
```
-The default pacing mode sleeps until input, resize, or a requested redraw.
-Use `YASL_PACING_CONTINUOUS` for an animation or game. The core API still supports
-manual dispatch/poll/flush when an application owns its event loop; see the guide.
+Use a writable prefix. For a custom prefix configure PKG_CONFIG_PATH and the
+runtime library search path. Static consumers use `pkg-config --static --libs yasl`.
+Public headers also compile as C++11. Native headers and the Vulkan header are
+installed only when their respective support is built.
-See the [application guide](guide.md) for drawing, input, popups, timers, and
-platform limits, or the comments in the [public header](../include/yasl/yasl.h)
-for each function's contract. To build the searchable HTML reference, install
-Doxygen and run `doxygen Doxyfile` from the repository root. Open
-`build-docs/html/index.html`; Doxygen is not needed to build the library.
+See the [guide](guide.md) for geometry, event lifetime, GPU ownership, input, and
+backend limits. `doxygen Doxyfile` builds the reference in build-docs/html.
diff --git a/docs/guide.md b/docs/guide.md
index 1b64880..d0db071 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -1,476 +1,141 @@
-# Application guide
-
-The contracts in [yasl.h](../include/yasl/yasl.h), [loop.h](../include/yasl/loop.h),
-[timing.h](../include/yasl/timing.h), [desktop.h](../include/yasl/desktop.h), and
-[native.h](../include/yasl/native.h) are the reference for individual calls. All examples here use the display's opening
-thread, which must be the main thread on macOS. Userdata stays owned by the
-application. Copy borrowed data before retaining it beyond its documented life.
-
-## Backend selection and capabilities
-
-Zero-initialize `yasl_display_spec` or pass NULL to select automatically. On
-Linux, `WAYLAND_DISPLAY`, `WAYLAND_SOCKET`, or `XDG_SESSION_TYPE=wayland` makes
-YASL try Wayland first; otherwise it tries X11 first. It falls back to the other
-built backend if opening fails. On macOS it uses Cocoa. Explicit
-`YASL_BACKEND_WAYLAND`, `YASL_BACKEND_X11`, or `YASL_BACKEND_COCOA` requests never
-fall back. Query the result with `yasl_display_backend()`.
-
-Use `yasl_display_caps()` for optional behavior. A bit means backend/server
-support, not that a device is attached or that the current input state allows
-an operation. Always check the operation's return value too.
-
-| Capability | Meaning | Wayland | X11 | Cocoa |
-| --- | --- | --- | --- | --- |
-| `YASL_CAP_POPUPS` | Child popup surfaces | Yes, input grab required | Yes | Yes |
-| `YASL_CAP_DRAG` | Surface-drag source and target events | Data device v3 | Yes | Yes |
-| `YASL_CAP_DRAG_CARRY` | Carry a toplevel with a surface drag | Optional toplevel-drag protocol | Yes | Yes |
-| `YASL_CAP_PLACE_WINDOW` | Place a window under the pointer | No | Yes | Yes |
-| `YASL_CAP_PRIMARY_SELECTION` | Separate primary text selection | Optional primary-selection protocol | Yes | Named pasteboard emulation |
-| `YASL_CAP_CSD` | Backend advertises client decoration support | Yes | No | Yes |
-| `YASL_CAP_NATIVE_GRAPHICS` | Application-managed native drawables | No | No | Metal |
-| `YASL_CAP_FRAME_PACING` | Display/compositor-driven drawing | Yes | No | Yes |
-| `YASL_CAP_TABLET` | Tablet event translation | Optional tablet-v2 protocol | XInput 2 | Yes |
-
-This table describes the implementation, not universal operating-system
-promises. Wayland protocols and XInput availability depend on the running
-server. A capability does not guarantee every related action: Cocoa currently
-returns false for interactive resize and window-menu requests even though
-it advertises CSD. Check each result and provide an application fallback.
-
-## Window creation and callbacks
-
-All examples include `<yasl/app.h>` and call `yasl_run()` with a
-`yasl_app_desc`. Set `window` to a `yasl_window_spec` for a single toplevel,
-which the runner creates before `init` with its first frame requested;
-`yasl_run()` returns an error if creation fails. Create further surfaces in the
-optional `init`, handle `YASL_EVENT_*` notifications in `event`, and render into
-`frame->target.pixels` in `frame`. Present with `yasl_frame_present(frame, NULL,
-0)` and return `YASL_APP_QUIT` from an event handler to exit. Without an `event`
-callback, closing any window stops the runner. The runner handles dispatch, queued event delivery, resize redraws,
-and remaining surface/display cleanup. Display quit stops the runner; connection
-loss and dispatch failures return an error after cleanup. The runner delivers
-the same display-wide FIFO consumed by `yasl_display_poll_event()`, including
-window state, drag hover, composition, clipboard completion, and display lifecycle
-events. `event.sequence` increases in enqueue order across all windows; it is
-YASL capture order, not a claim about ordering between native producers.
-Return `YASL_APP_ERROR` on application failure, or call
-`yasl_app_exit(display, YASL_APP_ERROR)` from a frame, timer, or fd callback.
-Cleanup still runs, and `yasl_run()` returns failure. During `init`,
-`yasl_app_window(display)` exposes the optional initial window. Application data
-can be supplied in `window.userdata` or attached with
-`yasl_surface_set_userdata()`; it is independent of callback userdata.
-
-Unified pointer button and motion events retain `device` and `pen` axes,
-including erasers and pressure-only motion. Cancel held input or active strokes
-on `YASL_EVENT_INPUT_RESET`, pointer leave, or focus loss. Reset also starts the
-next motion delta from zero. [hello.c](../examples/hello.c) shows the minimal
-lifecycle; [tablet.c](../examples/tablet.c) demonstrates device-aware drawing.
-
-For applications managing their own event loop, use the core API below.
-
-Open the display with application-wide options and notifications in
-`yasl_display_spec`. Create each toplevel with `yasl_window_spec`, including its
-title, `size_logical`, optional `yasl_window_cbs` table in `cbs`, and userdata in
-`ud`. One display can own multiple windows with separate callback tables.
-
-The window copies its callbacks during creation. Its callbacks cannot run before
-creation returns, so the application can store the returned surface first.
-Userdata stays borrowed until callbacks are replaced or the surface is destroyed.
-Read initial geometry with `yasl_surface_geometry()`; creation does not guarantee
-an initial `configure` notification. Request the first redraw once application
-state is ready. `yasl_surface_set_callbacks()` replaces callbacks later and also
-installs popup callbacks; the setter does not invoke the callbacks.
-
-When using `yasl_run()`, its runner installs callbacks for windows on its display.
-Use `yasl_app_desc.event` and `.frame` rather than `yasl_window_spec.cbs` and `.ud`.
-Replacing callbacks on a runner-owned surface fails with `EBUSY`.
-With a manual loop, install just the draw callback and drain the display FIFO
-after dispatch; lifecycle and input share the same event handler logic.
-
-Read window state with `yasl_surface_state()`. Its `YASL_STATE_*` flags can
-coexist, and the `state` callback receives the same current bitmask. Wayland
-also reports `YASL_STATE_TILED` when any window edge is tiled; X11 and Cocoa
-currently leave that flag unset. Unsupported flags are absent. On Wayland, an
-ordinary floating toplevel has fullscreen, maximized, and tiled all absent.
-`YASL_STATE_SUSPENDED` describes repaint suspension rather than minimization;
-there is no portable minimized flag.
-
-## Geometry and scaling
-
-| Value | Units |
-| --- | --- |
-| Toplevel `yasl_window_spec.size` | Logical pixels |
-| `yasl_geometry.size`, framebuffer size | Device pixels |
-| Pointer position, caret, window-menu position | Device pixels, relative to the surface |
-| Damage rectangles | Device pixels, relative to the draw target |
-| All popup fields, including `size` | Device pixels; anchor is relative to the parent |
-| Drag image size, hotspot, carry/place offsets | Device pixels |
-| `scale_120` | 120 = 1x, 180 = 1.5x, 240 = 2x |
-
-The origin is the top left, x increases right, and y increases down. Use
-`yasl_surface_geometry()` for the current size and scale, and update cached
-layout on `configure`. The native system may choose a different size from the
-request. X11 currently reports unit scale; automatic output DPI scaling is not
-implemented there.
-
-```c
-struct yasl_geometry geometry = yasl_surface_geometry(surface);
-int padding_px = yasl_scale_to_device(8.0, geometry.scale_120);
-int logical_width = yasl_scale_to_logical(geometry.size.x, geometry.scale_120);
-```
-
-Scale conversion rounds to the nearest integer, with ties away from zero,
-saturates out-of-range finite results, and returns zero for a zero scale.
-`yasl_scale_to_device()` also returns zero for non-finite logical coordinates.
-
-## Drawing and pacing
-
-`yasl_surface_request_frame()` coalesces requests. `yasl_display_flush()`
-services them when the backend can draw. During a native resize, dispatch can
-also draw synchronously. Avoid assuming callbacks occur only in flush.
-
-The CPU target is tightly packed premultiplied `0xAARRGGBB`. Row stride is
-`target.size.x * sizeof(uint32_t)`. For example, opaque red is `0xffff0000`,
-50% transparent red is approximately `0x80800000`, and transparent black is
-`0x00000000`. The numeric format describes a 32-bit value, not a portable byte
-sequence. Keep your application's durable content separately: a new target may
-contain no previous frame contents.
-
-```c
-static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud)
-{
- (void)ud;
- size_t count = (size_t)frame->target.size.x * frame->target.size.y;
- for (size_t i = 0; i < count; i++)
- frame->target.pixels[i] = 0xff306090;
- if (!yasl_frame_present(frame, NULL, 0)) {
- /* Arrange application error handling after returning. */
- }
-}
-```
-
-Present the exact active frame at most once during its callback. Do not mutate
-the target identity or retain frame/pixel pointers after return. NULL rectangles
-with count zero mean full damage; a non-NULL array with zero count means no damage.
-All supplied extents must be nonnegative. Rectangles are clipped, and more than
-`YASL_DAMAGE_MAX` rectangles fall back to full damage after all are validated.
-Damage may reduce uploads, but every target must contain complete valid pixels.
-Native rendering uses the same frame, with a drawable queried in the extension.
-
-`yasl_surface_set_pacing()` works in either loop style. ON_DEMAND coalesces
-explicit requests and sleeps when idle. CONTINUOUS uses native display signals
-where available and a refresh-based timer otherwise, with about 60 Hz as a
-fallback estimate. It reports allocation/argument failures instead of silently
-ignoring surfaces outside the runner. Suspension can suppress actual drawing.
-Legacy `yasl_surface_present()` and `yasl_surface_schedule_redraw()` remain
-migration adapters; new code uses the frame presentation and request functions.
-
-`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
-`YASL_TIMING_PRESENTATION` confirms compositor presentation. `UNKNOWN` means no
-sample. `refresh_ns` and `next_present_ns` remain estimates. Use
-`yasl_surface_frame_time_ns(surface, yasl_loop_now_ns())` as a stable animation
-clock; it does not establish actual scanout time.
-
-## Input, text, and payload lifetime
-
-Dispatch receives native input; `yasl_display_poll_event()` drains the display
-FIFO without waiting. Drain it once for the whole display, including multiple
-windows and popup input. `event.surface` identifies the live recipient and
-`surface_id` is its stable identity. Destruction cancels undelivered recipient
-events. `yasl_surface_poll_event()` remains an input-only migration adapter and
-discards pending non-input notifications, which legacy callbacks already
-delivered. Do not mix the two polling styles.
-
-Read the union member selected by `event.type`. `time_ns` is monotonic and
-`sequence` orders capture across the display. TEXT and DROP use `.text`, PASTE
-uses `.paste`, and COMPOSITION uses `.composition`. Each string is copied when
-queued, NUL-terminated, and borrowed until the next successful display/surface
-poll or display close. Dispatch, unsuccessful polls, and recipient destruction
-do not invalidate the last polled string. Copy it to retain it longer.
-
-Use `YASL_EVENT_TEXT` for document text insertion, including committed IME text.
-It contains UTF-8 and can contain several characters. Key actions are for
-commands and held-key state; releases and repeats have separate tags. A text
-commit need not correspond to one key event, and a key need not produce text.
-
-`key.key` is a portable physical identity from
-[keycodes.h](../include/yasl/keycodes.h), included by `<yasl/yasl.h>`. Names follow
-US keyboard positions: `YASL_KEY_A` identifies the same position on a French
-layout even though it types Q. Modifiers do not change this identity. Keypad
-keys and left/right modifiers have distinct names. Unknown native keys report
-`YASL_KEY_UNKNOWN`; `native_code` preserves their backend-specific identity
-(Wayland evdev code, X11 server keycode, or Cocoa virtual keycode).
-
-`codepoint` and `shifted` are optional Unicode scalar labels for the active
-layout without modifiers and with Shift. Zero means no single printable label,
-including function keys, unavailable translations, and mappings that produce
-multiple characters. Dead keys may have a spacing label or zero. These fields
-are useful for displaying shortcuts or
-choosing layout-sensitive commands; they are never document text. For example:
-
-```c
-if (event.type == YASL_EVENT_KEY && event.key.action == YASL_KEY_PRESS &&
- event.key.key == YASL_KEY_ESCAPE) {
- running = false;
-}
-/* Insert event.text.data[0..event.text.len) only for YASL_EVENT_TEXT. */
-```
-
-Wayland uses the supplied XKB layout and locale compose rules for ordinary text
-and dead keys, with text-input-v3 for IME commits. Compose rules use `LC_ALL`,
-`LC_CTYPE`, then `LANG`, falling back to `C.UTF-8`. X11 uses the X input method
-when available; Cocoa uses AppKit text services. X11 input methods use the
-application's `LC_CTYPE` locale. Initialize it before opening a display when
-using locale-specific XIM, for example with `setlocale(LC_CTYPE, "")`; YASL does
-not change the process locale. IME availability and system configuration still
-determine which compositions work. Physical key events
-remain available for commands; input methods may consume keys while composing.
-
-Pointer events use the `YASL_EVENT_POINTER_*` types and share `event.pointer`,
-with `device` distinguishing mouse, touch, pen, and eraser. `POINTER_DOWN` and
-`POINTER_UP` carry a `YASL_BUTTON_*` value: `PRIMARY` is the left button, pen tip,
-or touch contact. Other types leave `button` meaningless. Every pointer event
-also carries `buttons`, a `YASL_BUTTON_BIT()` mask of the buttons held after
-that event, tracked from the queue's down and up events. Use it to tell a drag
-from a hover. Count clicks against `yasl_display_double_click_ms()`, the
-system interval: XSETTINGS on X11, the KDE or GNOME setting through the
-Settings portal on Wayland, and the system value on Cocoa. It falls back to
-`YASL_DOUBLE_CLICK_MS_DEFAULT` (400 ms). The query can wait briefly on Linux, so
-read it when a window gains focus. `YASL_EVENT_INPUT_RESET` clears the mask. On Wayland,
-`POINTER_LEAVE` also clears it, because the compositor sends leave during a
-press only when it takes the pointer away. X11 and Cocoa keep the press across
-leave and still deliver the release. `POINTER_MOTION` reports `delta`, the device-pixel movement since
-the previous sample, which restarts from zero after enter, leave, or reset.
-`yasl_surface_set_pointer_mode()` with `YASL_POINTER_RELATIVE` hides and locks the
-cursor while the surface is focused; motion then carries unbounded `delta` and
-`pos` stays at the lock position. Wayland implements it with the
-relative-pointer and pointer-constraints protocols, reporting the compositor's
-accelerated motion in device pixels. The compositor decides when the lock
-engages; until then, and while it is released on focus loss, motion stays
-absolute with a computed `delta`. X11, Cocoa, and compositors without those
-protocols return false and keep the current mode. Pen
-pressure is normalized to 0..65535, signed tilt is in degrees, and missing axes
-are zero. Pressure-only changes can produce motion. Zero pressure alone does
-not mean release. This interface does not expose simultaneous touch contacts.
-
-For `YASL_EVENT_POINTER_SCROLL`, read `pointer.scroll.x` and `.y`: fractional line offsets,
-positive right and down, after system scroll preferences. Both axes may be
-nonzero in one event. Preserve fractions and choose your application's own
-pixels per line; one wheel detent normally gives one line. `button` is unused
-for scroll. Wayland high-resolution wheel values take precedence over duplicate
-discrete/raw descriptions. Smooth Wayland offsets and precise Cocoa offsets
-use ten native units per line. X11 currently reports whole wheel steps, so it
-does not provide the same smooth-scroll precision as the other backends.
-
-Cancel held keys, buttons, active strokes, and composition on `YASL_EVENT_INPUT_RESET`,
-focus loss, or pointer leave as appropriate to your application. RESET has no
-union payload. It is emitted after input-queue allocation failure and certain
-device interruptions; ignoring it can leave application input stuck.
-
-## Clipboard, file drops, and IME
-
-`yasl_clipboard_set(display, selection, text)` copies a NUL-terminated UTF-8
-offer and reports failure through its return and errno. Wayland may require a
-recent input serial. `yasl_clipboard_request(surface, selection)` returns a
-nonzero request ID, or zero with errno when rejected. Each accepted request
-produces one `YASL_EVENT_PASTE` carrying `.paste.request_id`, `.status`, `.data`,
-and `.len`, unless its target/display is destroyed. OK includes empty text;
-UNAVAILABLE means no text offer, and FAILED means transfer/decoding failure.
-External Linux transfers have a 30-second timeout. X11 does not implement INCR
-and reports unsupported large transfers as FAILED. Legacy STRING uses Latin-1
-conversion. Malformed UTF-8 completes as failure.
-
-Unsupported primary selection reports ENOTSUP and never aliases the clipboard.
-Cocoa uses a YASL named pasteboard for primary selection. Legacy copy/paste calls
-remain adapters; legacy paste completions carry request ID zero.
-
-External file drag hover uses `YASL_EVENT_DROP_HOVER`; actual file URLs
-arrive as `YASL_EVENT_DROP`. `text.data` is a `text/uri-list` string separated by
-CRLF, not a list of native paths. `yasl_drop_next_path()` walks it and decodes
-each local `file:` URL into a path. It skips comments, other schemes, remote
-hosts, and malformed lines:
-
-```c
-const char *cursor = event.text.data;
-char path[4096];
-size_t len;
-while ((len = yasl_drop_next_path(&cursor, path, sizeof(path))) != 0) {
- if (len >= sizeof(path)) break; /* Too long; grow the buffer instead. */
- /* Open path. */
-}
-```
-
-Decoded paths are raw bytes and may contain any character the file system
-allows, including newlines. Quote them before pasting into a shell.
-
-Handle `YASL_EVENT_COMPOSITION` for immutable UTF-8 preedit snapshots, byte
-selection offsets, and composition ending. Missing selection information uses
--1 offsets. Insert only committed `YASL_EVENT_TEXT` into the document. Set the
-candidate-window caret with `yasl_surface_set_ime_caret()` in device pixels.
-X11 uses XIM preedit callbacks when offered; fallback input methods may provide
-no inline composition. The old preedit getters remain migration helpers.
-
-## Popups and surface drags
-
-Create popups from a live parent, normally in response to input. All fields in
-`yasl_popup_spec` use device pixels. `size` includes transparent margins;
-`content_offset` and `content_size` describe the actual content rectangle used
-for positioning. A zero content-size axis extends to the corresponding size
-edge. `beside = true` prefers the anchor's right edge; false prefers below.
-Placement may flip or slide to stay on screen. Wayland requires a recent input
-serial to grab a popup, and older protocols may reject repositioning. Destroy
-children before parents. The close callback requests application destruction;
-it does not automatically free the popup.
-
-`YASL_EVENT_DRAG` handles a YASL surface drag; `YASL_EVENT_DROP_HOVER` handles
-external data hover. Legacy callbacks remain migration adapters. `yasl_display_drag_start()` is intended for window/tab tear-off
-workflows, not arbitrary MIME data sources. Keep transferred application state
-in your own registry. Start from a recent pointer press, and check the boolean
-result even when `YASL_CAP_DRAG` is present. Supply an optional copied image and
-completion listener. One active drag is supported per display. A successful
-start calls a non-NULL `end` exactly once, including explicit cancel or display
-close; a failed start never does. `DROPPED` does not prove that another
-application accepted your application object.
-
-Where `YASL_CAP_DRAG_CARRY` is present, `yasl_surface_drag_carry()` can attach a
-toplevel to the active drag. Otherwise an image can represent it. With
-`YASL_CAP_PLACE_WINDOW`, `yasl_surface_place_under_pointer()` requests placement
-relative to the pointer. These calls may fail due to current input/drag state.
-Completion can run synchronously during cancel/close; keep listener userdata
-alive until then and do not destroy the display recursively.
-
-## Timers and file descriptors
-
-Include `<yasl/loop.h>`. Use `yasl_display_loop(display)` for application timers
-so they participate in display dispatch. Intervals are nanoseconds; the
-`YASL_MS_TO_NS(ms)` macro converts a nonnegative integer millisecond value.
-
-```c
-static uint64_t tick(void *ud, yasl_timer_id id, uint64_t interval_ns)
-{
- (void)id;
- yasl_surface_schedule_redraw(ud);
- return interval_ns; /* Return zero for a one-shot timer. */
-}
-/* Check zero for failure. This timer is canceled with the surface. */
-yasl_timer_id timer = yasl_surface_add_timer(surface, YASL_MS_TO_NS(16), tick, surface);
-```
-
-A timer callback receives the scheduled interval, not actual elapsed time. A
-nonzero return repeats/reschedules; zero retires it. Missed periods coalesce.
-Timers can add/delete timers, including themselves. Loop timers end when retired,
-deleted, or the loop is destroyed; surface timers also end on surface destruction.
-Userdata remains application-owned. Delete surface timers with
-`yasl_surface_del_timer()` and ordinary timers with `yasl_loop_del_timer()`.
-
-File watches use `<poll.h>` masks. `yasl_loop_add_fd()` returns a positive watch
-ID or -1. Its callback receives `revents`, including error/hangup flags; its
-integer return is ignored. Consume readiness or disable/delete the watch.
-The loop never closes application file descriptors: delete the watch before
-closing its descriptor. Ready callbacks may add or remove watches. Do not call
-`yasl_loop_step()` or `yasl_loop_run()` recursively.
-
-Every backend's dispatch wakes when a loop descriptor becomes ready. Cocoa
-waits for native events and mirrors the loop's descriptors in a kqueue while it
-waits. If kqueue rejects a descriptor, Cocoa bounds the wait to 10 ms instead.
-
-Loop wake and post calls work from any thread. `yasl_loop_kick()` only wakes the loop, and it
-is also safe in a signal handler. `yasl_loop_post()` queues a function to run
-on the owning thread during a later step or dispatch. Use it to hand results
-from worker threads to the loop:
-
-```c
-static void results_ready(void *ud)
-{
- worker_drain(ud); /* Runs on the loop thread. */
-}
-/* On the worker thread; false means allocation failed. */
-yasl_loop_post(loop, results_ready, worker);
-```
-
-`yasl_loop_watch_signals()` delivers POSIX signals to a callback on the loop
-thread. It installs a process-wide handler for each signal, with `SA_RESTART`,
-and restores the previous action on unwatch. YASL leaves thread signal masks
-alone: a watched signal must stay unblocked in at least one thread. Only one
-loop can watch a given signal at a time.
-
-## Worker messages and window requests
-
-Capture `yasl_surface_id(surface)` on the display thread and pass that value to
-workers. `yasl_display_post(display, &message)` accepts an identity instead of a
-borrowed surface pointer. Zero targets the display. On success, optional
-`message.release(payload)` runs exactly once on the display thread after delivery
-or cancellation, including shutdown. It may post messages but must not poll,
-dispatch, or destroy YASL objects. Failed posting leaves ownership with the caller.
-Stop and join producers before closing the display. The same API works with a
-manual loop and with the runner; `yasl_post_event()` is a legacy adapter.
-
-Toplevel requests `yasl_surface_set_size(surface, logical_size)`,
-`yasl_surface_set_fullscreen`, `yasl_surface_set_maximized`, and
-`yasl_surface_minimize` return acceptance with errno on failure. Confirm actual
-geometry/state through queued events. Popup targets are invalid. Wayland cannot
-request an arbitrary live size and returns ENOTSUP for that operation; fullscreen,
-maximize, and minimize use xdg-shell requests. X11 and Cocoa implement all four.
-
-## Desktop services
-
-Include `<yasl/desktop.h>` for services that sit next to windowing. Display and
-surface calls use the display thread; path and locale calls work from any
-thread.
-
-| Call | Linux | Cocoa |
-| --- | --- | --- |
-| `yasl_display_open_uri()` | `xdg-open` from `PATH` | NSWorkspace |
-| `yasl_display_notify()` | Notifications service over D-Bus | User notifications; app bundle required |
-| `yasl_display_appearance()` | Settings portal over D-Bus, then `GTK_THEME` | Effective appearance and increase-contrast setting |
-| `yasl_surface_bell()` | Wayland xdg-system-bell; X11 XKB bell | `NSBeep` |
-| `yasl_executable_path()` | `/proc/self/exe` | Executable path, resolved |
-| `yasl_resource_dir()` | Always zero | App bundle resources |
-| `yasl_default_locale()` | `LC_ALL`, `LC_CTYPE`, `LANG`, then `C.UTF-8` | Current language and region |
-
-`yasl_display_open_uri()` checks only that the URI has a scheme. Decide which
-schemes and hosts your application trusts before calling it, for example
-before opening a link from untrusted text. On Linux the opener starts in its
-own session with default signal handling and none of the application's
-descriptors above stderr. D-Bus services need YASL built with `dbus-1`; without
-it, notifications return false and appearance falls back to `GTK_THEME`.
-Appearance changes send no notification, so query again when a window gains
-focus.
-
-Functions that fill a buffer return the full length, excluding the NUL. A
-result less than the buffer size means the string was written. Otherwise the
-buffer is unchanged, so call again with a larger one:
-
-```c
-char path[4096];
-size_t len = yasl_executable_path(path, sizeof(path));
-if (len == 0 || len >= sizeof(path)) {
- /* No path, or a longer buffer is needed. */
-}
-```
-
-## Native interoperability
-
-Portable CPU drawing needs no native headers. Opt into `<yasl/native.h>` and
-`<yasl/native/wayland.h>`, `<yasl/native/x11.h>`, or `<yasl/native/cocoa.h>` when
-integrating a platform API. Typed native functions are exported only for
-built backends. Accessors return borrowed handles and zero/NULL for the wrong
-backend. Do not pass freed objects, destroy borrowed handles, independently
-dispatch native connections, or change surface roles and buffer ownership.
-
-Application-managed native drawing currently supports Cocoa/Metal. Use
-`yasl_surface_create_native()` with a non-NULL Metal device, which YASL retains.
-During draw, query `yasl_surface_get_native_frame()` for a borrowed drawable,
-encode and present with your renderer, then call `yasl_frame_present()` to
-record submission. The drawable expires on successful present, callback return,
-or surface destruction. Do not infer GPU completion from the boolean result.
-Other backends return NULL with `errno = ENOTSUP` for native creation; having a
-native window handle does not imply this rendering path is supported.
+# Surface and input guide
+
+## Lifetime, threads, and errors
+
+The first successful `yasl_surface_create` or `yasl_gl_surface_create` initializes
+shared native state. Subsequent surfaces share one backend. Optional `yasl_set_app_id`
+and `yasl_set_backend` configure the next initialization; they fail after initialization.
+Destroying the final surface preserves shared state. `yasl_shutdown` destroys remaining
+surfaces, releases the backend, and restores defaults. Release application-owned GPU
+objects before their surface or shutdown. Parent destruction destroys popup descendants.
+
+Creation, window operations, event pumping, and shutdown use the first creation's
+thread, which must be the main thread on Cocoa. GL context binding/drawing/swapping
+may use a rendering thread; detach its context before destroying the window. The
+application synchronizes renderer state/geometry and joins workers before shutdown.
+`yasl_wake` is thread-safe and does nothing before initialization or after shutdown.
+
+Constructors return NULL on failure; fallible mutations return bool. Event reads
+return 1 for an event, 0 for empty/timeout/wake, -1 for failure. `yasl_get_error`
+returns a stable per-thread error and static description; `yasl_get_native_error`
+returns supplementary native diagnostics. Success does not clear a preceding error.
+Valid-object size/scale/userdata getters return ordinary values. Destroyed pointers
+are invalid. Strings passed to creation and setters are copied; userdata is borrowed.
+
+## Events and coordinates
+
+`yasl_poll_event` and `yasl_wait_event` process native events and consume one shared
+FIFO. No manual dispatch/flush cycle. A failed/empty read leaves output unchanged.
+Wait timeout zero polls, negative waits for work, positive milliseconds bound waiting.
+A wake can return zero. Polling before initialization returns zero; waiting reports
+NOT_INITIALIZED. Close and quit are requests; the application decides how to respond.
+Display loss is a terminal native failure; destroy resources and shut down.
+
+Read the union member selected by type. Events own their text through
+`yasl_event_release`, independently of later polls and source destruction. Release
+every successful event exactly once; numeric events need no allocation to release.
+Do not duplicate event ownership by shallow copying. Event surface pointers remain
+borrowed and become invalid when their surface is destroyed. Undelivered events for
+a destroyed surface are canceled. Sequence orders capture by YASL, not OS causality.
+Timestamps use monotonic nanoseconds; native_time states whether normalized native
+sample timing was available or capture time was used.
+
+Creation dimensions, pointer/pen coordinates, popup placement, caret, and CSD actions
+use logical content units. Fractional positions use doubles. Image dimensions,
+byte stride, framebuffer size, and damage use device pixels. Query actual framebuffer
+extent for swapchain/viewport sizing; scale alone need not determine integer rounding.
+Request functions report acceptance, while geometry/state events report actual results.
+There is no portable absolute window-position promise. State's known mask distinguishes
+reported false from unavailable state fields.
+
+## Rendering
+
+GPU creation (flags zero) does not create a device, context, or software framebuffer.
+One presenter attaches to each surface. Incompatible second attachments fail.
+
+- Vulkan: inject vkGetInstanceProcAddr, query instance extensions and flags through
+ the live GPU surface, create the instance, then `yasl_vulkan_create_surface`.
+ The application owns VkInstance/VkDevice/queues/swapchains/VkSurfaceKHR, handles
+ image acquisition, synchronization and recreation, and presents with Vulkan.
+- Metal: `yasl_metal_layer` returns a borrowed CAMetalLayer on Cocoa. YASL owns
+ the native view/layer and updates its dimensions. The app sets device/pixel format,
+ obtains drawables, and submits/presents commands. No additional YASL present call.
+- GL: `yasl_gl_surface_create` accepts title, dimensions, flags, a per-call int
+ key/value attribute list terminated by YASL_GL_END, and an optional sharing surface.
+ NULL attributes default to GL3.2 core, RGBA8, depth24/stencil8. The surface owns
+ its context. Bind, load procedures, set swap interval, and swap explicitly.
+ GL API support depends on the native driver; Cocoa rejects OpenGL ES/debug contexts
+ and supports its native legacy/core profiles.
+- CPU: create with YASL_SURFACE_CPU and call `yasl_cpu_present` with premultiplied
+ numeric 0xAARRGGBB, width/height, byte stride, and optional damage rows containing
+ x/y/width/height. Extent must match current framebuffer. NULL/zero damage means full;
+ explicit lists must be nonempty. Initial submission and changed extent require full
+ images. Later patches update the retained submitted image. Source bytes are copied
+ before return, so the app may reuse/free them. Native uploads may copy the entire
+ retained image, but never unsubmitted source changes. `yasl_cpu_read_pixels` reads
+ that submitted CPU image, not the composited screen.
+
+GPU creation/presentation uses native requests directly; YASL acknowledges Wayland
+configuration before external GPU submission. Native refresh events can be handled
+in the ordinary loop. An optional refresh callback permits drawing while the native
+window system remains in a modal resize path. It runs only during pumping and never
+recursively; it may query geometry and render, but must not pump, create/destroy
+surfaces, or shut down. Geometry events may not yet have been consumed, so query
+current geometry there. Without a hook, CPU exposes reuse committed pixels and GPU
+redraw waits for the application's loop to resume.
+
+## Keyboard, pen, and scrolling
+
+Physical YASL_KEY names identify US keyboard positions independent of layout.
+Layout labels support shortcuts; committed UTF-8 TEXT is for document insertion.
+Keys and text need not correspond one-to-one. Input RESET/focus loss cancels held
+application input. Keep pointer and pen streams separate to avoid duplicate strokes.
+
+PEN events carry connection-local device/tool IDs, tool type, proximity/contact,
+button transitions, coherent position and axes. Pressure is 0..1; zero does not end
+contact. Tilt uses degrees -90..90; rotation uses clockwise degrees; distance is
+normalized, not a physical unit. Axis validity distinguishes unavailable from zero.
+Changed masks indicate native updates. Tools/axes depend on hardware and backend;
+IDs do not guarantee persistent hardware serials. Pen-generated mouse duplicates
+are suppressed where recognized; do not treat tool class as a multi-contact API.
+
+SCROLL preserves wheel-line and precise-pixel values with validity, source, phase,
+and momentum metadata where supplied. Applications choose layout pixels per wheel
+line. X11 supplies native wheel/smooth valuator data where available. Missing native
+metadata stays explicitly unavailable instead of being inferred.
+
+## Text input, clipboard, popups, and drags
+
+Activate text input for a text control with `yasl_surface_set_text_input`. Set purpose,
+caret, and copied UTF-8 surrounding text/selection/revision through direct setters.
+Snapshot offsets and TEXT_EDIT replacement ranges are UTF-8 bytes; COMPOSITION is
+uncommitted preedit. Apply a matching-revision edit once, update your snapshot, and
+ignore/rebase stale edits. Activation generation distinguishes controls. Ordinary key
+text remains TEXT. Never insert preedit as committed document text. Wayland requires
+text-input-v3 for native IME; absence is reported as unsupported. Cocoa translates
+native UTF-16 replacement/deletion to snapshot UTF-8 ranges. X11 XIM supplies ordinary
+text/preedit but cannot promise surrounding-text replacement support.
+
+Clipboard set copies UTF-8; read returns a nonzero request ID and later CLIPBOARD
+completion with OK, UNAVAILABLE, or FAILED. Empty successful text is distinct from
+failure. Destroying the target cancels transfer and delivery. Primary selection stays
+independent and reports unsupported where unavailable. Linux external transfers time
+out after 30 seconds; X11 currently lacks INCR for large transfers. File drops carry
+URI-list text; `yasl_drop_next_path` decodes supported local paths into caller buffers.
+
+Popup arguments use logical parent anchor coordinates, anchor/gravity directions,
+and flip/slide/resize constraint bits. Popup size includes its content. Destroying a
+parent retires descendants too. Native grabs, CSD moves/resizes, menus, and surface
+drags require a live triggering press event; unsupported/expired input fails explicitly.
+Surface drags support application tab/window carry; they are not an arbitrary MIME
+source API. Check operation results even when feature bits advertise support.
+
+## Backend limits
+
+Wayland support varies by compositor protocols; no portable absolute placement or
+minimized-state query is assumed. X11 currently reports unit scale and lacks an
+implemented relative-pointer lock. Cocoa requires macOS 14+; native Metal/GL and
+Objective-C paths require validation on macOS. EGL/wayland-egl and Vulkan SDK support
+are independently optional at build time. Feature bits describe implementation/
+server support, not guaranteed permission or attached hardware. The current work was
+compiled and tested on Linux; macOS validation remains a separate required check.
diff --git a/docs/migrating.md b/docs/migrating.md
deleted file mode 100644
index 912ea4d..0000000
--- a/docs/migrating.md
+++ /dev/null
@@ -1,132 +0,0 @@
-# Migrating pre-release applications
-
-YASL 0.1 has no stable source or binary compatibility promise. Rebuild all
-consumers against matching public headers and the library after updating.
-See the [application guide](guide.md) for the full window and input workflow.
-
-## Display queue, frames, and application failures
-
-The runner now consumes the display-wide FIFO directly. Manual loops should
-replace per-surface polling with `yasl_display_poll_event(display, &event)`.
-Surface polling remains an input-only adapter that discards pending non-input
-notifications delivered through legacy callbacks. Do not mix polling styles. Strings from either poll
-now survive recipient destruction and expire at the next successful poll on
-that display, or display close. Queue allocation failure is reported by dispatch
-as an error instead of silently losing a lifecycle notification.
-
-Replace `struct yasl_frame_context` with `struct yasl_frame`, `frame->cpu.pixels`
-with `frame->target.pixels`, and `frame->size` with `frame->target.size`.
-`target.stride_bytes` replaces `cpu.stride_bytes`. Native drawing uses
-`yasl_surface_get_native_frame(surface, &frame->target, &native)`.
-`yasl_frame_present(frame, NULL, 0)` replaces the CPU presentation wrapper.
-A non-NULL damage array with count zero now explicitly means no damaged pixels.
-Lists larger than the backend limit become full damage instead of truncating.
-
-`YASL_APP_ERROR` and `yasl_app_exit(display, result)` report application failures
-through `yasl_run`. `yasl_app_window(display)` gives init access to desc.window.
-Use `yasl_surface_set_userdata` / `yasl_surface_userdata` for per-window state,
-or `window.userdata` during creation. `yasl_surface_set_callbacks` now returns a
-boolean and rejects runner-owned surfaces with EBUSY.
-
-Spell toplevel creation sizes `size_logical`; `size` remains an alias.
-New window request functions report acceptance with errno. Pacing now works
-outside the runner too, and returns a boolean instead of silently ignoring calls.
-
-Replace clipboard copy/paste with `yasl_clipboard_set` / `yasl_clipboard_request`.
-Check the returned request ID and `event.paste.status` on completion. Empty text
-is successful; missing data and transfer errors now have explicit outcomes.
-Preedit snapshots arrive through `YASL_EVENT_COMPOSITION`; state and hover use
-`YASL_EVENT_WINDOW_STATE`, `YASL_EVENT_DRAG`, and `YASL_EVENT_DROP_HOVER`.
-
-For worker messages, capture a surface ID on the display thread and use
-`yasl_display_post` with an optional release callback. Do not also free the
-payload in the event handler when release owns cleanup. Stop producer threads
-before display shutdown; retired IDs reject posting with ENOENT.
-
-## Application callbacks
-
-All examples now use `<yasl/app.h>` and `yasl_run()`. Describe a single window
-in `yasl_app_desc.window`, or create windows in `.init`; move input handling to
-`.event` and drawing to `.frame`.
-Replace manual dispatch/poll/flush loops with the runner and request redraws with
-`yasl_surface_request_frame()`. Present the frame context with
-`yasl_frame_present()`; the runner destroys remaining surfaces on exit.
-
-Window state remains a `YASL_STATE_*` bitmask; `YASL_STATE_TILED` is reported
-for tiled Wayland toplevels.
-
-## Events
-
-The core API and the runner now share one `struct yasl_event`, declared in
-`<yasl/yasl.h>`. `yasl_display_poll_event()` returns the same events that
-`yasl_app_desc.event` receives. Legacy surface polling returns only input types.
-
-| Previous use | Replacement |
-| --- | --- |
-| `yasl_surface_input_poll()`, `struct yasl_input_event` | `yasl_surface_poll_event()`, `struct yasl_event` |
-| `event.kind`, `YASL_INPUT_*` | `event.type`, `YASL_EVENT_*` |
-| `YASL_INPUT_MOUSE` with `mouse.kind` | `YASL_EVENT_POINTER_DOWN`, `_UP`, `_MOTION`, `_SCROLL`, `_LEAVE` with `event.pointer` |
-| Runner `YASL_EVENT_MOUSE_BUTTON` with `pressed` | `YASL_EVENT_POINTER_DOWN` / `_UP` |
-| Integer `button` 0/1/2 | `YASL_BUTTON_PRIMARY`, `_MIDDLE`, `_SECONDARY`, `_BACK`, `_FORWARD` |
-| `paste.text`, `drop_files.files`, `text.text` | `text.data` for `PASTE`, `DROP`, and `TEXT` |
-| `YASL_INPUT_DROP_FILES` / `YASL_EVENT_DROP_FILES` | `YASL_EVENT_DROP` |
-| `YASL_EVENT_WINDOW_FOCUS` | `YASL_EVENT_FOCUS` |
-| `YASL_EVENT_WINDOW_SCALE_CHANGED` | `YASL_EVENT_WINDOW_RESIZE` with `resize.scale_changed` |
-| `YASL_EVENT_WINDOW_CLOSE_REQUEST` | `YASL_EVENT_WINDOW_CLOSE` |
-| `user.type` | `user.code` |
-| `key.repeat` | `key.action == YASL_KEY_REPEAT` |
-| `yasl_surface_set_mouse_mode()` | `yasl_surface_set_pointer_mode()` |
-
-Every event carries a monotonic `time_ns`. Pointer motion carries `delta` on
-both paths. Handle `YASL_EVENT_INPUT_RESET` by clearing held input and active
-strokes.
-
-## Window creation
-
-Rename `struct yasl_surface_spec` to `struct yasl_window_spec` and
-`struct yasl_surface_cbs` to `struct yasl_window_cbs`. Surface handles and
-functions retain their names. Supply the window's callback table in `spec.cbs`
-and its userdata in `spec.ud` when calling `yasl_surface_create_toplevel()` or
-`yasl_surface_create_native()`. Callback tables are copied, and userdata is
-borrowed. The new window's callbacks cannot run before creation returns; read
-initial geometry with `yasl_surface_geometry()` and request the first redraw
-after initializing application state.
-
-`yasl_surface_set_callbacks()` remains available to replace callbacks after
-creation and to configure popups. A NULL `spec.cbs` creates a window without
-callbacks. Displays owned by `yasl_run()` use the runner's callbacks instead;
-provide application notifications through `yasl_app_desc.event` and `.frame`.
-
-## Keyboard events
-
-| Previous use | Replacement |
-| --- | --- |
-| Linux keysym or Cocoa virtual keycode in `key.key` | Portable physical `YASL_KEY_*` identity |
-| `key.base` | `key.native_code`, with explicit backend-specific encoding |
-| `key.keypad` | Distinct `YASL_KEY_KP_*` identities |
-| Advisory `key.text` character | `YASL_EVENT_TEXT` UTF-8 payload for insertion |
-| Layout label from a keysym | Optional `key.codepoint` and `key.shifted` Unicode scalars |
-
-Replace magic numbers and X11 symbols with named keys. For example, a quit
-command checks `key.key == YASL_KEY_ESCAPE` on a press. `YASL_KEY_A` means the
-US A position on every layout; use layout labels when a command should follow
-the printed letter instead. Do not cast key identities to characters. Keypad
-Enter and ordinary Enter now have distinct identities without a separate flag.
-
-Text comes only from `YASL_EVENT_TEXT`. Handle its byte length, copy it when
-retaining it after the next successful poll, and allow multiple characters per
-commit. Cocoa sends key events through its text input manager; Wayland emits
-ordinary XKB text and locale compose results in addition to IME commits.
-
-## Scroll events
-
-Replace `scroll_lines`, `scroll_delta`, and scroll button numbers with
-`pointer.scroll.x` and `.y` for `YASL_EVENT_POINTER_SCROLL`. These double
-values are fractional line offsets, positive right/down on every backend.
-Do not infer scroll direction from `button`. One event may include both axes.
-Remove backend-specific sign inversions and whole-line casts; multiply the
-offset by your application's preferred pixels per line instead.
-
-X11 still supplies whole wheel steps. Wayland high-resolution wheels and smooth
-offsets, and Cocoa precise scrolling, preserve fractions. Device acceleration
-and system preferences still affect the native offsets.
diff --git a/docs/testing.md b/docs/testing.md
index bd3f2f0..ce2082f 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -1,156 +1,36 @@
-# Testing and examples
-
-Configure isolated Linux display-server integration tests with Weston and
-Xwayland installed:
-
-```sh
-meson setup build-test -Dintegration_tests=true
-meson test -C build-test --print-errorlogs
-```
-
-The harness starts and cleans up its own servers. It checks explicit requests,
-automatic selection, fallback, both backends in one process, frame lifetimes,
-callback copying, clipboard ownership, and timers. It also runs the beginner
-and tablet examples on both backends. On macOS the option enables native window,
-input ownership, keyboard/text/scroll, cursor, tablet, and example tests; these
-require a desktop session. Linux results do not verify Cocoa behavior.
-
-For memory/undefined-behavior checks use a separate build:
-
-```sh
-meson setup build-sanitize -Db_sanitize=address,undefined -Dintegration_tests=true
-meson test -C build-sanitize --print-errorlogs
-```
-
-Default tests need no desktop connection. C++ header coverage is built when a
-C++ compiler is available. On Linux and macOS, the shared-export test ensures
-only public API declarations are exported. Test disabled-backend configurations separately with
-`-Dwayland=disabled` or `-Dx11=disabled`; the combined integration harness needs
-both backends.
-
-Keyboard tests check physical identities, distinct keypad/modifier keys, layout
-labels, Shift/Caps Lock, shortcut text suppression, AltGr, dead-key composition,
-repeat, and composition reset. The X11 integration test changes and restores
-the isolated server's keymap to verify that labels change while physical keys
-retain their identity. The X11 text fallback is also checked without an input
-method, including Latin-1, Cyrillic, and non-BMP characters encoded as UTF-8.
-Scroll tests cover both axes, fractional Wayland offsets,
-duplicate protocol representations, and X11 core/XInput wheel directions.
-Cocoa integration tests cover UTF-16 labels, modifier-only events, native text
-commit routing, and diagonal precise scrolling. Native IME interaction and
-hardware gestures still need testing on the target desktop.
-Clipboard tests cover Latin-1 interoperability, empty transfers, and rejection
-of malformed UTF-8 on Linux.
-
-## Cocoa validation
-
-On macOS 14 or newer, use a logged-in desktop session with a Metal-capable
-device. Configure `-Dintegration_tests=true`, compile, and run the full suite.
-Cross-compiling with an Apple SDK checks compilation and linking; run
-`meson test -C build-macos public-exports` to check the dylib's exports on the
-build machine. A cross file should provide an `nm` that can read Mach-O, such
-as `llvm-nm`. These checks do not execute the native window and input tests.
-
-To move an already compiled, unsanitized Cocoa suite to another Mac without
-rebuilding it:
-
-```sh
-python3 tests/helpers/package_cocoa_tests.py build-macos cocoa-tests.tar.gz
-```
-
-On the matching Mac, extract the archive and run:
-
-```sh
-tar -xzf cocoa-tests.tar.gz
-python3 yasl-cocoa-tests/run.py
-```
-
-The bundle includes the dylib, every executable test registered by Meson, and
-the license. The build-machine export check is separate. The runner checks file
-hashes, enforces each test's timeout, saves output under `logs/`, and writes
-`results.json`. A nonzero exit means validation failed. `--list` lists the
-bundled tests without running them. Use Python 3.8 or newer and a bundle matching
-its architecture; Apple Silicon uses `aarch64`, Intel uses `x86_64`.
-
-## Tablet drawing example
-
-Build and run the same C application on Wayland, X11, or Cocoa:
+# Tests
```sh
meson setup build
meson compile -C build
-./build/yasl-tablet
-./build/yasl-tablet --backend wayland
-./build/yasl-tablet --backend x11
-# On macOS:
-./build/yasl-tablet --backend cocoa
+meson test -C build --print-errorlogs
```
-Draw with the pen tip: pressure controls brush width. Flip to the eraser to
-remove ink. The window shows the selected backend, tool, contact state, raw
-pressure (0..65535), percentage, and tilt in degrees, with a live pressure bar,
-brush outline, and tilt vector. The system cursor is hidden so the brush overlay
-serves as the drawing cursor. Mouse drawing uses a fixed width. Press C or
-Space to clear and Escape to quit. Resizing and display scale changes preserve
-the drawing. `--smoke-test` presents one frame and exits for automated checks.
-
-[`examples/tablet.c`](../examples/tablet.c) includes `<yasl/app.h>` and the C
-standard library. It uses `yasl_run()`, frame callbacks, and unified input events
-on every backend. Tablet button and motion events retain `device`, which is
-`YASL_PTR_DEV_PEN` or `YASL_PTR_DEV_ERASER`; their `pen` member contains
-pressure and signed tilt. Button 0 is tip contact on every backend. Track tip
-press/release events: zero pressure alone does not mean release. Hover and side
-buttons do not paint, and focus loss, leave, or `YASL_EVENT_INPUT_RESET` cancels a stroke.
-Stationary pressure changes arrive as motion events and update the brush too.
-
-The example uses portable physical keys: C and Space clear, Escape exits.
-Letter names describe US positions, so the clear key follows the C position
-when the user selects another layout.
+Tests cover public static/shared C and C++11 consumers, exact shared exports,
+queue ownership/order/destruction, UTF-8, GL attributes, private native event loops,
+clipboard, IME editing, pointer/pen coherence, cursor, and popup positioning. Native
+unit fixtures deliberately use private implementation headers; applications must use
+installed public headers.
-`YASL_CAP_TABLET` indicates backend/protocol support, not whether a tablet is
-plugged in or which axes it has. Missing axes are zero. Wayland requires the
-compositor's `zwp_tablet_manager_v2`; X11 requires XInput 2 and driver-provided
-`Abs Pressure`/`Abs Tilt X`/`Abs Tilt Y` valuators. Cocoa handles both tablet mouse
-subtypes and native tablet point/proximity events. X11 identifies erasers by
-the Wacom tool property or the driver's device name. Hardware and driver
-support determine which readings are available. The example displays a message
-when the server has no tablet protocol and still allows mouse drawing.
-
-The test suite checks synthetic Wayland/XInput samples and the drawing behavior
-without hardware, including pressure-only changes, sparse axes, eraser input,
-hover, release, reset, and scale changes. Linux integration tests launch the
-example on isolated Wayland and X11 servers; macOS integration tests also check
-Cocoa tablet event translation. Physical tablet testing is still needed to
-validate a particular device/driver combination.
-
-For an end-to-end X11 pressure test without hardware, install Xorg's `dummy`
-video driver and `inputtest` input driver, then run:
+For private Linux display servers and actual GPU presentation, install Weston,
+Xwayland, EGL/wayland-egl, and Vulkan/Mesa software drivers, then:
```sh
-meson configure build -Dx11_tablet_tests=true
-meson test -C build tablet-x11-native --print-errorlogs
+meson configure build -Dintegration_tests=true
+meson compile -C build
+meson test -C build --print-errorlogs
```
-This starts an isolated Xorg server with virtual pen and eraser devices, sends
-real driver samples, checks YASL's shared events through the example, and verifies
-its presented window pixels. It checks stationary pressure changes, release
-without valuators, and eraser identification through the driver's tool property.
-The test disables automatic physical input/GPU attachment and cleans up its
-server. Set `YASL_XORG_MODULE_PATH` to a comma-separated module search path if
-the test drivers are outside Xorg's default directories.
-
-## Benchmarks
+The integration harness starts its own headless GL Weston and Xwayland, runs public
+surface ownership/CPU damage/wakeup/refresh checks, GL drawing/readback/context-sharing/
+resize, Vulkan swapchain clear/present, and example smoke tests. It selects Mesa
+software rendering and Lavapipe when available. It needs permission to create local
+Unix sockets. A sandbox blocking those sockets is not a backend test result.
-`yasl-benchmark` measures queue, timer, and surface-operation costs. Results
-depend on the build, backend, and display server; they do not measure confirmed
-screen presentation latency.
-
-```sh
-# Run via meson test runner
-meson test -C build --benchmark --verbose
-
-# Or invoke the executable directly
-ninja -C build tests/yasl-benchmark
-./build/tests/yasl-benchmark
-```
+`-Dx11_tablet_tests=true` additionally uses Xorg dummy/inputtest drivers for pen input
+without physical hardware. These are separate from the compositor-free native unit
+fixtures. Cocoa integration tests run only on macOS and need the native SDK/display.
+Linux results do not validate Cocoa compilation or runtime.
+For memory/undefined-behavior checks use a separate build with `-Db_sanitize=address,undefined`
+and run the same suites. Keep sanitizer builds isolated from the normal build.