detached 86bd490

docs: fix stale Wayland-era comments, rewrite ADR 0003 for the eventfd model, trim ADR 0007 and performance provenance

commit
86bd4909cad28f3c46cff4efb0b780d725ba0a32
author
brice · · 2026-10-01 17:55 UTC
parent
e3eeaa9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +32 -41
M docs/adr/0003-vulkan-dmabuf-presentation.md +15 -11
@@ -4,25 +4,29 @@ Status: accepted
44
55 ## Decision
66
7-NeferGUI renders with Vulkan into exportable images and presents them as `zwp_linux_dmabuf_v1` buffers. It does not use `VK_KHR_swapchain`.
7+NeferGUI renders with Vulkan into exportable images and hands them to the application as DMA-BUF buffers (`Renderer`, see ADR 0007). The application presents them through `zwp_linux_dmabuf_v1`. NeferGUI does not use `VK_KHR_swapchain`.
88
9-- Synchronization uses `wp_linux_drm_syncobj_v1` with Vulkan timeline semaphores. There is no CPU-fence fallback.
10-- The Vulkan physical device must match the `main_device` of linux-dmabuf feedback. There is no cross-GPU fallback.
11-- Formats: one-plane `XRGB8888` for opaque windows (default) and `ARGB8888` for transparent windows, both mapped to `VK_FORMAT_B8G8R8A8_UNORM`, with a modifier supported by both the compositor and the GPU.
12-- Three exportable images per window.
9+- Synchronization uses DRM syncobj timelines (`wp_linux_drm_syncobj_v1` on the Wayland side) with Vulkan timeline semaphores. There is no CPU-fence fallback.
10+- The Vulkan physical device must match the `main_device` of linux-dmabuf feedback, passed in `RendererConfig.MainDevice`. There is no cross-GPU fallback.
11+- Formats: one-plane `XRGB8888` by default and `ARGB8888` when `RendererConfig.Transparent` is set, both mapped to `VK_FORMAT_B8G8R8A8_UNORM`, with a modifier supported by both the compositor (`RendererConfig.Formats`) and the GPU.
12+- Three exportable images per Renderer, re-created on resize.
1313 - Shaders are GLSL compiled by `glslc` through `go generate`. SPIR-V is committed and embedded; consumers do not need `glslc`.
1414
1515 ## Buffer lifecycle
1616
1717 ```text
18-Available → Rendering → acquire point exported → committed with acquire/release points
19-→ compositor reads → release point of its latest commit reached → Available
18+Available → Rendering → acquire point exported → Output returned by Render
19+→ compositor reads → release point of its latest commit signaled → Released → Available
2020 ```
2121
22-While a `wp_linux_drm_syncobj_surface_v1` exists, `wl_buffer.release` delivery is undefined (linux-drm-syncobj-v1). A buffer is reused only after the release point set for its most recent commit is signaled; each buffer has its own release timeline because points may signal out of order. `wl_buffer.release` is traced when it arrives but never gates reuse. Frame callbacks schedule drawing; they never authorize reuse.
22+Each buffer has its own release timeline, because points may signal out of order. After each `Render` the Renderer arms an eventfd on the release point of that commit (`DRM_IOCTL_SYNCOBJ_EVENTFD`, one-shot). `Output.ReleaseFD` is that eventfd. When it becomes readable the application calls `Released(buffer)`, which reads the eventfd and returns the buffer to the pool. A buffer is reused only after the release point set for its most recent commit has signaled; `wl_buffer.release` never gates reuse, because its delivery is undefined while a syncobj surface exists. Frame callbacks schedule drawing; they never authorize reuse.
2323
24-The UI loop never blocks in Wayland dispatch while GPU or compositor completion is pending. Blocking sources (Wayland socket, release timeline waits) run on their own goroutines and only post events to the loop, which owns all presentation state. Resize creates a new generation; old images are retired after their latest release point signals.
24+The Renderer starts no goroutine and never blocks on the GPU or the compositor. `Render` returns false when no buffer is free; `Pending()` reports a built frame that waits for the GPU rather than the compositor, and the application retries after a short timer. Resize creates a new generation: buffers of the old one are reported in `Output.Retired` once their release point has signaled, and their descriptors stay open until the next `Render` or `Close`.
2525
26-At shutdown, release points protect reuse of contents, **not** the lifetime of exported DMA-BUF memory: kernel FD references and compositor imports are independent of the client's Vulkan allocation. Detach and commit null, destroy the syncobj surface, wait at most 200 ms for outstanding release points for tracing, wait for client GPU completion, destroy `wl_buffer`s and **all** client images/memory/semaphores/syncobj handles, then destroy the device. Never abandon Vulkan allocations solely because a compositor release point has not arrived.
26+All descriptors in `Output` (DMA-BUF planes, timelines, eventfds) stay owned by the Renderer. The application duplicates them before a transport consumes them.
2727
28-`Opaque()` sets a full-surface opaque region. `Transparent()` uses premultiplied alpha and no opaque region. Background blur is a compositor setting, not a NeferGUI feature.
28+## Shutdown
29+
30+Release points protect reuse of contents, **not** the lifetime of exported DMA-BUF memory: kernel descriptor references and compositor imports are independent of the client's Vulkan allocation. `Close` waits for client GPU completion, then frees **all** images, memory, semaphores, syncobj handles and eventfds, and destroys the device. It never abandons Vulkan allocations because a compositor release point has not arrived. The application detaches and destroys its own Wayland buffers and syncobj surface before or after `Close`; the Renderer does not know about them. `Close` also returns any Vulkan validation errors recorded while `NEFERGUI_VULKAN_VALIDATION=1` was set.
31+
32+Opaque or transparent regions, background blur and cursor mechanics belong to the application: `Output.InputRects` and `Output.Cursor` carry the requests, and `RendererConfig.Transparent` selects premultiplied alpha output.
@@ -4,25 +4,29 @@ Status: accepted
4 4
5 ## Decision5 ## Decision
6 6
7-NeferGUI renders with Vulkan into exportable images and presents them as `zwp_linux_dmabuf_v1` buffers. It does not use `VK_KHR_swapchain`.7+NeferGUI renders with Vulkan into exportable images and hands them to the application as DMA-BUF buffers (`Renderer`, see ADR 0007). The application presents them through `zwp_linux_dmabuf_v1`. NeferGUI does not use `VK_KHR_swapchain`.
8 8
9-- Synchronization uses `wp_linux_drm_syncobj_v1` with Vulkan timeline semaphores. There is no CPU-fence fallback.9+- Synchronization uses DRM syncobj timelines (`wp_linux_drm_syncobj_v1` on the Wayland side) with Vulkan timeline semaphores. There is no CPU-fence fallback.
10-- The Vulkan physical device must match the `main_device` of linux-dmabuf feedback. There is no cross-GPU fallback.10+- The Vulkan physical device must match the `main_device` of linux-dmabuf feedback, passed in `RendererConfig.MainDevice`. There is no cross-GPU fallback.
11-- Formats: one-plane `XRGB8888` for opaque windows (default) and `ARGB8888` for transparent windows, both mapped to `VK_FORMAT_B8G8R8A8_UNORM`, with a modifier supported by both the compositor and the GPU.11+- Formats: one-plane `XRGB8888` by default and `ARGB8888` when `RendererConfig.Transparent` is set, both mapped to `VK_FORMAT_B8G8R8A8_UNORM`, with a modifier supported by both the compositor (`RendererConfig.Formats`) and the GPU.
12-- Three exportable images per window.12+- Three exportable images per Renderer, re-created on resize.
13 - Shaders are GLSL compiled by `glslc` through `go generate`. SPIR-V is committed and embedded; consumers do not need `glslc`.13 - Shaders are GLSL compiled by `glslc` through `go generate`. SPIR-V is committed and embedded; consumers do not need `glslc`.
14 14
15 ## Buffer lifecycle15 ## Buffer lifecycle
16 16
17 ```text17 ```text
18-Available → Rendering → acquire point exported → committed with acquire/release points18+Available → Rendering → acquire point exported → Output returned by Render
19-→ compositor reads → release point of its latest commit reached → Available19+→ compositor reads → release point of its latest commit signaled → Released → Available
20 ```20 ```
21 21
22-While a `wp_linux_drm_syncobj_surface_v1` exists, `wl_buffer.release` delivery is undefined (linux-drm-syncobj-v1). A buffer is reused only after the release point set for its most recent commit is signaled; each buffer has its own release timeline because points may signal out of order. `wl_buffer.release` is traced when it arrives but never gates reuse. Frame callbacks schedule drawing; they never authorize reuse.22+Each buffer has its own release timeline, because points may signal out of order. After each `Render` the Renderer arms an eventfd on the release point of that commit (`DRM_IOCTL_SYNCOBJ_EVENTFD`, one-shot). `Output.ReleaseFD` is that eventfd. When it becomes readable the application calls `Released(buffer)`, which reads the eventfd and returns the buffer to the pool. A buffer is reused only after the release point set for its most recent commit has signaled; `wl_buffer.release` never gates reuse, because its delivery is undefined while a syncobj surface exists. Frame callbacks schedule drawing; they never authorize reuse.
23 23
24-The UI loop never blocks in Wayland dispatch while GPU or compositor completion is pending. Blocking sources (Wayland socket, release timeline waits) run on their own goroutines and only post events to the loop, which owns all presentation state. Resize creates a new generation; old images are retired after their latest release point signals.24+The Renderer starts no goroutine and never blocks on the GPU or the compositor. `Render` returns false when no buffer is free; `Pending()` reports a built frame that waits for the GPU rather than the compositor, and the application retries after a short timer. Resize creates a new generation: buffers of the old one are reported in `Output.Retired` once their release point has signaled, and their descriptors stay open until the next `Render` or `Close`.
25 25
26-At shutdown, release points protect reuse of contents, **not** the lifetime of exported DMA-BUF memory: kernel FD references and compositor imports are independent of the client's Vulkan allocation. Detach and commit null, destroy the syncobj surface, wait at most 200 ms for outstanding release points for tracing, wait for client GPU completion, destroy `wl_buffer`s and **all** client images/memory/semaphores/syncobj handles, then destroy the device. Never abandon Vulkan allocations solely because a compositor release point has not arrived.26+All descriptors in `Output` (DMA-BUF planes, timelines, eventfds) stay owned by the Renderer. The application duplicates them before a transport consumes them.
27 27
28-`Opaque()` sets a full-surface opaque region. `Transparent()` uses premultiplied alpha and no opaque region. Background blur is a compositor setting, not a NeferGUI feature.28+## Shutdown
29+
30+Release points protect reuse of contents, **not** the lifetime of exported DMA-BUF memory: kernel descriptor references and compositor imports are independent of the client's Vulkan allocation. `Close` waits for client GPU completion, then frees **all** images, memory, semaphores, syncobj handles and eventfds, and destroys the device. It never abandons Vulkan allocations because a compositor release point has not arrived. The application detaches and destroys its own Wayland buffers and syncobj surface before or after `Close`; the Renderer does not know about them. `Close` also returns any Vulkan validation errors recorded while `NEFERGUI_VULKAN_VALIDATION=1` was set.
31+
32+Opaque or transparent regions, background blur and cursor mechanics belong to the application: `Output.InputRects` and `Output.Cursor` carry the requests, and `RendererConfig.Transparent` selects premultiplied alpha output.
M docs/adr/0007-wayland-free-renderer.md +6 -7
@@ -1,10 +1,10 @@
11 # ADR 0007 — Wayland-free Renderer
22
3-Status: accepted. Supersedes ADR 0002 (Wayland bindings and FD ownership) and ADR 0004 (keyboard).
3+Status: accepted
44
55 ## Decision
66
7-NeferGUI is a rendering library. It builds views, lays them out, styles them with CSS and draws them with Vulkan into DMA-BUF buffers. It does not connect to Wayland, import a Wayland library, link xkbcommon, or own a window, an event loop or a goroutine.
7+NeferGUI is a rendering library. It builds views, lays them out, styles them with CSS and draws them with Vulkan into DMA-BUF buffers. The library does not own a Wayland connection.
88
99 The public entry point is `Renderer` (`NewRenderer`, `Resize`, `Input`, `Invalidate`, `Render`, `Released`, `Wake`, `Pending`, `Close`). The application owns the Wayland client and runs one owner goroutine:
1010
@@ -17,8 +17,7 @@ Layering is application → Wayland client library → bindings. NeferGUI sits b
1717
1818 ## Consequences
1919
20-- `Run`, `RunFrames`, `RunLock`, the window and layer-shell options, `OnInput`, `OnResize`, `OnSurface`, `Wake` as an option, `SecretBuffer` and `WaylandSurface` are removed, together with the Wayland platform, keyboard, harness and example code. This is a breaking change.
21-- The module depends only on purego, purego-vulkan, go-text, x/image and x/sys. `go list -deps` shows no Wayland or xkbcommon module.
22-- Clipboard and input-method ports (`Clipboard`, `IME`) remain interfaces the application implements.
23-- `Password(true)` still masks the displayed value of an in-memory editor value.
24-- ADR 0003 still describes the Vulkan, DMA-BUF and explicit-sync design; the Wayland object handling it mentions now belongs to the application.
20+- The module depends only on purego, purego-vulkan, go-text, x/image and x/sys.
21+- Clipboard and input-method ports (`Clipboard`, `IME`) are interfaces the application implements.
22+- `Password(true)` masks the displayed value of an in-memory editor value.
23+- ADR 0003 describes the Vulkan, DMA-BUF and explicit-sync design.
@@ -1,10 +1,10 @@
1 # ADR 0007 — Wayland-free Renderer1 # ADR 0007 — Wayland-free Renderer
2 2
3-Status: accepted. Supersedes ADR 0002 (Wayland bindings and FD ownership) and ADR 0004 (keyboard).3+Status: accepted
4 4
5 ## Decision5 ## Decision
6 6
7-NeferGUI is a rendering library. It builds views, lays them out, styles them with CSS and draws them with Vulkan into DMA-BUF buffers. It does not connect to Wayland, import a Wayland library, link xkbcommon, or own a window, an event loop or a goroutine.7+NeferGUI is a rendering library. It builds views, lays them out, styles them with CSS and draws them with Vulkan into DMA-BUF buffers. The library does not own a Wayland connection.
8 8
9 The public entry point is `Renderer` (`NewRenderer`, `Resize`, `Input`, `Invalidate`, `Render`, `Released`, `Wake`, `Pending`, `Close`). The application owns the Wayland client and runs one owner goroutine:9 The public entry point is `Renderer` (`NewRenderer`, `Resize`, `Input`, `Invalidate`, `Render`, `Released`, `Wake`, `Pending`, `Close`). The application owns the Wayland client and runs one owner goroutine:
10 10
@@ -17,8 +17,7 @@ Layering is application → Wayland client library → bindings. NeferGUI sits b
17 17
18 ## Consequences18 ## Consequences
19 19
20-- `Run`, `RunFrames`, `RunLock`, the window and layer-shell options, `OnInput`, `OnResize`, `OnSurface`, `Wake` as an option, `SecretBuffer` and `WaylandSurface` are removed, together with the Wayland platform, keyboard, harness and example code. This is a breaking change.20+- The module depends only on purego, purego-vulkan, go-text, x/image and x/sys.
21-- The module depends only on purego, purego-vulkan, go-text, x/image and x/sys. `go list -deps` shows no Wayland or xkbcommon module.21+- Clipboard and input-method ports (`Clipboard`, `IME`) are interfaces the application implements.
22-- Clipboard and input-method ports (`Clipboard`, `IME`) remain interfaces the application implements.22+- `Password(true)` masks the displayed value of an in-memory editor value.
23-- `Password(true)` still masks the displayed value of an in-memory editor value.23+- ADR 0003 describes the Vulkan, DMA-BUF and explicit-sync design.
24-- ADR 0003 still describes the Vulkan, DMA-BUF and explicit-sync design; the Wayland object handling it mentions now belongs to the application.
M docs/controls.md +1 -1
@@ -16,7 +16,7 @@ The pointer cursor follows the CSS `cursor` of the hovered element (`pointer`, `
1616
1717 `internal/edit` supplies the headless clipboard (`ReadText(limit)`, `WriteText`) and IME (`Enable`, `Disable`, `Surrounding`, `CursorRect`, `ContentType`) ports. A read is limited to 1 MiB and rejected unless UTF-8. IME batches represent preedit, commit and delete-surrounding relative to the UTF-8 cursor, processed at done. The preedit is shaped inline at the model cursor with an underline and its own cursor commands, without changing the model or its normal text run until commit. Password preedit is masked per grapheme before shaping or entering the display list. Ordinary keyboard text is inserted while an IME port is active, except while a preedit is shown; an input method that grabs the keyboard receives its keys instead of the editor. The ports do not negotiate MIME types or own FDs; the application implements them and passes them in `RendererConfig`.
1818
19-Controls are implemented in `internal/ui`; the root package re-exports their types. Display-list JSON snapshots under `internal/ui/testdata/control-display` cover every control's normal and hover states, interactive focus-visible and disabled states, checkbox/radio checked states, and Input LTR/Arabic selections and preedit. They are deterministic command snapshots, not raster screenshots. Platform `Run` uses the same control tree and display list.
19+Controls are implemented in `internal/ui`; the root package re-exports their types. Display-list JSON snapshots under `internal/ui/testdata/control-display` cover every control's normal and hover states, interactive focus-visible and disabled states, checkbox/radio checked states, and Input LTR/Arabic selections and preedit. They are deterministic command snapshots, not raster screenshots. `Renderer` uses the same control tree and display list.
2020
2121 The additive UA rules for `checkbox`, `radio`, `slider`, `separator`, `spacer`, and `icon` live in `internal/css/ua.css`. Their dimensions and colors are regular computed CSS and can be overridden by author styles. The control helpers hard-code no paint values; only the indicator painter fixes the shape details listed above.
2222
@@ -16,7 +16,7 @@ The pointer cursor follows the CSS `cursor` of the hovered element (`pointer`, `
16 16
17 `internal/edit` supplies the headless clipboard (`ReadText(limit)`, `WriteText`) and IME (`Enable`, `Disable`, `Surrounding`, `CursorRect`, `ContentType`) ports. A read is limited to 1 MiB and rejected unless UTF-8. IME batches represent preedit, commit and delete-surrounding relative to the UTF-8 cursor, processed at done. The preedit is shaped inline at the model cursor with an underline and its own cursor commands, without changing the model or its normal text run until commit. Password preedit is masked per grapheme before shaping or entering the display list. Ordinary keyboard text is inserted while an IME port is active, except while a preedit is shown; an input method that grabs the keyboard receives its keys instead of the editor. The ports do not negotiate MIME types or own FDs; the application implements them and passes them in `RendererConfig`.17 `internal/edit` supplies the headless clipboard (`ReadText(limit)`, `WriteText`) and IME (`Enable`, `Disable`, `Surrounding`, `CursorRect`, `ContentType`) ports. A read is limited to 1 MiB and rejected unless UTF-8. IME batches represent preedit, commit and delete-surrounding relative to the UTF-8 cursor, processed at done. The preedit is shaped inline at the model cursor with an underline and its own cursor commands, without changing the model or its normal text run until commit. Password preedit is masked per grapheme before shaping or entering the display list. Ordinary keyboard text is inserted while an IME port is active, except while a preedit is shown; an input method that grabs the keyboard receives its keys instead of the editor. The ports do not negotiate MIME types or own FDs; the application implements them and passes them in `RendererConfig`.
18 18
19-Controls are implemented in `internal/ui`; the root package re-exports their types. Display-list JSON snapshots under `internal/ui/testdata/control-display` cover every control's normal and hover states, interactive focus-visible and disabled states, checkbox/radio checked states, and Input LTR/Arabic selections and preedit. They are deterministic command snapshots, not raster screenshots. Platform `Run` uses the same control tree and display list.19+Controls are implemented in `internal/ui`; the root package re-exports their types. Display-list JSON snapshots under `internal/ui/testdata/control-display` cover every control's normal and hover states, interactive focus-visible and disabled states, checkbox/radio checked states, and Input LTR/Arabic selections and preedit. They are deterministic command snapshots, not raster screenshots. `Renderer` uses the same control tree and display list.
20 20
21 The additive UA rules for `checkbox`, `radio`, `slider`, `separator`, `spacer`, and `icon` live in `internal/css/ua.css`. Their dimensions and colors are regular computed CSS and can be overridden by author styles. The control helpers hard-code no paint values; only the indicator painter fixes the shape details listed above.21 The additive UA rules for `checkbox`, `radio`, `slider`, `separator`, `spacer`, and `icon` live in `internal/css/ua.css`. Their dimensions and colors are regular computed CSS and can be overridden by author styles. The control helpers hard-code no paint values; only the indicator painter fixes the shape details listed above.
22 22
M docs/performance.md +3 -14
@@ -29,7 +29,7 @@ The demo-frame benchmark builds the light, comfortable document workspace with a
2929
3030 Natural container measurement prevents nested flex text from collapsing, at a CPU cost that grows with nesting depth. Layout1000 measures about 36% slower than the earlier 656 µs run; allocation count stays at 1,027. A per-layout node/width cache increased time and retained allocation in a trial and is not used. LayoutPaintTree stays near its previous cost. These are correctness/performance tradeoffs, not a claim of universal speed improvement.
3131
32-`TestAllocDemoFrame` checks the 1,100 allocs/op limit with `testing.AllocsPerRun`. `TestAllocRendererSteadyFrame` measures `Renderer.Render` for a view whose text changes through `TextInt`; it measured 244 allocations per steady Render+Released cycle on 2026-10-01 (the guard fails above that baseline), needs a GPU, and is skipped unless `NEFERGUI_RENDER_NODE` is set. Focused tests also check constant quad allocation, bounded paint allocation, allocation-free warm list-buffer reuse, and allocation-free idle wait results. The real-ioctl allocation test is opt-in via `NEFERGUI_RENDER_NODE`. The other table budgets are reference targets, not automated assertions.
32+`TestAllocDemoFrame` checks the 1,100 allocs/op limit with `testing.AllocsPerRun`. `TestAllocRendererSteadyFrame` measures `Renderer.Render` for a view whose text changes through `TextInt`; it measured 244 allocations per steady Render+Released cycle on 2026-10-01 (the guard fails above that baseline), needs a GPU, and is skipped unless `NEFERGUI_RENDER_NODE` is set. Focused tests also check constant quad allocation, bounded paint allocation, allocation-free warm list-buffer reuse and allocation-free idle wait results (the real-ioctl variant is opt-in via `NEFERGUI_RENDER_NODE`). The other table budgets are reference targets, not automated assertions.
3333
3434 Frame construction limits allocation churn through:
3535
@@ -39,14 +39,7 @@ Frame construction limits allocation churn through:
3939 - font family normalization at font load and identity path keys computed once per element;
4040 - `text.Engine.Measure` caching by text, request and width. Entries unused for two frames are evicted; the cache holds at most 4,096 entries. Results are immutable; callers use `Layout.Clone` before adjusting positions.
4141
42-`Renderer.Render` builds a frame only on input or a redraw request. Idle frame construction stops. Expected release-wait timeouts return without constructing errors; real ioctl failures retain their context and wrapped errno.
43-
44-On commit `9064a73`, measured **2026-09-30, 06:34 CEST**, `BenchmarkWaitPointIdle` on an unsignaled real DRM timeline took a median **21,370,548 ns/op, 0 B/op, 0 allocs/op**. This includes the requested 20 ms timeout, not active CPU time:
45-
46-```sh
47-NEFERGUI_RENDER_NODE=/dev/dri/renderD128 CGO_ENABLED=0 GOWORK=off \
48- go test -run '^$' -bench BenchmarkWaitPointIdle -benchtime=10x -benchmem -count=5 ./internal/presentation/syncobj
49-```
42+`Renderer.Render` builds a frame only on input or a redraw request. Idle frame construction stops.
5043
5144 Profile frame construction with:
5245
@@ -77,14 +70,10 @@ An external Go module with minimal-counter and demo views, a 960×640 window at
7770
7871 Heap/RSS/PSS use the default Go profiling rate. Allocation figures use separate `GODEBUG=memprofilerate=1` runs: subtract cumulative profiles at frames 1 and 1,000, exclude stacks containing the measurement sampler or `runtime/pprof`, sum the remaining flat allocated bytes, then divide by 999 frames. These runs recorded about **41–43% less allocation churn** than the baseline. They do not demonstrate an equivalent reduction in RSS or frame latency. Single-run process-memory values vary with runtime and driver behavior.
7972
80-The minimal view starts at about 0.75 MiB live Go heap, reaches 5.1 MiB after the first frame, and returns to about 0.9 MiB after closing. No sustained live-heap growth was observed over 3,000 frames. Retained heap stayed close to baseline; the session's reusable conversion buffers retain a small amount of storage. Driver mappings and Go heap capacity can remain resident after closing; RSS is not a leak measurement on its own.
73+The minimal view starts at about 0.75 MiB live Go heap, reaches 5.1 MiB after the first frame, and returns to about 0.9 MiB after closing. No sustained live-heap growth was observed over 3,000 frames. Retained heap stayed close to baseline; the reusable conversion buffers retain a small amount of storage. Driver mappings and Go heap capacity can remain resident after closing; RSS is not a leak measurement on its own.
8174
8275 DRM counters reported about 41 MiB VRAM and 4 MiB GTT once all three presentation buffers were active, unchanged from baseline and released at shutdown. Window dimensions, scale, fonts and driver affect these figures. These are measurements on one stack, not portable limits.
8376
84-A separate approximately 20-second idle profile recorded about **91 KiB** allocated under `syncobj.WaitPoint` in the baseline and **no allocations attributed to that path** on the measured implementation. Other runtime and consumer instrumentation allocations remain; this is not a claim that the whole process allocates nothing at idle.
85-
86-The measured commit includes `purego-vulkan` v0.6.0, `wlturbo` v0.3.0 and `typesetting` v0.3.5. The subsequent `2e4532d` commit adds an empty-glyph regression test and raises the minimum compositor version checked at startup; it does not change the measured frame paths on this version-6 compositor. The comparison therefore includes both library optimizations and dependency updates, not isolated effects for each.
87-
8877 Prepared quads and layout painting still allocate detached outputs. Editor and indicator command insertion can also reallocate the display list; those UI-owned costs are included in the demo measurements and are not optimized here. Glyph rasterization and glyph-key hashing also appear in cumulative allocation profiles.
8978
9079 Use `pprof`'s `alloc_space` to find cumulative allocation churn and `inuse_space` after GC to inspect retained Go memory. Use `/proc/<pid>/smaps_rollup` for RSS/PSS and DRM fdinfo for GPU counters; heap profiles do not include driver or GPU memory.
@@ -29,7 +29,7 @@ The demo-frame benchmark builds the light, comfortable document workspace with a
29 29
30 Natural container measurement prevents nested flex text from collapsing, at a CPU cost that grows with nesting depth. Layout1000 measures about 36% slower than the earlier 656 µs run; allocation count stays at 1,027. A per-layout node/width cache increased time and retained allocation in a trial and is not used. LayoutPaintTree stays near its previous cost. These are correctness/performance tradeoffs, not a claim of universal speed improvement.30 Natural container measurement prevents nested flex text from collapsing, at a CPU cost that grows with nesting depth. Layout1000 measures about 36% slower than the earlier 656 µs run; allocation count stays at 1,027. A per-layout node/width cache increased time and retained allocation in a trial and is not used. LayoutPaintTree stays near its previous cost. These are correctness/performance tradeoffs, not a claim of universal speed improvement.
31 31
32-`TestAllocDemoFrame` checks the 1,100 allocs/op limit with `testing.AllocsPerRun`. `TestAllocRendererSteadyFrame` measures `Renderer.Render` for a view whose text changes through `TextInt`; it measured 244 allocations per steady Render+Released cycle on 2026-10-01 (the guard fails above that baseline), needs a GPU, and is skipped unless `NEFERGUI_RENDER_NODE` is set. Focused tests also check constant quad allocation, bounded paint allocation, allocation-free warm list-buffer reuse, and allocation-free idle wait results. The real-ioctl allocation test is opt-in via `NEFERGUI_RENDER_NODE`. The other table budgets are reference targets, not automated assertions.32+`TestAllocDemoFrame` checks the 1,100 allocs/op limit with `testing.AllocsPerRun`. `TestAllocRendererSteadyFrame` measures `Renderer.Render` for a view whose text changes through `TextInt`; it measured 244 allocations per steady Render+Released cycle on 2026-10-01 (the guard fails above that baseline), needs a GPU, and is skipped unless `NEFERGUI_RENDER_NODE` is set. Focused tests also check constant quad allocation, bounded paint allocation, allocation-free warm list-buffer reuse and allocation-free idle wait results (the real-ioctl variant is opt-in via `NEFERGUI_RENDER_NODE`). The other table budgets are reference targets, not automated assertions.
33 33
34 Frame construction limits allocation churn through:34 Frame construction limits allocation churn through:
35 35
@@ -39,14 +39,7 @@ Frame construction limits allocation churn through:
39 - font family normalization at font load and identity path keys computed once per element;39 - font family normalization at font load and identity path keys computed once per element;
40 - `text.Engine.Measure` caching by text, request and width. Entries unused for two frames are evicted; the cache holds at most 4,096 entries. Results are immutable; callers use `Layout.Clone` before adjusting positions.40 - `text.Engine.Measure` caching by text, request and width. Entries unused for two frames are evicted; the cache holds at most 4,096 entries. Results are immutable; callers use `Layout.Clone` before adjusting positions.
41 41
42-`Renderer.Render` builds a frame only on input or a redraw request. Idle frame construction stops. Expected release-wait timeouts return without constructing errors; real ioctl failures retain their context and wrapped errno.42+`Renderer.Render` builds a frame only on input or a redraw request. Idle frame construction stops.
43-
44-On commit `9064a73`, measured **2026-09-30, 06:34 CEST**, `BenchmarkWaitPointIdle` on an unsignaled real DRM timeline took a median **21,370,548 ns/op, 0 B/op, 0 allocs/op**. This includes the requested 20 ms timeout, not active CPU time:
45-
46-```sh
47-NEFERGUI_RENDER_NODE=/dev/dri/renderD128 CGO_ENABLED=0 GOWORK=off \
48- go test -run '^$' -bench BenchmarkWaitPointIdle -benchtime=10x -benchmem -count=5 ./internal/presentation/syncobj
49-```
50 43
51 Profile frame construction with:44 Profile frame construction with:
52 45
@@ -77,14 +70,10 @@ An external Go module with minimal-counter and demo views, a 960×640 window at
77 70
78 Heap/RSS/PSS use the default Go profiling rate. Allocation figures use separate `GODEBUG=memprofilerate=1` runs: subtract cumulative profiles at frames 1 and 1,000, exclude stacks containing the measurement sampler or `runtime/pprof`, sum the remaining flat allocated bytes, then divide by 999 frames. These runs recorded about **41–43% less allocation churn** than the baseline. They do not demonstrate an equivalent reduction in RSS or frame latency. Single-run process-memory values vary with runtime and driver behavior.71 Heap/RSS/PSS use the default Go profiling rate. Allocation figures use separate `GODEBUG=memprofilerate=1` runs: subtract cumulative profiles at frames 1 and 1,000, exclude stacks containing the measurement sampler or `runtime/pprof`, sum the remaining flat allocated bytes, then divide by 999 frames. These runs recorded about **41–43% less allocation churn** than the baseline. They do not demonstrate an equivalent reduction in RSS or frame latency. Single-run process-memory values vary with runtime and driver behavior.
79 72
80-The minimal view starts at about 0.75 MiB live Go heap, reaches 5.1 MiB after the first frame, and returns to about 0.9 MiB after closing. No sustained live-heap growth was observed over 3,000 frames. Retained heap stayed close to baseline; the session's reusable conversion buffers retain a small amount of storage. Driver mappings and Go heap capacity can remain resident after closing; RSS is not a leak measurement on its own.73+The minimal view starts at about 0.75 MiB live Go heap, reaches 5.1 MiB after the first frame, and returns to about 0.9 MiB after closing. No sustained live-heap growth was observed over 3,000 frames. Retained heap stayed close to baseline; the reusable conversion buffers retain a small amount of storage. Driver mappings and Go heap capacity can remain resident after closing; RSS is not a leak measurement on its own.
81 74
82 DRM counters reported about 41 MiB VRAM and 4 MiB GTT once all three presentation buffers were active, unchanged from baseline and released at shutdown. Window dimensions, scale, fonts and driver affect these figures. These are measurements on one stack, not portable limits.75 DRM counters reported about 41 MiB VRAM and 4 MiB GTT once all three presentation buffers were active, unchanged from baseline and released at shutdown. Window dimensions, scale, fonts and driver affect these figures. These are measurements on one stack, not portable limits.
83 76
84-A separate approximately 20-second idle profile recorded about **91 KiB** allocated under `syncobj.WaitPoint` in the baseline and **no allocations attributed to that path** on the measured implementation. Other runtime and consumer instrumentation allocations remain; this is not a claim that the whole process allocates nothing at idle.
85-
86-The measured commit includes `purego-vulkan` v0.6.0, `wlturbo` v0.3.0 and `typesetting` v0.3.5. The subsequent `2e4532d` commit adds an empty-glyph regression test and raises the minimum compositor version checked at startup; it does not change the measured frame paths on this version-6 compositor. The comparison therefore includes both library optimizations and dependency updates, not isolated effects for each.
87-
88 Prepared quads and layout painting still allocate detached outputs. Editor and indicator command insertion can also reallocate the display list; those UI-owned costs are included in the demo measurements and are not optimized here. Glyph rasterization and glyph-key hashing also appear in cumulative allocation profiles.77 Prepared quads and layout painting still allocate detached outputs. Editor and indicator command insertion can also reallocate the display list; those UI-owned costs are included in the demo measurements and are not optimized here. Glyph rasterization and glyph-key hashing also appear in cumulative allocation profiles.
89 78
90 Use `pprof`'s `alloc_space` to find cumulative allocation churn and `inuse_space` after GC to inspect retained Go memory. Use `/proc/<pid>/smaps_rollup` for RSS/PSS and DRM fdinfo for GPU counters; heap profiles do not include driver or GPU memory.79 Use `pprof`'s `alloc_space` to find cumulative allocation churn and `inuse_space` after GC to inspect retained Go memory. Use `/proc/<pid>/smaps_rollup` for RSS/PSS and DRM fdinfo for GPU counters; heap profiles do not include driver or GPU memory.
M internal/presentation/buffers/buffers.go +2 -2
@@ -1,5 +1,5 @@
1-// Package buffers models Wayland/Vulkan buffer ownership without GPU dependencies.
2-// The caller serializes events from the renderer, the Wayland dispatch loop, and DRM.
1+// Package buffers models buffer ownership between the renderer and the
2+// compositor without GPU dependencies. The caller serializes all events.
33 package buffers
44
55 import (
@@ -1,5 +1,5 @@
1-// Package buffers models Wayland/Vulkan buffer ownership without GPU dependencies.1+// Package buffers models buffer ownership between the renderer and the
2-// The caller serializes events from the renderer, the Wayland dispatch loop, and DRM.2+// compositor without GPU dependencies. The caller serializes all events.
3 package buffers3 package buffers
4 4
5 import (5 import (
M internal/presentation/syncobj/syncobj.go +3 -4
@@ -118,9 +118,8 @@ func (n *Node) Destroy(handle uint32) error {
118118 return n.ioctl(ioctlDestroy, unsafe.Pointer(&destroyArg{Handle: handle}))
119119 }
120120
121-// ExportTimeline returns a new owned syncobj FD (not a sync_file). The caller
122-// hands it to WLTurbo's ImportTimeline, which closes only after a successful
123-// request send; on send failure the caller must close the FD.
121+// ExportTimeline returns a new owned syncobj FD (not a sync_file). The
122+// caller owns it and closes it, or passes a duplicate to its Wayland client.
124123 func (n *Node) ExportTimeline(handle uint32) (int, error) {
125124 a := handleArg{Handle: handle, FD: -1}
126125 if err := n.ioctl(ioctlHandleToFD, unsafe.Pointer(&a)); err != nil {
@@ -210,7 +209,7 @@ func (n *Node) ReleaseFence(timeline uint32, point uint64) (int, error) {
210209 }
211210
212211 // WaitPoint waits for a submitted release point for at most timeout. Call it
213-// only from a waiter goroutine, never from the presentation loop. WAIT_FOR_SUBMIT
212+// only off the render path: it can block for up to timeout. WAIT_FOR_SUBMIT
214213 // handles the interval before the compositor attaches its fence to the point.
215214 func (n *Node) WaitPoint(handle uint32, point uint64, timeout time.Duration) (bool, error) {
216215 if timeout < 0 || timeout > time.Second {
@@ -118,9 +118,8 @@ func (n *Node) Destroy(handle uint32) error {
118 return n.ioctl(ioctlDestroy, unsafe.Pointer(&destroyArg{Handle: handle}))118 return n.ioctl(ioctlDestroy, unsafe.Pointer(&destroyArg{Handle: handle}))
119 }119 }
120 120
121-// ExportTimeline returns a new owned syncobj FD (not a sync_file). The caller121+// ExportTimeline returns a new owned syncobj FD (not a sync_file). The
122-// hands it to WLTurbo's ImportTimeline, which closes only after a successful122+// caller owns it and closes it, or passes a duplicate to its Wayland client.
123-// request send; on send failure the caller must close the FD.
124 func (n *Node) ExportTimeline(handle uint32) (int, error) {123 func (n *Node) ExportTimeline(handle uint32) (int, error) {
125 a := handleArg{Handle: handle, FD: -1}124 a := handleArg{Handle: handle, FD: -1}
126 if err := n.ioctl(ioctlHandleToFD, unsafe.Pointer(&a)); err != nil {125 if err := n.ioctl(ioctlHandleToFD, unsafe.Pointer(&a)); err != nil {
@@ -210,7 +209,7 @@ func (n *Node) ReleaseFence(timeline uint32, point uint64) (int, error) {
210 }209 }
211 210
212 // WaitPoint waits for a submitted release point for at most timeout. Call it211 // WaitPoint waits for a submitted release point for at most timeout. Call it
213-// only from a waiter goroutine, never from the presentation loop. WAIT_FOR_SUBMIT212+// only off the render path: it can block for up to timeout. WAIT_FOR_SUBMIT
214 // handles the interval before the compositor attaches its fence to the point.213 // handles the interval before the compositor attaches its fence to the point.
215 func (n *Node) WaitPoint(handle uint32, point uint64, timeout time.Duration) (bool, error) {214 func (n *Node) WaitPoint(handle uint32, point uint64, timeout time.Duration) (bool, error) {
216 if timeout < 0 || timeout > time.Second {215 if timeout < 0 || timeout > time.Second {
M internal/render/instances.go +1 -1
@@ -30,7 +30,7 @@ func ListBatches(frame Frame) ([]vkdevice.Instance, []vkdevice.Batch, ListStats)
3030 // The slices it returns alias its storage: they are valid only until the next
3131 // List or Release call, and must not be retained, shared across goroutines, or
3232 // used while another List may run. This is safe for consumers that copy the
33-// data synchronously (Session.tick uploads instances into GPU memory and
33+// data synchronously (Target.submit uploads instances into GPU memory and
3434 // resolve copies the batches before recording returns); anything that keeps the
3535 // slices past that point must use ListBatches instead. Retained frames hold
3636 // only render.Frame, never these slices, so a blocked frame is re-listed on retry.
@@ -30,7 +30,7 @@ func ListBatches(frame Frame) ([]vkdevice.Instance, []vkdevice.Batch, ListStats)
30 // The slices it returns alias its storage: they are valid only until the next30 // The slices it returns alias its storage: they are valid only until the next
31 // List or Release call, and must not be retained, shared across goroutines, or31 // List or Release call, and must not be retained, shared across goroutines, or
32 // used while another List may run. This is safe for consumers that copy the32 // used while another List may run. This is safe for consumers that copy the
33-// data synchronously (Session.tick uploads instances into GPU memory and33+// data synchronously (Target.submit uploads instances into GPU memory and
34 // resolve copies the batches before recording returns); anything that keeps the34 // resolve copies the batches before recording returns); anything that keeps the
35 // slices past that point must use ListBatches instead. Retained frames hold35 // slices past that point must use ListBatches instead. Retained frames hold
36 // only render.Frame, never these slices, so a blocked frame is re-listed on retry.36 // only render.Frame, never these slices, so a blocked frame is re-listed on retry.
M internal/ui/tree.go +1 -1
@@ -1,5 +1,5 @@
11 // Package ui owns the immediate-mode frame tree, controls, editors, input
2-// routing and the Wayland run loop behind the public nefergui facade.
2+// routing and the Renderer behind the public nefergui facade.
33 package ui
44
55 import (
@@ -1,5 +1,5 @@
1 // Package ui owns the immediate-mode frame tree, controls, editors, input1 // Package ui owns the immediate-mode frame tree, controls, editors, input
2-// routing and the Wayland run loop behind the public nefergui facade.2+// routing and the Renderer behind the public nefergui facade.
3 package ui3 package ui
4 4
5 import (5 import (

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 1

Tags 12

↑↓ moveenter openesc close