docs(readme): restore original Go rationale section (#141)
- commit
- 6bfd2d43683c90547b7374e8781a2e2fee8ee839
- author
- bnema · · 2026-10-05 17:43 UTC
- committer
- GitHub · · 2026-10-05 17:43 UTC
- parent
- 06e1112
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
1 file · +24 -16
M README.md +24 -16
| @@ -24,31 +24,39 @@ About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen | |||
| 24 | 24 | ||
| 25 | 25 | ## OK, but why Go? | |
| 26 | 26 | ||
| 27 | - | Most Wayland compositors are written in C, C++ or Rust. The usual objection to Go is the garbage collector. Fair, so here is how NeferWL deals with it. | |
| 27 | + | Most Wayland compositors are written in C (wlroots, Sway, Mutter), C++ (KWin, Hyprland, gamescope) or Rust (niri, COSMIC). The usual objection to Go is the garbage collector. It is a fair one, so here is how NeferWL deals with it, and why Go turns out to fit a compositor well. | |
| 28 | 28 | ||
| 29 | - | A compositor is a pile of streams that never stop: input, client commits, one vblank per monitor, GPU fences, captures, hotplug. A slow client must not delay a flip, and rendering must not delay input. Go gives me cheap goroutines and channels to keep them apart. NeferWL follows one rule throughout: **each piece of state has one owner goroutine, and others talk to it over channels.** | |
| 29 | + | ### A compositor is a concurrency problem | |
| 30 | 30 | ||
| 31 | - | Input, core, the Wayland server and each output run independently. No mutex on window state, no lock ordering to get wrong, and the race detector checks every test. With CAP_SYS_NICE, input and output threads request real-time scheduling, so a busy CPU cannot delay a cursor move or a flip. | |
| 31 | + | A compositor sits in the middle of many independent streams that never stop: keyboard and pointer events, client requests and buffer commits, one vblank and page-flip cycle per monitor, frame fences from the GPU, presentation feedback, screen captures, config reloads, hotplug, idle and lock state. None of them should wait on another. A slow client must not delay a flip, and rendering must not delay input. | |
| 32 | 32 | ||
| 33 | - | ### Keeping GC away from frames | |
| 33 | + | Go has this built in. Goroutines are cheap, channels carry messages between them, and the runtime spreads them over every CPU core. NeferWL follows one idiom everywhere: **each piece of state has one owner goroutine, and others talk to it over channels.** | |
| 34 | 34 | ||
| 35 | - | - **Reuse memory on hot paths.** `make check` includes allocation guards for frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates. | |
| 36 | - | - **Keep buffers off the Go heap.** Client buffers live in shared or GPU memory. The live Go heap is only a few megabytes: playing 4K HDR video with 65 tiled subsurfaces, GC takes under 9 % of a process that itself uses 13 % of a core. | |
| 37 | - | - **Build without cgo.** libwayland, Vulkan and libinput load at runtime through [purego](https://github.com/ebitengine/purego). | |
| 38 | - | - **Render only changes.** Damage tracking limits redraws, opaque surfaces hide work behind them, and idle outputs do nothing. | |
| 39 | - | - **Tune on real hardware.** Under VRR, cursor moves ride on the game's next frame, and game frames keep a short gap after each flip so the panel does not drop to its slowest rate. | |
| 35 | + | - **Input** runs on its own thread, reads libinput and sends events to core. | |
| 36 | + | - **Core** owns windows, workspaces and focus. It turns input and client events into layouts and scenes. | |
| 37 | + | - **The Wayland server** owns client connections. It sends client events and surface content, and receives commands back. | |
| 38 | + | - **Each output** owns its DRM state, renders its scenes with Vulkan, commits frames and reports flips and presentation times. | |
| 39 | + | - **Capture, config, idle, sessions and leases** each run in their own goroutine. | |
| 40 | 40 | ||
| 41 | - | A full build takes seconds, and testing, race detection, profiling and formatting come with Go. That makes it a pleasure to work on. For measurements and the rendering pipeline, see [Performance](docs/performance.md). | |
| 41 | + | About 35 channels connect them, all created in one place, so the full data flow is easy to read. There is no mutex on window state, no lock ordering to get wrong, and the race detector checks every test. When CAP_SYS_NICE is granted, the input and output goroutines lock their OS thread and request real-time scheduling, so a busy CPU cannot delay a cursor move or a flip. | |
| 42 | 42 | ||
| 43 | - | ### Reusable libraries | |
| 43 | + | ### Keeping the garbage collector out of the way | |
| 44 | 44 | ||
| 45 | - | I like bringing more tools to the Go ecosystem, so NeferWL grew its own libraries along the way. Other Go projects can use them: | |
| 45 | + | - **The hot path does not allocate.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse memory owned by the goroutine that handles them. `make check` runs allocation guards that fail the build when one of these paths starts allocating. | |
| 46 | + | - **The GC has almost nothing to collect.** Client buffers live in shared and GPU memory, not on the Go heap, so the live heap is a few megabytes. Playing a 4K HDR video with 65 tiled subsurfaces committing every frame, GC takes under 9 % of a process that itself takes 13 % of a core. See [Performance](docs/performance.md). | |
| 47 | + | - **No cgo.** NeferWL builds with `CGO_ENABLED=0`. libwayland, Vulkan and libinput are loaded at runtime through [purego](https://github.com/ebitengine/purego) with fixed-arity calls: no reflection, no allocation at the boundary. | |
| 48 | + | - **Work only when something changes.** Damage tracking redraws only what changed, surfaces hidden behind opaque ones are skipped, and an idle output does nothing. | |
| 49 | + | - **Tuned on real hardware.** Under VRR, cursor moves ride on the game's next frame instead of forcing their own refresh, and game frames keep a short gap after each flip so the panel does not fall back to its slowest rate. | |
| 50 | + | ||
| 51 | + | ### A pleasure to work on | |
| 52 | + | ||
| 53 | + | A full build takes seconds, and tests, race detection, profiling and formatting come with the language. I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them: | |
| 46 | 54 | ||
| 47 | 55 | - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo. | |
| 48 | - | - [purego-vulkan](https://github.com/bnema/purego-vulkan): Vulkan bindings generated from `vk.xml`, without cgo. | |
| 49 | - | - [go-wayland-bindings](https://github.com/bnema/go-wayland-bindings): protocol bindings generated from upstream XML. | |
| 50 | - | - [wlturbo](https://github.com/bnema/wlturbo): a Wayland client library. | |
| 51 | - | - [neferclient](https://github.com/bnema/neferclient): a client toolkit for connections, outputs, surface roles, seats and dmabuf presentation. | |
| 56 | + | - [purego-vulkan](https://github.com/bnema/purego-vulkan): Vulkan without cgo, generated from `vk.xml`. | |
| 57 | + | - [go-wayland-bindings](https://github.com/bnema/go-wayland-bindings): Wayland protocol bindings, generated from the upstream XML. | |
| 58 | + | - [wlturbo](https://github.com/bnema/wlturbo): a fast Wayland client. | |
| 59 | + | - [neferclient](https://github.com/bnema/neferclient): a client toolkit on top of them: connection, outputs, surface roles, seat and dmabuf presentation. | |
| 52 | 60 | ||
| 53 | 61 | ## Features | |
| 54 | 62 | ||
| @@ -24,31 +24,39 @@ About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen | |||||
| 24 | 24 | ||||
| 25 | ## OK, but why Go? | 25 | ## OK, but why Go? | ||
| 26 | 26 | ||||
| 27 | - | Most Wayland compositors are written in C, C++ or Rust. The usual objection to Go is the garbage collector. Fair, so here is how NeferWL deals with it. | 27 | + | Most Wayland compositors are written in C (wlroots, Sway, Mutter), C++ (KWin, Hyprland, gamescope) or Rust (niri, COSMIC). The usual objection to Go is the garbage collector. It is a fair one, so here is how NeferWL deals with it, and why Go turns out to fit a compositor well. |
| 28 | 28 | ||||
| 29 | - | A compositor is a pile of streams that never stop: input, client commits, one vblank per monitor, GPU fences, captures, hotplug. A slow client must not delay a flip, and rendering must not delay input. Go gives me cheap goroutines and channels to keep them apart. NeferWL follows one rule throughout: **each piece of state has one owner goroutine, and others talk to it over channels.** | 29 | + | ### A compositor is a concurrency problem |
| 30 | 30 | ||||
| 31 | - | Input, core, the Wayland server and each output run independently. No mutex on window state, no lock ordering to get wrong, and the race detector checks every test. With CAP_SYS_NICE, input and output threads request real-time scheduling, so a busy CPU cannot delay a cursor move or a flip. | 31 | + | A compositor sits in the middle of many independent streams that never stop: keyboard and pointer events, client requests and buffer commits, one vblank and page-flip cycle per monitor, frame fences from the GPU, presentation feedback, screen captures, config reloads, hotplug, idle and lock state. None of them should wait on another. A slow client must not delay a flip, and rendering must not delay input. |
| 32 | 32 | ||||
| 33 | - | ### Keeping GC away from frames | 33 | + | Go has this built in. Goroutines are cheap, channels carry messages between them, and the runtime spreads them over every CPU core. NeferWL follows one idiom everywhere: **each piece of state has one owner goroutine, and others talk to it over channels.** |
| 34 | 34 | ||||
| 35 | - | - **Reuse memory on hot paths.** `make check` includes allocation guards for frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates. | 35 | + | - **Input** runs on its own thread, reads libinput and sends events to core. |
| 36 | - | - **Keep buffers off the Go heap.** Client buffers live in shared or GPU memory. The live Go heap is only a few megabytes: playing 4K HDR video with 65 tiled subsurfaces, GC takes under 9 % of a process that itself uses 13 % of a core. | 36 | + | - **Core** owns windows, workspaces and focus. It turns input and client events into layouts and scenes. |
| 37 | - | - **Build without cgo.** libwayland, Vulkan and libinput load at runtime through [purego](https://github.com/ebitengine/purego). | 37 | + | - **The Wayland server** owns client connections. It sends client events and surface content, and receives commands back. |
| 38 | - | - **Render only changes.** Damage tracking limits redraws, opaque surfaces hide work behind them, and idle outputs do nothing. | 38 | + | - **Each output** owns its DRM state, renders its scenes with Vulkan, commits frames and reports flips and presentation times. |
| 39 | - | - **Tune on real hardware.** Under VRR, cursor moves ride on the game's next frame, and game frames keep a short gap after each flip so the panel does not drop to its slowest rate. | 39 | + | - **Capture, config, idle, sessions and leases** each run in their own goroutine. |
| 40 | 40 | ||||
| 41 | - | A full build takes seconds, and testing, race detection, profiling and formatting come with Go. That makes it a pleasure to work on. For measurements and the rendering pipeline, see [Performance](docs/performance.md). | 41 | + | About 35 channels connect them, all created in one place, so the full data flow is easy to read. There is no mutex on window state, no lock ordering to get wrong, and the race detector checks every test. When CAP_SYS_NICE is granted, the input and output goroutines lock their OS thread and request real-time scheduling, so a busy CPU cannot delay a cursor move or a flip. |
| 42 | 42 | ||||
| 43 | - | ### Reusable libraries | 43 | + | ### Keeping the garbage collector out of the way |
| 44 | 44 | ||||
| 45 | - | I like bringing more tools to the Go ecosystem, so NeferWL grew its own libraries along the way. Other Go projects can use them: | 45 | + | - **The hot path does not allocate.** Frame callbacks, presentation reports, DRM flips, Vulkan submissions and surface updates reuse memory owned by the goroutine that handles them. `make check` runs allocation guards that fail the build when one of these paths starts allocating. |
| 46 | + | - **The GC has almost nothing to collect.** Client buffers live in shared and GPU memory, not on the Go heap, so the live heap is a few megabytes. Playing a 4K HDR video with 65 tiled subsurfaces committing every frame, GC takes under 9 % of a process that itself takes 13 % of a core. See [Performance](docs/performance.md). | |||
| 47 | + | - **No cgo.** NeferWL builds with `CGO_ENABLED=0`. libwayland, Vulkan and libinput are loaded at runtime through [purego](https://github.com/ebitengine/purego) with fixed-arity calls: no reflection, no allocation at the boundary. | |||
| 48 | + | - **Work only when something changes.** Damage tracking redraws only what changed, surfaces hidden behind opaque ones are skipped, and an idle output does nothing. | |||
| 49 | + | - **Tuned on real hardware.** Under VRR, cursor moves ride on the game's next frame instead of forcing their own refresh, and game frames keep a short gap after each flip so the panel does not fall back to its slowest rate. | |||
| 50 | + | ||||
| 51 | + | ### A pleasure to work on | |||
| 52 | + | ||||
| 53 | + | A full build takes seconds, and tests, race detection, profiling and formatting come with the language. I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them: | |||
| 46 | 54 | ||||
| 47 | - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo. | 55 | - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo. | ||
| 48 | - | - [purego-vulkan](https://github.com/bnema/purego-vulkan): Vulkan bindings generated from `vk.xml`, without cgo. | 56 | + | - [purego-vulkan](https://github.com/bnema/purego-vulkan): Vulkan without cgo, generated from `vk.xml`. |
| 49 | - | - [go-wayland-bindings](https://github.com/bnema/go-wayland-bindings): protocol bindings generated from upstream XML. | 57 | + | - [go-wayland-bindings](https://github.com/bnema/go-wayland-bindings): Wayland protocol bindings, generated from the upstream XML. |
| 50 | - | - [wlturbo](https://github.com/bnema/wlturbo): a Wayland client library. | 58 | + | - [wlturbo](https://github.com/bnema/wlturbo): a fast Wayland client. |
| 51 | - | - [neferclient](https://github.com/bnema/neferclient): a client toolkit for connections, outputs, surface roles, seats and dmabuf presentation. | 59 | + | - [neferclient](https://github.com/bnema/neferclient): a client toolkit on top of them: connection, outputs, surface roles, seat and dmabuf presentation. |
| 52 | 60 | ||||
| 53 | ## Features | 61 | ## Features | ||
| 54 | 62 | ||||