feat(config): turn animations off by default and mark them experimental
- commit
- 4ad7ed151594873be726e79f4b682f7cb6797ef6
- author
- brice · · 2026-10-05 04:02 UTC
- parent
- efdcf43
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +18 -9
M README.md +1 -1
| @@ -18,7 +18,7 @@ | |||
| 18 | 18 | ||
| 19 | 19 | I wanted a compositor I would never have to think about. It takes the newest Wayland protocols and kernel features as they land, gives games the whole GPU, and otherwise stays out of sight. | |
| 20 | 20 | ||
| 21 | - | It is small on purpose, and it will stay small. Drawing is kept to simple primitives: no blur, no shadows, no rounded corners, no themes. There is no built-in bar or wallpaper either; for those, bring your own: any layer-shell client works. Windows and the view move with short springs (about 300 ms) when you scroll, switch workspaces, move columns or windows, resize them, drop a dragged tile or send a workspace to another monitor; windows fade in and out when they open, close or leave with the stash, the overview zooms its cards, and a brief pulse marks the window you just focused. They stay cheap: nothing runs while the screen is still, frames follow the display, only the region that moves is redrawn, apps are resized once, to their final size, and do not redraw during a transition, and nothing is copied or blurred. `animations = off` makes every change instant. About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. | |
| 21 | + | It is small on purpose, and it will stay small. Drawing is kept to simple primitives: no blur, no shadows, no rounded corners, no themes. There is no built-in bar or wallpaper either; for those, bring your own: any layer-shell client works. Animations are experimental and off by default (`animations = on` turns them on). With them, windows and the view move with short springs (about 300 ms) when you scroll, switch workspaces, move columns or windows, resize them, drop a dragged tile or send a workspace to another monitor; windows fade in and out when they open, close or leave with the stash, the overview zooms its cards, and a brief pulse marks the window you just focused. They stay cheap: nothing runs while the screen is still, frames follow the display, only the region that moves is redrawn, apps are resized once, to their final size, and do not redraw during a transition, and nothing is copied or blurred. About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. | |
| 22 | 22 | ||
| 23 | 23 | ## OK, but why Go? | |
| 24 | 24 | ||
| @@ -18,7 +18,7 @@ | |||||
| 18 | 18 | ||||
| 19 | I wanted a compositor I would never have to think about. It takes the newest Wayland protocols and kernel features as they land, gives games the whole GPU, and otherwise stays out of sight. | 19 | I wanted a compositor I would never have to think about. It takes the newest Wayland protocols and kernel features as they land, gives games the whole GPU, and otherwise stays out of sight. | ||
| 20 | 20 | ||||
| 21 | - | It is small on purpose, and it will stay small. Drawing is kept to simple primitives: no blur, no shadows, no rounded corners, no themes. There is no built-in bar or wallpaper either; for those, bring your own: any layer-shell client works. Windows and the view move with short springs (about 300 ms) when you scroll, switch workspaces, move columns or windows, resize them, drop a dragged tile or send a workspace to another monitor; windows fade in and out when they open, close or leave with the stash, the overview zooms its cards, and a brief pulse marks the window you just focused. They stay cheap: nothing runs while the screen is still, frames follow the display, only the region that moves is redrawn, apps are resized once, to their final size, and do not redraw during a transition, and nothing is copied or blurred. `animations = off` makes every change instant. About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. | 21 | + | It is small on purpose, and it will stay small. Drawing is kept to simple primitives: no blur, no shadows, no rounded corners, no themes. There is no built-in bar or wallpaper either; for those, bring your own: any layer-shell client works. Animations are experimental and off by default (`animations = on` turns them on). With them, windows and the view move with short springs (about 300 ms) when you scroll, switch workspaces, move columns or windows, resize them, drop a dragged tile or send a workspace to another monitor; windows fade in and out when they open, close or leave with the stash, the overview zooms its cards, and a brief pulse marks the window you just focused. They stay cheap: nothing runs while the screen is still, frames follow the display, only the region that moves is redrawn, apps are resized once, to their final size, and do not redraw during a transition, and nothing is copied or blurred. About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change. |
| 22 | 22 | ||||
| 23 | ## OK, but why Go? | 23 | ## OK, but why Go? | ||
| 24 | 24 | ||||
M docs/config.md +1 -1
| @@ -44,7 +44,7 @@ NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`). | |||
| 44 | 44 | | `mouse.accel-profile` | `adaptive` | `adaptive`: faster moves go further; `flat`: constant speed | | |
| 45 | 45 | | `mouse.left-handed` | `off` | Swaps the left and right mouse buttons | | |
| 46 | 46 | | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` | | |
| 47 | - | | `animations` | `on` | `off` makes every change instant: swipe landings, scrolling, workspace switches, window moves and resizes, drag drops, workspaces sent to another monitor, the overview, windows opening and closing, the stash showing and hiding, the focus pulse | | |
| 47 | + | | `animations` | `off` | **Experimental.** `on` animates changes; `off` makes every change instant: swipe landings, scrolling, workspace switches, window moves and resizes, drag drops, workspaces sent to another monitor, the overview, windows opening and closing, the stash showing and hiding, the focus pulse | | |
| 48 | 48 | | `animations.slowdown` | `1` | Multiplies the duration of every spring (not the focus pulse), `0.1` to `10` | | |
| 49 | 49 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | |
| 50 | 50 | | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, a window alone on screen (e.g. a maximized column), the overview and a window focused as it opens. Off while `animations = off` | | |
| @@ -44,7 +44,7 @@ NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`). | |||||
| 44 | | `mouse.accel-profile` | `adaptive` | `adaptive`: faster moves go further; `flat`: constant speed | | 44 | | `mouse.accel-profile` | `adaptive` | `adaptive`: faster moves go further; `flat`: constant speed | | ||
| 45 | | `mouse.left-handed` | `off` | Swaps the left and right mouse buttons | | 45 | | `mouse.left-handed` | `off` | Swaps the left and right mouse buttons | | ||
| 46 | | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` | | 46 | | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` | | ||
| 47 | - | | `animations` | `on` | `off` makes every change instant: swipe landings, scrolling, workspace switches, window moves and resizes, drag drops, workspaces sent to another monitor, the overview, windows opening and closing, the stash showing and hiding, the focus pulse | | 47 | + | | `animations` | `off` | **Experimental.** `on` animates changes; `off` makes every change instant: swipe landings, scrolling, workspace switches, window moves and resizes, drag drops, workspaces sent to another monitor, the overview, windows opening and closing, the stash showing and hiding, the focus pulse | |
| 48 | | `animations.slowdown` | `1` | Multiplies the duration of every spring (not the focus pulse), `0.1` to `10` | | 48 | | `animations.slowdown` | `1` | Multiplies the duration of every spring (not the focus pulse), `0.1` to `10` | | ||
| 49 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | 49 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | ||
| 50 | | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, a window alone on screen (e.g. a maximized column), the overview and a window focused as it opens. Off while `animations = off` | | 50 | | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, a window alone on screen (e.g. a maximized column), the overview and a window focused as it opens. Off while `animations = off` | | ||
M docs/performance.md +2 -2
| @@ -8,8 +8,8 @@ How NeferWL keeps its CPU and memory use low, how to measure it, and the referen | |||
| 8 | 8 | - **Allocation-free steady-state presentation.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse owner-goroutine scratch or immutable snapshots. Surface updates (one per `wl_surface.commit`) are recycled once no queued update references them. | |
| 9 | 9 | - **Cursor rides on game frames under VRR.** While a fullscreen game drives VRR, a cursor move waits for the game's next frame instead of its own commit: a cursor-only commit refreshes the panel at its slowest rate (~21 ms at 48 Hz) and delays the next game frame by as much. If the game stops drawing, the cursor still commits alone at 24 Hz. | |
| 10 | 10 | - **Game frames keep a short gap after each VRR flip.** On amdgpu, a flip committed right after the previous flip event can miss the early refresh and wait for the panel's slowest rate (~21 ms at 48 Hz); the next frame then lands at the same point, and the screen stays at 48 Hz while the game renders at 150 fps. Game frames under VRR therefore commit at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. `internal/adapters/drm/vrr_flip_gap.go` has the measurements. | |
| 11 | - | - **Transitions.** Swipe landings, scrolling, workspace switches, moving columns and windows, consume and expel, column width and window height, maximize, fullscreen, the floating toggle, drag drops and workspaces sent to another monitor each move by one critically damped spring per moving value, advanced on the flips of the output that shows them. Windows that open, close or leave with the stash fade and scale from 90 %, and the overview zooms its cards; both reuse the per-window alpha and preview zoom of the compose pass, so no transition adds a GPU pass, a blur or a texture copy. A closing window is drawn from its last content, which the renderer already imported; the veil under a closing float goes at once, so the close redraws only the float's rect. A window that fades or zooms is never scanned out directly. Client-driven resizes, panels and hotplug never animate. Clients get one configure, to the final size. During a move or a resize the window frame moves and the content is not scaled, only clipped or padded; the content zooms with the frame only while a window opens, closes or is an overview card. A spring that moves pixels settles as soon as it can no longer leave half a pixel of its target, so its last rendered frame is already the settled one and the sub-pixel tail (about a third of a 100 px move's frames) is not drawn; fades keep their fine epsilon. The frame fallback timer is made once and reset per frame. | |
| 12 | - | - **Redraw only what moved.** An output whose scene is unchanged keeps its scene `Seq` and is not recomposed; a content-only change redraws only the damaged region. When only windows move, the renderer compares the new scene with the one each render target holds and redraws the union of the old and new rects of the windows that changed; anything else (layers, scale, transform, dim, 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. Advancing the springs, the scene comparison (separator lists of the same length are compared index by index, as core emits them in layout order) and the draw list of a moving window cost 0 allocations; publishing the scenes of two outputs with six moving windows (position, fade, dim and zoom motions) costs a pinned 32 on both the page-flip and the fallback-timer paths, 24 with closing windows and 70 while the overview opens. All are guarded by `make perf-check`. Measured cost of a full composed frame at 2560×1600: ~0.16 ms on an RX 9070 XT, ~1.4 ms on a Raphael iGPU, ~3 ms on a Radeon 680M, against an 11.1 ms frame at 90 Hz. | |
| 11 | + | - **Transitions** (experimental, `animations = on`; off by default). Swipe landings, scrolling, workspace switches, moving columns and windows, consume and expel, column width and window height, maximize, fullscreen, the floating toggle, drag drops and workspaces sent to another monitor each move by one critically damped spring per moving value, advanced on the flips of the output that shows them. Windows that open, close or leave with the stash fade and scale from 90 %, and the overview zooms its cards; both reuse the per-window alpha and preview zoom of the compose pass, so no transition adds a GPU pass, a blur or a texture copy. A closing window is drawn from its last content, which the renderer already imported; the veil under a closing float goes at once, so the close redraws only the float's rect. A window that fades or zooms is never scanned out directly. Client-driven resizes, panels and hotplug never animate. Clients get one configure, to the final size. During a move or a resize the window frame moves and the content is not scaled, only clipped or padded; the content zooms with the frame only while a window opens, closes or is an overview card. A spring that moves pixels settles as soon as it can no longer leave half a pixel of its target, so its last rendered frame is already the settled one and the sub-pixel tail (about a third of a 100 px move's frames) is not drawn; fades keep their fine epsilon. The frame fallback timer is made once and reset per frame. | |
| 12 | + | - **Redraw only what moved.** An output whose scene is unchanged keeps its scene `Seq` and is not recomposed; a content-only change redraws only the damaged region. When only windows move, the renderer compares the new scene with the one each render target holds and redraws the union of the old and new rects of the windows that changed; anything else (layers, scale, transform, dim, 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. Advancing the springs, the scene comparison (separator lists of the same length are compared index by index, as core emits them in layout order) and the draw list of a moving window cost 0 allocations; publishing the scenes of two outputs with six moving windows (position, fade, dim and zoom motions) costs a pinned 6 on both the page-flip and the fallback-timer paths, 4 with closing windows and 18 while the overview opens. All are guarded by `make perf-check`. Measured cost of a full composed frame at 2560×1600: ~0.16 ms on an RX 9070 XT, ~1.4 ms on a Raphael iGPU, ~3 ms on a Radeon 680M, against an 11.1 ms frame at 90 Hz. | |
| 13 | 13 | - **Compressed render targets.** Render targets are allocated with a modifier the primary plane lists in IN_FORMATS for XRGB8888 (XRGB2101010 in HDR); KMS refuses any other (`ADDFB2: EINVAL`). A multi-plane (AMD DCC) modifier is used only when the plane lists the exact variant the GPU exports; otherwise NeferWL uses a single-plane tiled modifier, then linear. On Radeon 680M and Raphael (GFX10.3) with Mesa 26.2 RADV exports only the pipe-aligned DCC variant (`…56bb03`) while amdgpu lists only the non-pipe-aligned ones (`…563b03`), so the targets are single-plane tiled (`…401b03`): DCC is not used there. The chosen modifier and its plane count are logged as `zero-copy output`. | |
| 14 | 14 | - **Bounded capture.** Protocol requests and sync-file handles still need small allocations; pixel storage and worker batches are reused. At most 32 protocol requests are outstanding globally, with up to eight per output frame, one SHM-copy worker and two reusable GPU staging slots per output. Saturation fails requests instead of waiting or growing a queue. Two 4K slots use about 63 MiB of host-visible memory, capped at 128 MiB per output; HDR adds one GPU-local 8-bit conversion image (about 32 MiB at 4K, up to 64 MiB), outside the host-memory cap. Display capture resources are allocated on first capture and retained until the output stops. Hidden workspace capture uses a separate, demand-driven child renderer and pipeline with its own worker and two staging slots (up to 128 MiB host-visible memory), plus child render targets; each child image is capped at 256 MiB and 16384 pixels per side. The child retires once unused and idle, and is recreated for session or size changes. Copies cost GPU time and bandwidth. Capture of the displayed frame forces composition, and so does the capture indicator (red border or pill, shown while a session lives and for one second after each captured frame; see `docs/headless.md`), which also costs a second composition per capture frame because no capture holds it (a request waits at most 250 ms for the scene that shows its indicator, without drawing anything while it waits); hidden workspace capture can keep display scanout. Offscreen capture currently serves only a whole-workspace target and requests matching its exact physical dimensions, not crops. There is no per-client reservation of the shared request limit. | |
| 15 | 15 | - **Default GOGC.** The Go live heap is a few MB (client buffers live in shared and GPU memory), so the runtime default `GOGC=100` with a 256 MiB soft limit gives 2 collections on the tiled playback test where `GOGC=50` gave 13. `GOGC` and `GOMEMLIMIT` from the environment win. | |
| @@ -8,8 +8,8 @@ How NeferWL keeps its CPU and memory use low, how to measure it, and the referen | |||||
| 8 | - **Allocation-free steady-state presentation.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse owner-goroutine scratch or immutable snapshots. Surface updates (one per `wl_surface.commit`) are recycled once no queued update references them. | 8 | - **Allocation-free steady-state presentation.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse owner-goroutine scratch or immutable snapshots. Surface updates (one per `wl_surface.commit`) are recycled once no queued update references them. | ||
| 9 | - **Cursor rides on game frames under VRR.** While a fullscreen game drives VRR, a cursor move waits for the game's next frame instead of its own commit: a cursor-only commit refreshes the panel at its slowest rate (~21 ms at 48 Hz) and delays the next game frame by as much. If the game stops drawing, the cursor still commits alone at 24 Hz. | 9 | - **Cursor rides on game frames under VRR.** While a fullscreen game drives VRR, a cursor move waits for the game's next frame instead of its own commit: a cursor-only commit refreshes the panel at its slowest rate (~21 ms at 48 Hz) and delays the next game frame by as much. If the game stops drawing, the cursor still commits alone at 24 Hz. | ||
| 10 | - **Game frames keep a short gap after each VRR flip.** On amdgpu, a flip committed right after the previous flip event can miss the early refresh and wait for the panel's slowest rate (~21 ms at 48 Hz); the next frame then lands at the same point, and the screen stays at 48 Hz while the game renders at 150 fps. Game frames under VRR therefore commit at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. `internal/adapters/drm/vrr_flip_gap.go` has the measurements. | 10 | - **Game frames keep a short gap after each VRR flip.** On amdgpu, a flip committed right after the previous flip event can miss the early refresh and wait for the panel's slowest rate (~21 ms at 48 Hz); the next frame then lands at the same point, and the screen stays at 48 Hz while the game renders at 150 fps. Game frames under VRR therefore commit at least `render.vrr-flip-gap` (default 1 ms) after the previous flip. `internal/adapters/drm/vrr_flip_gap.go` has the measurements. | ||
| 11 | - | - **Transitions.** Swipe landings, scrolling, workspace switches, moving columns and windows, consume and expel, column width and window height, maximize, fullscreen, the floating toggle, drag drops and workspaces sent to another monitor each move by one critically damped spring per moving value, advanced on the flips of the output that shows them. Windows that open, close or leave with the stash fade and scale from 90 %, and the overview zooms its cards; both reuse the per-window alpha and preview zoom of the compose pass, so no transition adds a GPU pass, a blur or a texture copy. A closing window is drawn from its last content, which the renderer already imported; the veil under a closing float goes at once, so the close redraws only the float's rect. A window that fades or zooms is never scanned out directly. Client-driven resizes, panels and hotplug never animate. Clients get one configure, to the final size. During a move or a resize the window frame moves and the content is not scaled, only clipped or padded; the content zooms with the frame only while a window opens, closes or is an overview card. A spring that moves pixels settles as soon as it can no longer leave half a pixel of its target, so its last rendered frame is already the settled one and the sub-pixel tail (about a third of a 100 px move's frames) is not drawn; fades keep their fine epsilon. The frame fallback timer is made once and reset per frame. | 11 | + | - **Transitions** (experimental, `animations = on`; off by default). Swipe landings, scrolling, workspace switches, moving columns and windows, consume and expel, column width and window height, maximize, fullscreen, the floating toggle, drag drops and workspaces sent to another monitor each move by one critically damped spring per moving value, advanced on the flips of the output that shows them. Windows that open, close or leave with the stash fade and scale from 90 %, and the overview zooms its cards; both reuse the per-window alpha and preview zoom of the compose pass, so no transition adds a GPU pass, a blur or a texture copy. A closing window is drawn from its last content, which the renderer already imported; the veil under a closing float goes at once, so the close redraws only the float's rect. A window that fades or zooms is never scanned out directly. Client-driven resizes, panels and hotplug never animate. Clients get one configure, to the final size. During a move or a resize the window frame moves and the content is not scaled, only clipped or padded; the content zooms with the frame only while a window opens, closes or is an overview card. A spring that moves pixels settles as soon as it can no longer leave half a pixel of its target, so its last rendered frame is already the settled one and the sub-pixel tail (about a third of a 100 px move's frames) is not drawn; fades keep their fine epsilon. The frame fallback timer is made once and reset per frame. |
| 12 | - | - **Redraw only what moved.** An output whose scene is unchanged keeps its scene `Seq` and is not recomposed; a content-only change redraws only the damaged region. When only windows move, the renderer compares the new scene with the one each render target holds and redraws the union of the old and new rects of the windows that changed; anything else (layers, scale, transform, dim, 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. Advancing the springs, the scene comparison (separator lists of the same length are compared index by index, as core emits them in layout order) and the draw list of a moving window cost 0 allocations; publishing the scenes of two outputs with six moving windows (position, fade, dim and zoom motions) costs a pinned 32 on both the page-flip and the fallback-timer paths, 24 with closing windows and 70 while the overview opens. All are guarded by `make perf-check`. Measured cost of a full composed frame at 2560×1600: ~0.16 ms on an RX 9070 XT, ~1.4 ms on a Raphael iGPU, ~3 ms on a Radeon 680M, against an 11.1 ms frame at 90 Hz. | 12 | + | - **Redraw only what moved.** An output whose scene is unchanged keeps its scene `Seq` and is not recomposed; a content-only change redraws only the damaged region. When only windows move, the renderer compares the new scene with the one each render target holds and redraws the union of the old and new rects of the windows that changed; anything else (layers, scale, transform, dim, 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. Advancing the springs, the scene comparison (separator lists of the same length are compared index by index, as core emits them in layout order) and the draw list of a moving window cost 0 allocations; publishing the scenes of two outputs with six moving windows (position, fade, dim and zoom motions) costs a pinned 6 on both the page-flip and the fallback-timer paths, 4 with closing windows and 18 while the overview opens. All are guarded by `make perf-check`. Measured cost of a full composed frame at 2560×1600: ~0.16 ms on an RX 9070 XT, ~1.4 ms on a Raphael iGPU, ~3 ms on a Radeon 680M, against an 11.1 ms frame at 90 Hz. |
| 13 | - **Compressed render targets.** Render targets are allocated with a modifier the primary plane lists in IN_FORMATS for XRGB8888 (XRGB2101010 in HDR); KMS refuses any other (`ADDFB2: EINVAL`). A multi-plane (AMD DCC) modifier is used only when the plane lists the exact variant the GPU exports; otherwise NeferWL uses a single-plane tiled modifier, then linear. On Radeon 680M and Raphael (GFX10.3) with Mesa 26.2 RADV exports only the pipe-aligned DCC variant (`…56bb03`) while amdgpu lists only the non-pipe-aligned ones (`…563b03`), so the targets are single-plane tiled (`…401b03`): DCC is not used there. The chosen modifier and its plane count are logged as `zero-copy output`. | 13 | - **Compressed render targets.** Render targets are allocated with a modifier the primary plane lists in IN_FORMATS for XRGB8888 (XRGB2101010 in HDR); KMS refuses any other (`ADDFB2: EINVAL`). A multi-plane (AMD DCC) modifier is used only when the plane lists the exact variant the GPU exports; otherwise NeferWL uses a single-plane tiled modifier, then linear. On Radeon 680M and Raphael (GFX10.3) with Mesa 26.2 RADV exports only the pipe-aligned DCC variant (`…56bb03`) while amdgpu lists only the non-pipe-aligned ones (`…563b03`), so the targets are single-plane tiled (`…401b03`): DCC is not used there. The chosen modifier and its plane count are logged as `zero-copy output`. | ||
| 14 | - **Bounded capture.** Protocol requests and sync-file handles still need small allocations; pixel storage and worker batches are reused. At most 32 protocol requests are outstanding globally, with up to eight per output frame, one SHM-copy worker and two reusable GPU staging slots per output. Saturation fails requests instead of waiting or growing a queue. Two 4K slots use about 63 MiB of host-visible memory, capped at 128 MiB per output; HDR adds one GPU-local 8-bit conversion image (about 32 MiB at 4K, up to 64 MiB), outside the host-memory cap. Display capture resources are allocated on first capture and retained until the output stops. Hidden workspace capture uses a separate, demand-driven child renderer and pipeline with its own worker and two staging slots (up to 128 MiB host-visible memory), plus child render targets; each child image is capped at 256 MiB and 16384 pixels per side. The child retires once unused and idle, and is recreated for session or size changes. Copies cost GPU time and bandwidth. Capture of the displayed frame forces composition, and so does the capture indicator (red border or pill, shown while a session lives and for one second after each captured frame; see `docs/headless.md`), which also costs a second composition per capture frame because no capture holds it (a request waits at most 250 ms for the scene that shows its indicator, without drawing anything while it waits); hidden workspace capture can keep display scanout. Offscreen capture currently serves only a whole-workspace target and requests matching its exact physical dimensions, not crops. There is no per-client reservation of the shared request limit. | 14 | - **Bounded capture.** Protocol requests and sync-file handles still need small allocations; pixel storage and worker batches are reused. At most 32 protocol requests are outstanding globally, with up to eight per output frame, one SHM-copy worker and two reusable GPU staging slots per output. Saturation fails requests instead of waiting or growing a queue. Two 4K slots use about 63 MiB of host-visible memory, capped at 128 MiB per output; HDR adds one GPU-local 8-bit conversion image (about 32 MiB at 4K, up to 64 MiB), outside the host-memory cap. Display capture resources are allocated on first capture and retained until the output stops. Hidden workspace capture uses a separate, demand-driven child renderer and pipeline with its own worker and two staging slots (up to 128 MiB host-visible memory), plus child render targets; each child image is capped at 256 MiB and 16384 pixels per side. The child retires once unused and idle, and is recreated for session or size changes. Copies cost GPU time and bandwidth. Capture of the displayed frame forces composition, and so does the capture indicator (red border or pill, shown while a session lives and for one second after each captured frame; see `docs/headless.md`), which also costs a second composition per capture frame because no capture holds it (a request waits at most 250 ms for the scene that shows its indicator, without drawing anything while it waits); hidden workspace capture can keep display scanout. Offscreen capture currently serves only a whole-workspace target and requests matching its exact physical dimensions, not crops. There is no per-client reservation of the shared request limit. | ||
| 15 | - **Default GOGC.** The Go live heap is a few MB (client buffers live in shared and GPU memory), so the runtime default `GOGC=100` with a 256 MiB soft limit gives 2 collections on the tiled playback test where `GOGC=50` gave 13. `GOGC` and `GOMEMLIMIT` from the environment win. | 15 | - **Default GOGC.** The Go live heap is a few MB (client buffers live in shared and GPU memory), so the runtime default `GOGC=100` with a 256 MiB soft limit gives 2 collections on the tiled playback test where `GOGC=50` gave 13. `GOGC` and `GOMEMLIMIT` from the environment win. | ||
M examples/config +2 -1
| @@ -72,11 +72,12 @@ cursor.hide-after = 5s | |||
| 72 | 72 | ||
| 73 | 73 | # on: moving a column or window to another workspace shows that workspace. | |
| 74 | 74 | focus.follow-move = off | |
| 75 | + | # Experimental, off by default. | |
| 75 | 76 | # on: windows and the view move with springs, windows fade in and out as | |
| 76 | 77 | # they open, close or leave with the stash, the overview zooms its cards; | |
| 77 | 78 | # off: every change is instant. | |
| 78 | 79 | # slowdown multiplies every spring's duration (0.1 to 10). | |
| 79 | - | animations = on | |
| 80 | + | animations = off | |
| 80 | 81 | animations.slowdown = 1 | |
| 81 | 82 | # Focus indicator on the newly focused window: an animation (pulse or off) | |
| 82 | 83 | # of an effect (screen: a slight brightening), peaking at strength | |
| @@ -72,11 +72,12 @@ cursor.hide-after = 5s | |||||
| 72 | 72 | ||||
| 73 | # on: moving a column or window to another workspace shows that workspace. | 73 | # on: moving a column or window to another workspace shows that workspace. | ||
| 74 | focus.follow-move = off | 74 | focus.follow-move = off | ||
| 75 | + | # Experimental, off by default. | |||
| 75 | # on: windows and the view move with springs, windows fade in and out as | 76 | # on: windows and the view move with springs, windows fade in and out as | ||
| 76 | # they open, close or leave with the stash, the overview zooms its cards; | 77 | # they open, close or leave with the stash, the overview zooms its cards; | ||
| 77 | # off: every change is instant. | 78 | # off: every change is instant. | ||
| 78 | # slowdown multiplies every spring's duration (0.1 to 10). | 79 | # slowdown multiplies every spring's duration (0.1 to 10). | ||
| 79 | - | animations = on | 80 | + | animations = off |
| 80 | animations.slowdown = 1 | 81 | animations.slowdown = 1 | ||
| 81 | # Focus indicator on the newly focused window: an animation (pulse or off) | 82 | # Focus indicator on the newly focused window: an animation (pulse or off) | ||
| 82 | # of an effect (screen: a slight brightening), peaking at strength | 83 | # of an effect (screen: a slight brightening), peaking at strength | ||
M internal/adapters/config/config.go +2 -1
| @@ -169,7 +169,8 @@ func Defaults() ports.Config { | |||
| 169 | 169 | c.Touchpad.ScrollFactor = 1 | |
| 170 | 170 | c.Mouse.AccelProfile = ports.AccelAdaptive | |
| 171 | 171 | c.Cursor.HideAfter = 5 * time.Second | |
| 172 | - | c.Animations.On = true | |
| 172 | + | // Animations are experimental: off unless the config turns them on. | |
| 173 | + | c.Animations.On = false | |
| 173 | 174 | c.Animations.Slowdown = 1 | |
| 174 | 175 | c.Focus.Animation = ports.FocusAnimationPulse | |
| 175 | 176 | c.Focus.Effect = ports.FocusEffectScreen | |
| @@ -169,7 +169,8 @@ func Defaults() ports.Config { | |||||
| 169 | c.Touchpad.ScrollFactor = 1 | 169 | c.Touchpad.ScrollFactor = 1 | ||
| 170 | c.Mouse.AccelProfile = ports.AccelAdaptive | 170 | c.Mouse.AccelProfile = ports.AccelAdaptive | ||
| 171 | c.Cursor.HideAfter = 5 * time.Second | 171 | c.Cursor.HideAfter = 5 * time.Second | ||
| 172 | - | c.Animations.On = true | 172 | + | // Animations are experimental: off unless the config turns them on. |
| 173 | + | c.Animations.On = false | |||
| 173 | c.Animations.Slowdown = 1 | 174 | c.Animations.Slowdown = 1 | ||
| 174 | c.Focus.Animation = ports.FocusAnimationPulse | 175 | c.Focus.Animation = ports.FocusAnimationPulse | ||
| 175 | c.Focus.Effect = ports.FocusEffectScreen | 176 | c.Focus.Effect = ports.FocusEffectScreen | ||
M internal/adapters/config/config_test.go +7 -3
| @@ -80,11 +80,15 @@ func TestFollowMove(t *testing.T) { | |||
| 80 | 80 | ||
| 81 | 81 | func TestAnimations(t *testing.T) { | |
| 82 | 82 | d := Defaults().Animations | |
| 83 | - | if !d.On || d.Slowdown != 1 { | |
| 83 | + | if d.On || d.Slowdown != 1 { | |
| 84 | 84 | t.Fatal("animations defaults", d) | |
| 85 | 85 | } | |
| 86 | - | c, w := parseString(t, "animations = off\nanimations.slowdown = 2\n") | |
| 87 | - | if c.Animations.On || c.Animations.Slowdown != 2 || len(w) != 0 { | |
| 86 | + | c, w := parseString(t, "animations = on\nanimations.slowdown = 2\n") | |
| 87 | + | if !c.Animations.On || c.Animations.Slowdown != 2 || len(w) != 0 { | |
| 88 | + | t.Fatal(c.Animations, w) | |
| 89 | + | } | |
| 90 | + | c, w = parseString(t, "animations = off\n") | |
| 91 | + | if c.Animations.On || len(w) != 0 { | |
| 88 | 92 | t.Fatal(c.Animations, w) | |
| 89 | 93 | } | |
| 90 | 94 | for _, line := range []string{"animations = maybe", "animations.slowdown = 0", "animations.slowdown = 11"} { | |
| @@ -80,11 +80,15 @@ func TestFollowMove(t *testing.T) { | |||||
| 80 | 80 | ||||
| 81 | func TestAnimations(t *testing.T) { | 81 | func TestAnimations(t *testing.T) { | ||
| 82 | d := Defaults().Animations | 82 | d := Defaults().Animations | ||
| 83 | - | if !d.On || d.Slowdown != 1 { | 83 | + | if d.On || d.Slowdown != 1 { |
| 84 | t.Fatal("animations defaults", d) | 84 | t.Fatal("animations defaults", d) | ||
| 85 | } | 85 | } | ||
| 86 | - | c, w := parseString(t, "animations = off\nanimations.slowdown = 2\n") | 86 | + | c, w := parseString(t, "animations = on\nanimations.slowdown = 2\n") |
| 87 | - | if c.Animations.On || c.Animations.Slowdown != 2 || len(w) != 0 { | 87 | + | if !c.Animations.On || c.Animations.Slowdown != 2 || len(w) != 0 { |
| 88 | + | t.Fatal(c.Animations, w) | |||
| 89 | + | } | |||
| 90 | + | c, w = parseString(t, "animations = off\n") | |||
| 91 | + | if c.Animations.On || len(w) != 0 { | |||
| 88 | t.Fatal(c.Animations, w) | 92 | t.Fatal(c.Animations, w) | ||
| 89 | } | 93 | } | ||
| 90 | for _, line := range []string{"animations = maybe", "animations.slowdown = 0", "animations.slowdown = 11"} { | 94 | for _, line := range []string{"animations = maybe", "animations.slowdown = 0", "animations.slowdown = 11"} { | ||
M internal/core/swipe_test.go +3 -0
| @@ -36,6 +36,9 @@ func startSwipeOn(t *testing.T, edit func(*ports.Config), outs ...ports.OutputIn | |||
| 36 | 36 | cfg.Border.Width = 0 | |
| 37 | 37 | cfg.Layout.Gaps = 0 | |
| 38 | 38 | cfg.Terminal.AutoOpen = "never" | |
| 39 | + | // The rig steps animations frame by frame: they are on unless edit | |
| 40 | + | // turns them off. | |
| 41 | + | cfg.Animations.On = true | |
| 39 | 42 | if edit != nil { | |
| 40 | 43 | edit(&cfg) | |
| 41 | 44 | } | |
| @@ -36,6 +36,9 @@ func startSwipeOn(t *testing.T, edit func(*ports.Config), outs ...ports.OutputIn | |||||
| 36 | cfg.Border.Width = 0 | 36 | cfg.Border.Width = 0 | ||
| 37 | cfg.Layout.Gaps = 0 | 37 | cfg.Layout.Gaps = 0 | ||
| 38 | cfg.Terminal.AutoOpen = "never" | 38 | cfg.Terminal.AutoOpen = "never" | ||
| 39 | + | // The rig steps animations frame by frame: they are on unless edit | |||
| 40 | + | // turns them off. | |||
| 41 | + | cfg.Animations.On = true | |||
| 39 | if edit != nil { | 42 | if edit != nil { | ||
| 40 | edit(&cfg) | 43 | edit(&cfg) | ||
| 41 | } | 44 | } | ||