docs: correct capture, performance and lock details found in review
- commit
- d4a1cd5801b14df3201c9afd3ec61cb9e0f15365
- author
- brice · · 2026-10-09 10:09 UTC
- parent
- 87ff6bb
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 67 | 67 | - **Explicit sync:** client fences go to the GPU and KMS. Enabled by default, with no flag to set. | |
| 68 | 68 | - **HDR:** HDR10 on capable displays, with configurable brightness for SDR content. | |
| 69 | 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 | 71 | - **X11 apps:** Steam and other X11 clients run through xwayland-satellite. Wine can run natively on Wayland. | |
| 72 | 72 | ||
| 73 | 73 | ### 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 workspaces | 73 | ### Windows and workspaces | ||
M docs/capture.md +12 -9
| @@ -19,20 +19,21 @@ With ext-image-copy-capture, a client can capture: | |||
| 19 | 19 | ||
| 20 | 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 | 23 | - Captures are opaque 8-bit sRGB. HDR content is tone-mapped on the GPU. | |
| 23 | 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 | 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 | 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 | 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 | 30 | ## 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 | 34 | - A live session keeps the indicator on. A one-shot screenshot flashes it for one second. | |
| 34 | 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 | 37 | - While it shows, the output is composed (no direct scanout), and capturing the displayed frame costs a second composition. | |
| 37 | 38 | ||
| 38 | 39 | ## 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 | 44 | 2. The HUD attaches up to 4 layer surfaces (top or overlay, not yet mapped) with that token. | |
| 44 | 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 | 49 | ## Who may capture | |
| 49 | 50 | ||
| 50 | 51 | A client that may not capture gets its protocol's failure (`failed`, or `stopped` for a session). In order: | |
| 51 | 52 | ||
| 52 | 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 | 55 | 3. **Allowlist:** the executable must be listed. | |
| 55 | 56 | ||
| 56 | 57 | ## The allowlist | |
| @@ -82,19 +83,21 @@ sudoedit /etc/neferwl/capture-allow | |||
| 82 | 83 | - List the real file, as `/proc/<pid>/exe` shows it: `/usr/bin/python3.12`, not a `python3` symlink. | |
| 83 | 84 | - Paths must match exactly; there are no globs. | |
| 84 | 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 | - | - 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. | |
| 87 | 88 | - If the file is invalid or unsafe, no client may capture. The log names the problem. | |
| 88 | 89 | ||
| 89 | 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 | 92 | ### 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 | 96 | ### 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 | 102 | On kernels older than 6.5, no client may capture unless the list is `*`. | |
| 100 | 103 | ||
| @@ -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 indicator | 30 | ## 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 HUD | 39 | ## 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 capture | 49 | ## 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 allowlist | 57 | ## 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 root | 92 | ### 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 identified | 96 | ### 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 | |||
| 80 | 80 | ||
| 81 | 81 | ## 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 | 85 | ## 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 | 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 | 136 | ## State for scripts | |
| 137 | 137 | ||
| @@ -80,7 +80,7 @@ External lockers use `ext-session-lock-v1`. NeferWL isolates desktop input and c | |||||
| 80 | 80 | ||||
| 81 | ## Screen capture | 81 | ## 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 off | 85 | ## 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 scripts | 136 | ## State for scripts | ||
| 137 | 137 | ||||
M docs/headless.md +1 -0
| @@ -8,6 +8,7 @@ WAYLAND_DISPLAY=<name from the log> foot | |||
| 8 | 8 | ``` | |
| 9 | 9 | ||
| 10 | 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 | 12 | - `--timeout 5s` stops NeferWL after 5 seconds. | |
| 12 | 13 | - While the session is locked, the PNG stops updating and keeps the last unlocked frame. See [Session locking](session-lock.md). | |
| 13 | 14 | - 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 | |||
| 5 | 5 | ## How it stays light | |
| 6 | 6 | ||
| 7 | 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. | |
| 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. | |
| 11 | 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). | |
| 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. | |
| 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 | 20 | ## VRR | |
| 18 | 21 | ||
| 19 | 22 | With a fullscreen game under VRR: | |
| 20 | 23 | ||
| 21 | 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 | 27 | ## 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`. | |
| 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`. | |
| 30 | 33 | ||
| 31 | 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 | 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 | 57 | ### 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 | 61 | | Field | Meaning | | |
| 59 | 62 | |---|---| | |
| 60 | 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 | 65 | | `max_commit_delay_ms` | Longest wait from "frame due" to commit | | |
| 63 | 66 | | `max_flip_to_read_ms` | Longest wait from a flip to NeferWL reading its event | | |
| 64 | 67 | | `late_fences` | Missed vblanks where the GPU finished too late. Needs `--debug=drm-flip` | | |
| 65 | 68 | | `redrawn_pixels` | Pixels the renderer drew | | |
| 66 | 69 | ||
| 70 | + | It also counts `frames`, `flips`, `pending`, `cursor_moves` and `cursor_commits`. | |
| 71 | + | ||
| 67 | 72 | To find the cause of missed vblanks: | |
| 68 | 73 | ||
| 69 | 74 | - With `late_fences`: the GPU composition was late. | |
| 70 | 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 | 81 | ## Reference: 4K HDR video with tiled subsurfaces | |
| 75 | 82 | ||
| @@ -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 light | 5 | ## 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 | ## VRR | 20 | ## 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 priority | 27 | ## 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 stats | 57 | ### 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 subsurfaces | 81 | ## 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 | |||
| 9 | 9 | - Every running capture session stops and the capture indicator clears. Clients start new sessions after unlock. | |
| 10 | 10 | - Keys held during lock or unlock do not reach the desktop afterwards. | |
| 11 | 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 | 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 | ||
| @@ -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 | ||||