detached d4a1cd5

docs: correct capture, performance and lock details found in review

commit
d4a1cd5801b14df3201c9afd3ec61cb9e0f15365
author
brice · · 2026-10-09 10:09 UTC
parent
87ff6bb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +39 -26
M README.md +1 -1
@@ -67,7 +67,7 @@ A full build takes seconds, and tests, race detection, profiling and formatting
6767 - **Explicit sync:** client fences go to the GPU and KMS. Enabled by default, with no flag to set.
6868 - **HDR:** HDR10 on capable displays, with configurable brightness for SDR content.
6969 - **Multi-monitor:** separate workspaces and fractional scale per output. Workspaces move when a monitor is unplugged and return when it reconnects.
70-- **Hidden apps rest:** a window you cannot see, on another workspace or in a hidden stash, is told it is suspended and may draw once per second. Browsers and games stop burning CPU and GPU on frames nobody sees. A window being captured keeps its full rate.
70+- **Hidden apps rest:** a window you cannot see (another workspace, a hidden stash, scrolled off screen, under a fullscreen window) gets frame callbacks once per second and the suspended state. Browsers and games stop drawing frames nobody sees. A window being captured keeps its output's refresh rate.
7171 - **X11 apps:** Steam and other X11 clients run through xwayland-satellite. Wine can run natively on Wayland.
7272
7373 ### Windows and workspaces
@@ -67,7 +67,7 @@ A full build takes seconds, and tests, race detection, profiling and formatting
67 - **Explicit sync:** client fences go to the GPU and KMS. Enabled by default, with no flag to set.67 - **Explicit sync:** client fences go to the GPU and KMS. Enabled by default, with no flag to set.
68 - **HDR:** HDR10 on capable displays, with configurable brightness for SDR content.68 - **HDR:** HDR10 on capable displays, with configurable brightness for SDR content.
69 - **Multi-monitor:** separate workspaces and fractional scale per output. Workspaces move when a monitor is unplugged and return when it reconnects.69 - **Multi-monitor:** separate workspaces and fractional scale per output. Workspaces move when a monitor is unplugged and return when it reconnects.
70-- **Hidden apps rest:** a window you cannot see, on another workspace or in a hidden stash, is told it is suspended and may draw once per second. Browsers and games stop burning CPU and GPU on frames nobody sees. A window being captured keeps its full rate.70+- **Hidden apps rest:** a window you cannot see (another workspace, a hidden stash, scrolled off screen, under a fullscreen window) gets frame callbacks once per second and the suspended state. Browsers and games stop drawing frames nobody sees. A window being captured keeps its output's refresh rate.
71 - **X11 apps:** Steam and other X11 clients run through xwayland-satellite. Wine can run natively on Wayland.71 - **X11 apps:** Steam and other X11 clients run through xwayland-satellite. Wine can run natively on Wayland.
72 72
73 ### Windows and workspaces73 ### Windows and workspaces
M docs/capture.md +12 -9
@@ -19,20 +19,21 @@ With ext-image-copy-capture, a client can capture:
1919
2020 The NeferWL protocol XML is in `internal/adapters/wayland/imagecapture/`.
2121
22+- Captures need Vulkan sync-file export; on devices without it, such as lavapipe, capture requests fail.
2223 - Captures are opaque 8-bit sRGB. HDR content is tone-mapped on the GPU.
2324 - The cursor is never included.
24-- Only one off-screen target (a window or hidden workspace) renders at a time. Other sessions wait for it, then fail.
25+- Only one off-screen target (a window or hidden workspace) renders at a time. Other sessions wait for it, then fail. Off-screen targets are limited to 16384 px per side and 256 MiB per image.
2526 - A session stops when its output, workspace or window goes away. A window that unmaps and maps again is a new window: capture it again.
2627 - A frame whose target is briefly unavailable is retried for up to one second, so a short hiccup does not end a screencast.
2728 - `neferwl_workspace_frame_v1` (from `get_workspace_frame`) reports a workspace's position and size, which ext-workspace does not carry.
2829
2930 ## The indicator
3031
31-Every capture is visible, and no client can turn that off. NeferWL draws a 2-pixel red border (`#ff3b30`) around the captured output, region or window. A workspace or window that is not on screen shows a small red square in the top-right corner of its output instead.
32+Every capture is visible, and no client can turn that off. NeferWL draws a 2-logical-pixel red border (`#ff3b30`) around the captured output, region or on-screen workspace. A captured window always shows a small red square in the top-right corner of its output, plus its border while it is on screen. A hidden workspace shows only the square.
3233
3334 - A live session keeps the indicator on. A one-shot screenshot flashes it for one second.
3435 - The indicator is never in the captured image.
35-- The indicator is on screen before or with the frame it captures. If it cannot be shown, the capture fails after 250 ms.
36+- The indicator is on screen before or with the frame it captures. If it cannot be shown, a screencopy frame fails after 250 ms; an ext-image-copy-capture frame keeps retrying for up to 1 s.
3637 - While it shows, the output is composed (no direct scanout), and capturing the displayed frame costs a second composition.
3738
3839 ## Hiding a recorder's own HUD
@@ -43,14 +44,14 @@ Every capture is visible, and no client can turn that off. NeferWL draws a 2-pix
4344 2. The HUD attaches up to 4 layer surfaces (top or overlay, not yet mapped) with that token.
4445 3. The HUD stays visible over fullscreen windows without taking keyboard focus. That session's frames leave it out; every other capture keeps it.
4546
46-The HUD must run as the same user and be allowed to capture itself. The exclusion ends with the session.
47+The HUD must run as the same user and be allowed to capture itself. The exclusion ends with the session, or when the recorder destroys it; its HUD surfaces are then closed.
4748
4849 ## Who may capture
4950
5051 A client that may not capture gets its protocol's failure (`failed`, or `stopped` for a session). In order:
5152
5253 1. **Locked session:** nobody captures. See [Session locking](session-lock.md).
53-2. **Sandboxed client:** a client connected through `wp_security_context_v1` (Flatpak, for instance) never captures directly; it uses the portal. Sandboxed clients still see the capture globals, but their requests are refused. Other privileged protocols are not restricted.
54+2. **Sandboxed client:** a client connected through `wp_security_context_v1` (Flatpak, for instance) never captures directly; it uses the portal. Sandboxed clients still see the capture globals, but their requests are refused. Other privileged protocols are not restricted. A sandbox engine must call `set_sandbox_engine` before `commit`, and a sandboxed client cannot create its own security context; both are protocol errors.
5455 3. **Allowlist:** the executable must be listed.
5556
5657 ## The allowlist
@@ -82,19 +83,21 @@ sudoedit /etc/neferwl/capture-allow
8283 - List the real file, as `/proc/<pid>/exe` shows it: `/usr/bin/python3.12`, not a `python3` symlink.
8384 - Paths must match exactly; there are no globs.
8485 - A line with only `*` lets every client capture.
85-- Changes apply within a second, to new requests only. Running sessions are not revoked.
86-- If the file is missing, the built-in list applies.
86+- Changes apply within a fraction of a second, or about 2 s after creating `/etc/neferwl`. They apply to the next request and to the next frame of running sessions; images already delivered cannot be taken back. If the file cannot be watched, NeferWL logs a warning and changes need a restart.
87+- If the file or `/etc/neferwl` is missing, the built-in list applies.
8788 - If the file is invalid or unsafe, no client may capture. The log names the problem.
8889
8990 The path is fixed and not a config key: any program running as you could edit your config, but not a root-owned file.
9091
9192 ### The file must belong to root
9293
93-The file and every directory above it (`/`, `/etc`, `/etc/neferwl`) must be owned by root, not writable by group or others, and not symlinks. Otherwise no client may capture.
94+The file must be a regular file. It and every directory above it (`/`, `/etc`, `/etc/neferwl`) must be owned by root, not writable by group or others (sticky directories are accepted), and not symlinks. Otherwise no client may capture, even if `/` or `/etc` is the problem: the built-in list does not apply.
9495
9596 ### How a client is identified
9697
97-By the executable actually running, not its name or app ID. NeferWL takes a pidfd of the client (`SO_PEERPIDFD`, Linux 6.5+) and checks `/proc/<pid>/exe` on each new capture. A process that exits, or whose executable was deleted or replaced, is refused.
98+By the executable actually running, not its name or app ID. NeferWL takes a pidfd of the client (`SO_PEERPIDFD`, Linux 6.5+) and checks `/proc/<pid>/exe` at every screencopy frame, every new ext session and every HUD attach. A process that exits, or whose executable was deleted or replaced, is refused.
99+
100+Frames of an open ext session reuse the executable found when the client was last checked. A program that `exec`s into another binary keeps its open sessions, and is judged by the new binary at its next request.
98101
99102 On kernels older than 6.5, no client may capture unless the list is `*`.
100103
@@ -19,20 +19,21 @@ With ext-image-copy-capture, a client can capture:
19 19
20 The NeferWL protocol XML is in `internal/adapters/wayland/imagecapture/`.20 The NeferWL protocol XML is in `internal/adapters/wayland/imagecapture/`.
21 21
22+- Captures need Vulkan sync-file export; on devices without it, such as lavapipe, capture requests fail.
22 - Captures are opaque 8-bit sRGB. HDR content is tone-mapped on the GPU.23 - Captures are opaque 8-bit sRGB. HDR content is tone-mapped on the GPU.
23 - The cursor is never included.24 - The cursor is never included.
24-- Only one off-screen target (a window or hidden workspace) renders at a time. Other sessions wait for it, then fail.25+- Only one off-screen target (a window or hidden workspace) renders at a time. Other sessions wait for it, then fail. Off-screen targets are limited to 16384 px per side and 256 MiB per image.
25 - A session stops when its output, workspace or window goes away. A window that unmaps and maps again is a new window: capture it again.26 - A session stops when its output, workspace or window goes away. A window that unmaps and maps again is a new window: capture it again.
26 - A frame whose target is briefly unavailable is retried for up to one second, so a short hiccup does not end a screencast.27 - A frame whose target is briefly unavailable is retried for up to one second, so a short hiccup does not end a screencast.
27 - `neferwl_workspace_frame_v1` (from `get_workspace_frame`) reports a workspace's position and size, which ext-workspace does not carry.28 - `neferwl_workspace_frame_v1` (from `get_workspace_frame`) reports a workspace's position and size, which ext-workspace does not carry.
28 29
29 ## The indicator30 ## The indicator
30 31
31-Every capture is visible, and no client can turn that off. NeferWL draws a 2-pixel red border (`#ff3b30`) around the captured output, region or window. A workspace or window that is not on screen shows a small red square in the top-right corner of its output instead.32+Every capture is visible, and no client can turn that off. NeferWL draws a 2-logical-pixel red border (`#ff3b30`) around the captured output, region or on-screen workspace. A captured window always shows a small red square in the top-right corner of its output, plus its border while it is on screen. A hidden workspace shows only the square.
32 33
33 - A live session keeps the indicator on. A one-shot screenshot flashes it for one second.34 - A live session keeps the indicator on. A one-shot screenshot flashes it for one second.
34 - The indicator is never in the captured image.35 - The indicator is never in the captured image.
35-- The indicator is on screen before or with the frame it captures. If it cannot be shown, the capture fails after 250 ms.36+- The indicator is on screen before or with the frame it captures. If it cannot be shown, a screencopy frame fails after 250 ms; an ext-image-copy-capture frame keeps retrying for up to 1 s.
36 - While it shows, the output is composed (no direct scanout), and capturing the displayed frame costs a second composition.37 - While it shows, the output is composed (no direct scanout), and capturing the displayed frame costs a second composition.
37 38
38 ## Hiding a recorder's own HUD39 ## Hiding a recorder's own HUD
@@ -43,14 +44,14 @@ Every capture is visible, and no client can turn that off. NeferWL draws a 2-pix
43 2. The HUD attaches up to 4 layer surfaces (top or overlay, not yet mapped) with that token.44 2. The HUD attaches up to 4 layer surfaces (top or overlay, not yet mapped) with that token.
44 3. The HUD stays visible over fullscreen windows without taking keyboard focus. That session's frames leave it out; every other capture keeps it.45 3. The HUD stays visible over fullscreen windows without taking keyboard focus. That session's frames leave it out; every other capture keeps it.
45 46
46-The HUD must run as the same user and be allowed to capture itself. The exclusion ends with the session.47+The HUD must run as the same user and be allowed to capture itself. The exclusion ends with the session, or when the recorder destroys it; its HUD surfaces are then closed.
47 48
48 ## Who may capture49 ## Who may capture
49 50
50 A client that may not capture gets its protocol's failure (`failed`, or `stopped` for a session). In order:51 A client that may not capture gets its protocol's failure (`failed`, or `stopped` for a session). In order:
51 52
52 1. **Locked session:** nobody captures. See [Session locking](session-lock.md).53 1. **Locked session:** nobody captures. See [Session locking](session-lock.md).
53-2. **Sandboxed client:** a client connected through `wp_security_context_v1` (Flatpak, for instance) never captures directly; it uses the portal. Sandboxed clients still see the capture globals, but their requests are refused. Other privileged protocols are not restricted.54+2. **Sandboxed client:** a client connected through `wp_security_context_v1` (Flatpak, for instance) never captures directly; it uses the portal. Sandboxed clients still see the capture globals, but their requests are refused. Other privileged protocols are not restricted. A sandbox engine must call `set_sandbox_engine` before `commit`, and a sandboxed client cannot create its own security context; both are protocol errors.
54 3. **Allowlist:** the executable must be listed.55 3. **Allowlist:** the executable must be listed.
55 56
56 ## The allowlist57 ## The allowlist
@@ -82,19 +83,21 @@ sudoedit /etc/neferwl/capture-allow
82 - List the real file, as `/proc/<pid>/exe` shows it: `/usr/bin/python3.12`, not a `python3` symlink.83 - List the real file, as `/proc/<pid>/exe` shows it: `/usr/bin/python3.12`, not a `python3` symlink.
83 - Paths must match exactly; there are no globs.84 - Paths must match exactly; there are no globs.
84 - A line with only `*` lets every client capture.85 - A line with only `*` lets every client capture.
85-- Changes apply within a second, to new requests only. Running sessions are not revoked.86+- Changes apply within a fraction of a second, or about 2 s after creating `/etc/neferwl`. They apply to the next request and to the next frame of running sessions; images already delivered cannot be taken back. If the file cannot be watched, NeferWL logs a warning and changes need a restart.
86-- If the file is missing, the built-in list applies.87+- If the file or `/etc/neferwl` is missing, the built-in list applies.
87 - If the file is invalid or unsafe, no client may capture. The log names the problem.88 - If the file is invalid or unsafe, no client may capture. The log names the problem.
88 89
89 The path is fixed and not a config key: any program running as you could edit your config, but not a root-owned file.90 The path is fixed and not a config key: any program running as you could edit your config, but not a root-owned file.
90 91
91 ### The file must belong to root92 ### The file must belong to root
92 93
93-The file and every directory above it (`/`, `/etc`, `/etc/neferwl`) must be owned by root, not writable by group or others, and not symlinks. Otherwise no client may capture.94+The file must be a regular file. It and every directory above it (`/`, `/etc`, `/etc/neferwl`) must be owned by root, not writable by group or others (sticky directories are accepted), and not symlinks. Otherwise no client may capture, even if `/` or `/etc` is the problem: the built-in list does not apply.
94 95
95 ### How a client is identified96 ### How a client is identified
96 97
97-By the executable actually running, not its name or app ID. NeferWL takes a pidfd of the client (`SO_PEERPIDFD`, Linux 6.5+) and checks `/proc/<pid>/exe` on each new capture. A process that exits, or whose executable was deleted or replaced, is refused.98+By the executable actually running, not its name or app ID. NeferWL takes a pidfd of the client (`SO_PEERPIDFD`, Linux 6.5+) and checks `/proc/<pid>/exe` at every screencopy frame, every new ext session and every HUD attach. A process that exits, or whose executable was deleted or replaced, is refused.
99+
100+Frames of an open ext session reuse the executable found when the client was last checked. A program that `exec`s into another binary keeps its open sessions, and is judged by the new binary at its next request.
98 101
99 On kernels older than 6.5, no client may capture unless the list is `*`.102 On kernels older than 6.5, no client may capture unless the list is `*`.
100 103
M docs/desktop.md +2 -2
@@ -80,7 +80,7 @@ External lockers use `ext-session-lock-v1`. NeferWL isolates desktop input and c
8080
8181 ## Screen capture
8282
83-nefercap, `grim`, recorders and screen sharing work out of the box. Every capture shows a red border, and only allowed executables may capture. See [Screen capture](capture.md).
83+nefercap, `grim` and screen sharing through `xdg-desktop-portal-wlr` work out of the box. Other recorders must be added to `/etc/neferwl/capture-allow`. Every capture shows a red border. See [Screen capture](capture.md).
8484
8585 ## Idle and screen off
8686
@@ -131,7 +131,7 @@ Each workspace has an `id`, the same as `workspace_id` in the [state file](#stat
131131
132132 Only store settings keyed on named workspace IDs.
133133
134-Taskbars can list, focus, close and fullscreen windows through `zwlr_foreign_toplevel_manager_v1`, and notification daemons such as Dunst can detect fullscreen windows. `ext_foreign_toplevel_list_v1` lists the same windows with an `identifier` (`neferwl-<n>`) that is never reused in a session; portals use it to share single windows. While the session is locked, bars and taskbars get no updates.
134+Taskbars can list, focus, close and fullscreen windows through `zwlr_foreign_toplevel_manager_v1`, and notification daemons such as Dunst can detect fullscreen windows. `ext_foreign_toplevel_list_v1` lists the same windows with an `identifier` (`neferwl-<window>-<mapping>`) that is never reused in a session (a window that maps again gets a new one); portals use it to share single windows. While the session is locked, bars and taskbars get no updates.
135135
136136 ## State for scripts
137137
@@ -80,7 +80,7 @@ External lockers use `ext-session-lock-v1`. NeferWL isolates desktop input and c
80 80
81 ## Screen capture81 ## Screen capture
82 82
83-nefercap, `grim`, recorders and screen sharing work out of the box. Every capture shows a red border, and only allowed executables may capture. See [Screen capture](capture.md).83+nefercap, `grim` and screen sharing through `xdg-desktop-portal-wlr` work out of the box. Other recorders must be added to `/etc/neferwl/capture-allow`. Every capture shows a red border. See [Screen capture](capture.md).
84 84
85 ## Idle and screen off85 ## Idle and screen off
86 86
@@ -131,7 +131,7 @@ Each workspace has an `id`, the same as `workspace_id` in the [state file](#stat
131 131
132 Only store settings keyed on named workspace IDs.132 Only store settings keyed on named workspace IDs.
133 133
134-Taskbars can list, focus, close and fullscreen windows through `zwlr_foreign_toplevel_manager_v1`, and notification daemons such as Dunst can detect fullscreen windows. `ext_foreign_toplevel_list_v1` lists the same windows with an `identifier` (`neferwl-<n>`) that is never reused in a session; portals use it to share single windows. While the session is locked, bars and taskbars get no updates.134+Taskbars can list, focus, close and fullscreen windows through `zwlr_foreign_toplevel_manager_v1`, and notification daemons such as Dunst can detect fullscreen windows. `ext_foreign_toplevel_list_v1` lists the same windows with an `identifier` (`neferwl-<window>-<mapping>`) that is never reused in a session (a window that maps again gets a new one); portals use it to share single windows. While the session is locked, bars and taskbars get no updates.
135 135
136 ## State for scripts136 ## State for scripts
137 137
M docs/headless.md +1 -0
@@ -8,6 +8,7 @@ WAYLAND_DISPLAY=<name from the log> foot
88 ```
99
1010 - `/tmp/neferwl-shots/latest.png` shows the current frame, cursor included. A frame that cannot be read back is skipped, never written black.
11+- Every frame is also saved as `frame-NNNNNN.png`, so the directory keeps growing. With several outputs, each writes to `<dir>/HEADLESS-<n>/`.
1112 - `--timeout 5s` stops NeferWL after 5 seconds.
1213 - While the session is locked, the PNG stops updating and keeps the last unlocked frame. See [Session locking](session-lock.md).
1314 - The cursor is not drawn on screen. After a layout change, pointer focus updates on the next move.
@@ -8,6 +8,7 @@ WAYLAND_DISPLAY=<name from the log> foot
8 ```8 ```
9 9
10 - `/tmp/neferwl-shots/latest.png` shows the current frame, cursor included. A frame that cannot be read back is skipped, never written black.10 - `/tmp/neferwl-shots/latest.png` shows the current frame, cursor included. A frame that cannot be read back is skipped, never written black.
11+- Every frame is also saved as `frame-NNNNNN.png`, so the directory keeps growing. With several outputs, each writes to `<dir>/HEADLESS-<n>/`.
11 - `--timeout 5s` stops NeferWL after 5 seconds.12 - `--timeout 5s` stops NeferWL after 5 seconds.
12 - While the session is locked, the PNG stops updating and keeps the last unlocked frame. See [Session locking](session-lock.md).13 - While the session is locked, the PNG stops updating and keeps the last unlocked frame. See [Session locking](session-lock.md).
13 - The cursor is not drawn on screen. After a layout change, pointer focus updates on the next move.14 - The cursor is not drawn on screen. After a layout change, pointer focus updates on the next move.
M docs/performance.md +21 -14
@@ -5,28 +5,31 @@ NeferWL uses about 40–80 MB of RAM with two 4K monitors and almost no CPU whil
55 ## How it stays light
66
77 - **No reflection across the C boundary.** libwayland and Vulkan calls go through fixed-arity `purego` syscalls and callbacks, with zero allocations per call.
8-- **No allocations per frame.** Frame callbacks, flips, Vulkan submissions and surface updates reuse buffers. `make perf-check` fails if a hot path starts allocating.
9-- **Redraw only what changed.** An unchanged output is not recomposed, and a moving window redraws only its old and new area. Resizing one of three columns at 2560×1600 redraws about 0.2 M pixels per frame instead of 4.1 M.
10-- **Cheap animations.** Springs reuse the existing composition pass: no extra GPU pass, blur or texture copy. Clients get one configure, at the final size.
8+- **Few allocations per frame.** Frame callbacks, flips, Vulkan submissions and surface updates reuse buffers; scene publication and `Render` stay within small fixed allocation counts. `make perf-check` fails if a hot path allocates more than its budget.
9+- **Paced frame callbacks.** Clients are told to draw on each output's flip or refresh, so a 165 Hz output paces them at 165 Hz. Cursors and surfaces without an output get 60 Hz. The pacer sleeps while no callback waits.
10+- **Hidden windows rest.** A window nobody sees (another workspace, a hidden stash, scrolled off screen, under a fullscreen window) gets frame callbacks once per second, and the `suspended` state if its client supports xdg-shell v6. A captured window keeps its output's rate.
11+- **Redraw only what changed.** An unchanged output is not recomposed. A content change redraws only its damage, and a moving window only its old and new area. A change to layers, scale, transform, dim or capture redraws the whole output. Resizing one of three columns at 2560×1600 redraws about 0.2 M pixels per frame instead of 4.1 M.
12+- **Direct scanout.** A fullscreen buffer goes straight to the display, with tearing for games, when `render.direct-scanout` and `render.tearing` allow it (see [Configuration](config.md#keys)).
13+- **Cheap animations** (experimental, off by default: `animations = on`). Springs reuse the composition pass: no extra GPU pass, blur or texture copy. Clients get one configure, at the final size. A window that is fading or zooming is composed, not scanned out directly.
1114 - **Compressed render targets.** Output images use the best modifier the display plane accepts (AMD DCC, then tiled, then linear). The log line `zero-copy output` shows the choice.
12-- **Bounded capture.** At most 32 capture requests are in flight; when full, requests fail instead of queueing. Capture buffers are capped at 128 MiB per output. While a capture indicator is shown, the output is composed (no direct scanout).
13-- **Default GC settings.** The Go heap is a few MB because client buffers live in shared and GPU memory. `GOGC` and `GOMEMLIMIT` from the environment still apply.
15+- **Bounded capture.** At most 32 capture requests are in flight. A new request is never queued: wlr-screencopy fails it at once, and ext-image-copy retries for up to 1 s, then fails. Display capture uses up to 128 MiB of host memory per output (two 5K frames; larger outputs cannot be captured). Capturing a hidden workspace adds a separate renderer with its own 128 MiB, freed when idle. The capture indicator also costs composition; see [Screen capture](capture.md#the-indicator).
16+- **GC settings.** The Go heap is a few MB because client buffers live in shared and GPU memory. NeferWL runs with `GOGC=100` and a 256 MiB soft memory limit; `GOGC` and `GOMEMLIMIT` from the environment override them.
1417
15-A full composed frame at 2560×1600 costs about 0.16 ms on an RX 9070 XT, 1.4 ms on a Raphael iGPU and 3 ms on a Radeon 680M, against 11.1 ms per frame at 90 Hz.
18+A full composed frame at 2560×1600 costs about 0.16 ms on an RX 9070 XT, 1.4 ms on a Raphael iGPU and 3 ms on a Radeon 680M, against 11.1 ms per frame at 90 Hz. To measure a GPU yourself: `go test ./internal/adapters/vulkan -bench ComposeFullTarget` (`NEFERWL_VK_DEVICE` picks the GPU).
1619
1720 ## VRR
1821
1922 With a fullscreen game under VRR:
2023
2124 - The cursor moves with the game's next frame instead of forcing its own refresh, which would drop the panel to its slowest rate. If the game stops drawing, the cursor updates alone at 24 Hz.
22-- A game frame commits at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. On amdgpu, committing earlier can lock the panel at its minimum rate (48 Hz) while the game renders at 150 fps. Measurements are in `internal/adapters/drm/vrr_flip_gap.go`.
25+- A game frame commits at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. On amdgpu, committing earlier can hold the panel at its slowest refresh (48 Hz on the measured panel) while the game renders at 150 fps. Measurements are in `internal/adapters/drm/vrr_flip_gap.go`.
2326
2427 ## Real-time priority
2528
26-Output and input threads request real-time scheduling, and the Vulkan queue requests high priority. This needs `CAP_SYS_NICE`, which the Arch and Debian packages set on `/usr/bin/neferwl`. Apps started by NeferWL do not inherit it.
29+Output and input threads request real-time scheduling, which needs `CAP_SYS_NICE`. The Arch and Debian packages set it on `/usr/bin/neferwl`. Apps started by NeferWL do not inherit it. The Vulkan queue also requests realtime GPU priority, falling back to high.
2730
28-- Turn it off: `performance.realtime = off`.
29-- Grant it to a local build: `sudo setcap cap_sys_nice+ep bin/neferwl`.
31+- Turn off real-time threads: `performance.realtime = off`. The GPU queue priority is still requested.
32+- Grant the capability to a local build: `sudo setcap cap_sys_nice+ep bin/neferwl`.
3033
3134 With the capability, the loader ignores `LD_PRELOAD` and `LD_LIBRARY_PATH`, and debuggers need root to attach.
3235
@@ -49,27 +52,31 @@ GODEBUG=gctrace=1 neferwl --backend=drm
4952
5053 `make tty` serves pprof on `localhost:6060` (`make tty PPROF=` turns it off). It also serves `/debug/pprof/profile` and `/debug/pprof/trace`.
5154
52-Logs are in `$XDG_STATE_HOME/neferwl/runs/<backend>/`, with `latest.log` pointing at the newest run. Each backend keeps 20 runs.
55+Logs are in `$XDG_STATE_HOME/neferwl/runs/<backend>/` (default `~/.local/state`), with `latest.log` pointing at the newest run. Each backend keeps 20 runs. `--debug=all` leaves out the noisy categories `drm-flip`, `input-motion` and `input-keys`; name them to turn them on.
5356
5457 ### Frame stats
5558
56-Every 10 s, each output logs a `stats` entry for the last interval:
59+Every 10 s, each output logs a `stats` entry for the last interval. The main fields:
5760
5861 | Field | Meaning |
5962 |---|---|
6063 | `missed_vblanks` | Due frames that flipped more than 1.5 refresh periods after the previous one. Not counted under VRR |
61-| `max_flip_interval_ms` | Longest gap between two flips of due frames |
64+| `max_flip_interval_ms` | Longest gap between two flips of due frames, VRR included. Restarts after a modeset |
6265 | `max_commit_delay_ms` | Longest wait from "frame due" to commit |
6366 | `max_flip_to_read_ms` | Longest wait from a flip to NeferWL reading its event |
6467 | `late_fences` | Missed vblanks where the GPU finished too late. Needs `--debug=drm-flip` |
6568 | `redrawn_pixels` | Pixels the renderer drew |
6669
70+It also counts `frames`, `flips`, `pending`, `cursor_moves` and `cursor_commits`.
71+
6772 To find the cause of missed vblanks:
6873
6974 - With `late_fences`: the GPU composition was late.
7075 - Without `late_fences` but with a large `max_commit_delay_ms`: the commit was late (CPU work or a late event read).
7176
72-`--debug=drm-flip` (or `make tty TTY_DEBUG=drm-flip`) logs every flip with its commit, fence and flip timings. `input stats` shows coalesced pointer motion and the longest wait for core.
77+Few `missed_vblanks` mean flips were on time only if `max_commit_delay_ms` is also small.
78+
79+`--debug=drm-flip` (or `make tty TTY_DEBUG=drm-flip`) logs every flip as a `flip` entry with `commit_to_flip_ms`, `flip_interval_ms`, `fence_ready_at_commit`, and, when the client fence reports it, `commit_to_fence_ms` and `fence_to_flip_ms`. A negative `commit_to_fence_ms` means the client finished before the commit. `input stats` shows coalesced pointer motion and the longest wait for core.
7380
7481 ## Reference: 4K HDR video with tiled subsurfaces
7582
@@ -5,28 +5,31 @@ NeferWL uses about 40–80 MB of RAM with two 4K monitors and almost no CPU whil
5 ## How it stays light5 ## How it stays light
6 6
7 - **No reflection across the C boundary.** libwayland and Vulkan calls go through fixed-arity `purego` syscalls and callbacks, with zero allocations per call.7 - **No reflection across the C boundary.** libwayland and Vulkan calls go through fixed-arity `purego` syscalls and callbacks, with zero allocations per call.
8-- **No allocations per frame.** Frame callbacks, flips, Vulkan submissions and surface updates reuse buffers. `make perf-check` fails if a hot path starts allocating.8+- **Few allocations per frame.** Frame callbacks, flips, Vulkan submissions and surface updates reuse buffers; scene publication and `Render` stay within small fixed allocation counts. `make perf-check` fails if a hot path allocates more than its budget.
9-- **Redraw only what changed.** An unchanged output is not recomposed, and a moving window redraws only its old and new area. Resizing one of three columns at 2560×1600 redraws about 0.2 M pixels per frame instead of 4.1 M.9+- **Paced frame callbacks.** Clients are told to draw on each output's flip or refresh, so a 165 Hz output paces them at 165 Hz. Cursors and surfaces without an output get 60 Hz. The pacer sleeps while no callback waits.
10-- **Cheap animations.** Springs reuse the existing composition pass: no extra GPU pass, blur or texture copy. Clients get one configure, at the final size.10+- **Hidden windows rest.** A window nobody sees (another workspace, a hidden stash, scrolled off screen, under a fullscreen window) gets frame callbacks once per second, and the `suspended` state if its client supports xdg-shell v6. A captured window keeps its output's rate.
11+- **Redraw only what changed.** An unchanged output is not recomposed. A content change redraws only its damage, and a moving window only its old and new area. A change to layers, scale, transform, dim or capture redraws the whole output. Resizing one of three columns at 2560×1600 redraws about 0.2 M pixels per frame instead of 4.1 M.
12+- **Direct scanout.** A fullscreen buffer goes straight to the display, with tearing for games, when `render.direct-scanout` and `render.tearing` allow it (see [Configuration](config.md#keys)).
13+- **Cheap animations** (experimental, off by default: `animations = on`). Springs reuse the composition pass: no extra GPU pass, blur or texture copy. Clients get one configure, at the final size. A window that is fading or zooming is composed, not scanned out directly.
11 - **Compressed render targets.** Output images use the best modifier the display plane accepts (AMD DCC, then tiled, then linear). The log line `zero-copy output` shows the choice.14 - **Compressed render targets.** Output images use the best modifier the display plane accepts (AMD DCC, then tiled, then linear). The log line `zero-copy output` shows the choice.
12-- **Bounded capture.** At most 32 capture requests are in flight; when full, requests fail instead of queueing. Capture buffers are capped at 128 MiB per output. While a capture indicator is shown, the output is composed (no direct scanout).15+- **Bounded capture.** At most 32 capture requests are in flight. A new request is never queued: wlr-screencopy fails it at once, and ext-image-copy retries for up to 1 s, then fails. Display capture uses up to 128 MiB of host memory per output (two 5K frames; larger outputs cannot be captured). Capturing a hidden workspace adds a separate renderer with its own 128 MiB, freed when idle. The capture indicator also costs composition; see [Screen capture](capture.md#the-indicator).
13-- **Default GC settings.** The Go heap is a few MB because client buffers live in shared and GPU memory. `GOGC` and `GOMEMLIMIT` from the environment still apply.16+- **GC settings.** The Go heap is a few MB because client buffers live in shared and GPU memory. NeferWL runs with `GOGC=100` and a 256 MiB soft memory limit; `GOGC` and `GOMEMLIMIT` from the environment override them.
14 17
15-A full composed frame at 2560×1600 costs about 0.16 ms on an RX 9070 XT, 1.4 ms on a Raphael iGPU and 3 ms on a Radeon 680M, against 11.1 ms per frame at 90 Hz.18+A full composed frame at 2560×1600 costs about 0.16 ms on an RX 9070 XT, 1.4 ms on a Raphael iGPU and 3 ms on a Radeon 680M, against 11.1 ms per frame at 90 Hz. To measure a GPU yourself: `go test ./internal/adapters/vulkan -bench ComposeFullTarget` (`NEFERWL_VK_DEVICE` picks the GPU).
16 19
17 ## VRR20 ## VRR
18 21
19 With a fullscreen game under VRR:22 With a fullscreen game under VRR:
20 23
21 - The cursor moves with the game's next frame instead of forcing its own refresh, which would drop the panel to its slowest rate. If the game stops drawing, the cursor updates alone at 24 Hz.24 - The cursor moves with the game's next frame instead of forcing its own refresh, which would drop the panel to its slowest rate. If the game stops drawing, the cursor updates alone at 24 Hz.
22-- A game frame commits at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. On amdgpu, committing earlier can lock the panel at its minimum rate (48 Hz) while the game renders at 150 fps. Measurements are in `internal/adapters/drm/vrr_flip_gap.go`.25+- A game frame commits at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. On amdgpu, committing earlier can hold the panel at its slowest refresh (48 Hz on the measured panel) while the game renders at 150 fps. Measurements are in `internal/adapters/drm/vrr_flip_gap.go`.
23 26
24 ## Real-time priority27 ## Real-time priority
25 28
26-Output and input threads request real-time scheduling, and the Vulkan queue requests high priority. This needs `CAP_SYS_NICE`, which the Arch and Debian packages set on `/usr/bin/neferwl`. Apps started by NeferWL do not inherit it.29+Output and input threads request real-time scheduling, which needs `CAP_SYS_NICE`. The Arch and Debian packages set it on `/usr/bin/neferwl`. Apps started by NeferWL do not inherit it. The Vulkan queue also requests realtime GPU priority, falling back to high.
27 30
28-- Turn it off: `performance.realtime = off`.31+- Turn off real-time threads: `performance.realtime = off`. The GPU queue priority is still requested.
29-- Grant it to a local build: `sudo setcap cap_sys_nice+ep bin/neferwl`.32+- Grant the capability to a local build: `sudo setcap cap_sys_nice+ep bin/neferwl`.
30 33
31 With the capability, the loader ignores `LD_PRELOAD` and `LD_LIBRARY_PATH`, and debuggers need root to attach.34 With the capability, the loader ignores `LD_PRELOAD` and `LD_LIBRARY_PATH`, and debuggers need root to attach.
32 35
@@ -49,27 +52,31 @@ GODEBUG=gctrace=1 neferwl --backend=drm
49 52
50 `make tty` serves pprof on `localhost:6060` (`make tty PPROF=` turns it off). It also serves `/debug/pprof/profile` and `/debug/pprof/trace`.53 `make tty` serves pprof on `localhost:6060` (`make tty PPROF=` turns it off). It also serves `/debug/pprof/profile` and `/debug/pprof/trace`.
51 54
52-Logs are in `$XDG_STATE_HOME/neferwl/runs/<backend>/`, with `latest.log` pointing at the newest run. Each backend keeps 20 runs.55+Logs are in `$XDG_STATE_HOME/neferwl/runs/<backend>/` (default `~/.local/state`), with `latest.log` pointing at the newest run. Each backend keeps 20 runs. `--debug=all` leaves out the noisy categories `drm-flip`, `input-motion` and `input-keys`; name them to turn them on.
53 56
54 ### Frame stats57 ### Frame stats
55 58
56-Every 10 s, each output logs a `stats` entry for the last interval:59+Every 10 s, each output logs a `stats` entry for the last interval. The main fields:
57 60
58 | Field | Meaning |61 | Field | Meaning |
59 |---|---|62 |---|---|
60 | `missed_vblanks` | Due frames that flipped more than 1.5 refresh periods after the previous one. Not counted under VRR |63 | `missed_vblanks` | Due frames that flipped more than 1.5 refresh periods after the previous one. Not counted under VRR |
61-| `max_flip_interval_ms` | Longest gap between two flips of due frames |64+| `max_flip_interval_ms` | Longest gap between two flips of due frames, VRR included. Restarts after a modeset |
62 | `max_commit_delay_ms` | Longest wait from "frame due" to commit |65 | `max_commit_delay_ms` | Longest wait from "frame due" to commit |
63 | `max_flip_to_read_ms` | Longest wait from a flip to NeferWL reading its event |66 | `max_flip_to_read_ms` | Longest wait from a flip to NeferWL reading its event |
64 | `late_fences` | Missed vblanks where the GPU finished too late. Needs `--debug=drm-flip` |67 | `late_fences` | Missed vblanks where the GPU finished too late. Needs `--debug=drm-flip` |
65 | `redrawn_pixels` | Pixels the renderer drew |68 | `redrawn_pixels` | Pixels the renderer drew |
66 69
70+It also counts `frames`, `flips`, `pending`, `cursor_moves` and `cursor_commits`.
71+
67 To find the cause of missed vblanks:72 To find the cause of missed vblanks:
68 73
69 - With `late_fences`: the GPU composition was late.74 - With `late_fences`: the GPU composition was late.
70 - Without `late_fences` but with a large `max_commit_delay_ms`: the commit was late (CPU work or a late event read).75 - Without `late_fences` but with a large `max_commit_delay_ms`: the commit was late (CPU work or a late event read).
71 76
72-`--debug=drm-flip` (or `make tty TTY_DEBUG=drm-flip`) logs every flip with its commit, fence and flip timings. `input stats` shows coalesced pointer motion and the longest wait for core.77+Few `missed_vblanks` mean flips were on time only if `max_commit_delay_ms` is also small.
78+
79+`--debug=drm-flip` (or `make tty TTY_DEBUG=drm-flip`) logs every flip as a `flip` entry with `commit_to_flip_ms`, `flip_interval_ms`, `fence_ready_at_commit`, and, when the client fence reports it, `commit_to_fence_ms` and `fence_to_flip_ms`. A negative `commit_to_fence_ms` means the client finished before the commit. `input stats` shows coalesced pointer motion and the longest wait for core.
73 80
74 ## Reference: 4K HDR video with tiled subsurfaces81 ## Reference: 4K HDR video with tiled subsurfaces
75 82
M docs/session-lock.md +2 -0
@@ -9,6 +9,8 @@ NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock
99 - Every running capture session stops and the capture indicator clears. Clients start new sessions after unlock.
1010 - Keys held during lock or unlock do not reach the desktop afterwards.
1111 - `Ctrl+Alt+Backspace` and the `--debug=input-keys` log are disabled. VT switching still works.
12+- A second locker is refused while the first is alive. Activity can wake outputs but never unlocks.
13+- A locker that uses a screenshot as background (hyprlock `screenshot`, swaylock-effects `--screenshots`) must be in the [capture allowlist](capture.md#the-allowlist), and capture is refused once locked.
1214
1315 The locker gets `locked` only once every output really shows a protected frame (or is off), DRM leases are revoked and captures started before the lock have finished. If that cannot be confirmed, `locked` is withheld.
1416
@@ -9,6 +9,8 @@ NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock
9 - Every running capture session stops and the capture indicator clears. Clients start new sessions after unlock.9 - Every running capture session stops and the capture indicator clears. Clients start new sessions after unlock.
10 - Keys held during lock or unlock do not reach the desktop afterwards.10 - Keys held during lock or unlock do not reach the desktop afterwards.
11 - `Ctrl+Alt+Backspace` and the `--debug=input-keys` log are disabled. VT switching still works.11 - `Ctrl+Alt+Backspace` and the `--debug=input-keys` log are disabled. VT switching still works.
12+- A second locker is refused while the first is alive. Activity can wake outputs but never unlocks.
13+- A locker that uses a screenshot as background (hyprlock `screenshot`, swaylock-effects `--screenshots`) must be in the [capture allowlist](capture.md#the-allowlist), and capture is refused once locked.
12 14
13 The locker gets `locked` only once every output really shows a protected frame (or is off), DRM leases are revoked and captures started before the lock have finished. If that cannot be confirmed, `locked` is withheld.15 The locker gets `locked` only once every output really shows a protected frame (or is off), DRM leases are revoked and captures started before the lock have finished. If that cannot be confirmed, `locked` is withheld.
14 16

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close