detached 1e24fe6

merge: game-perf (zero-copy game path)

commit
1e24fe6016e61ad21ea90e582b46a1cca5ad8172
author
brice · · 2026-09-26 14:00 UTC
merge of
1d3dfca + aadcd19 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
82 files · +9455 -1931
M .mockery.yml +7 -2
@@ -18,11 +18,11 @@ packages:
1818 # Unexported adapter seams: mocks live next to the code, in test files.
1919 github.com/bnema/nefertty/internal/adapters/drm:
2020 interfaces:
21- cursorPlane:
21+ kms:
2222 config:
2323 dir: internal/adapters/drm
2424 pkgname: drm
25- filename: cursor_plane_mock_test.go
25+ filename: kms_mock_test.go
2626 github.com/bnema/nefertty/internal/adapters/wayland:
2727 interfaces:
2828 procEnv:
@@ -30,6 +30,11 @@ packages:
3030 dir: internal/adapters/wayland
3131 pkgname: wayland
3232 filename: proc_env_mock_test.go
33+ syncobjDevice:
34+ config:
35+ dir: internal/adapters/wayland
36+ pkgname: wayland
37+ filename: syncobj_device_mock_test.go
3338 github.com/bnema/nefertty/internal/adapters/statefile:
3439 interfaces:
3540 procTree:
M README.md +3 -1
@@ -6,8 +6,10 @@ Requires Linux and Go 1.27.
66
77 ## Hardware support
88
9+NeferTTY needs a GPU and kernel driver with atomic KMS and a Vulkan 1.3 device that can export images as dmabufs; without them it stops at startup with an error. Frames are composed on the GPU only: no CPU copy and no CPU wait for the GPU before a page flip.
10+
911 - **AMD GPUs:** developed and tested on AMD (radv).
10-- **NVIDIA GPUs:** untested. NVIDIA drivers need explicit sync (`linux-drm-syncobj`), which NeferTTY does not implement yet. Testers with NVIDIA hardware are welcome to report issues or contribute.
12+- **NVIDIA GPUs:** untested. Explicit sync (`linux-drm-syncobj`), which NVIDIA drivers need, is implemented. Testers with NVIDIA hardware are welcome to report issues or contribute.
1113 - **Intel GPUs:** untested.
1214
1315 Build: `make build`.
M cmd/nefertty/main.go +1 -1
@@ -119,7 +119,7 @@ func run() error {
119119 noTerminal := flags.Bool("no-terminal", false, "skip initial terminal")
120120 noXwayland := flags.Bool("no-xwayland", false, "no X11 display for X11 apps")
121121 timeout := flags.Duration("timeout", 0, "duration before exit (0 disables timeout)")
122- debugFlag := flags.String("debug", "", "debug components (comma-separated or all)")
122+ debugFlag := flags.String("debug", "", "debug components (comma-separated or all; input-motion logs every pointer motion)")
123123 configFlag := flags.String("config", "", "config path (empty uses XDG default)")
124124 session := flags.Bool("session", false, "run as the login session: share WAYLAND_DISPLAY and DISPLAY with systemd and D-Bus user services")
125125 if err := flags.Parse(os.Args[1:]); err != nil {
M go.mod +1 -1
@@ -4,7 +4,7 @@ go 1.27
44
55 require (
66 github.com/bnema/purego v0.11.0-bnema.4
7- github.com/bnema/purego-libwayland v0.1.1
7+ github.com/bnema/purego-libwayland v0.2.0
88 github.com/bnema/purego-vulkan v0.4.1
99 github.com/bnema/wlturbo v0.2.0
1010 github.com/bnema/zerowrap v1.4.1
M go.sum +2 -8 generated file: collapsed
A internal/adapters/drm/atomic.go +372 -0
@@ -0,0 +1,372 @@
1+package drm
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "runtime"
7+ "slices"
8+ "sync"
9+ "time"
10+ "unsafe"
11+
12+ "github.com/bnema/nefertty/internal/ports"
13+)
14+
15+// Atomic KMS (ADR 015): every change on screen is one atomic commit. A
16+// frame commit flips the primary plane, moves the cursor plane and sets
17+// VRR together; the kernel waits for the frame's GPU fence (IN_FENCE_FD)
18+// before flipping, so no thread waits for the GPU or a vblank.
19+
20+const (
21+ ioctlSetClientCap = 0x4010640D // DRM_IOW('d', 0x0D, struct drm_set_client_cap)
22+ ioctlGetPlaneRes = 0xC01064B5 // DRM_IOWR('d', 0xB5, struct drm_mode_get_plane_res)
23+ ioctlGetPlane = 0xC02064B6 // DRM_IOWR('d', 0xB6, struct drm_mode_get_plane)
24+ ioctlGetPropBlob = 0xC01064AC // DRM_IOWR('d', 0xAC, struct drm_mode_get_blob)
25+ ioctlAtomic = 0xC03864BC // DRM_IOWR('d', 0xBC, struct drm_mode_atomic)
26+ ioctlCreateBlob = 0xC01064BD // DRM_IOWR('d', 0xBD, struct drm_mode_create_blob)
27+ ioctlDestroyBlob = 0xC00464BE // DRM_IOWR('d', 0xBE, struct drm_mode_destroy_blob)
28+
29+ clientCapUniversalPlanes = 2
30+ clientCapAtomic = 3
31+ capTimestampMonotonic = 0x6
32+ capAtomicAsync = 0x15
33+
34+ objPlane = 0xeeeeeeee
35+
36+ flipEventFlag = 0x1 // DRM_MODE_PAGE_FLIP_EVENT
37+ flipAsyncFlag = 0x2 // DRM_MODE_PAGE_FLIP_ASYNC
38+ atomicTestOnly = 0x100 // DRM_MODE_ATOMIC_TEST_ONLY
39+ atomicNonblock = 0x200 // DRM_MODE_ATOMIC_NONBLOCK
40+ atomicAllowModes = 0x400 // DRM_MODE_ATOMIC_ALLOW_MODESET
41+
42+ planeOverlay = 0
43+ planePrimary = 1
44+ planeCursor = 2
45+)
46+
47+type setClientCap struct{ capability, value uint64 }
48+
49+type getPlaneRes struct {
50+ planeIDs uint64
51+ count, _ uint32
52+}
53+
54+type getPlane struct {
55+ planeID, crtcID, fbID, possibleCrtcs, gammaSize, countFormats uint32
56+ formats uint64
57+}
58+
59+type getBlob struct {
60+ blobID, length uint32
61+ data uint64
62+}
63+
64+type createBlob struct {
65+ data uint64
66+ length, blobID uint32
67+}
68+
69+type modeAtomic struct {
70+ flags, countObjs uint32
71+ objs, countProps, props, values, reserved, userData uint64
72+}
73+
74+// kms is the kernel side of the output path. kmsDevice is the real one;
75+// tests use its Mockery mock.
76+type kms interface {
77+ // commit issues DRM_IOCTL_MODE_ATOMIC; userData comes back in the flip event.
78+ commit(req *atomicReq, flags uint32, userData uint64) error
79+ createBlob(data []byte) (uint32, error)
80+ destroyBlob(id uint32) error
81+ getBlob(id uint32) ([]byte, error)
82+ // planes lists every plane with the CRTCs it can show on.
83+ planes() ([]planeRes, error)
84+ objProps(obj, typ uint32) (map[string][2]uint64, error)
85+ // addFB imports a single-plane dmabuf as a framebuffer of format.
86+ addFB(b *ports.DMABuf, format uint32) (uint32, error)
87+ rmFB(id uint32) error
88+}
89+
90+// planeRes is one plane of the card: possible is a bitmask of CRTC indices.
91+type planeRes struct {
92+ id, possible uint32
93+ // formats is the plane's format list (GETPLANE): used, all linear,
94+ // when the plane has no IN_FORMATS (drivers without modifiers).
95+ formats []uint32
96+}
97+
98+// atomicReq collects (object, property, value) triples in insertion order.
99+type atomicReq struct {
100+ objs []uint32
101+ props [][]propValue
102+}
103+
104+type propValue struct {
105+ prop uint32
106+ val uint64
107+}
108+
109+// set adds a property; unknown properties (id 0) are skipped.
110+func (r *atomicReq) set(obj, prop uint32, v uint64) {
111+ if prop == 0 || obj == 0 {
112+ return
113+ }
114+ i := slices.Index(r.objs, obj)
115+ if i < 0 {
116+ r.objs = append(r.objs, obj)
117+ r.props = append(r.props, nil)
118+ i = len(r.objs) - 1
119+ }
120+ for j, p := range r.props[i] {
121+ if p.prop == prop {
122+ r.props[i][j].val = v
123+ return
124+ }
125+ }
126+ r.props[i] = append(r.props[i], propValue{prop, v})
127+}
128+
129+// value returns a property set on an object.
130+func (r *atomicReq) value(obj, prop uint32) (uint64, bool) {
131+ i := slices.Index(r.objs, obj)
132+ if i < 0 {
133+ return 0, false
134+ }
135+ for _, p := range r.props[i] {
136+ if p.prop == prop {
137+ return p.val, true
138+ }
139+ }
140+ return 0, false
141+}
142+
143+// kmsDevice drives a card fd.
144+type kmsDevice struct {
145+ fd int
146+ // gemMu serialises GEM handle import and close: handles are shared by
147+ // every import of a buffer on the fd, and outputs run on their own
148+ // goroutines. modifiers is DRM_CAP_ADDFB2_MODIFIERS.
149+ gemMu *sync.Mutex
150+ modifiers bool
151+}
152+
153+func (k kmsDevice) commit(req *atomicReq, flags uint32, userData uint64) error {
154+ if len(req.objs) == 0 {
155+ return errors.New("empty atomic commit")
156+ }
157+ counts := make([]uint32, len(req.objs))
158+ var props []uint32
159+ var vals []uint64
160+ for i, ps := range req.props {
161+ counts[i] = uint32(len(ps))
162+ for _, p := range ps {
163+ props = append(props, p.prop)
164+ vals = append(vals, p.val)
165+ }
166+ }
167+ a := modeAtomic{flags: flags, countObjs: uint32(len(req.objs)), userData: userData,
168+ objs: uint64(uintptr(unsafe.Pointer(&req.objs[0]))), countProps: uint64(uintptr(unsafe.Pointer(&counts[0]))),
169+ props: uint64(uintptr(unsafe.Pointer(&props[0]))), values: uint64(uintptr(unsafe.Pointer(&vals[0])))}
170+ err := ioctl(k.fd, ioctlAtomic, unsafe.Pointer(&a))
171+ runtime.KeepAlive(req)
172+ runtime.KeepAlive(counts)
173+ runtime.KeepAlive(props)
174+ runtime.KeepAlive(vals)
175+ return err
176+}
177+
178+func (k kmsDevice) createBlob(data []byte) (uint32, error) {
179+ b := createBlob{data: uint64(uintptr(unsafe.Pointer(&data[0]))), length: uint32(len(data))}
180+ err := ioctl(k.fd, ioctlCreateBlob, unsafe.Pointer(&b))
181+ runtime.KeepAlive(data)
182+ return b.blobID, err
183+}
184+
185+func (k kmsDevice) destroyBlob(id uint32) error {
186+ v := id
187+ return ioctl(k.fd, ioctlDestroyBlob, unsafe.Pointer(&v))
188+}
189+
190+func (k kmsDevice) getBlob(id uint32) ([]byte, error) {
191+ b := getBlob{blobID: id}
192+ if err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b)); err != nil {
193+ return nil, err
194+ }
195+ if b.length == 0 {
196+ return nil, nil
197+ }
198+ data := make([]byte, b.length)
199+ b.data = uint64(uintptr(unsafe.Pointer(&data[0])))
200+ err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b))
201+ runtime.KeepAlive(data)
202+ return data[:min(int(b.length), len(data))], err
203+}
204+
205+func (k kmsDevice) planes() ([]planeRes, error) {
206+ var r getPlaneRes
207+ if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil {
208+ return nil, err
209+ }
210+ if r.count == 0 {
211+ return nil, nil
212+ }
213+ ids := make([]uint32, r.count)
214+ r.planeIDs = uint64(uintptr(unsafe.Pointer(&ids[0])))
215+ if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil {
216+ return nil, err
217+ }
218+ runtime.KeepAlive(ids)
219+ var out []planeRes
220+ for _, id := range ids[:min(int(r.count), len(ids))] {
221+ p := getPlane{planeID: id}
222+ if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) != nil {
223+ continue
224+ }
225+ res := planeRes{id: id, possible: p.possibleCrtcs}
226+ if p.countFormats > 0 {
227+ fs := make([]uint32, p.countFormats)
228+ p.formats = uint64(uintptr(unsafe.Pointer(&fs[0])))
229+ if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) == nil {
230+ res.formats = fs[:min(int(p.countFormats), len(fs))]
231+ }
232+ runtime.KeepAlive(fs)
233+ }
234+ out = append(out, res)
235+ }
236+ return out, nil
237+}
238+
239+func (k kmsDevice) objProps(obj, typ uint32) (map[string][2]uint64, error) {
240+ return objProps(k.fd, obj, typ)
241+}
242+
243+func (k kmsDevice) rmFB(id uint32) error {
244+ v := id
245+ return ioctl(k.fd, ioctlRmFB, unsafe.Pointer(&v))
246+}
247+
248+// enableAtomic turns on atomic modesetting and checks the caps outputs rely on.
249+func enableAtomic(fd int) error {
250+ for _, c := range []uint64{clientCapUniversalPlanes, clientCapAtomic} {
251+ v := setClientCap{capability: c, value: 1}
252+ if err := ioctl(fd, ioctlSetClientCap, unsafe.Pointer(&v)); err != nil {
253+ return err
254+ }
255+ }
256+ if !hasCap(fd, capTimestampMonotonic) {
257+ return errors.New("flip timestamps are not CLOCK_MONOTONIC")
258+ }
259+ return nil
260+}
261+
262+func hasCap(fd int, c uint64) bool {
263+ v := getCap{capability: c}
264+ return ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value == 1
265+}
266+
267+// plane is a KMS plane an output drives, with its property IDs by name.
268+type plane struct {
269+ id, typ uint32
270+ props map[string]uint32
271+ crtc uint32 // CRTC_ID when read
272+ formats []ports.DMABufFormat
273+ // zposValue is the plane's zpos when read (its "zpos" property).
274+ zposValue uint64
275+}
276+
277+func (p *plane) prop(name string) uint32 {
278+ if p == nil {
279+ return 0
280+ }
281+ return p.props[name]
282+}
283+
284+// readPlanes returns the planes that can show on the CRTC at index pipe.
285+func readPlanes(k kms, pipe int) ([]*plane, error) {
286+ res, err := k.planes()
287+ if err != nil {
288+ return nil, err
289+ }
290+ var out []*plane
291+ for _, r := range res {
292+ if r.possible&(1<<pipe) == 0 {
293+ continue
294+ }
295+ props, err := k.objProps(r.id, objPlane)
296+ if err != nil {
297+ continue
298+ }
299+ p := &plane{id: r.id, typ: uint32(props["type"][1]), props: map[string]uint32{}, crtc: uint32(props["CRTC_ID"][1]), zposValue: props["zpos"][1]}
300+ for name, v := range props {
301+ p.props[name] = uint32(v[0])
302+ }
303+ if blob := uint32(props["IN_FORMATS"][1]); blob != 0 {
304+ if data, err := k.getBlob(blob); err == nil {
305+ p.formats = parseInFormats(data)
306+ }
307+ }
308+ if p.formats == nil {
309+ for _, f := range r.formats {
310+ p.formats = append(p.formats, ports.DMABufFormat{Format: f})
311+ }
312+ }
313+ out = append(out, p)
314+ }
315+ return out, nil
316+}
317+
318+// parseInFormats decodes an IN_FORMATS blob (struct drm_format_modifier_blob)
319+// into format and modifier pairs.
320+func parseInFormats(b []byte) []ports.DMABufFormat {
321+ if len(b) < 24 {
322+ return nil
323+ }
324+ le := binary.LittleEndian
325+ nf, fo := int(le.Uint32(b[8:])), int(le.Uint32(b[12:]))
326+ nm, mo := int(le.Uint32(b[16:])), int(le.Uint32(b[20:]))
327+ if fo < 0 || fo+nf*4 > len(b) || mo < 0 || mo+nm*24 > len(b) {
328+ return nil
329+ }
330+ var out []ports.DMABufFormat
331+ for m := range nm {
332+ e := b[mo+m*24:]
333+ mask, offset, mod := le.Uint64(e), int(le.Uint32(e[8:])), le.Uint64(e[16:])
334+ for bit := range 64 {
335+ i := offset + bit
336+ if mask&(1<<bit) == 0 || i >= nf {
337+ continue
338+ }
339+ out = append(out, ports.DMABufFormat{Format: le.Uint32(b[fo+i*4:]), Modifier: mod})
340+ }
341+ }
342+ return out
343+}
344+
345+// flipEvent is a completed commit on a CRTC: when is its CLOCK_MONOTONIC
346+// time, seq the CRTC's vblank counter, user the commit's userData.
347+type flipEvent struct {
348+ crtc uint32
349+ user uint64
350+ when time.Duration
351+ seq uint32
352+}
353+
354+// parseFlips parses DRM events read from the card fd.
355+func parseFlips(buf []byte) []flipEvent {
356+ var out []flipEvent
357+ le := binary.LittleEndian
358+ for len(buf) >= 8 {
359+ typ := le.Uint32(buf)
360+ length := int(le.Uint32(buf[4:]))
361+ if length < 8 || length > len(buf) {
362+ break
363+ }
364+ // struct drm_event_vblank: base(8) user_data(8) sec usec sequence crtc_id.
365+ if typ == eventFlipDone && length >= 32 {
366+ sec, usec := le.Uint32(buf[16:]), le.Uint32(buf[20:])
367+ out = append(out, flipEvent{user: le.Uint64(buf[8:]), when: time.Duration(sec)*time.Second + time.Duration(usec)*time.Microsecond, seq: le.Uint32(buf[24:]), crtc: le.Uint32(buf[28:])})
368+ }
369+ buf = buf[length:]
370+ }
371+ return out
372+}
M internal/adapters/drm/card.go +63 -27
@@ -6,15 +6,16 @@ import (
66 "fmt"
77 "slices"
88 "sync"
9- "unsafe"
9+ "sync/atomic"
1010
11+ "github.com/bnema/nefertty/internal/ports"
1112 "github.com/bnema/zerowrap"
1213 "golang.org/x/sys/unix"
1314 )
1415
1516 // Card is one DRM device and the outputs it drives. Scan finds connected
16-// connectors; each gets its own Output (CRTC, buffers, cursor) run by one
17-// goroutine. One reader goroutine reads page-flip events for all of them
17+// connectors; each gets its own Output (CRTC, planes, cursor) run by one
18+// goroutine. One reader goroutine reads commit events for all of them
1819 // and hands each to its output by CRTC.
1920 type Card struct {
2021 fd int
@@ -24,35 +25,54 @@ type Card struct {
2425 crtcs []uint32
2526 // outputs by connector name; owned by the goroutine that calls Scan.
2627 outputs map[string]*Output
27- flips map[uint32]chan int // by CRTC; set before the reader starts
28- // gemMu serialises GEM handle import and close on fd (outputs run on
29- // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS.
30- gemMu sync.Mutex
31- modifiers bool
32- // async is DRM_CAP_ASYNC_PAGE_FLIP.
28+ flips map[uint32]chan flipEvent // by CRTC; set before the reader starts
29+ k kmsDevice
30+ // taken are planes driven by an output, by plane ID.
31+ taken map[uint32]bool
32+ // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP.
3333 async bool
34+ // serials numbers the commits of every output (event userData).
35+ serials atomic.Uint64
36+ // formats receives each output's direct scanout formats (nil: none
37+ // sent); set before the first Scan.
38+ formats chan<- ports.OutputFormats
3439 }
3540
36-// OpenCard reads the card's CRTCs. fd stays owned by the caller.
41+// OpenCard turns on atomic modesetting and reads the card's CRTCs. fd
42+// stays owned by the caller. A card without atomic KMS is refused.
3743 func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error) {
44+ if err := enableAtomic(fd); err != nil {
45+ return nil, fmt.Errorf("drm: atomic modesetting unsupported by %s: %w", path, err)
46+ }
3847 crtcs, _, err := resources(fd)
3948 if err != nil {
4049 return nil, fmt.Errorf("%s: %w", path, err)
4150 }
42- flips := make(map[uint32]chan int, len(crtcs))
51+ flips := make(map[uint32]chan flipEvent, len(crtcs))
4352 for _, c := range crtcs {
44- flips[c] = make(chan int, 4)
53+ flips[c] = make(chan flipEvent, 4)
4554 }
46- cp := getCap{capability: capAddFB2Modifiers}
47- mods := ioctl(fd, ioctlGetCap, unsafe.Pointer(&cp)) == nil && cp.value == 1
48- ap := getCap{capability: capAsyncPageFlip}
49- async := ioctl(fd, ioctlGetCap, unsafe.Pointer(&ap)) == nil && ap.value == 1
50- return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, modifiers: mods, async: async}, nil
55+ k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)}
56+ return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync)}, nil
5157 }
5258
5359 // Path is the device path, e.g. /dev/dri/card1.
5460 func (c *Card) Path() string { return c.path }
5561
62+// Device is the card's dev_t, the KMS device clients allocate scanout
63+// buffers for (0 when unknown).
64+func (c *Card) Device() uint64 {
65+ var st unix.Stat_t
66+ if unix.Fstat(c.fd, &st) != nil {
67+ return 0
68+ }
69+ return st.Rdev
70+}
71+
72+// SetFormats sets where outputs report their direct scanout formats.
73+// Call before the first Scan.
74+func (c *Card) SetFormats(ch chan<- ports.OutputFormats) { c.formats = ch }
75+
5676 // SetWant replaces the connector config; the next Scan applies it.
5777 func (c *Card) SetWant(w Want) { c.want = w }
5878
@@ -103,9 +123,16 @@ func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) {
103123 return added, removed, replaced, nil
104124 }
105125
106-// Release forgets an output closed by its goroutine, freeing its CRTC for the
107-// next Scan.
108-func (c *Card) Release(name string) { delete(c.outputs, name) }
126+// Release forgets an output closed by its goroutine, freeing its CRTC and
127+// planes for the next Scan.
128+func (c *Card) Release(name string) {
129+ if o := c.outputs[name]; o != nil {
130+ for _, p := range o.owned() {
131+ delete(c.taken, p.id)
132+ }
133+ }
134+ delete(c.outputs, name)
135+}
109136
110137 // Outputs returns the driven outputs.
111138 func (c *Card) Outputs() []*Output {
@@ -131,11 +158,19 @@ func (c *Card) open(conn connector, mode modeInfo) (*Output, error) {
131158 if err != nil {
132159 return nil, err
133160 }
134- return newOutput(c, conn, mode, crtc)
161+ o, err := newOutput(c, conn, mode, crtc)
162+ if err != nil {
163+ return nil, err
164+ }
165+ for _, p := range o.owned() {
166+ c.taken[p.id] = true
167+ }
168+ return o, nil
135169 }
136170
137-// ReadEvents reads page-flip events until ctx ends and routes them to the
138-// output of their CRTC. Run it once per card.
171+// ReadEvents reads commit events until ctx ends and routes them to the
172+// output of their CRTC. Run it once per card. An event is never dropped:
173+// the output owes it a pending commit.
139174 func (c *Card) ReadEvents(ctx context.Context) error {
140175 buf := make([]byte, 1024)
141176 for ctx.Err() == nil {
@@ -154,14 +189,15 @@ func (c *Card) ReadEvents(ctx context.Context) error {
154189 }
155190 return fmt.Errorf("drm read: %w", err)
156191 }
157- for _, crtc := range flipCrtcs(buf[:m]) {
158- ch := c.flips[crtc]
192+ for _, ev := range parseFlips(buf[:m]) {
193+ ch := c.flips[ev.crtc]
159194 if ch == nil {
160195 continue
161196 }
162197 select {
163- case ch <- 1:
164- default: // the output is busy; the flip is counted on its next event
198+ case ch <- ev:
199+ case <-ctx.Done():
200+ return nil
165201 }
166202 }
167203 }
A internal/adapters/drm/commit_test.go +410 -0
@@ -0,0 +1,410 @@
1+package drm
2+
3+import (
4+ "context"
5+ "os"
6+ "testing"
7+ "time"
8+
9+ portsmocks "github.com/bnema/nefertty/internal/mocks/ports"
10+ "github.com/bnema/nefertty/internal/ports"
11+ "github.com/stretchr/testify/mock"
12+ "golang.org/x/sys/unix"
13+)
14+
15+// An event queued before a modeset (or for an earlier output on the CRTC)
16+// must not complete the commit that follows.
17+func TestStaleEventDoesNotCompleteNewCommit(t *testing.T) {
18+ o, _, commits := testOutput(t)
19+ if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
20+ t.Fatal(err)
21+ }
22+ old := eventOf((*commits)[0])
23+ o.pending = false // a modeset dropped the commit; its event is queued
24+ if err := o.commitFrame(71, nil, false, false, pendingFrame{}); err != nil {
25+ t.Fatal(err)
26+ }
27+ if o.completed(old, nil) || !o.pending {
28+ t.Fatal("stale event completed the new commit")
29+ }
30+ if !o.completed(eventOf((*commits)[1]), nil) || o.pending {
31+ t.Fatal("own event did not complete the commit")
32+ }
33+}
34+
35+// After EBUSY no commit of ours is known: the next event of the CRTC ends
36+// the wait, then the frame commits.
37+func TestBusyFrameCommitsAfterEvent(t *testing.T) {
38+ o, _, commits := testOutput(t, unix.EBUSY)
39+ enabled := true
40+ if err := o.commitFrame(70, nil, false, false, pendingFrame{}); !o.commitFailed(err, &enabled) {
41+ t.Fatalf("EBUSY not handled: %v", err)
42+ }
43+ if !o.completed(flipEvent{crtc: tCrtc, user: 12345 << userKindBits}, nil) || o.pending {
44+ t.Fatal("EBUSY wait not ended by the CRTC's event")
45+ }
46+ if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil || len(*commits) != 2 || !o.pending {
47+ t.Fatalf("frame after EBUSY: %v, %d commits", err, len(*commits))
48+ }
49+}
50+
51+// A cursor image change writes only an image the plane neither shows nor
52+// is about to show; with both busy it waits for the event.
53+func TestCursorImageNeverOverwritesScreen(t *testing.T) {
54+ o, _, _ := testOutput(t)
55+ c := o.cursor
56+ r := portsmocks.NewMockRenderer(t)
57+ var written []int
58+ r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
59+ written = append(written, i)
60+ return nil
61+ })
62+ px := []byte{1, 2, 3, 4}
63+ // Image 0 on screen: two changes before a commit both go to image 1.
64+ c.screen, c.cur, c.image = 90, 0, true
65+ for range 2 {
66+ if err := c.setImage(r, px, 1, 1, 0, 0); err != nil {
67+ t.Fatal(err)
68+ }
69+ }
70+ if len(written) != 2 || written[0] != 1 || written[1] != 1 {
71+ t.Fatalf("written %v", written)
72+ }
73+ // Image 1 in flight, image 0 on screen: nothing is free.
74+ c.committed(cursorState{on: true, fb: 91})
75+ if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil {
76+ t.Fatalf("wrote a busy image: %v %v", err, written)
77+ }
78+ // The flip frees image 0.
79+ c.landed()
80+ if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {
81+ t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur)
82+ }
83+}
84+
85+// KMS refusing the cursor plane turns the hardware cursor off and the
86+// frame commits without it; the output keeps running.
87+func TestCursorRefusedFrameCommitsWithout(t *testing.T) {
88+ o, _, commits := testOutput(t, unix.EINVAL, unix.EINVAL)
89+ o.cursor.image = true
90+ o.cursor.Move(5, 5)
91+ if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
92+ t.Fatal(err)
93+ }
94+ if !o.cursor.off || len(*commits) != 3 || (*commits)[1].flags != atomicTestOnly {
95+ t.Fatalf("off=%v commits %d", o.cursor.off, len(*commits))
96+ }
97+ if v, _ := (*commits)[2].req.value(tCursor, pFB); v != 0 {
98+ t.Fatalf("retry shows cursor fb %d", v)
99+ }
100+}
101+
102+// A refused cursor is not blamed on the client buffer.
103+func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) {
104+ o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
105+ o.cursor.image = true
106+ o.cursor.Move(5, 5)
107+ o.clientFBs[9] = &clientFB{fbID: 80}
108+ direct, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
109+ if err != nil || !direct || o.clientFBs[9].failed != "" {
110+ t.Fatalf("direct=%v err=%v failed=%q", direct, err, o.clientFBs[9].failed)
111+ }
112+}
113+
114+// showImages validates the images with a TEST_ONLY modeset and falls back
115+// to linear images when KMS refuses the driver's.
116+func TestShowImagesTestsBeforeModeset(t *testing.T) {
117+ o, k, commits := testOutput(t, unix.EINVAL)
118+ o.cursor = nil
119+ r := portsmocks.NewMockRenderer(t)
120+ buf := func() ports.DMABuf {
121+ f, w, _ := os.Pipe()
122+ w.Close()
123+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
124+ }
125+ r.EXPECT().ExportTargets(2, []uint64(nil)).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
126+ r.EXPECT().ExportTargets(2, []uint64{0}).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
127+ r.EXPECT().UseTarget(mock.Anything).Return()
128+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
129+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
130+ k.EXPECT().rmFB(mock.Anything).Return(nil)
131+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
132+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
133+ if err := o.showImages(r, imagesDriver, nil); err != nil {
134+ t.Fatal(err)
135+ }
136+ want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes}
137+ if len(*commits) != len(want) {
138+ t.Fatalf("%d commits", len(*commits))
139+ }
140+ for i, f := range want {
141+ if (*commits)[i].flags != f {
142+ t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f)
143+ }
144+ }
145+ if o.kind != imagesLinear {
146+ t.Fatalf("kind %d", o.kind)
147+ }
148+}
149+
150+// Enabling an output whose images were not made yet (started switched
151+// away) tests them and modesets once.
152+func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
153+ o, k, commits, commitMu := testOutputMu(t)
154+ o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
155+ r := portsmocks.NewMockRenderer(t)
156+ buf := func() ports.DMABuf {
157+ f, w, _ := os.Pipe()
158+ w.Close()
159+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
160+ }
161+ r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
162+ r.EXPECT().UseTarget(mock.Anything).Return()
163+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
164+ r.EXPECT().Close().Return().Once()
165+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
166+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
167+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
168+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
169+ active := make(chan bool, 1)
170+ active <- false
171+ ctx, cancel := context.WithCancel(context.Background())
172+ done := make(chan error, 1)
173+ go func() {
174+ done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1))
175+ }()
176+ count := func(flags uint32) int {
177+ commitMu.Lock()
178+ defer commitMu.Unlock()
179+ n := 0
180+ for _, c := range *commits {
181+ if c.flags == flags {
182+ n++
183+ }
184+ }
185+ return n
186+ }
187+ time.Sleep(20 * time.Millisecond)
188+ if n := count(atomicAllowModes); n != 0 {
189+ t.Fatalf("%d modesets while away", n)
190+ }
191+ active <- true
192+ waitFor(t, func() bool { return count(atomicAllowModes) == 1 })
193+ cancel()
194+ if err := <-done; err != nil {
195+ t.Fatal(err)
196+ }
197+ if n := count(atomicTestOnly | atomicAllowModes); n != 1 {
198+ t.Fatalf("%d test modesets", n)
199+ }
200+}
201+
202+// Close without a mode to restore turns the CRTC off.
203+func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) {
204+ o, k, commits := testOutput(t)
205+ k.EXPECT().rmFB(mock.Anything).Return(nil)
206+ o.Close()
207+ c := (*commits)[0]
208+ for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} {
209+ if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 {
210+ t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok)
211+ }
212+ }
213+}
214+
215+// Close restores the saved mode and gives the connector its CRTC back.
216+func TestCloseRestoresSavedMode(t *testing.T) {
217+ o, k, commits := testOutput(t)
218+ o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}}
219+ k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once()
220+ k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once()
221+ k.EXPECT().rmFB(mock.Anything).Return(nil)
222+ o.Close()
223+ c := (*commits)[0]
224+ if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
225+ t.Fatalf("connector crtc %d", v)
226+ }
227+ if v, _ := c.req.value(tPrimary, pFB); v != 5 {
228+ t.Fatalf("primary fb %d", v)
229+ }
230+}
231+
232+// With the cursor off no image is loaded (no warning per change).
233+func TestCursorOffSkipsLoads(t *testing.T) {
234+ o, _, _ := testOutput(t)
235+ o.cursor.off = true
236+ loaded := false
237+ o.setCursor(nil, func(ports.CursorChange, float64, int) (ports.CursorImage, error) {
238+ loaded = true
239+ return ports.CursorImage{}, nil
240+ }, ports.CursorChange{}, 1)
241+ if loaded || o.cursor.desired().on {
242+ t.Fatal("cursor used while off")
243+ }
244+}
245+
246+// Content read while only a cursor commit is in flight is reported when
247+// that commit's event arrives, not held until a later frame.
248+func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
249+ o, k, commits, commitMu := testOutputMu(t)
250+ o.tearing, o.cursor.fbs = false, [2]uint32{}
251+ flips := make(chan flipEvent, 1)
252+ o.flipped = flips
253+ r := portsmocks.NewMockRenderer(t)
254+ buf := func() ports.DMABuf {
255+ f, w, _ := os.Pipe()
256+ w.Close()
257+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
258+ }
259+ r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once()
260+ r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil)
261+ r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
262+ r.EXPECT().UseTarget(mock.Anything).Return()
263+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
264+ r.EXPECT().Close().Return().Once()
265+ k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()
266+ k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()
267+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
268+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
269+ k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
270+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
271+ load := func(ports.CursorChange, float64, int) (ports.CursorImage, error) {
272+ return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil
273+ }
274+ last := func(kind uint64) (commitRec, bool) {
275+ commitMu.Lock()
276+ defer commitMu.Unlock()
277+ for i := len(*commits) - 1; i >= 0; i-- {
278+ if c := (*commits)[i]; c.user&3 == kind {
279+ return c, true
280+ }
281+ }
282+ return commitRec{}, false
283+ }
284+ scenes := make(chan ports.Scene, 1)
285+ contents := make(chan ports.SurfaceContent, 1)
286+ presented := make(chan ports.OutputPresented, 8)
287+ ctx, cancel := context.WithCancel(context.Background())
288+ done := make(chan error, 1)
289+ go func() {
290+ done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented)
291+ }()
292+ scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
293+ var frame commitRec
294+ waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok })
295+ flips <- eventOf(frame)
296+ <-presented // the frame's flip
297+ o.cursor.Move(3, 3)
298+ var cur commitRec
299+ waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
300+ // A window this output does not show commits while the cursor flies.
301+ contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}
302+ time.Sleep(20 * time.Millisecond)
303+ flips <- eventOf(cur)
304+ select {
305+ case p := <-presented:
306+ if p.Seen[7] != 2 {
307+ t.Fatalf("report %+v", p)
308+ }
309+ case <-time.After(time.Second):
310+ t.Fatal("Seen not reported after the cursor commit")
311+ }
312+ cancel()
313+ if err := <-done; err != nil {
314+ t.Fatal(err)
315+ }
316+}
317+
318+// Frame fences are closed on every path: flipped, EBUSY (re-rendered
319+// after the event) and lost master (re-rendered after the modeset). The
320+// fd count is unchanged after many frames.
321+func TestRunClosesFrameFences(t *testing.T) {
322+ fds := func() int {
323+ ents, err := os.ReadDir("/proc/self/fd")
324+ if err != nil {
325+ t.Skip(err)
326+ }
327+ return len(ents)
328+ }
329+ const rounds = 30
330+ // Commit results: modeset test and modeset, then per round
331+ // EBUSY + flip, or lost master + modeset + flip, or a plain flip.
332+ errs := []error{nil, nil}
333+ for i := range rounds {
334+ switch i % 3 {
335+ case 0:
336+ errs = append(errs, unix.EBUSY, nil)
337+ case 1:
338+ errs = append(errs, unix.EACCES, nil, nil)
339+ default:
340+ errs = append(errs, nil)
341+ }
342+ }
343+ o, k, commits, commitMu := testOutputMu(t, errs...)
344+ o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
345+ flips := make(chan flipEvent, 1)
346+ o.flipped = flips
347+ r := portsmocks.NewMockRenderer(t)
348+ buf := func() ports.DMABuf {
349+ f, w, _ := os.Pipe()
350+ w.Close()
351+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
352+ }
353+ r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
354+ r.EXPECT().UseTarget(mock.Anything).Return()
355+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
356+ f, w, _ := os.Pipe()
357+ _, _ = w.Write([]byte{1})
358+ w.Close()
359+ return f, nil
360+ })
361+ r.EXPECT().Close().Return().Once()
362+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
363+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
364+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
365+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
366+ before := fds()
367+ active := make(chan bool, 1)
368+ scenes := make(chan ports.Scene, 1)
369+ presented := make(chan ports.OutputPresented, 4*rounds)
370+ ctx, cancel := context.WithCancel(context.Background())
371+ done := make(chan error, 1)
372+ go func() {
373+ done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented)
374+ }()
375+ count := func() int {
376+ commitMu.Lock()
377+ defer commitMu.Unlock()
378+ return len(*commits)
379+ }
380+ last := func() commitRec {
381+ commitMu.Lock()
382+ defer commitMu.Unlock()
383+ return (*commits)[len(*commits)-1]
384+ }
385+ waitFor(t, func() bool { return count() == 2 })
386+ for i := range rounds {
387+ n := count()
388+ scenes <- ports.Scene{Seq: uint64(i)}
389+ switch i % 3 {
390+ case 0:
391+ waitFor(t, func() bool { return count() == n+1 })
392+ flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait
393+ waitFor(t, func() bool { return count() == n+2 })
394+ case 1:
395+ waitFor(t, func() bool { return count() == n+1 })
396+ active <- true
397+ waitFor(t, func() bool { return count() == n+3 })
398+ default:
399+ waitFor(t, func() bool { return count() == n+1 })
400+ }
401+ flips <- eventOf(last())
402+ }
403+ cancel()
404+ if err := <-done; err != nil {
405+ t.Fatal(err)
406+ }
407+ if after := fds(); after > before {
408+ t.Fatalf("fds %d -> %d", before, after)
409+ }
410+}
M internal/adapters/drm/cursor.go +184 -224
@@ -1,281 +1,241 @@
11 package drm
22
33 import (
4- "fmt"
4+ "errors"
5+ "slices"
56 "sync"
6- "time"
7- "unsafe"
7+
8+ "github.com/bnema/nefertty/internal/ports"
89 )
910
10-// Hardware cursor through the legacy cursor ioctls: the GPU overlays a small
11-// buffer at a position, so moving it never re-renders the frame.
11+// Hardware cursor on the CRTC's cursor plane. Move and Hide come from the
12+// input goroutine: they only record the position and wake the output,
13+// whose goroutine puts the cursor plane in its next atomic commit. A move
14+// while a commit is pending waits for it, so the cursor moves at most once
15+// per flip and never blocks input.
1216
1317 const (
14- ioctlGetCap = 0xC010640C // DRM_IOWR('d', 0x0C, struct drm_get_cap)
15- ioctlCursor2 = 0xC02464BB // DRM_IOWR('d', 0xBB, struct drm_mode_cursor2)
16- ioctlVblank = 0xC018643A // DRM_IOWR('d', 0x3A, union drm_wait_vblank)
17- vblankRel = 0x1 // _DRM_VBLANK_RELATIVE
18- vblankCrtcSh = 1 // _DRM_VBLANK_HIGH_CRTC_SHIFT
19- vblankCrtcMk = 0x3e // _DRM_VBLANK_HIGH_CRTC_MASK
20- capCursorW = 0x8
21- capCursorH = 0x9
22- cursorBO = 0x01
23- cursorMove = 0x02
18+ ioctlGetCap = 0xC010640C // DRM_IOWR('d', 0x0C, struct drm_get_cap)
19+ capCursorW = 0x8
20+ capCursorH = 0x9
2421 )
2522
2623 type getCap struct{ capability, value uint64 }
2724
28-// waitVblank is union drm_wait_vblank (request and reply share 24 bytes).
29-type waitVblank struct {
30- typ, sequence uint32
31- sec, usec int64
25+// Cursor is the hardware cursor of one output.
26+type Cursor struct {
27+ mu sync.Mutex // guards x, y, away and moves
28+ x, y int // physical position of the hotspot
29+ away bool // Hide was called and no Move since
30+ moves int
31+ wake chan struct{}
32+
33+ // Owned by the output goroutine.
34+ plane *plane
35+ size int
36+ fbs [2]uint32 // the two images; cur is on screen or next
37+ cur int
38+ image bool // an image is loaded
39+ hotX, hotY int
40+ applied cursorState // what the last commit put on the plane
41+ commits int
42+ // screen is the image the plane scans out (0: none); flight is what
43+ // the pending commit puts on it when flying. An image is written only
44+ // when it is neither, so the plane never shows a half-written image.
45+ screen uint32
46+ flight cursorState
47+ flying bool
48+ // later is an image that waits for a free slot (both on screen or in
49+ // flight); off is set when KMS refused the cursor plane.
50+ later *laterImage
51+ off bool
3252 }
3353
34-// cursorPlane is the kernel side of the hardware cursor.
35-type cursorPlane interface {
36- // Set issues DRM_IOCTL_MODE_CURSOR2.
37- Set(v *modeCursor2) error
38- // WaitVblank blocks until the next vblank of the cursor's CRTC.
39- WaitVblank() error
54+type laterImage struct {
55+ pixels []byte
56+ w, h, hotX, hotY int
4057 }
4158
42-// kmsCursorPlane drives the cursor of the CRTC at index pipe.
43-type kmsCursorPlane struct{ fd, pipe int }
44-
45-func (p kmsCursorPlane) Set(v *modeCursor2) error {
46- return ioctl(p.fd, ioctlCursor2, unsafe.Pointer(v))
59+// cursorState is the cursor plane as committed: off, or fb at x, y.
60+type cursorState struct {
61+ on bool
62+ fb uint32
63+ x, y int
4764 }
4865
49-func (p kmsCursorPlane) WaitVblank() error {
50- v := waitVblank{typ: vblankRel | uint32(p.pipe<<vblankCrtcSh)&vblankCrtcMk, sequence: 1}
51- return ioctl(p.fd, ioctlVblank, unsafe.Pointer(&v))
66+// CursorStats counts cursor work since the last TakeStats.
67+type CursorStats struct {
68+ Moves int // Move calls
69+ Commits int // commits that moved or changed the cursor
5270 }
5371
54-type modeCursor2 struct {
55- flags, crtcID uint32
56- x, y int32
57- width, height uint32
58- handle uint32
59- hotX, hotY int32
72+func newCursor(p *plane, size int) *Cursor {
73+ return &Cursor{plane: p, size: size, wake: make(chan struct{}, 1)}
6074 }
6175
62-// Cursor is the hardware cursor of one output. Move is safe to call from the
63-// input goroutine while the output goroutine changes the image.
64-//
65-// Move only records the position. A worker waits for the next vblank and then
66-// applies the latest one, so the cursor moves at most once per frame, between
67-// scanouts: moving it mid-scanout tears it into doubled images, and input is
68-// never throttled by the ioctl.
69-type Cursor struct {
70- io sync.Mutex // serialises cursor ioctls and guards the fields below
71- fd int
72- crtc uint32
73- buf *dumbBuffer
74- size int // buffer side
75- hotX, hotY int
76- shown bool
77- hidden bool // the plane is off because the pointer left this output
78- plane cursorPlane
76+// TakeStats returns and resets the counters. Output goroutine only.
77+func (c *Cursor) TakeStats() CursorStats {
78+ c.mu.Lock()
79+ s := CursorStats{Moves: c.moves, Commits: c.commits}
80+ c.moves, c.commits = 0, 0
81+ c.mu.Unlock()
82+ return s
83+}
7984
80- mu sync.Mutex // guards x, y, away and stats; never held across an ioctl
81- x, y int // physical position of the hotspot
82- away bool // Hide was called and no Move since
83- stats CursorStats
85+// Limit is the largest image side the cursor plane accepts.
86+func (c *Cursor) Limit() int { return c.size }
8487
85- wake chan struct{}
86- stop chan struct{}
87- done chan struct{}
88+// Move places the hotspot at physical (x, y). It never blocks.
89+func (c *Cursor) Move(x, y float64) {
90+ c.mu.Lock()
91+ c.x, c.y = int(x), int(y)
92+ c.away = false
93+ c.moves++
94+ c.mu.Unlock()
95+ c.poke()
8896 }
8997
90-// CursorStats counts cursor work since the last TakeStats.
91-type CursorStats struct {
92- Moves int // Move calls
93- Ioctls int // move ioctls issued by the worker
94- MaxIoctl time.Duration // slowest move ioctl
98+// Hide removes the cursor from the output until the next move (the pointer
99+// is on another output).
100+func (c *Cursor) Hide() {
101+ c.mu.Lock()
102+ c.away = true
103+ c.mu.Unlock()
104+ c.poke()
95105 }
96106
97-// TakeStats returns and resets the counters.
98-func (c *Cursor) TakeStats() CursorStats {
99- c.mu.Lock()
100- defer c.mu.Unlock()
101- s := c.stats
102- c.stats = CursorStats{}
103- return s
107+func (c *Cursor) poke() {
108+ select {
109+ case c.wake <- struct{}{}:
110+ default: // a wake is queued; the output reads the latest state
111+ }
104112 }
105113
106-// newCursor allocates a cursor buffer at the driver's preferred size (64 when
107-// the driver does not say).
108-func newCursor(fd int, crtc uint32, pipe int) (*Cursor, error) {
109- // The plane is square: the smaller of the width and height caps.
110- size := 0
111- for _, c := range []uint64{capCursorW, capCursorH} {
112- v := getCap{capability: c}
113- if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 {
114- if size == 0 || int(v.value) < size {
115- size = int(v.value)
116- }
117- }
114+// setup allocates the two cursor images and their framebuffers.
115+func (c *Cursor) setup(k kms, r ports.Renderer) error {
116+ bufs, err := r.CursorBuffers(c.size)
117+ if err != nil {
118+ return err
118119 }
119- if size == 0 {
120- size = 64
120+ for i := range bufs {
121+ if err == nil {
122+ c.fbs[i], err = k.addFB(&bufs[i], fourccARGB)
123+ }
124+ for _, p := range bufs[i].Planes {
125+ p.File.Close()
126+ }
121127 }
122- buf, err := newDumb(fd, size, size)
123128 if err != nil {
124- return nil, err
125- }
126- // The cursor ioctl passes no pitch: drivers assume width*4.
127- if int(buf.pitch) != size*4 {
128- buf.destroy(fd)
129- return nil, fmt.Errorf("cursor buffer pitch %d, want %d", buf.pitch, size*4)
129+ c.free(k)
130130 }
131- c := &Cursor{fd: fd, crtc: crtc, buf: buf, size: size, plane: kmsCursorPlane{fd: fd, pipe: pipe}}
132- c.start()
133- return c, nil
134-}
135-
136-// start runs the worker that applies coalesced moves.
137-func (c *Cursor) start() {
138- c.wake = make(chan struct{}, 1)
139- c.stop = make(chan struct{})
140- c.done = make(chan struct{})
141- go func() {
142- defer close(c.done)
143- for {
144- select {
145- case <-c.stop:
146- return
147- case <-c.wake:
148- // Errors (VT switched away) fall through: the move
149- // fails too and Reapply restores the cursor.
150- _ = c.plane.WaitVblank()
151- c.io.Lock()
152- c.mu.Lock()
153- away := c.away
154- c.mu.Unlock()
155- if away {
156- // A move queued before Hide: the pointer is elsewhere.
157- } else if c.hidden {
158- // A move shows it again, even before its first image.
159- c.hidden = false
160- _ = c.apply()
161- } else if c.shown && c.buf != nil {
162- x, y := c.position()
163- v := modeCursor2{flags: cursorMove, crtcID: c.crtc, x: int32(x - c.hotX), y: int32(y - c.hotY)}
164- // Errors while another session owns the card (VT switched
165- // away) are ignored; Reapply restores the cursor.
166- start := time.Now()
167- _ = c.plane.Set(&v)
168- d := time.Since(start)
169- c.mu.Lock()
170- c.stats.Ioctls++
171- c.stats.MaxIoctl = max(c.stats.MaxIoctl, d)
172- c.mu.Unlock()
173- }
174- c.io.Unlock()
175- }
176- }
177- }()
131+ return err
178132 }
179133
180-func (c *Cursor) position() (int, int) {
181- c.mu.Lock()
182- defer c.mu.Unlock()
183- return c.x, c.y
184-}
185-
186-// Limit is the largest image side the cursor plane accepts.
187-func (c *Cursor) Limit() int { return c.size }
188-
189-// SetImage shows premultiplied ARGB8888 pixels (w×h, w*4 per row) with the
190-// click point at (hotX, hotY). Larger images are cropped to the plane; an
191-// empty image hides the cursor (a client asked for none).
192-func (c *Cursor) SetImage(pixels []byte, w, h, hotX, hotY int) error {
193- c.io.Lock()
194- defer c.io.Unlock()
195- if c.buf == nil {
134+// setImage loads premultiplied ARGB8888 pixels into an image neither on
135+// screen nor in flight; when there is none it waits for the next commit
136+// event (flushLater). An empty image hides the cursor (a client asked for
137+// none).
138+func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error {
139+ c.later = nil
140+ if w <= 0 || h <= 0 || len(pixels) < w*h*4 {
141+ c.image = false
196142 return nil
197143 }
198- if w <= 0 || h <= 0 || len(pixels) < w*h*4 {
199- wasShown := c.shown
200- c.shown = false
201- if !wasShown || c.hidden {
202- return nil
203- }
204- v := modeCursor2{flags: cursorBO, crtcID: c.crtc}
205- return c.plane.Set(&v)
144+ if c.fbs[0] == 0 {
145+ return errors.New("no cursor images")
146+ }
147+ next := c.freeSlot()
148+ if next < 0 {
149+ c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY}
150+ return nil
206151 }
207- pitch := int(c.buf.pitch)
208- clear(c.buf.mem)
209- for y := range min(h, c.size) {
210- row := pixels[y*w*4:]
211- copy(c.buf.mem[y*pitch:y*pitch+min(w, c.size)*4], row[:min(w, c.size)*4])
152+ if err := r.WriteCursor(next, pixels, w, h); err != nil {
153+ return err
212154 }
213- c.hotX, c.hotY, c.shown = hotX, hotY, true
214- return c.apply()
155+ c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY
156+ return nil
215157 }
216158
217-// apply (re)attaches the buffer at the current position. Callers hold c.io.
218-func (c *Cursor) apply() error {
219- if !c.shown || c.hidden || c.buf == nil {
220- return nil
159+// freeSlot is an image the plane neither shows nor is about to show, -1
160+// when both are busy.
161+func (c *Cursor) freeSlot() int {
162+ for i, fb := range c.fbs {
163+ if fb != c.screen && !(c.flying && c.flight.on && c.flight.fb == fb) {
164+ return i
165+ }
221166 }
222- x, y := c.position()
223- v := modeCursor2{flags: cursorBO | cursorMove, crtcID: c.crtc, x: int32(x - c.hotX), y: int32(y - c.hotY), width: uint32(c.size), height: uint32(c.size), handle: c.buf.handle, hotX: int32(c.hotX), hotY: int32(c.hotY)}
224- return c.plane.Set(&v)
167+ return -1
225168 }
226169
227-// Move places the hotspot at physical (x, y). It never blocks on the GPU:
228-// the worker applies the latest position.
229-func (c *Cursor) Move(x, y float64) {
230- c.mu.Lock()
231- c.x, c.y = int(x), int(y)
232- c.away = false
233- c.stats.Moves++
234- c.mu.Unlock()
235- select {
236- case c.wake <- struct{}{}:
237- default: // a move is already queued; it will read this position
170+// flushLater loads an image that waited for a free slot. It reports
171+// whether the cursor changed.
172+func (c *Cursor) flushLater(r ports.Renderer) (bool, error) {
173+ l := c.later
174+ if l == nil || c.freeSlot() < 0 {
175+ return false, nil
238176 }
177+ return true, c.setImage(r, l.pixels, l.w, l.h, l.hotX, l.hotY)
239178 }
240179
241-// Hide removes the cursor from the output until the next move (the pointer
242-// is on another output).
243-func (c *Cursor) Hide() {
244- c.io.Lock()
245- defer c.io.Unlock()
246- c.mu.Lock()
247- c.away = true
248- c.mu.Unlock()
249- if c.buf == nil || c.hidden {
250- return
180+// committed records that a commit carrying state s is in flight.
181+func (c *Cursor) committed(s cursorState) {
182+ if s != c.applied {
183+ c.mu.Lock()
184+ c.commits++
185+ c.mu.Unlock()
251186 }
252- c.hidden = true
253- v := modeCursor2{flags: cursorBO, crtcID: c.crtc}
254- _ = c.plane.Set(&v)
187+ c.applied, c.flight, c.flying = s, s, true
255188 }
256189
257-// Reapply shows the cursor again after a modeset (VT switch back).
258-func (c *Cursor) Reapply() error {
259- c.io.Lock()
260- defer c.io.Unlock()
261- return c.apply()
190+// landed records that the pending commit reached the screen.
191+func (c *Cursor) landed() {
192+ if c.flying {
193+ c.screen, c.flying = 0, false
194+ if c.flight.on {
195+ c.screen = c.flight.fb
196+ }
197+ }
262198 }
263199
264-// close stops the worker, hides the cursor and frees its buffer.
265-func (c *Cursor) close() {
266- if c.stop != nil {
267- close(c.stop)
268- <-c.done
269- c.stop = nil
200+// desired is what the cursor plane should show now.
201+func (c *Cursor) desired() cursorState {
202+ c.mu.Lock()
203+ x, y, away := c.x, c.y, c.away
204+ c.mu.Unlock()
205+ if !c.image || away || c.off || c.fbs[c.cur] == 0 {
206+ return cursorState{}
270207 }
271- c.io.Lock()
272- defer c.io.Unlock()
273- if c.buf == nil {
208+ return cursorState{on: true, fb: c.fbs[c.cur], x: x - c.hotX, y: y - c.hotY}
209+}
210+
211+// props adds the cursor plane in state s to req.
212+func (c *Cursor) props(req *atomicReq, crtc uint32, s cursorState) {
213+ p := c.plane
214+ if !s.on {
215+ req.set(p.id, p.prop("FB_ID"), 0)
216+ req.set(p.id, p.prop("CRTC_ID"), 0)
274217 return
275218 }
276- // Best effort: at exit the card may already belong to another session.
277- v := modeCursor2{flags: cursorBO, crtcID: c.crtc}
278- _ = c.plane.Set(&v)
279- c.buf.destroy(c.fd)
280- c.buf, c.shown = nil, false
219+ size := uint64(c.size)
220+ req.set(p.id, p.prop("FB_ID"), uint64(s.fb))
221+ req.set(p.id, p.prop("CRTC_ID"), uint64(crtc))
222+ req.set(p.id, p.prop("SRC_X"), 0)
223+ req.set(p.id, p.prop("SRC_Y"), 0)
224+ req.set(p.id, p.prop("SRC_W"), size<<16)
225+ req.set(p.id, p.prop("SRC_H"), size<<16)
226+ req.set(p.id, p.prop("CRTC_X"), uint64(int64(s.x)))
227+ req.set(p.id, p.prop("CRTC_Y"), uint64(int64(s.y)))
228+ req.set(p.id, p.prop("CRTC_W"), size)
229+ req.set(p.id, p.prop("CRTC_H"), size)
230+}
231+
232+func (c *Cursor) free(k kms) {
233+ for i, fb := range c.fbs {
234+ if fb != 0 {
235+ _ = k.rmFB(fb)
236+ c.fbs[i] = 0
237+ }
238+ }
239+ c.image, c.later = false, nil
240+ c.screen, c.flying = 0, false
281241 }
D internal/adapters/drm/cursor_plane_mock_test.go +0 -140
@@ -1,140 +0,0 @@
1-// Code generated by mockery; DO NOT EDIT.
2-// github.com/vektra/mockery
3-// template: testify
4-
5-package drm
6-
7-import (
8- mock "github.com/stretchr/testify/mock"
9-)
10-
11-// newMockcursorPlane creates a new instance of mockcursorPlane. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12-// The first argument is typically a *testing.T value.
13-func newMockcursorPlane(t interface {
14- mock.TestingT
15- Cleanup(func())
16-}) *mockcursorPlane {
17- if helper, ok := t.(interface{ Helper() }); ok {
18- helper.Helper()
19- }
20-
21- mock := &mockcursorPlane{}
22- mock.Mock.Test(t)
23-
24- t.Cleanup(func() {
25- if helper, ok := t.(interface{ Helper() }); ok {
26- helper.Helper()
27- }
28- mock.AssertExpectations(t)
29- })
30-
31- return mock
32-}
33-
34-// mockcursorPlane is an autogenerated mock type for the cursorPlane type
35-type mockcursorPlane struct {
36- mock.Mock
37-}
38-
39-type mockcursorPlane_Expecter struct {
40- mock *mock.Mock
41-}
42-
43-func (_m *mockcursorPlane) EXPECT() *mockcursorPlane_Expecter {
44- return &mockcursorPlane_Expecter{mock: &_m.Mock}
45-}
46-
47-// Set provides a mock function for the type mockcursorPlane
48-func (_mock *mockcursorPlane) Set(v *modeCursor2) error {
49- ret := _mock.Called(v)
50-
51- if len(ret) == 0 {
52- panic("no return value specified for Set")
53- }
54-
55- var r0 error
56- if returnFunc, ok := ret.Get(0).(func(*modeCursor2) error); ok {
57- r0 = returnFunc(v)
58- } else {
59- r0 = ret.Error(0)
60- }
61- return r0
62-}
63-
64-// mockcursorPlane_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set'
65-type mockcursorPlane_Set_Call struct {
66- *mock.Call
67-}
68-
69-// Set is a helper method to define mock.On call
70-// - v *modeCursor2
71-func (_e *mockcursorPlane_Expecter) Set(v any) *mockcursorPlane_Set_Call {
72- return &mockcursorPlane_Set_Call{Call: _e.mock.On("Set", v)}
73-}
74-
75-func (_c *mockcursorPlane_Set_Call) Run(run func(v *modeCursor2)) *mockcursorPlane_Set_Call {
76- _c.Call.Run(func(args mock.Arguments) {
77- var arg0 *modeCursor2
78- if args[0] != nil {
79- arg0 = args[0].(*modeCursor2)
80- }
81- run(
82- arg0,
83- )
84- })
85- return _c
86-}
87-
88-func (_c *mockcursorPlane_Set_Call) Return(err error) *mockcursorPlane_Set_Call {
89- _c.Call.Return(err)
90- return _c
91-}
92-
93-func (_c *mockcursorPlane_Set_Call) RunAndReturn(run func(v *modeCursor2) error) *mockcursorPlane_Set_Call {
94- _c.Call.Return(run)
95- return _c
96-}
97-
98-// WaitVblank provides a mock function for the type mockcursorPlane
99-func (_mock *mockcursorPlane) WaitVblank() error {
100- ret := _mock.Called()
101-
102- if len(ret) == 0 {
103- panic("no return value specified for WaitVblank")
104- }
105-
106- var r0 error
107- if returnFunc, ok := ret.Get(0).(func() error); ok {
108- r0 = returnFunc()
109- } else {
110- r0 = ret.Error(0)
111- }
112- return r0
113-}
114-
115-// mockcursorPlane_WaitVblank_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitVblank'
116-type mockcursorPlane_WaitVblank_Call struct {
117- *mock.Call
118-}
119-
120-// WaitVblank is a helper method to define mock.On call
121-func (_e *mockcursorPlane_Expecter) WaitVblank() *mockcursorPlane_WaitVblank_Call {
122- return &mockcursorPlane_WaitVblank_Call{Call: _e.mock.On("WaitVblank")}
123-}
124-
125-func (_c *mockcursorPlane_WaitVblank_Call) Run(run func()) *mockcursorPlane_WaitVblank_Call {
126- _c.Call.Run(func(args mock.Arguments) {
127- run()
128- })
129- return _c
130-}
131-
132-func (_c *mockcursorPlane_WaitVblank_Call) Return(err error) *mockcursorPlane_WaitVblank_Call {
133- _c.Call.Return(err)
134- return _c
135-}
136-
137-func (_c *mockcursorPlane_WaitVblank_Call) RunAndReturn(run func() error) *mockcursorPlane_WaitVblank_Call {
138- _c.Call.Return(run)
139- return _c
140-}
D internal/adapters/drm/cursor_test.go +0 -195
@@ -1,195 +0,0 @@
1-package drm
2-
3-import (
4- "sync"
5- "testing"
6- "time"
7- "unsafe"
8-
9- "github.com/stretchr/testify/mock"
10-)
11-
12-// The ioctl numbers encode the struct size; they must match the kernel ABI.
13-func TestCursorABI(t *testing.T) {
14- if s := unsafe.Sizeof(modeCursor2{}); s != 36 || ioctlCursor2>>16&0x3fff != 36 {
15- t.Fatalf("drm_mode_cursor2 size %d", s)
16- }
17- if s := unsafe.Sizeof(getCap{}); s != 16 || ioctlGetCap>>16&0x3fff != 16 {
18- t.Fatalf("drm_get_cap size %d", s)
19- }
20- if s := unsafe.Sizeof(waitVblank{}); s != 24 || ioctlVblank>>16&0x3fff != 24 {
21- t.Fatalf("drm_wait_vblank size %d", s)
22- }
23-}
24-
25-// testCursor returns a shown cursor on a mocked plane; the worker is started.
26-func testCursor(t *testing.T) (*Cursor, *mockcursorPlane) {
27- plane := newMockcursorPlane(t)
28- c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true, plane: plane}
29- return c, plane
30-}
31-
32-// The cursor moves once per vblank, to the latest position: moving it during
33-// scanout tears it into doubled images.
34-func TestCursorMovesOncePerVblank(t *testing.T) {
35- c, plane := testCursor(t)
36- vblank := make(chan struct{})
37- moved := make(chan modeCursor2, 16)
38- plane.EXPECT().WaitVblank().RunAndReturn(func() error { <-vblank; return nil })
39- plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { moved <- *v; return nil })
40- c.start()
41- defer func() { close(vblank); c.close() }()
42- for i := range 100 {
43- c.Move(float64(i), 0)
44- }
45- select {
46- case v := <-moved:
47- t.Fatalf("moved to %d before vblank", v.x)
48- case <-time.After(20 * time.Millisecond):
49- }
50- vblank <- struct{}{}
51- if v := <-moved; v.x != 99 || v.flags != cursorMove {
52- t.Fatalf("applied %+v, want a move to the latest x=99", v)
53- }
54- select {
55- case v := <-moved:
56- t.Fatalf("extra move to %d without new input", v.x)
57- case <-time.After(20 * time.Millisecond):
58- }
59-}
60-
61-// Move must never wait for the plane, or libinput stops reading and the
62-// kernel drops mouse reports. Moves made meanwhile are coalesced.
63-func TestCursorMoveDoesNotBlockOnPlane(t *testing.T) {
64- c, plane := testCursor(t)
65- c.hotX, c.hotY = 3, 4
66- started, release := make(chan struct{}), make(chan struct{})
67- var once sync.Once
68- moved := make(chan modeCursor2, 1000)
69- plane.EXPECT().WaitVblank().RunAndReturn(func() error {
70- once.Do(func() { close(started) })
71- <-release
72- return nil
73- })
74- plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { moved <- *v; return nil })
75- c.start()
76- defer c.close()
77- c.Move(0, 0)
78- <-started // the worker is now held in WaitVblank
79- done := make(chan struct{})
80- go func() {
81- for i := range 1000 {
82- c.Move(float64(i), float64(2*i))
83- }
84- close(done)
85- }()
86- select {
87- case <-done:
88- case <-time.After(2 * time.Second):
89- t.Fatal("Move blocks while the plane waits for vblank")
90- }
91- close(release)
92- for n := 1; ; n++ {
93- v := <-moved
94- if v.flags != cursorMove {
95- t.Fatalf("flags %#x", v.flags)
96- }
97- if v.x == 999-3 && v.y == 1998-4 {
98- if n >= 1000 {
99- t.Fatalf("%d moves applied; want coalesced", n)
100- }
101- return
102- }
103- }
104-}
105-
106-func TestCursorCloseStopsWorker(t *testing.T) {
107- c, plane := testCursor(t)
108- plane.EXPECT().WaitVblank().Return(nil).Maybe()
109- plane.EXPECT().Set(mock.Anything).Return(nil)
110- c.start()
111- c.Move(1, 2)
112- c.close()
113- c.close() // idempotent
114- c.Move(3, 4) // after close: no worker, must not block or panic
115-}
116-
117-func TestCursorStats(t *testing.T) {
118- c, plane := testCursor(t)
119- applied := make(chan struct{}, 16)
120- plane.EXPECT().WaitVblank().Return(nil)
121- plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error {
122- if v.flags == cursorMove {
123- applied <- struct{}{}
124- }
125- return nil
126- })
127- c.start()
128- defer c.close()
129- c.Move(1, 1)
130- <-applied
131- c.Move(2, 2)
132- <-applied
133- // Set signals before the worker counts the ioctl.
134- s := c.TakeStats()
135- if s.Moves != 2 || s.Ioctls < 1 || s.Ioctls > 2 {
136- t.Fatalf("stats %+v", s)
137- }
138- if s := c.TakeStats(); s.Moves != 0 {
139- t.Fatalf("not reset: %+v", s)
140- }
141-}
142-
143-// A move still waiting for its vblank when the pointer leaves the output
144-// must not show the cursor again.
145-func TestCursorHideDropsQueuedMove(t *testing.T) {
146- c, plane := testCursor(t)
147- vblank := make(chan struct{})
148- set := make(chan modeCursor2, 16)
149- plane.EXPECT().WaitVblank().RunAndReturn(func() error { <-vblank; return nil })
150- plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { set <- *v; return nil })
151- c.start()
152- defer func() { close(vblank); c.close() }()
153- c.Move(10, 10)
154- c.Hide()
155- if v := <-set; v.handle != 0 || v.flags != cursorBO {
156- t.Fatalf("hide: %+v", v)
157- }
158- vblank <- struct{}{}
159- select {
160- case v := <-set:
161- t.Fatalf("queued move applied after hide: %+v", v)
162- case <-time.After(20 * time.Millisecond):
163- }
164- // The next move shows it again.
165- c.Move(20, 20)
166- vblank <- struct{}{}
167- if v := <-set; v.flags != cursorBO|cursorMove || v.x != 20 {
168- t.Fatalf("show: %+v", v)
169- }
170-}
171-
172-// An empty image takes the buffer off the plane; the next image shows it.
173-func TestCursorEmptyImageHides(t *testing.T) {
174- c, plane := testCursor(t)
175- c.buf.mem = make([]byte, 64*64*4)
176- c.buf.pitch = 64 * 4
177- var sets []modeCursor2
178- plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { sets = append(sets, *v); return nil })
179- if err := c.SetImage(nil, 0, 0, 0, 0); err != nil {
180- t.Fatal(err)
181- }
182- if len(sets) != 1 || sets[0].flags != cursorBO || sets[0].handle != 0 || c.shown {
183- t.Fatalf("hide: %+v shown=%v", sets, c.shown)
184- }
185- // Still hidden: no second ioctl.
186- if err := c.SetImage(nil, 0, 0, 0, 0); err != nil || len(sets) != 1 {
187- t.Fatalf("second hide: %+v %v", sets, err)
188- }
189- if err := c.SetImage(make([]byte, 4), 1, 1, 0, 0); err != nil {
190- t.Fatal(err)
191- }
192- if len(sets) != 2 || sets[1].flags != cursorBO|cursorMove || !c.shown {
193- t.Fatalf("show: %+v", sets)
194- }
195-}
M internal/adapters/drm/kms.go +7 -109
@@ -1,12 +1,12 @@
1-// Package drm drives KMS outputs with raw ioctls and dumb buffers.
1+// Package drm drives KMS outputs with raw ioctls and atomic commits.
22 package drm
33
44 import (
5- "encoding/binary"
65 "fmt"
76 "math"
87 "unsafe"
98
9+ "github.com/bnema/nefertty/internal/ports"
1010 "golang.org/x/sys/unix"
1111 )
1212
@@ -14,18 +14,10 @@ import (
1414 const (
1515 ioctlGetResources = 0xC04064A0
1616 ioctlGetCrtc = 0xC06864A1
17- ioctlSetCrtc = 0xC06864A2
1817 ioctlGetEncoder = 0xC01464A6
1918 ioctlGetConnector = 0xC05064A7
20- ioctlAddFB = 0xC01C64AE
2119 ioctlRmFB = 0xC00464AF
22- ioctlPageFlip = 0xC01864B0
23- ioctlCreateDumb = 0xC02064B2
24- ioctlMapDumb = 0xC01064B3
25- ioctlDestroyDumb = 0xC00464B4
2620
27- pageFlipEvent = 1
28- pageFlipAsync = 2
2921 eventFlipDone = 2
3022 modeTypePrefered = 1 << 3
3123 connected = 1
@@ -72,24 +64,6 @@ type modeCrtc struct {
7264 mode modeInfo
7365 }
7466
75-type pageFlip struct {
76- crtcID, fbID, flags, reserved uint32
77- userData uint64
78-}
79-
80-type createDumb struct {
81- height, width, bpp, flags uint32
82- handle, pitch uint32
83- size uint64
84-}
85-
86-type mapDumb struct {
87- handle, pad uint32
88- offset uint64
89-}
90-
91-type fbCmd struct{ fbID, width, height, pitch, bpp, depth, handle uint32 }
92-
9367 func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
9468 for {
9569 _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
@@ -126,6 +100,11 @@ type Want struct {
126100 NoTearing bool
127101 // NoVRR keeps variable refresh off (render.vrr = off).
128102 NoVRR bool
103+ // Sampled are the dmabuf formats the renderer composes: direct
104+ // scanout is offered only for those, so a refused buffer still draws.
105+ Sampled []ports.DMABufFormat
106+ // Device is the KMS device (dev_t) offered in scanout tranches.
107+ Device uint64
129108 }
130109
131110 // usable reports whether a connector should be driven.
@@ -261,89 +240,8 @@ func pickCrtc(fd int, c connector, crtcs []uint32, used map[uint32]bool) (uint32
261240 return 0, fmt.Errorf("no free CRTC for %s", c.name)
262241 }
263242
264-type dumbBuffer struct {
265- handle, fbID, pitch uint32
266- mem []byte
267-}
268-
269-func newDumb(fd int, w, h int) (*dumbBuffer, error) {
270- c := createDumb{width: uint32(w), height: uint32(h), bpp: 32}
271- if err := ioctl(fd, ioctlCreateDumb, unsafe.Pointer(&c)); err != nil {
272- return nil, fmt.Errorf("create dumb: %w", err)
273- }
274- b := &dumbBuffer{handle: c.handle, pitch: c.pitch}
275- f := fbCmd{width: uint32(w), height: uint32(h), pitch: c.pitch, bpp: 32, depth: 24, handle: c.handle}
276- if err := ioctl(fd, ioctlAddFB, unsafe.Pointer(&f)); err != nil {
277- b.destroy(fd)
278- return nil, fmt.Errorf("add fb: %w", err)
279- }
280- b.fbID = f.fbID
281- m := mapDumb{handle: c.handle}
282- if err := ioctl(fd, ioctlMapDumb, unsafe.Pointer(&m)); err != nil {
283- b.destroy(fd)
284- return nil, fmt.Errorf("map dumb: %w", err)
285- }
286- mem, err := unix.Mmap(fd, int64(m.offset), int(c.size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
287- if err != nil {
288- b.destroy(fd)
289- return nil, fmt.Errorf("mmap dumb: %w", err)
290- }
291- b.mem = mem
292- return b, nil
293-}
294-
295-func (b *dumbBuffer) destroy(fd int) {
296- if b.mem != nil {
297- _ = unix.Munmap(b.mem)
298- b.mem = nil
299- }
300- if b.fbID != 0 {
301- id := b.fbID
302- _ = ioctl(fd, ioctlRmFB, unsafe.Pointer(&id))
303- b.fbID = 0
304- }
305- h := b.handle
306- _ = ioctl(fd, ioctlDestroyDumb, unsafe.Pointer(&h))
307-}
308-
309-func setCrtc(fd int, crtc, conn, fb uint32, mode *modeInfo) error {
310- ids := []uint32{conn}
311- c := modeCrtc{crtcID: crtc, fbID: fb, countConnectors: 1, setConnectors: unsafe.Pointer(&ids[0])}
312- if mode != nil {
313- c.mode, c.modeValid = *mode, 1
314- }
315- return ioctl(fd, ioctlSetCrtc, unsafe.Pointer(&c))
316-}
317-
318243 func getCrtc(fd int, crtc uint32) (modeCrtc, error) {
319244 c := modeCrtc{crtcID: crtc}
320245 err := ioctl(fd, ioctlGetCrtc, unsafe.Pointer(&c))
321246 return c, err
322247 }
323-
324-func flip(fd int, crtc, fb uint32, async bool) error {
325- p := pageFlip{crtcID: crtc, fbID: fb, flags: pageFlipEvent}
326- if async {
327- p.flags |= pageFlipAsync
328- }
329- return ioctl(fd, ioctlPageFlip, unsafe.Pointer(&p))
330-}
331-
332-// flipCrtcs parses DRM events read from the card fd and returns the CRTC of
333-// each completed page flip.
334-func flipCrtcs(buf []byte) []uint32 {
335- var out []uint32
336- for len(buf) >= 8 {
337- typ := binary.LittleEndian.Uint32(buf)
338- length := int(binary.LittleEndian.Uint32(buf[4:]))
339- if length < 8 || length > len(buf) {
340- break
341- }
342- // struct drm_event_vblank: base(8) user_data(8) sec usec sequence crtc_id.
343- if typ == eventFlipDone && length >= 32 {
344- out = append(out, binary.LittleEndian.Uint32(buf[28:]))
345- }
346- buf = buf[length:]
347- }
348- return out
349-}
A internal/adapters/drm/kms_mock_test.go +522 -0
@@ -0,0 +1,522 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package drm
6+
7+import (
8+ "github.com/bnema/nefertty/internal/ports"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// newMockkms creates a new instance of mockkms. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func newMockkms(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *mockkms {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &mockkms{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// mockkms is an autogenerated mock type for the kms type
36+type mockkms struct {
37+ mock.Mock
38+}
39+
40+type mockkms_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *mockkms) EXPECT() *mockkms_Expecter {
45+ return &mockkms_Expecter{mock: &_m.Mock}
46+}
47+
48+// addFB provides a mock function for the type mockkms
49+func (_mock *mockkms) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
50+ ret := _mock.Called(b, format)
51+
52+ if len(ret) == 0 {
53+ panic("no return value specified for addFB")
54+ }
55+
56+ var r0 uint32
57+ var r1 error
58+ if returnFunc, ok := ret.Get(0).(func(*ports.DMABuf, uint32) (uint32, error)); ok {
59+ return returnFunc(b, format)
60+ }
61+ if returnFunc, ok := ret.Get(0).(func(*ports.DMABuf, uint32) uint32); ok {
62+ r0 = returnFunc(b, format)
63+ } else {
64+ r0 = ret.Get(0).(uint32)
65+ }
66+ if returnFunc, ok := ret.Get(1).(func(*ports.DMABuf, uint32) error); ok {
67+ r1 = returnFunc(b, format)
68+ } else {
69+ r1 = ret.Error(1)
70+ }
71+ return r0, r1
72+}
73+
74+// mockkms_addFB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'addFB'
75+type mockkms_addFB_Call struct {
76+ *mock.Call
77+}
78+
79+// addFB is a helper method to define mock.On call
80+// - b *ports.DMABuf
81+// - format uint32
82+func (_e *mockkms_Expecter) addFB(b any, format any) *mockkms_addFB_Call {
83+ return &mockkms_addFB_Call{Call: _e.mock.On("addFB", b, format)}
84+}
85+
86+func (_c *mockkms_addFB_Call) Run(run func(b *ports.DMABuf, format uint32)) *mockkms_addFB_Call {
87+ _c.Call.Run(func(args mock.Arguments) {
88+ var arg0 *ports.DMABuf
89+ if args[0] != nil {
90+ arg0 = args[0].(*ports.DMABuf)
91+ }
92+ var arg1 uint32
93+ if args[1] != nil {
94+ arg1 = args[1].(uint32)
95+ }
96+ run(
97+ arg0,
98+ arg1,
99+ )
100+ })
101+ return _c
102+}
103+
104+func (_c *mockkms_addFB_Call) Return(v uint32, err error) *mockkms_addFB_Call {
105+ _c.Call.Return(v, err)
106+ return _c
107+}
108+
109+func (_c *mockkms_addFB_Call) RunAndReturn(run func(b *ports.DMABuf, format uint32) (uint32, error)) *mockkms_addFB_Call {
110+ _c.Call.Return(run)
111+ return _c
112+}
113+
114+// commit provides a mock function for the type mockkms
115+func (_mock *mockkms) commit(req *atomicReq, flags uint32, userData uint64) error {
116+ ret := _mock.Called(req, flags, userData)
117+
118+ if len(ret) == 0 {
119+ panic("no return value specified for commit")
120+ }
121+
122+ var r0 error
123+ if returnFunc, ok := ret.Get(0).(func(*atomicReq, uint32, uint64) error); ok {
124+ r0 = returnFunc(req, flags, userData)
125+ } else {
126+ r0 = ret.Error(0)
127+ }
128+ return r0
129+}
130+
131+// mockkms_commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'commit'
132+type mockkms_commit_Call struct {
133+ *mock.Call
134+}
135+
136+// commit is a helper method to define mock.On call
137+// - req *atomicReq
138+// - flags uint32
139+// - userData uint64
140+func (_e *mockkms_Expecter) commit(req any, flags any, userData any) *mockkms_commit_Call {
141+ return &mockkms_commit_Call{Call: _e.mock.On("commit", req, flags, userData)}
142+}
143+
144+func (_c *mockkms_commit_Call) Run(run func(req *atomicReq, flags uint32, userData uint64)) *mockkms_commit_Call {
145+ _c.Call.Run(func(args mock.Arguments) {
146+ var arg0 *atomicReq
147+ if args[0] != nil {
148+ arg0 = args[0].(*atomicReq)
149+ }
150+ var arg1 uint32
151+ if args[1] != nil {
152+ arg1 = args[1].(uint32)
153+ }
154+ var arg2 uint64
155+ if args[2] != nil {
156+ arg2 = args[2].(uint64)
157+ }
158+ run(
159+ arg0,
160+ arg1,
161+ arg2,
162+ )
163+ })
164+ return _c
165+}
166+
167+func (_c *mockkms_commit_Call) Return(err error) *mockkms_commit_Call {
168+ _c.Call.Return(err)
169+ return _c
170+}
171+
172+func (_c *mockkms_commit_Call) RunAndReturn(run func(req *atomicReq, flags uint32, userData uint64) error) *mockkms_commit_Call {
173+ _c.Call.Return(run)
174+ return _c
175+}
176+
177+// createBlob provides a mock function for the type mockkms
178+func (_mock *mockkms) createBlob(data []byte) (uint32, error) {
179+ ret := _mock.Called(data)
180+
181+ if len(ret) == 0 {
182+ panic("no return value specified for createBlob")
183+ }
184+
185+ var r0 uint32
186+ var r1 error
187+ if returnFunc, ok := ret.Get(0).(func([]byte) (uint32, error)); ok {
188+ return returnFunc(data)
189+ }
190+ if returnFunc, ok := ret.Get(0).(func([]byte) uint32); ok {
191+ r0 = returnFunc(data)
192+ } else {
193+ r0 = ret.Get(0).(uint32)
194+ }
195+ if returnFunc, ok := ret.Get(1).(func([]byte) error); ok {
196+ r1 = returnFunc(data)
197+ } else {
198+ r1 = ret.Error(1)
199+ }
200+ return r0, r1
201+}
202+
203+// mockkms_createBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'createBlob'
204+type mockkms_createBlob_Call struct {
205+ *mock.Call
206+}
207+
208+// createBlob is a helper method to define mock.On call
209+// - data []byte
210+func (_e *mockkms_Expecter) createBlob(data any) *mockkms_createBlob_Call {
211+ return &mockkms_createBlob_Call{Call: _e.mock.On("createBlob", data)}
212+}
213+
214+func (_c *mockkms_createBlob_Call) Run(run func(data []byte)) *mockkms_createBlob_Call {
215+ _c.Call.Run(func(args mock.Arguments) {
216+ var arg0 []byte
217+ if args[0] != nil {
218+ arg0 = args[0].([]byte)
219+ }
220+ run(
221+ arg0,
222+ )
223+ })
224+ return _c
225+}
226+
227+func (_c *mockkms_createBlob_Call) Return(v uint32, err error) *mockkms_createBlob_Call {
228+ _c.Call.Return(v, err)
229+ return _c
230+}
231+
232+func (_c *mockkms_createBlob_Call) RunAndReturn(run func(data []byte) (uint32, error)) *mockkms_createBlob_Call {
233+ _c.Call.Return(run)
234+ return _c
235+}
236+
237+// destroyBlob provides a mock function for the type mockkms
238+func (_mock *mockkms) destroyBlob(id uint32) error {
239+ ret := _mock.Called(id)
240+
241+ if len(ret) == 0 {
242+ panic("no return value specified for destroyBlob")
243+ }
244+
245+ var r0 error
246+ if returnFunc, ok := ret.Get(0).(func(uint32) error); ok {
247+ r0 = returnFunc(id)
248+ } else {
249+ r0 = ret.Error(0)
250+ }
251+ return r0
252+}
253+
254+// mockkms_destroyBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroyBlob'
255+type mockkms_destroyBlob_Call struct {
256+ *mock.Call
257+}
258+
259+// destroyBlob is a helper method to define mock.On call
260+// - id uint32
261+func (_e *mockkms_Expecter) destroyBlob(id any) *mockkms_destroyBlob_Call {
262+ return &mockkms_destroyBlob_Call{Call: _e.mock.On("destroyBlob", id)}
263+}
264+
265+func (_c *mockkms_destroyBlob_Call) Run(run func(id uint32)) *mockkms_destroyBlob_Call {
266+ _c.Call.Run(func(args mock.Arguments) {
267+ var arg0 uint32
268+ if args[0] != nil {
269+ arg0 = args[0].(uint32)
270+ }
271+ run(
272+ arg0,
273+ )
274+ })
275+ return _c
276+}
277+
278+func (_c *mockkms_destroyBlob_Call) Return(err error) *mockkms_destroyBlob_Call {
279+ _c.Call.Return(err)
280+ return _c
281+}
282+
283+func (_c *mockkms_destroyBlob_Call) RunAndReturn(run func(id uint32) error) *mockkms_destroyBlob_Call {
284+ _c.Call.Return(run)
285+ return _c
286+}
287+
288+// getBlob provides a mock function for the type mockkms
289+func (_mock *mockkms) getBlob(id uint32) ([]byte, error) {
290+ ret := _mock.Called(id)
291+
292+ if len(ret) == 0 {
293+ panic("no return value specified for getBlob")
294+ }
295+
296+ var r0 []byte
297+ var r1 error
298+ if returnFunc, ok := ret.Get(0).(func(uint32) ([]byte, error)); ok {
299+ return returnFunc(id)
300+ }
301+ if returnFunc, ok := ret.Get(0).(func(uint32) []byte); ok {
302+ r0 = returnFunc(id)
303+ } else {
304+ if ret.Get(0) != nil {
305+ r0 = ret.Get(0).([]byte)
306+ }
307+ }
308+ if returnFunc, ok := ret.Get(1).(func(uint32) error); ok {
309+ r1 = returnFunc(id)
310+ } else {
311+ r1 = ret.Error(1)
312+ }
313+ return r0, r1
314+}
315+
316+// mockkms_getBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'getBlob'
317+type mockkms_getBlob_Call struct {
318+ *mock.Call
319+}
320+
321+// getBlob is a helper method to define mock.On call
322+// - id uint32
323+func (_e *mockkms_Expecter) getBlob(id any) *mockkms_getBlob_Call {
324+ return &mockkms_getBlob_Call{Call: _e.mock.On("getBlob", id)}
325+}
326+
327+func (_c *mockkms_getBlob_Call) Run(run func(id uint32)) *mockkms_getBlob_Call {
328+ _c.Call.Run(func(args mock.Arguments) {
329+ var arg0 uint32
330+ if args[0] != nil {
331+ arg0 = args[0].(uint32)
332+ }
333+ run(
334+ arg0,
335+ )
336+ })
337+ return _c
338+}
339+
340+func (_c *mockkms_getBlob_Call) Return(bytes []byte, err error) *mockkms_getBlob_Call {
341+ _c.Call.Return(bytes, err)
342+ return _c
343+}
344+
345+func (_c *mockkms_getBlob_Call) RunAndReturn(run func(id uint32) ([]byte, error)) *mockkms_getBlob_Call {
346+ _c.Call.Return(run)
347+ return _c
348+}
349+
350+// objProps provides a mock function for the type mockkms
351+func (_mock *mockkms) objProps(obj uint32, typ uint32) (map[string][2]uint64, error) {
352+ ret := _mock.Called(obj, typ)
353+
354+ if len(ret) == 0 {
355+ panic("no return value specified for objProps")
356+ }
357+
358+ var r0 map[string][2]uint64
359+ var r1 error
360+ if returnFunc, ok := ret.Get(0).(func(uint32, uint32) (map[string][2]uint64, error)); ok {
361+ return returnFunc(obj, typ)
362+ }
363+ if returnFunc, ok := ret.Get(0).(func(uint32, uint32) map[string][2]uint64); ok {
364+ r0 = returnFunc(obj, typ)
365+ } else {
366+ if ret.Get(0) != nil {
367+ r0 = ret.Get(0).(map[string][2]uint64)
368+ }
369+ }
370+ if returnFunc, ok := ret.Get(1).(func(uint32, uint32) error); ok {
371+ r1 = returnFunc(obj, typ)
372+ } else {
373+ r1 = ret.Error(1)
374+ }
375+ return r0, r1
376+}
377+
378+// mockkms_objProps_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'objProps'
379+type mockkms_objProps_Call struct {
380+ *mock.Call
381+}
382+
383+// objProps is a helper method to define mock.On call
384+// - obj uint32
385+// - typ uint32
386+func (_e *mockkms_Expecter) objProps(obj any, typ any) *mockkms_objProps_Call {
387+ return &mockkms_objProps_Call{Call: _e.mock.On("objProps", obj, typ)}
388+}
389+
390+func (_c *mockkms_objProps_Call) Run(run func(obj uint32, typ uint32)) *mockkms_objProps_Call {
391+ _c.Call.Run(func(args mock.Arguments) {
392+ var arg0 uint32
393+ if args[0] != nil {
394+ arg0 = args[0].(uint32)
395+ }
396+ var arg1 uint32
397+ if args[1] != nil {
398+ arg1 = args[1].(uint32)
399+ }
400+ run(
401+ arg0,
402+ arg1,
403+ )
404+ })
405+ return _c
406+}
407+
408+func (_c *mockkms_objProps_Call) Return(stringToUint64s map[string][2]uint64, err error) *mockkms_objProps_Call {
409+ _c.Call.Return(stringToUint64s, err)
410+ return _c
411+}
412+
413+func (_c *mockkms_objProps_Call) RunAndReturn(run func(obj uint32, typ uint32) (map[string][2]uint64, error)) *mockkms_objProps_Call {
414+ _c.Call.Return(run)
415+ return _c
416+}
417+
418+// planes provides a mock function for the type mockkms
419+func (_mock *mockkms) planes() ([]planeRes, error) {
420+ ret := _mock.Called()
421+
422+ if len(ret) == 0 {
423+ panic("no return value specified for planes")
424+ }
425+
426+ var r0 []planeRes
427+ var r1 error
428+ if returnFunc, ok := ret.Get(0).(func() ([]planeRes, error)); ok {
429+ return returnFunc()
430+ }
431+ if returnFunc, ok := ret.Get(0).(func() []planeRes); ok {
432+ r0 = returnFunc()
433+ } else {
434+ if ret.Get(0) != nil {
435+ r0 = ret.Get(0).([]planeRes)
436+ }
437+ }
438+ if returnFunc, ok := ret.Get(1).(func() error); ok {
439+ r1 = returnFunc()
440+ } else {
441+ r1 = ret.Error(1)
442+ }
443+ return r0, r1
444+}
445+
446+// mockkms_planes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'planes'
447+type mockkms_planes_Call struct {
448+ *mock.Call
449+}
450+
451+// planes is a helper method to define mock.On call
452+func (_e *mockkms_Expecter) planes() *mockkms_planes_Call {
453+ return &mockkms_planes_Call{Call: _e.mock.On("planes")}
454+}
455+
456+func (_c *mockkms_planes_Call) Run(run func()) *mockkms_planes_Call {
457+ _c.Call.Run(func(args mock.Arguments) {
458+ run()
459+ })
460+ return _c
461+}
462+
463+func (_c *mockkms_planes_Call) Return(planeResMoqParams []planeRes, err error) *mockkms_planes_Call {
464+ _c.Call.Return(planeResMoqParams, err)
465+ return _c
466+}
467+
468+func (_c *mockkms_planes_Call) RunAndReturn(run func() ([]planeRes, error)) *mockkms_planes_Call {
469+ _c.Call.Return(run)
470+ return _c
471+}
472+
473+// rmFB provides a mock function for the type mockkms
474+func (_mock *mockkms) rmFB(id uint32) error {
475+ ret := _mock.Called(id)
476+
477+ if len(ret) == 0 {
478+ panic("no return value specified for rmFB")
479+ }
480+
481+ var r0 error
482+ if returnFunc, ok := ret.Get(0).(func(uint32) error); ok {
483+ r0 = returnFunc(id)
484+ } else {
485+ r0 = ret.Error(0)
486+ }
487+ return r0
488+}
489+
490+// mockkms_rmFB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'rmFB'
491+type mockkms_rmFB_Call struct {
492+ *mock.Call
493+}
494+
495+// rmFB is a helper method to define mock.On call
496+// - id uint32
497+func (_e *mockkms_Expecter) rmFB(id any) *mockkms_rmFB_Call {
498+ return &mockkms_rmFB_Call{Call: _e.mock.On("rmFB", id)}
499+}
500+
501+func (_c *mockkms_rmFB_Call) Run(run func(id uint32)) *mockkms_rmFB_Call {
502+ _c.Call.Run(func(args mock.Arguments) {
503+ var arg0 uint32
504+ if args[0] != nil {
505+ arg0 = args[0].(uint32)
506+ }
507+ run(
508+ arg0,
509+ )
510+ })
511+ return _c
512+}
513+
514+func (_c *mockkms_rmFB_Call) Return(err error) *mockkms_rmFB_Call {
515+ _c.Call.Return(err)
516+ return _c
517+}
518+
519+func (_c *mockkms_rmFB_Call) RunAndReturn(run func(id uint32) error) *mockkms_rmFB_Call {
520+ _c.Call.Return(run)
521+ return _c
522+}
M internal/adapters/drm/kms_test.go +56 -15
@@ -2,8 +2,12 @@ package drm
22
33 import (
44 "encoding/binary"
5+ "slices"
56 "testing"
7+ "time"
68 "unsafe"
9+
10+ "github.com/bnema/nefertty/internal/ports"
711 )
812
913 func TestStructSizesMatchIoctls(t *testing.T) {
@@ -13,19 +17,22 @@ func TestStructSizesMatchIoctls(t *testing.T) {
1317 req uintptr
1418 }{
1519 {"card_res", unsafe.Sizeof(cardRes{}), ioctlGetResources},
16- {"mode_crtc", unsafe.Sizeof(modeCrtc{}), ioctlSetCrtc},
1720 {"get_encoder", unsafe.Sizeof(getEncoder{}), ioctlGetEncoder},
1821 {"get_connector", unsafe.Sizeof(getConnector{}), ioctlGetConnector},
19- {"fb_cmd", unsafe.Sizeof(fbCmd{}), ioctlAddFB},
20- {"page_flip", unsafe.Sizeof(pageFlip{}), ioctlPageFlip},
21- {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb},
22- {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb},
2322 {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2},
2423 {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle},
2524 {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose},
2625 {"obj_get_props", unsafe.Sizeof(objGetProps{}), ioctlObjGetProps},
2726 {"get_prop", unsafe.Sizeof(getProp{}), ioctlGetProp},
28- {"obj_set_prop", unsafe.Sizeof(objSetProp{}), ioctlObjSetProp},
27+ {"mode_crtc", unsafe.Sizeof(modeCrtc{}), ioctlGetCrtc},
28+ {"set_client_cap", unsafe.Sizeof(setClientCap{}), ioctlSetClientCap},
29+ {"get_plane_res", unsafe.Sizeof(getPlaneRes{}), ioctlGetPlaneRes},
30+ {"get_plane", unsafe.Sizeof(getPlane{}), ioctlGetPlane},
31+ {"get_blob", unsafe.Sizeof(getBlob{}), ioctlGetPropBlob},
32+ {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic},
33+ {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob},
34+ {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap},
35+ {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob},
2936 } {
3037 if want := (tc.req >> 16) & 0x3fff; tc.got != want {
3138 t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)
@@ -88,19 +95,53 @@ func TestParseEDID(t *testing.T) {
8895 }
8996 }
9097
91-func TestFlipCrtcs(t *testing.T) {
92- ev := func(typ, crtc uint32) []byte {
98+func TestParseFlips(t *testing.T) {
99+ ev := func(typ, crtc uint32, user uint64, sec, usec, seq uint32) []byte {
93100 b := make([]byte, 32)
94- binary.LittleEndian.PutUint32(b, typ)
95- binary.LittleEndian.PutUint32(b[4:], 32)
96- binary.LittleEndian.PutUint32(b[28:], crtc)
101+ le := binary.LittleEndian
102+ le.PutUint32(b, typ)
103+ le.PutUint32(b[4:], 32)
104+ le.PutUint64(b[8:], user)
105+ le.PutUint32(b[16:], sec)
106+ le.PutUint32(b[20:], usec)
107+ le.PutUint32(b[24:], seq)
108+ le.PutUint32(b[28:], crtc)
97109 return b
98110 }
99- buf := append(append(ev(eventFlipDone, 41), ev(1, 7)...), ev(eventFlipDone, 42)...)
100- if got := flipCrtcs(buf); len(got) != 2 || got[0] != 41 || got[1] != 42 {
101- t.Fatalf("got %v", got)
111+ buf := append(append(ev(eventFlipDone, 41, userFrame, 3, 250, 7), ev(1, 7, 0, 0, 0, 0)...), ev(eventFlipDone, 42, userState, 0, 1, 8)...)
112+ got := parseFlips(buf)
113+ want := []flipEvent{{crtc: 41, user: userFrame, when: 3*time.Second + 250*time.Microsecond, seq: 7}, {crtc: 42, user: userState, when: time.Microsecond, seq: 8}}
114+ if !slices.Equal(got, want) {
115+ t.Fatalf("got %+v, want %+v", got, want)
102116 }
103- if got := flipCrtcs(buf[:20]); len(got) != 0 {
117+ if got := parseFlips(buf[:20]); len(got) != 0 {
104118 t.Fatalf("truncated: got %v", got)
105119 }
106120 }
121+
122+func TestParseInFormats(t *testing.T) {
123+ le := binary.LittleEndian
124+ // 3 formats at 24, 2 modifiers at 40.
125+ b := make([]byte, 40+2*24)
126+ le.PutUint32(b, 1)
127+ le.PutUint32(b[8:], 3)
128+ le.PutUint32(b[12:], 24)
129+ le.PutUint32(b[16:], 2)
130+ le.PutUint32(b[20:], 40)
131+ for i, f := range []uint32{fourccXRGB, fourccARGB, 0x3432564e} {
132+ le.PutUint32(b[24+i*4:], f)
133+ }
134+ // linear on formats 0 and 2, a tiled modifier on format 1.
135+ le.PutUint64(b[40:], 0b101)
136+ le.PutUint64(b[56:], 0)
137+ le.PutUint64(b[64:], 0b010)
138+ le.PutUint64(b[80:], 0x0200000000000001)
139+ got := parseInFormats(b)
140+ want := []ports.DMABufFormat{{Format: fourccXRGB}, {Format: 0x3432564e}, {Format: fourccARGB, Modifier: 0x0200000000000001}}
141+ if !slices.Equal(got, want) {
142+ t.Fatalf("got %+v, want %+v", got, want)
143+ }
144+ if parseInFormats(b[:30]) != nil {
145+ t.Fatal("truncated blob parsed")
146+ }
147+}
M internal/adapters/drm/output.go +974 -247
@@ -5,82 +5,371 @@ import (
55 "errors"
66 "fmt"
77 "maps"
8+ "os"
89 "slices"
10+ "sync/atomic"
911 "time"
1012 "unsafe"
1113
14+ "github.com/bnema/nefertty/internal/adapters/syncfile"
1215 "github.com/bnema/nefertty/internal/ports"
1316 "github.com/bnema/zerowrap"
1417 "golang.org/x/sys/unix"
1518 )
1619
17-// Output is one KMS connector flipping between two images. One goroutine
18-// runs it (Run) and closes it.
20+// Output is one KMS connector on a CRTC, driven by atomic commits (ADR
21+// 015). One goroutine runs it (Run) and closes it. Every commit that
22+// changes the screen asks for an event and leaves the output pending until
23+// it arrives: the next commit waits for it. The kernel may still refuse
24+// it with EBUSY (it retries soon), and an event that never comes is
25+// bounded (stuckTimeout, then a modeset).
1926 type Output struct {
20- fd int
21- flipped <-chan int // page flips of this CRTC, from Card.ReadEvents
27+ k kms
28+ flipped <-chan flipEvent // commit events of this CRTC, from Card.ReadEvents
2229 crtc uint32
2330 conn connector
2431 mode modeInfo
2532 saved modeCrtc
26- // fbs are the two images frames alternate between, back the one the
27- // next frame draws into. They are renderer images exported as dmabufs
28- // (ADR 014: zero copy), or dumb buffers filled by the CPU when the GPU
29- // cannot export a scanout-capable image (logged fallback).
30- fbs [2]uint32
31- bufs [2]*dumbBuffer // fallback only
32- back int
33- pending bool
34- flipStart time.Time
35- flips int
36- monitor Monitor
37- cursor *Cursor
38- log zerowrap.Logger
39- // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts,
40- // and the buffer on screen and queued (0: the composed image).
41- card *Card
33+ monitor Monitor
34+ log zerowrap.Logger
35+ // Properties: CRTC and connector property IDs by name.
36+ crtcProps map[string]uint32
37+ connCrtc uint32 // the connector's CRTC_ID property
38+ modeBlob uint32
39+ // primary shows frames; cursor is the cursor plane (nil: none);
40+ // stray are other planes that can show on this CRTC, turned off at
41+ // every modeset when a previous master left them on it.
42+ primary *plane
43+ cursor *Cursor
44+ stray []*plane
45+ // overlay shows one window's buffer above the composed frame
46+ // (overlay.go); overlayOn is the buffer on it in the pending or last
47+ // frame (0: off), overlayReason why it is off.
48+ overlay *plane
49+ overlayOn uint64
50+ overlayReason string
51+ // fbs are the two renderer images frames alternate between, back the
52+ // one the next frame draws into (ADR 014: zero copy).
53+ fbs [2]uint32
54+ back int
55+ // pending: a commit waits for its event; pendingFrame when it flips a
56+ // new frame (else it only changed the cursor or VRR).
57+ pending bool
58+ pendingFrame pendingFrame
59+ // serials numbers commits per card, so an event queued before a
60+ // modeset, or for an earlier output on this CRTC, never completes a
61+ // newer commit; pendingSerial is the pending commit's.
62+ serials *atomic.Uint64
63+ pendingSerial, nextSerial uint64
64+ flipStart time.Time
65+ flips int
66+ // A commit whose event does not come by stuckAt is abandoned with a
67+ // modeset once its fences signalled (stuckAfter, default
68+ // stuckTimeout; fenceWarned once it waits for a fence). A commit
69+ // refused with EBUSY is retried after busyRetry; busySince is when
70+ // the EBUSY run began, lastBusy the latest EBUSY.
71+ stuckAfter time.Duration
72+ stuckAt time.Time
73+ fenceWarned bool
74+ busySince, lastBusy time.Time
75+ // readFences are fences of frames rendered but not committed: the
76+ // GPU may still read client buffers until they signal, so what was
77+ // seen is reported only then (or after a later frame flipped).
78+ readFences []*os.File
79+ // Direct scanout: client framebuffers by DMABuf ID and the buffer on
80+ // screen and queued (0: the composed image).
4281 scanout bool
4382 clientFBs map[uint64]*clientFB
4483 shown, queued uint64
4584 reason string // why the last frame was composed ("" = scanout)
46- unsent ports.OutputPresented
47- // Tearing (ADR 006): async flips of scanned-out buffers that ask for
48- // them, while the card accepts them. vrrProp is the CRTC's VRR_ENABLED
49- // property (0: no VRR), turned on while a buffer is scanned out.
50- tearing bool
51- vrrProp uint32
52- vrrOn, async bool
53- // composedSince is when composition took over from scanout.
85+ unsent []ports.OutputPresented
86+ // Tearing (ADR 006): async commits of scanned-out buffers that ask for
87+ // them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence
88+ // is set when an async commit may carry IN_FENCE_FD (probed once).
89+ tearing bool
90+ asyncFence bool
91+ asyncProbed bool
92+ async bool
93+ // VRR: the CRTC's VRR_ENABLED property (0: none), on while a buffer is
94+ // scanned out, set inside frame commits.
95+ vrrProp uint32
96+ vrrOn bool
5497 composedSince time.Time
55- // kind is how the images were made; validated once a modeset took them.
56- kind imageKind
57- validated bool
98+ // kind is how the images were made.
99+ kind imageKind
100+ // formats receives the direct scanout formats after each modeset;
101+ // sampled and device are what they are built from (Want).
102+ formats chan<- ports.OutputFormats
103+ sampled []ports.DMABufFormat
104+ device uint64
105+}
106+
107+// pendingFrame is what the pending frame commit shows.
108+type pendingFrame struct {
109+ frame bool
110+ queued uint64
111+ zeroCopy ports.WindowID // window shown without composition
112+ async bool
113+ shows map[ports.WindowID]uint64
114+ // fences are duplicates of the fences the commit waits on, owned by
115+ // the output until the commit ends. composed is set when the frame
116+ // was rendered (its fence is the renderer's).
117+ fences []*os.File
118+ composed bool
119+}
120+
121+// closeFences closes the fences of f.
122+func (f *pendingFrame) closeFences() {
123+ for _, fd := range f.fences {
124+ fd.Close()
125+ }
126+ f.fences = nil
127+}
128+
129+// dupFences duplicates the non-nil fences.
130+func dupFences(fs ...*os.File) []*os.File {
131+ var out []*os.File
132+ for _, f := range fs {
133+ if d := dupFence(f); d != nil {
134+ out = append(out, d)
135+ }
136+ }
137+ return out
138+}
139+
140+// signalled reports whether every fence signalled (readable); an fd that
141+// cannot be polled counts as signalled.
142+func signalled(fs []*os.File) bool {
143+ if len(fs) == 0 {
144+ return true
145+ }
146+ pfds := make([]unix.PollFd, len(fs))
147+ for i, f := range fs {
148+ pfds[i] = unix.PollFd{Fd: int32(f.Fd()), Events: unix.POLLIN}
149+ }
150+ if _, err := unix.Poll(pfds, 0); err != nil {
151+ return true
152+ }
153+ for _, p := range pfds {
154+ if p.Revents == 0 {
155+ return false
156+ }
157+ }
158+ return true
159+}
160+
161+// holdRead keeps the fences of a frame that was not committed.
162+func (o *Output) holdRead(fs ...*os.File) {
163+ o.readFences = append(o.readFences, dupFences(fs...)...)
164+}
165+
166+// readDone reports whether no uncommitted frame may still read client
167+// buffers, closing the fences that signalled.
168+func (o *Output) readDone() bool {
169+ kept := o.readFences[:0]
170+ for _, f := range o.readFences {
171+ if signalled([]*os.File{f}) {
172+ f.Close()
173+ continue
174+ }
175+ kept = append(kept, f)
176+ }
177+ clear(o.readFences[len(kept):])
178+ o.readFences = kept
179+ return len(kept) == 0
180+}
181+
182+// dropRead closes the fences of uncommitted frames.
183+func (o *Output) dropRead() {
184+ for _, f := range o.readFences {
185+ f.Close()
186+ }
187+ o.readFences = nil
188+}
189+
190+// endPending ends the pending commit, closing its fences.
191+func (o *Output) endPending() {
192+ o.pendingFrame.closeFences()
193+ o.pending, o.pendingFrame = false, pendingFrame{}
194+}
195+
196+// Commit event userData: the commit serial above userKindBits, the kind
197+// of commit below.
198+const (
199+ userFrame = 1
200+ userState = 2
201+ userKindBits = 2
202+)
203+
204+// userData takes a new serial for the next commit of kind and returns
205+// what the commit carries.
206+func (o *Output) userData(kind uint64) uint64 {
207+ o.nextSerial = o.serials.Add(1)
208+ return o.nextSerial<<userKindBits | kind
209+}
210+
211+// begin marks the commit made with the last userData pending.
212+func (o *Output) begin(f pendingFrame) {
213+ o.pendingSerial = o.nextSerial
214+ o.pendingFrame.closeFences()
215+ o.pending, o.pendingFrame, o.flipStart = true, f, time.Now()
216+ o.stuckAt, o.fenceWarned = o.flipStart.Add(o.stuckLimit()), false
217+ o.busySince = time.Time{}
218+}
219+
220+// Bounds on waiting for a commit event: no path may leave an output
221+// pending forever.
222+const (
223+ stuckTimeout = time.Second
224+ busyRetry = 50 * time.Millisecond
225+)
226+
227+func (o *Output) stuckLimit() time.Duration {
228+ if o.stuckAfter > 0 {
229+ return o.stuckAfter
230+ }
231+ return stuckTimeout
232+}
233+
234+// expire handles a pending commit whose event did not come by stuckAt.
235+// A commit still waiting for a fence (a long GPU job) keeps waiting.
236+// Else the commit is abandoned so the next can go; it reports whether
237+// the output needs a modeset: an event is missing (the kernel state is
238+// unknown) or EBUSY lasted past the limit.
239+func (o *Output) expire() bool {
240+ now := time.Now()
241+ age := now.Sub(o.flipStart)
242+ if o.pendingSerial != 0 && !signalled(o.pendingFrame.fences) {
243+ if !o.fenceWarned {
244+ o.fenceWarned = true
245+ o.log.Warn().Str("connector", o.conn.name).Uint64("serial", o.pendingSerial).Dur("age", age).Msg("frame waits for GPU fence")
246+ }
247+ o.stuckAt = now.Add(o.stuckLimit())
248+ return false
249+ }
250+ kind := "state"
251+ if o.pendingFrame.frame {
252+ kind = "frame"
253+ }
254+ o.endPending()
255+ if o.pendingSerial == 0 {
256+ busy := now.Sub(o.busySince)
257+ if o.busySince.IsZero() || busy < o.stuckLimit() {
258+ return false
259+ }
260+ o.log.Warn().Str("connector", o.conn.name).Str("kind", "busy").Dur("age", busy).Msg("commits refused as busy; modeset")
261+ return true
262+ }
263+ o.log.Warn().Str("connector", o.conn.name).Uint64("serial", o.pendingSerial).Str("kind", kind).Dur("age", age).Msg("commit event missing; modeset")
264+ return true
58265 }
59266
60267 // CursorLoader returns the image of a cursor at an output scale, at most
61268 // limit pixels on a side; an empty image hides the cursor.
62269 type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)
63270
64-// newOutput prepares the cursor and saves the CRTC for a connector on crtc.
271+// newOutput reads the CRTC's planes and properties for a connector on crtc.
65272 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
66273 log := card.log
67- o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
274+ pipe := slices.Index(card.crtcs, crtc)
275+ o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
68276 var err error
69277 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
70278 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
71279 }
280+ if err := o.readProps(pipe, card.taken, cursorSize(card.fd)); err != nil {
281+ return nil, err
282+ }
72283 o.tearing = card.async && !card.want.NoTearing
73284 if !card.want.NoVRR {
74- o.vrrProp = vrrProperty(card.fd, c.id, crtc)
75- }
76- if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil {
77- log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor")
285+ o.vrrProp = vrrProperty(o.k, c.id, crtc)
78286 }
79- log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Bool("tearing", o.tearing).Bool("vrr", o.vrrProp != 0).Msg("output")
287+ log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Uint32("primary", o.primary.id).Bool("cursor", o.cursor != nil).Bool("tearing", o.tearing).Bool("vrr", o.vrrProp != 0).Msg("output")
80288 return o, nil
81289 }
82290
83-// Cursor is the hardware cursor, or nil when the driver has none.
291+// readProps finds the output's planes and property IDs. taken are planes
292+// other outputs drive.
293+func (o *Output) readProps(pipe int, taken map[uint32]bool, cursorSide int) error {
294+ cp, err := o.k.objProps(o.crtc, objCrtc)
295+ if err != nil {
296+ return fmt.Errorf("crtc properties: %w", err)
297+ }
298+ o.crtcProps = map[string]uint32{}
299+ for name, v := range cp {
300+ o.crtcProps[name] = uint32(v[0])
301+ }
302+ np, err := o.k.objProps(o.conn.id, objConnector)
303+ if err != nil {
304+ return fmt.Errorf("connector properties: %w", err)
305+ }
306+ o.connCrtc = uint32(np["CRTC_ID"][0])
307+ if o.crtcProps["MODE_ID"] == 0 || o.crtcProps["ACTIVE"] == 0 || o.connCrtc == 0 {
308+ return errors.New("crtc or connector lacks atomic properties")
309+ }
310+ planes, err := readPlanes(o.k, pipe)
311+ if err != nil {
312+ return fmt.Errorf("planes: %w", err)
313+ }
314+ pick := func(typ uint32) *plane {
315+ var best *plane
316+ for _, p := range planes {
317+ if p.typ != typ || taken[p.id] {
318+ continue
319+ }
320+ // Prefer the plane already on this CRTC.
321+ if best == nil || p.crtc == o.crtc && best.crtc != o.crtc {
322+ best = p
323+ }
324+ }
325+ return best
326+ }
327+ if o.primary = pick(planePrimary); o.primary == nil {
328+ return errors.New("no free primary plane")
329+ }
330+ if p := pick(planeCursor); p != nil {
331+ o.cursor = newCursor(p, cursorSide)
332+ }
333+ for _, p := range planes {
334+ if p != o.primary && (o.cursor == nil || p != o.cursor.plane) && !taken[p.id] {
335+ o.stray = append(o.stray, p)
336+ }
337+ }
338+ o.pickOverlay()
339+ return nil
340+}
341+
342+// owned are the planes this output drives.
343+func (o *Output) owned() []*plane {
344+ out := []*plane{o.primary}
345+ if o.cursor != nil {
346+ out = append(out, o.cursor.plane)
347+ }
348+ if o.overlay != nil {
349+ out = append(out, o.overlay)
350+ }
351+ return out
352+}
353+
354+// cursorSize is the cursor plane side: the smaller of the driver's width
355+// and height caps, 64 when it does not say.
356+func cursorSize(fd int) int {
357+ size := 0
358+ for _, c := range []uint64{capCursorW, capCursorH} {
359+ v := getCap{capability: c}
360+ if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 {
361+ if size == 0 || int(v.value) < size {
362+ size = int(v.value)
363+ }
364+ }
365+ }
366+ if size == 0 {
367+ size = 64
368+ }
369+ return size
370+}
371+
372+// Cursor is the hardware cursor, or nil when the CRTC has no cursor plane.
84373 func (o *Output) Cursor() *Cursor { return o.cursor }
85374
86375 // Info describes the output for core and clients. Unknown EDID fields are empty.
@@ -97,52 +386,309 @@ func (o *Output) Info() ports.OutputInfo {
97386 func (o *Output) Width() int { return int(o.mode.HDisplay) }
98387 func (o *Output) Height() int { return int(o.mode.VDisplay) }
99388
100-// modeset shows the front buffer; needed at start and after every VT resume.
389+// primaryProps shows fb full screen on the primary plane.
390+func (o *Output) primaryProps(req *atomicReq, fb uint32) {
391+ p := o.primary
392+ w, h := uint64(o.Width()), uint64(o.Height())
393+ req.set(p.id, p.prop("FB_ID"), uint64(fb))
394+ req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
395+ req.set(p.id, p.prop("SRC_X"), 0)
396+ req.set(p.id, p.prop("SRC_Y"), 0)
397+ req.set(p.id, p.prop("SRC_W"), w<<16)
398+ req.set(p.id, p.prop("SRC_H"), h<<16)
399+ req.set(p.id, p.prop("CRTC_X"), 0)
400+ req.set(p.id, p.prop("CRTC_Y"), 0)
401+ req.set(p.id, p.prop("CRTC_W"), w)
402+ req.set(p.id, p.prop("CRTC_H"), h)
403+}
404+
405+// modeset shows the front image with the output's mode, turning off every
406+// other plane on the CRTC; needed at start and after every VT resume.
407+// It blocks until the kernel applied it.
101408 func (o *Output) modeset() error {
102- o.pending = false
409+ blob, err := o.modeBlobFor(o.mode)
410+ if err != nil {
411+ return err
412+ }
413+ if err := o.k.commit(o.modesetReq(blob), atomicAllowModes, 0); err != nil {
414+ _ = o.k.destroyBlob(blob)
415+ return fmt.Errorf("modeset: %w", err)
416+ }
417+ // Only now is nothing of ours pending or on screen: on failure the
418+ // previous buffers may still show.
419+ o.endPending()
420+ o.busySince = time.Time{}
103421 o.shown, o.queued = 0, 0
104- if err := setCrtc(o.fd, o.crtc, o.conn.id, o.fbs[1-o.back], &o.mode); err != nil {
105- return fmt.Errorf("set crtc: %w", err)
422+ if o.modeBlob != 0 {
423+ _ = o.k.destroyBlob(o.modeBlob)
424+ }
425+ o.modeBlob = blob
426+ o.vrrOn, o.overlayOn = false, 0
427+ if o.cursor != nil {
428+ o.cursor.applied = cursorState{}
429+ o.cursor.screen, o.cursor.flying = 0, false
106430 }
107431 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
108- // Another master may have left VRR on: start from off.
109- o.vrrOn = true
110- o.setVRR(false)
432+ o.testCursor()
433+ o.sendFormats()
434+ return nil
435+}
436+
437+// sendFormats reports the formats clients may allocate for direct
438+// scanout on this output (none when scanout is off). It never blocks:
439+// wayland keeps the latest per output, a dropped report is resent at the
440+// next modeset.
441+func (o *Output) sendFormats() {
442+ if o.formats == nil {
443+ return
444+ }
445+ f := ports.OutputFormats{Output: o.conn.name, Device: o.device}
446+ if o.scanout {
447+ f.Formats = o.scanoutFormats(o.sampled)
448+ }
449+ select {
450+ case o.formats <- f:
451+ default:
452+ o.log.Warn().Str("connector", o.conn.name).Msg("scanout formats not sent: wayland busy")
453+ }
454+}
455+
456+// testModeset asks KMS whether it would take the modeset of the output
457+// images without applying it.
458+func (o *Output) testModeset() error {
459+ blob, err := o.modeBlobFor(o.mode)
460+ if err != nil {
461+ return err
462+ }
463+ defer func() { _ = o.k.destroyBlob(blob) }()
464+ if err := o.k.commit(o.modesetReq(blob), atomicTestOnly|atomicAllowModes, 0); err != nil {
465+ return fmt.Errorf("modeset test: %w", err)
466+ }
111467 return nil
112468 }
113469
114-// present queues a page flip to the frame just drawn into the back image
115-// (copied into it by the CPU on the dumb-buffer fallback).
116-func (o *Output) present(r ports.Renderer) error {
117- if b := o.bufs[o.back]; b != nil {
118- r.CopyBGRX(b.mem, int(b.pitch))
470+func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) {
471+ blob, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode)))
472+ if err != nil {
473+ return 0, fmt.Errorf("mode blob: %w", err)
474+ }
475+ return blob, nil
476+}
477+
478+// testCursor turns the hardware cursor off when KMS refuses its images on
479+// the cursor plane (e.g. a pitch the plane cannot scan out).
480+func (o *Output) testCursor() {
481+ c := o.cursor
482+ if c == nil || c.off || c.fbs[0] == 0 {
483+ return
484+ }
485+ req := &atomicReq{}
486+ c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]})
487+ if err := o.k.commit(req, atomicTestOnly, 0); refused(err) {
488+ o.cursorOff(err)
489+ }
490+}
491+
492+// cursorOff stops using the cursor plane for the rest of the output's life.
493+func (o *Output) cursorOff(err error) {
494+ o.cursor.off = true
495+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off")
496+}
497+
498+// modesetReq is the modeset of the front image with mode blob.
499+func (o *Output) modesetReq(blob uint32) *atomicReq {
500+ req := &atomicReq{}
501+ req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob))
502+ req.set(o.crtc, o.crtcProps["ACTIVE"], 1)
503+ req.set(o.crtc, o.vrrProp, 0)
504+ req.set(o.conn.id, o.connCrtc, uint64(o.crtc))
505+ o.primaryProps(req, o.fbs[1-o.back])
506+ if o.cursor != nil {
507+ o.cursor.props(req, o.crtc, cursorState{})
508+ }
509+ o.overlayProps(req, overlayWin{})
510+ for _, p := range o.stray {
511+ // Only planes a previous master left on this CRTC.
512+ props, err := o.k.objProps(p.id, objPlane)
513+ if err != nil || uint32(props["CRTC_ID"][1]) != o.crtc {
514+ continue
515+ }
516+ req.set(p.id, p.prop("FB_ID"), 0)
517+ req.set(p.id, p.prop("CRTC_ID"), 0)
518+ }
519+ return req
520+}
521+
522+// probeAsync checks once whether async commits may carry IN_FENCE_FD.
523+func (o *Output) probeAsync(r ports.Renderer) {
524+ if o.asyncProbed || !o.tearing {
525+ return
526+ }
527+ o.asyncProbed = true
528+ r.UseTarget(o.back)
529+ done, err := r.Render(ports.Scene{Background: "#000000"}, nil)
530+ if err != nil {
531+ return
532+ }
533+ req := &atomicReq{}
534+ req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(o.fbs[o.back]))
535+ if done != nil {
536+ defer done.Close()
537+ req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(done.Fd()))
538+ }
539+ err = o.k.commit(req, atomicTestOnly|flipAsyncFlag, 0)
540+ o.asyncFence = err == nil && done != nil
541+ o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe")
542+}
543+
544+// wantVRR is the VRR state the next frame commit sets: on while a buffer
545+// is scanned out, off after composing for vrrHold (each toggle may
546+// flicker, so short compositions keep it).
547+func (o *Output) wantVRR(direct bool) bool {
548+ if o.vrrProp == 0 {
549+ return false
550+ }
551+ if direct {
552+ o.composedSince = time.Time{}
553+ return true
119554 }
120- // VRR goes off only after composing for vrrHold: each toggle is a
121- // blocking commit and may flicker, so short compositions keep it.
122555 if o.composedSince.IsZero() {
123556 o.composedSince = time.Now()
124- } else if time.Since(o.composedSince) > vrrHold {
125- o.setVRR(false)
126557 }
127- o.setAsync(false)
128- if err := flip(o.fd, o.crtc, o.fbs[o.back], false); err != nil {
558+ return o.vrrOn && time.Since(o.composedSince) <= vrrHold
559+}
560+
561+// commitFrame flips fb in one commit with the cursor and VRR. An async
562+// commit carries only the primary plane (kernel rule): a frame that must
563+// also move the cursor or change VRR flips at vblank. fence is the
564+// frame's GPU fence (nil: none); the caller keeps and closes it.
565+func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error {
566+ return o.commitWith(fb, fence, async, vrr, f, overlayWin{})
567+}
568+
569+// commitWith is commitFrame with ov on the overlay plane (zero: off).
570+func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error {
571+ cur := cursorState{}
572+ if o.cursor != nil {
573+ cur = o.cursor.desired()
574+ }
575+ if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn) {
576+ async = false
577+ }
578+ req := &atomicReq{}
579+ flags := uint32(atomicNonblock | flipEventFlag)
580+ if async {
581+ flags |= flipAsyncFlag
582+ req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(fb))
583+ } else {
584+ o.primaryProps(req, fb)
585+ req.set(o.crtc, o.vrrProp, boolValue(vrr))
586+ if o.cursor != nil {
587+ o.cursor.props(req, o.crtc, cur)
588+ }
589+ if ov.buf != 0 || o.overlayOn != 0 {
590+ o.overlayProps(req, ov)
591+ }
592+ }
593+ if fence != nil {
594+ req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
595+ }
596+ // The kernel takes its own reference on the fence.
597+ if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil {
598+ if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
599+ return o.commitWith(fb, fence, false, vrr, f, ov)
600+ }
601+ if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {
602+ // Retry without turning VRR on: if that passes, the driver
603+ // refuses VRR on this output.
604+ if o.commitFrame(fb, fence, false, false, f) == nil {
605+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled")
606+ o.vrrProp = 0
607+ return nil
608+ }
609+ }
610+ return err
611+ }
612+ if !async {
613+ if o.vrrOn != vrr {
614+ o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr")
615+ }
616+ o.vrrOn = vrr
617+ }
618+ if o.cursor != nil {
619+ // An async commit leaves the cursor plane as applied.
620+ o.cursor.committed(cur)
621+ }
622+ if !async {
623+ o.overlayOn = ov.buf
624+ }
625+ f.frame, f.async = true, async
626+ f.fences = dupFences(fence, ov.acquire)
627+ o.setAsync(async)
628+ o.begin(f)
629+ return nil
630+}
631+
632+// cursorRefused reports, after a commit carrying the cursor failed with
633+// EINVAL, whether the cursor plane was the cause; then the cursor is off
634+// and the commit can be retried without it.
635+func (o *Output) cursorRefused() bool {
636+ o.testCursor()
637+ return o.cursor.off
638+}
639+
640+// commitState commits a cursor or VRR change without a new frame.
641+func (o *Output) commitState(vrr bool) error {
642+ req := &atomicReq{}
643+ cur := cursorState{}
644+ if o.cursor != nil {
645+ cur = o.cursor.desired()
646+ if cur != o.cursor.applied {
647+ o.cursor.props(req, o.crtc, cur)
648+ }
649+ }
650+ if vrr != o.vrrOn {
651+ req.set(o.crtc, o.vrrProp, boolValue(vrr))
652+ }
653+ if len(req.objs) == 0 {
654+ return nil
655+ }
656+ if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.userData(userState)); err != nil {
657+ if o.overlayConflict(err, o.overlayOn) {
658+ // The next frame is composed without the overlay.
659+ return errOverlayDropped
660+ }
661+ if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
662+ return o.commitState(vrr)
663+ }
129664 return err
130665 }
131- o.pending, o.flipStart = true, time.Now()
132- o.queued = 0
133- o.back = 1 - o.back
666+ if o.cursor != nil {
667+ o.cursor.committed(cur)
668+ }
669+ if vrr != o.vrrOn {
670+ o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr")
671+ }
672+ o.vrrOn = vrr
673+ o.begin(pendingFrame{})
134674 return nil
135675 }
136676
137-// tryScanout flips a fullscreen client buffer directly. It reports false,
138-// with the reason logged on change, when the frame must be composed.
139-func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (bool, error) {
677+func boolValue(b bool) uint64 {
678+ if b {
679+ return 1
680+ }
681+ return 0
682+}
683+
684+// scanoutFrame picks a fullscreen client buffer to flip directly. It
685+// returns fb 0, with the reason logged on change, when the frame must be
686+// composed.
687+func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (fb uint32, c ports.SurfaceContent) {
140688 reason := "disabled"
141- var c ports.SurfaceContent
142689 if o.scanout {
143690 c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height())
144691 }
145- var fb uint32
146692 if reason == "" {
147693 fb, reason = o.scanoutFB(c.DMABuf, time.Now())
148694 }
@@ -151,71 +697,51 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports
151697 o.reason = reason
152698 }
153699 if reason != "" {
154- return false, nil
155- }
156- if o.shown == c.DMABuf.ID && o.queued == 0 {
157- // Already on screen: nothing to flip.
158- return true, nil
700+ return 0, c
159701 }
702+ return fb, c
703+}
704+
705+// commitScanout flips a client buffer. It reports false when KMS refused
706+// the buffer: the frame must be composed.
707+func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame) (bool, error) {
708+ cfb := o.clientFBs[c.DMABuf.ID]
160709 // Drivers refuse async flips that change the format or modifier: the
161710 // flip from the composed image into scanout waits for vblank.
162- cfb := o.clientFBs[c.DMABuf.ID]
163711 async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync
164- err := flip(o.fd, o.crtc, fb, async)
712+ vrr := o.wantVRR(true)
713+ f.queued, f.zeroCopy = c.DMABuf.ID, c.ID
714+ // Explicit sync: KMS waits on the client's acquire fence. Wayland may
715+ // close its file at any time: commit a duplicate taken while it is
716+ // held open (the kernel keeps its own reference).
717+ fence := dupFence(c.Acquire)
718+ if fence != nil {
719+ defer fence.Close()
720+ }
721+ err := o.commitFrame(fb, fence, async, vrr, f)
165722 if err != nil && async && errors.Is(err, unix.EINVAL) {
166- // Refused (e.g. a layout change, or not a fast update): this
167- // buffer flips at vblank from now on.
723+ // Refused (e.g. not a fast update): this buffer flips at vblank
724+ // from now on.
168725 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
169- cfb.noAsync, async = true, false
170- err = flip(o.fd, o.crtc, fb, false)
726+ cfb.noAsync = true
727+ err = o.commitFrame(fb, fence, false, vrr, f)
171728 }
172- if err == nil {
173- o.setAsync(async)
174- o.setVRR(true)
175- o.composedSince = time.Time{}
729+ if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) {
730+ // KMS refuses this buffer on the plane: compose it instead.
731+ o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused")
732+ cfb.failed, o.reason = "flip_refused", "flip_refused"
733+ o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout")
734+ return false, nil
176735 }
177- if err != nil {
178- if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) {
179- // KMS refuses this buffer on the plane: compose it instead.
180- o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused")
181- cfb.failed = "flip_refused"
182- o.reason = "flip_refused"
183- o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout")
184- return false, nil
185- }
186- return true, err
736+ if err == nil {
737+ o.queued = c.DMABuf.ID
187738 }
188- o.pending, o.flipStart = true, time.Now()
189- o.queued = c.DMABuf.ID
190- return true, nil
739+ return true, err
191740 }
192741
193742 // vrrHold is how long composition runs before VRR turns off.
194743 const vrrHold = 500 * time.Millisecond
195744
196-// setVRR turns variable refresh on or off, when the output has it.
197-func (o *Output) setVRR(on bool) {
198- if o.vrrProp == 0 || on == o.vrrOn {
199- return
200- }
201- v := uint64(0)
202- if on {
203- v = 1
204- }
205- // SETPROPERTY is a blocking atomic commit (about one refresh).
206- start := time.Now()
207- if err := setProp(o.fd, o.crtc, objCrtc, o.vrrProp, v); err != nil {
208- if lostMaster(err) {
209- return // switched away: retried on the next change
210- }
211- o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr; disabled")
212- o.vrrProp, o.vrrOn = 0, false
213- return
214- }
215- o.vrrOn = on
216- o.log.Info().Bool("vrr", on).Str("connector", o.conn.name).Dur("took", time.Since(start)).Msg("vrr")
217-}
218-
219745 // setAsync logs when the output starts or stops tearing.
220746 func (o *Output) setAsync(on bool) {
221747 if on != o.async {
@@ -227,30 +753,77 @@ func (o *Output) setAsync(on bool) {
227753 // Close restores the CRTC state found at startup, when it had a mode, and
228754 // frees buffers.
229755 func (o *Output) Close() {
756+ req := &atomicReq{}
230757 if o.cursor != nil {
231- o.cursor.close()
758+ o.cursor.props(req, o.crtc, cursorState{})
232759 }
233- o.setVRR(false)
234- if o.saved.crtcID != 0 {
235- ids := []uint32{o.conn.id}
236- s := o.saved
237- if s.modeValid != 0 {
238- _ = setCrtc(o.fd, s.crtcID, ids[0], s.fbID, &s.mode)
760+ o.overlayProps(req, overlayWin{})
761+ req.set(o.crtc, o.vrrProp, 0)
762+ s := o.saved
763+ var blob uint32
764+ p := o.primary
765+ restored := false
766+ if s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 {
767+ if b, err := o.modeBlobFor(s.mode); err == nil {
768+ blob, restored = b, true
769+ req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(b))
770+ req.set(o.crtc, o.crtcProps["ACTIVE"], 1)
771+ req.set(o.conn.id, o.connCrtc, uint64(o.crtc))
772+ req.set(p.id, p.prop("FB_ID"), uint64(s.fbID))
773+ req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
774+ req.set(p.id, p.prop("SRC_X"), 0)
775+ req.set(p.id, p.prop("SRC_Y"), 0)
776+ req.set(p.id, p.prop("SRC_W"), uint64(s.mode.HDisplay)<<16)
777+ req.set(p.id, p.prop("SRC_H"), uint64(s.mode.VDisplay)<<16)
778+ req.set(p.id, p.prop("CRTC_X"), 0)
779+ req.set(p.id, p.prop("CRTC_Y"), 0)
780+ req.set(p.id, p.prop("CRTC_W"), uint64(s.mode.HDisplay))
781+ req.set(p.id, p.prop("CRTC_H"), uint64(s.mode.VDisplay))
239782 }
240783 }
784+ if !restored {
785+ // Nothing to give back: leave the CRTC off.
786+ req.set(o.crtc, o.crtcProps["ACTIVE"], 0)
787+ req.set(o.crtc, o.crtcProps["MODE_ID"], 0)
788+ req.set(p.id, p.prop("FB_ID"), 0)
789+ req.set(p.id, p.prop("CRTC_ID"), 0)
790+ req.set(o.conn.id, o.connCrtc, 0)
791+ }
792+ // Best effort: at exit the card may already belong to another session.
793+ if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
794+ o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("restore crtc")
795+ }
796+ if blob != 0 {
797+ _ = o.k.destroyBlob(blob)
798+ }
799+ if o.modeBlob != 0 {
800+ _ = o.k.destroyBlob(o.modeBlob)
801+ o.modeBlob = 0
802+ }
803+ if o.cursor != nil {
804+ o.cursor.free(o.k)
805+ }
241806 o.freeImages()
807+ o.endPending()
808+ o.dropRead()
242809 o.shown, o.queued = 0, 0
243810 o.dropClientFBs(time.Now(), true)
244811 }
245812
246-// Run renders scenes and flips until ctx ends. active reports seat enable/disable.
247-// Each completed page flip is reported on presented (dropped when full).
813+// Run renders scenes and commits them until ctx ends. active reports seat
814+// enable/disable. What the output shows and read is reported on presented.
248815 func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, presented chan<- ports.OutputPresented) error {
249816 r, err := newRenderer(o.Width(), o.Height())
250817 if err != nil {
251818 return fmt.Errorf("create renderer: %w", err)
252819 }
253820 defer r.Close()
821+ if o.cursor != nil {
822+ if err := o.cursor.setup(o.k, r); err != nil {
823+ o.cursor.off = true
824+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off")
825+ }
826+ }
254827 // The seat sends its state first; while switched away (e.g. a monitor
255828 // plugged in on another VT) the modeset waits for the enable.
256829 enabled := true
@@ -258,15 +831,13 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
258831 case enabled = <-active:
259832 default:
260833 }
261- // Output images, from the best to the fallback (ADR 014). While
262- // switched away they are validated by the first modeset on enable.
834+ // Output images, from the best to the linear fallback (ADR 014).
835+ // While switched away they are made on the first enable.
263836 if enabled {
264- err = o.showImages(r, imagesDriver, nil)
265- } else {
266- o.kind, err = o.setupImages(r, imagesDriver, nil)
267- }
268- if err != nil {
269- return err
837+ if err := o.showImages(r, imagesDriver, nil); err != nil {
838+ return err
839+ }
840+ o.probeAsync(r)
270841 }
271842 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
272843 var scene ports.Scene
@@ -276,91 +847,113 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
276847 frame := 0
277848 stats := time.NewTicker(10 * time.Second)
278849 defer stats.Stop()
279- // seen is the latest content Seq per window, reported to wayland with
280- // what the output scans out, after each frame decision.
850+ // seen is the latest content Seq per window. It is reported only while
851+ // no frame is in flight: then the GPU has finished every frame that
852+ // read older buffers (the kernel waited for their fence to flip).
281853 seen := map[ports.WindowID]uint64{}
282854 reportDirty := false
283- flipped := false
855+ var cursorWake <-chan struct{}
856+ if o.cursor != nil {
857+ cursorWake = o.cursor.wake
858+ }
284859 for {
285- if flipped || reportDirty {
286- o.report(presented, flipped, seen)
287- flipped, reportDirty = false, false
288- } else {
289- o.flushReport(presented)
860+ readWait := false
861+ if reportDirty && !o.pending {
862+ if readWait = !o.readDone(); !readWait {
863+ o.report(nil, seen)
864+ reportDirty = false
865+ }
290866 }
291- // An unsent report is retried soon, not only on the next event.
867+ o.flushReport(presented)
868+ // An unsent report is retried soon, not only on the next event;
869+ // so is one that waits for an uncommitted frame's fence.
292870 var retry <-chan time.Time
293- if o.unsent.Output != "" {
871+ if len(o.unsent) > 0 || readWait {
294872 retry = time.After(time.Millisecond)
295873 }
296874 // A composed screen that stopped changing still drops VRR on time.
297875 var vrrOff <-chan time.Time
298- if enabled && o.vrrOn && !o.composedSince.IsZero() {
876+ if enabled && o.vrrOn && !o.pending && !o.composedSince.IsZero() {
299877 vrrOff = time.After(max(0, vrrHold-time.Since(o.composedSince)))
300878 }
879+ // A commit whose event never comes must not stop the output.
880+ var stuck <-chan time.Time
881+ if enabled && o.pending {
882+ stuck = time.After(max(0, time.Until(o.stuckAt)))
883+ }
884+ stateDirty := false
301885 select {
302886 case <-retry:
303887 continue
304- case <-vrrOff:
305- if o.setVRR(false); o.vrrOn {
306- o.composedSince = time.Now() // failed: retry later, not in a loop
888+ case <-stuck:
889+ if o.pending && o.stuckAt.After(time.Now()) {
890+ continue // waits for a fence
307891 }
308- continue
892+ if o.expire() {
893+ if err := o.modeset(); err != nil {
894+ if !o.commitFailed(err, &enabled) {
895+ return fmt.Errorf("recovery modeset: %w", err)
896+ }
897+ continue
898+ }
899+ }
900+ dirty = haveScene
901+ stateDirty = true
902+ case <-vrrOff:
903+ stateDirty = true
904+ case <-cursorWake:
905+ stateDirty = true
309906 case <-ctx.Done():
310907 return nil
311908 case on := <-active:
312909 // Modeset on every enable: a fast disable+enable can coalesce to one true.
313910 if on {
314- err := o.modeset()
911+ var err error
912+ if o.fbs[0] == 0 {
913+ err = o.showImages(r, imagesDriver, nil)
914+ } else {
915+ err = o.modeset()
916+ }
315917 if lostMaster(err) {
316918 // The seat took DRM master back: wait for the next enable.
317919 o.log.Warn().Err(err).Msg("resume")
318920 continue
319921 }
320- if refused(err) && !o.validated && o.kind < imagesDumb {
321- o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(o.kind)).Msg("modeset refused the output images")
322- o.freeImages()
323- err = o.showImages(r, o.kind+1, err)
324- }
325922 if err != nil {
326923 o.log.Error().Err(err).Msg("resume")
327924 return err
328925 }
329- o.validated = true
926+ o.probeAsync(r)
330927 dirty = haveScene
331- if o.cursor != nil {
332- if err := o.cursor.Reapply(); err != nil {
333- o.log.Warn().Err(err).Msg("cursor")
334- }
335- }
928+ stateDirty = true
336929 }
337930 enabled = on
338931 o.log.Info().Bool("enabled", on).Msg("output")
339- case n := <-o.flipped:
340- if !o.pending {
932+ case ev := <-o.flipped:
933+ if !o.completed(ev, seen) {
341934 continue // stale event from before a modeset
342935 }
343- o.pending = false
344- o.flips += n
345- o.shown = o.queued
346- o.queued = 0
347- flipped = true
348- if d := time.Since(o.flipStart); d > 20*time.Millisecond {
349- o.log.Info().Dur("flip_ms", d).Msg("slow flip")
936+ stateDirty = true
937+ if o.cursor != nil {
938+ // An image that waited for a free slot loads now.
939+ if _, err := o.cursor.flushLater(r); err != nil {
940+ o.log.Warn().Err(err).Msg("cursor")
941+ }
350942 }
351943 case s := <-scenes:
352944 if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale {
353945 cursorScale = s.Scale
354- o.setCursor(loadCursor, want, s.Scale)
946+ o.setCursor(r, loadCursor, want, s.Scale)
947+ stateDirty = true
355948 }
356949 scene, haveScene, dirty = s, true, true
357950 case c := <-cursor:
358951 want = c
359952 // Before the first scene the scale is unknown: loaded then.
360953 if o.cursor != nil && loadCursor != nil && cursorScale > 0 {
361- o.setCursor(loadCursor, want, cursorScale)
954+ o.setCursor(r, loadCursor, want, cursorScale)
955+ stateDirty = true
362956 }
363- continue
364957 case c := <-contents:
365958 if c.Empty() {
366959 delete(surfaces, c.ID)
@@ -375,53 +968,167 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
375968 dirty = dirty || scene.Shows(c.ID)
376969 case <-stats.C:
377970 o.dropClientFBs(time.Now(), false)
378- ev := o.log.Info().Int("frames", frame).Int("flips", o.flips)
971+ ev := o.log.Info().Str("connector", o.conn.name).Int("frames", frame).Int("flips", o.flips).Bool("pending", o.pending)
379972 if o.cursor != nil {
380973 cs := o.cursor.TakeStats()
381- ev = ev.Int("cursor_moves", cs.Moves).Int("cursor_ioctls", cs.Ioctls).Dur("cursor_max_ioctl_ms", cs.MaxIoctl)
974+ ev = ev.Int("cursor_moves", cs.Moves).Int("cursor_commits", cs.Commits)
382975 }
383976 ev.Msg("stats")
384977 }
385- if !dirty || !haveScene || !enabled || o.pending {
978+ if !enabled || o.pending {
979+ continue
980+ }
981+ if !dirty || !haveScene {
982+ if stateDirty {
983+ err := o.commitState(o.wantVRR(o.shown != 0))
984+ if errors.Is(err, errOverlayDropped) {
985+ dirty = haveScene
986+ continue
987+ }
988+ if err != nil && !o.commitFailed(err, &enabled) {
989+ return fmt.Errorf("commit: %w", err)
990+ }
991+ }
386992 continue
387993 }
388994 start := time.Now()
389- direct, err := o.tryScanout(scene, surfaces)
995+ // shows is what the frame puts on screen: presentation feedback
996+ // of windows it does not draw is discarded, not presented.
997+ f := pendingFrame{shows: shownBy(scene, seen)}
998+ fb, c := o.scanoutFrame(scene, surfaces)
999+ direct := false
1000+ if fb != 0 {
1001+ direct, err = o.commitScanout(fb, c, pendingFrame{shows: map[ports.WindowID]uint64{c.ID: seen[c.ID]}})
1002+ }
3901003 if !direct {
1004+ ov, composed := o.overlayFrame(scene, surfaces)
3911005 r.UseTarget(o.back)
392- if err := r.Render(scene, surfaces); err != nil {
393- return fmt.Errorf("render frame: %w", err)
1006+ done, rerr := r.Render(composed, surfaces)
1007+ if rerr != nil {
1008+ ov.close()
1009+ return fmt.Errorf("render frame: %w", rerr)
1010+ }
1011+ if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) {
1012+ // Refused: compose the window too (cached per buffer).
1013+ if done != nil {
1014+ done.Close()
1015+ }
1016+ ov.close()
1017+ ov = overlayWin{}
1018+ if done, rerr = r.Render(scene, surfaces); rerr != nil {
1019+ return fmt.Errorf("render frame: %w", rerr)
1020+ }
1021+ }
1022+ // The overlay buffer is on screen like a scanned-out one: it
1023+ // is reported shown, so it is not released under the plane.
1024+ f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true
1025+ err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov)
1026+ if err != nil {
1027+ // The GPU may still read client buffers for this frame.
1028+ o.holdRead(done)
1029+ }
1030+ if done != nil {
1031+ done.Close()
1032+ }
1033+ ov.close()
1034+ if err != nil && o.overlayConflict(err, ov.buf) {
1035+ err = errOverlayDropped
1036+ }
1037+ if err == nil {
1038+ o.queued = ov.buf
1039+ o.back = 1 - o.back
3941040 }
395- err = o.present(r)
3961041 }
397- copyStart := time.Now()
3981042 if err != nil {
399- if errors.Is(err, unix.EBUSY) {
400- // A flip is still in flight; retry after its event.
401- o.pending, o.flipStart = true, time.Now()
402- continue // dirty stays true
403- }
404- if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) {
405- // Lost DRM master mid-switch; the seat disable follows.
406- o.log.Warn().Err(err).Msg("flip refused")
407- enabled = false
1043+ if errors.Is(err, errOverlayDropped) {
1044+ dirty = true
4081045 continue
4091046 }
410- o.log.Error().Err(err).Msg("page flip")
411- return fmt.Errorf("page flip: %w", err)
1047+ if !o.commitFailed(err, &enabled) {
1048+ return fmt.Errorf("page flip: %w", err)
1049+ }
1050+ continue // dirty stays true
4121051 }
4131052 dirty = false
414- reportDirty = true
4151053 frame++
416- o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("render", time.Since(start)).Dur("copy", time.Since(copyStart)).Msg("frame")
1054+ o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Bool("direct", direct).Dur("took", time.Since(start)).Msg("frame")
4171055 }
4181056 }
4191057
420-// setCursor loads a cursor for scale onto the cursor plane.
421-func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float64) {
1058+// commitFailed handles a commit error that does not stop the output: a
1059+// commit still in flight (EBUSY, retried after its event) or DRM master
1060+// lost mid-switch (the seat disable follows).
1061+func (o *Output) commitFailed(err error, enabled *bool) bool {
1062+ switch {
1063+ case errors.Is(err, unix.EBUSY):
1064+ // The kernel still works on an earlier commit (e.g. across a VT
1065+ // switch, or one whose event already came): wait for any event
1066+ // of this CRTC, or retry after busyRetry since none may come.
1067+ o.endPending()
1068+ now := time.Now()
1069+ o.pending, o.flipStart, o.stuckAt = true, now, now.Add(busyRetry)
1070+ o.pendingSerial = 0
1071+ // A run of EBUSY: each refusal follows the previous retry.
1072+ if o.busySince.IsZero() || now.Sub(o.lastBusy) > 4*busyRetry {
1073+ o.busySince = now
1074+ o.log.Info().Err(err).Str("connector", o.conn.name).Msg("commit busy")
1075+ }
1076+ o.lastBusy = now
1077+ return true
1078+ case lostMaster(err):
1079+ o.log.Warn().Err(err).Msg("commit refused")
1080+ *enabled = false
1081+ return true
1082+ }
1083+ o.log.Error().Err(err).Msg("commit")
1084+ return false
1085+}
1086+
1087+// completed handles the event of the pending commit: a flipped frame is
1088+// reported with its kernel timestamp. It reports false for an event of
1089+// another commit, which is dropped.
1090+func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool {
1091+ if !o.pending || o.pendingSerial != 0 && ev.user>>userKindBits != o.pendingSerial {
1092+ return false
1093+ }
1094+ f := o.pendingFrame
1095+ o.endPending()
1096+ o.busySince = time.Time{}
1097+ if d := time.Since(o.flipStart); d > 20*time.Millisecond {
1098+ o.log.Info().Dur("flip_ms", d).Str("connector", o.conn.name).Msg("slow flip")
1099+ }
1100+ if o.cursor != nil {
1101+ o.cursor.landed()
1102+ }
1103+ if !f.frame {
1104+ return true
1105+ }
1106+ if f.composed {
1107+ // Frames render in order: this flip's fence signalled after
1108+ // those of frames rendered before it and not committed.
1109+ o.dropRead()
1110+ }
1111+ o.flips++
1112+ o.shown = f.queued
1113+ if o.queued == f.queued {
1114+ o.queued = 0
1115+ }
1116+ refresh := time.Duration(0)
1117+ if !o.vrrOn {
1118+ refresh = time.Duration(int64(time.Second) * 1000 / int64(max(1, o.mode.refreshMilli())))
1119+ }
1120+ o.report(&ports.FlipInfo{When: ev.when, Seq: uint64(ev.seq), Refresh: refresh, ZeroCopy: f.zeroCopy, Async: f.async, HardwareClock: true, Shows: f.shows}, seen)
1121+ return true
1122+}
1123+
1124+// setCursor loads a cursor for scale into the cursor images.
1125+func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64) {
1126+ if o.cursor.off {
1127+ return
1128+ }
4221129 img, err := load(c, scale, o.cursor.Limit())
4231130 if err == nil {
424- err = o.cursor.SetImage(img.Pixels, img.W, img.H, img.HotX, img.HotY)
1131+ err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY)
4251132 }
4261133 if err != nil {
4271134 o.log.Warn().Err(err).Msg("cursor")
@@ -430,26 +1137,47 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6
4301137 o.log.Debug().Float64("scale", scale).Str("shape", c.Shape).Bool("client", c.Image != nil).Int("w", img.W).Int("h", img.H).Msg("cursor image")
4311138 }
4321139
433-// report tells wayland what the output shows and has read, so replaced
434-// client buffers can be released. It must follow the frame decision: a
435-// buffer is safe once a later content is seen and it is neither shown nor
436-// queued. A report the channel cannot take waits in unsent, replaced by
437-// newer ones (Flip kept), and is retried on the next loop.
438-func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) {
439- r := ports.OutputPresented{Output: o.conn.name, Flip: flip || o.unsent.Flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)}
440- o.unsent = r
441- o.flushReport(presented)
442-}
1140+// maxUnsent bounds reports waiting for wayland; past it the oldest flip is
1141+// folded into the next one.
1142+const maxUnsent = 16
4431143
444-// flushReport sends the unsent report, if any, without blocking.
445-func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
446- if o.unsent.Output == "" {
1144+// report queues what the output shows and has read, so replaced client
1145+// buffers can be released. A flip-less report merges into the newest
1146+// queued one; flips are never merged unless maxUnsent is reached.
1147+func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) {
1148+ r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)}
1149+ if n := len(o.unsent); n > 0 && (flip == nil || o.unsent[n-1].Flip == nil) {
1150+ if flip == nil {
1151+ r.Flip = o.unsent[n-1].Flip
1152+ }
1153+ o.unsent[n-1] = r
4471154 return
4481155 }
449- select {
450- case presented <- o.unsent:
451- o.unsent = ports.OutputPresented{}
452- default:
1156+ o.unsent = append(o.unsent, r)
1157+ if len(o.unsent) > maxUnsent {
1158+ old, next := o.unsent[0].Flip, o.unsent[1].Flip
1159+ if next != nil && old != nil {
1160+ next.Merged += 1 + old.Merged
1161+ for id, seq := range old.Shows {
1162+ if _, ok := next.Shows[id]; !ok {
1163+ next.Shows[id] = seq
1164+ }
1165+ }
1166+ }
1167+ o.unsent = slices.Delete(o.unsent, 0, 1)
1168+ o.log.Warn().Str("connector", o.conn.name).Msg("wayland is not reading output reports; flips merged")
1169+ }
1170+}
1171+
1172+// flushReport sends queued reports without blocking.
1173+func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
1174+ for len(o.unsent) > 0 {
1175+ select {
1176+ case presented <- o.unsent[0]:
1177+ o.unsent = slices.Delete(o.unsent, 0, 1)
1178+ default:
1179+ return
1180+ }
4531181 }
4541182 }
4551183
@@ -459,11 +1187,11 @@ type imageKind int
4591187 const (
4601188 imagesDriver imageKind = iota // exported, modifier chosen by the driver
4611189 imagesLinear // exported, linear
462- imagesDumb // dumb buffers filled by CPU copies
4631190 )
4641191
4651192 // showImages sets up images from kind on and modesets them. KMS may refuse
466-// an image only at modeset: then the next kind is tried. cause is why the
1193+// an image only in a modeset: a TEST_ONLY modeset checks it first, and the
1194+// next kind is tried on refusal. cause is why the
4671195 // previous kind failed, for the log.
4681196 func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
4691197 for {
@@ -472,18 +1200,15 @@ func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error
4721200 return err
4731201 }
4741202 o.kind = got
475- if err = o.modeset(); err == nil {
476- o.validated = true
477- return nil
1203+ if err = o.testModeset(); err == nil {
1204+ return o.modeset()
4781205 }
479- if !refused(err) {
1206+ if !refused(err) || got == imagesLinear {
1207+ o.freeImages()
4801208 return err
4811209 }
4821210 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
4831211 o.freeImages()
484- if got == imagesDumb {
485- return err
486- }
4871212 kind, cause = got+1, err
4881213 }
4891214 }
@@ -493,17 +1218,17 @@ func refused(err error) bool {
4931218 return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)
4941219 }
4951220
496-// lostMaster reports a modeset error meaning the seat is switched away.
1221+// lostMaster reports a commit error meaning the seat is switched away.
4971222 func lostMaster(err error) bool {
4981223 return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM)
4991224 }
5001225
501-// setupImages gives the output the two images it flips between: renderer
502-// targets exported as dmabufs, or dumb buffers as a logged fallback. A
503-// kind that fails falls through to the next one.
1226+// setupImages gives the output the two renderer images it flips between,
1227+// exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that
1228+// cannot export a scanout-capable image cannot drive the output.
5041229 func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
5051230 err := cause
506- for ; kind < imagesDumb; kind++ {
1231+ for ; kind <= imagesLinear; kind++ {
5071232 mods := []uint64(nil)
5081233 if kind == imagesLinear {
5091234 mods = []uint64{0}
@@ -513,17 +1238,7 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
5131238 }
5141239 o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
5151240 }
516- o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("output falls back to CPU copies (ADR 014)")
517- // The renderer draws into its own image again, read back by CopyBGRX.
518- _, _ = r.ExportTargets(0, nil)
519- for i := range o.bufs {
520- if o.bufs[i], err = newDumb(o.fd, o.Width(), o.Height()); err != nil {
521- o.freeImages()
522- return imagesDumb, err
523- }
524- o.fbs[i] = o.bufs[i].fbID
525- }
526- return imagesDumb, nil
1241+ return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
5271242 }
5281243
5291244 // exportImages makes the renderer's exported targets the output images.
@@ -534,7 +1249,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
5341249 }
5351250 for i := range bufs {
5361251 if err == nil {
537- o.fbs[i], err = o.card.addFB(&bufs[i])
1252+ o.fbs[i], err = o.k.addFB(&bufs[i], fourccXRGB)
5381253 }
5391254 bufs[i].Planes[0].File.Close()
5401255 }
@@ -545,7 +1260,12 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
5451260 break
5461261 }
5471262 r.UseTarget(i)
548- err = r.Render(ports.Scene{Background: "#000000"}, nil)
1263+ var done *os.File
1264+ if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
1265+ // The modeset is a blocking commit: wait for the clear.
1266+ err = errors.Join(err, syncfile.Wait(context.Background(), done))
1267+ done.Close()
1268+ }
5491269 }
5501270 if err != nil {
5511271 o.freeImages()
@@ -556,16 +1276,23 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
5561276 return nil
5571277 }
5581278
559-// freeImages removes the output images; dumb buffers own their fb.
1279+// freeImages removes the output images' framebuffers.
5601280 func (o *Output) freeImages() {
5611281 for i, fb := range o.fbs {
562- if b := o.bufs[i]; b != nil {
563- b.destroy(o.fd)
564- o.bufs[i] = nil
565- } else if fb != 0 {
566- v := fb
567- _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v))
1282+ if fb != 0 {
1283+ _ = o.k.rmFB(fb)
5681284 }
5691285 o.fbs[i] = 0
5701286 }
5711287 }
1288+
1289+// shownBy is the content Seq of each window the scene draws.
1290+func shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[ports.WindowID]uint64 {
1291+ out := map[ports.WindowID]uint64{}
1292+ for id, seq := range seen {
1293+ if s.Shows(id) {
1294+ out[id] = seq
1295+ }
1296+ }
1297+ return out
1298+}
A internal/adapters/drm/output_test.go +449 -0
@@ -0,0 +1,449 @@
1+package drm
2+
3+import (
4+ "context"
5+ "os"
6+ "sync"
7+ "sync/atomic"
8+ "testing"
9+ "time"
10+
11+ portsmocks "github.com/bnema/nefertty/internal/mocks/ports"
12+ "github.com/bnema/nefertty/internal/ports"
13+ "github.com/bnema/zerowrap"
14+ "github.com/stretchr/testify/mock"
15+ "golang.org/x/sys/unix"
16+)
17+
18+// Object and property IDs of the test output.
19+const (
20+ tCrtc, tConn, tPrimary, tCursor, tOverlay = 30, 40, 50, 51, 52
21+ pFB, pCrtcID, pFence, pMode, pActive = 1, 2, 3, 4, 5
22+ pVRR, pConnCrtc = 6, 7
23+)
24+
25+var planeProps = map[string]uint32{"FB_ID": pFB, "CRTC_ID": pCrtcID, "IN_FENCE_FD": pFence, "SRC_X": 10, "SRC_Y": 11, "SRC_W": 12, "SRC_H": 13, "CRTC_X": 14, "CRTC_Y": 15, "CRTC_W": 16, "CRTC_H": 17}
26+
27+// commitRec is one recorded atomic commit.
28+type commitRec struct {
29+ req atomicReq
30+ flags uint32
31+ user uint64
32+}
33+
34+// testOutput returns an output on a mocked kms that records commits;
35+// commit returns the next error of errs (nil when empty).
36+func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) {
37+ o, k, commits, _ := testOutputMu(t, errs...)
38+ return o, k, commits
39+}
40+
41+// testOutputMu is testOutput with the mutex that guards the recorded
42+// commits (Run commits on its goroutine).
43+func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
44+ k := newMockkms(t)
45+ var commits []commitRec
46+ commitMu := &sync.Mutex{}
47+ k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error {
48+ commitMu.Lock()
49+ defer commitMu.Unlock()
50+ c := commitRec{flags: flags, user: user}
51+ c.req.objs = append([]uint32(nil), r.objs...)
52+ for _, ps := range r.props {
53+ c.req.props = append(c.req.props, append([]propValue(nil), ps...))
54+ }
55+ commits = append(commits, c)
56+ if len(errs) > 0 {
57+ err := errs[0]
58+ errs = errs[1:]
59+ return err
60+ }
61+ return nil
62+ }).Maybe()
63+ primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps}
64+ cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64)
65+ cur.fbs = [2]uint32{90, 91}
66+ o := &Output{k: k, serials: &atomic.Uint64{}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(),
67+ crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR,
68+ primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true}
69+ return o, k, &commits, commitMu
70+}
71+
72+// eventOf is the flip event of commit c.
73+func eventOf(c commitRec) flipEvent { return flipEvent{crtc: tCrtc, user: c.user} }
74+
75+func objCount(c commitRec) int { return len(c.req.objs) }
76+
77+func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) {
78+ o, _, commits := testOutput(t)
79+ o.cursor.image = true
80+ o.cursor.Move(12, 34)
81+ fence, w, err := os.Pipe()
82+ if err != nil {
83+ t.Fatal(err)
84+ }
85+ defer fence.Close()
86+ defer w.Close()
87+ if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
88+ t.Fatal(err)
89+ }
90+ c := (*commits)[0]
91+ if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userFrame || c.user>>userKindBits != o.pendingSerial {
92+ t.Fatalf("flags %#x user %d", c.flags, c.user)
93+ }
94+ if v, _ := c.req.value(tPrimary, pFB); v != 70 {
95+ t.Fatalf("primary fb %d", v)
96+ }
97+ if v, ok := c.req.value(tPrimary, pFence); !ok || v != uint64(fence.Fd()) {
98+ t.Fatalf("fence %d %v", v, ok)
99+ }
100+ if v, _ := c.req.value(tCursor, 14); v != 12 {
101+ t.Fatalf("cursor x %d", v)
102+ }
103+ if v, _ := c.req.value(tCrtc, pVRR); v != 1 {
104+ t.Fatalf("vrr %d", v)
105+ }
106+ if !o.pending || !o.vrrOn || o.cursor.applied.x != 12 {
107+ t.Fatalf("state pending=%v vrr=%v cursor=%+v", o.pending, o.vrrOn, o.cursor.applied)
108+ }
109+}
110+
111+func TestAsyncCommitHasOnlyPrimaryFBAndFence(t *testing.T) {
112+ o, _, commits := testOutput(t)
113+ fence, w, _ := os.Pipe()
114+ defer fence.Close()
115+ defer w.Close()
116+ if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
117+ t.Fatal(err)
118+ }
119+ c := (*commits)[0]
120+ if c.flags&flipAsyncFlag == 0 {
121+ t.Fatal("not async")
122+ }
123+ if objCount(c) != 1 || c.req.objs[0] != tPrimary || len(c.req.props[0]) != 2 {
124+ t.Fatalf("async commit %+v", c.req)
125+ }
126+}
127+
128+func TestAsyncFallsBackToSyncWhenCursorMoves(t *testing.T) {
129+ o, _, commits := testOutput(t)
130+ o.cursor.image = true
131+ o.cursor.Move(5, 5)
132+ if err := o.commitFrame(70, nil, true, false, pendingFrame{}); err != nil {
133+ t.Fatal(err)
134+ }
135+ if (*commits)[0].flags&flipAsyncFlag != 0 {
136+ t.Fatal("async commit moved the cursor")
137+ }
138+}
139+
140+func TestAsyncWithoutFenceSupportStaysSync(t *testing.T) {
141+ o, _, commits := testOutput(t)
142+ o.asyncFence = false
143+ fence, w, _ := os.Pipe()
144+ defer fence.Close()
145+ defer w.Close()
146+ if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
147+ t.Fatal(err)
148+ }
149+ if (*commits)[0].flags&flipAsyncFlag != 0 {
150+ t.Fatal("async commit with a fence the driver refuses")
151+ }
152+}
153+
154+func TestScanoutAsyncRefusedRetriesSync(t *testing.T) {
155+ o, _, commits := testOutput(t, unix.EINVAL)
156+ o.shown, o.vrrOn = 1, true
157+ buf := &ports.DMABuf{ID: 9}
158+ o.clientFBs[9] = &clientFB{fbID: 80}
159+ ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: buf, Async: true}, pendingFrame{})
160+ if !ok || err != nil {
161+ t.Fatalf("ok=%v err=%v", ok, err)
162+ }
163+ if len(*commits) != 2 || (*commits)[0].flags&flipAsyncFlag == 0 || (*commits)[1].flags&flipAsyncFlag != 0 {
164+ t.Fatalf("commits %+v", *commits)
165+ }
166+ if !o.clientFBs[9].noAsync || o.queued != 9 {
167+ t.Fatalf("noAsync=%v queued=%d", o.clientFBs[9].noAsync, o.queued)
168+ }
169+}
170+
171+func TestCursorCommitWaitsForEvent(t *testing.T) {
172+ o, _, commits := testOutput(t)
173+ o.cursor.image = true
174+ o.cursor.Move(1, 1)
175+ if err := o.commitState(false); err != nil {
176+ t.Fatal(err)
177+ }
178+ c := (*commits)[0]
179+ if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userState || !o.pending {
180+ t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.pending)
181+ }
182+ // Its event is not a frame: nothing is reported.
183+ seen := map[ports.WindowID]uint64{}
184+ o.completed(eventOf(c), seen)
185+ if o.pending || len(o.unsent) != 0 || o.flips != 0 {
186+ t.Fatalf("pending=%v unsent=%d flips=%d", o.pending, len(o.unsent), o.flips)
187+ }
188+ // Nothing changed: no commit.
189+ if err := o.commitState(false); err != nil || len(*commits) != 1 {
190+ t.Fatalf("idle commit: %v, %d commits", err, len(*commits))
191+ }
192+}
193+
194+func TestBusyCommitStaysPending(t *testing.T) {
195+ o, _, _ := testOutput(t, unix.EBUSY)
196+ err := o.commitFrame(70, nil, false, false, pendingFrame{})
197+ enabled := true
198+ if !o.commitFailed(err, &enabled) || !o.pending || !enabled {
199+ t.Fatalf("EBUSY: pending=%v enabled=%v", o.pending, enabled)
200+ }
201+}
202+
203+func TestFlipReportsTimestampAndShownFrame(t *testing.T) {
204+ o, _, commits := testOutput(t)
205+ o.vrrOn = false
206+ o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0
207+ if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil {
208+ t.Fatal(err)
209+ }
210+ o.completed(flipEvent{user: (*commits)[0].user, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4})
211+ if len(o.unsent) != 1 {
212+ t.Fatalf("reports %d", len(o.unsent))
213+ }
214+ f := o.unsent[0].Flip
215+ if f == nil || f.When != 5*time.Second || f.Seq != 11 || f.Refresh != time.Second/60 || f.Shows[1] != 4 || !f.HardwareClock {
216+ t.Fatalf("flip %+v", f)
217+ }
218+}
219+
220+func TestReportsKeepEveryFlip(t *testing.T) {
221+ o, _, _ := testOutput(t)
222+ seen := map[ports.WindowID]uint64{1: 1}
223+ o.report(&ports.FlipInfo{Seq: 1}, seen)
224+ o.report(nil, seen)
225+ o.report(&ports.FlipInfo{Seq: 2}, seen)
226+ o.report(nil, map[ports.WindowID]uint64{1: 3})
227+ if len(o.unsent) != 2 || o.unsent[0].Flip.Seq != 1 || o.unsent[1].Flip.Seq != 2 || o.unsent[1].Seen[1] != 3 {
228+ t.Fatalf("unsent %+v", o.unsent)
229+ }
230+ for i := range maxUnsent + 3 {
231+ o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen)
232+ }
233+ if len(o.unsent) != maxUnsent || o.unsent[0].Flip.Merged == 0 {
234+ t.Fatalf("cap: %d reports, first merged %d", len(o.unsent), o.unsent[0].Flip.Merged)
235+ }
236+}
237+
238+func TestModesetDisablesCursorAndStrayPlanes(t *testing.T) {
239+ o, k, commits := testOutput(t)
240+ stray := &plane{id: tOverlay, typ: planeOverlay, props: planeProps}
241+ o.stray = []*plane{stray}
242+ k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
243+ k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once()
244+ if err := o.modeset(); err != nil {
245+ t.Fatal(err)
246+ }
247+ c := (*commits)[0]
248+ if c.flags != atomicAllowModes {
249+ t.Fatalf("flags %#x", c.flags)
250+ }
251+ if v, _ := c.req.value(tCrtc, pMode); v != 99 {
252+ t.Fatalf("mode blob %d", v)
253+ }
254+ for _, p := range []uint32{tCursor, tOverlay} {
255+ if v, ok := c.req.value(p, pFB); !ok || v != 0 {
256+ t.Fatalf("plane %d fb %d %v", p, v, ok)
257+ }
258+ }
259+ if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
260+ t.Fatalf("connector crtc %d", v)
261+ }
262+ // The next modeset replaces the blob.
263+ k.EXPECT().createBlob(mock.Anything).Return(100, nil).Once()
264+ k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Once()
265+ k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once()
266+ if err := o.modeset(); err != nil {
267+ t.Fatal(err)
268+ }
269+ if _, ok := (*commits)[1].req.value(tOverlay, pFB); ok {
270+ t.Fatal("plane on another CRTC turned off")
271+ }
272+}
273+
274+func TestAsyncProbeAfterModeset(t *testing.T) {
275+ o, _, commits := testOutput(t, unix.EINVAL)
276+ o.asyncFence = true
277+ r := portsmocks.NewMockRenderer(t)
278+ fence, w, _ := os.Pipe()
279+ defer w.Close()
280+ r.EXPECT().UseTarget(mock.Anything).Return().Once()
281+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(fence, nil).Once()
282+ o.probeAsync(r)
283+ c := (*commits)[0]
284+ if c.flags != atomicTestOnly|flipAsyncFlag {
285+ t.Fatalf("flags %#x", c.flags)
286+ }
287+ if _, ok := c.req.value(tPrimary, pFence); !ok {
288+ t.Fatal("probe without a fence")
289+ }
290+ if o.asyncFence {
291+ t.Fatal("refused probe kept asyncFence")
292+ }
293+ if _, err := fence.Stat(); err == nil {
294+ t.Fatal("probe fence not closed")
295+ }
296+ o.probeAsync(r) // once only
297+}
298+
299+func TestVRRToggleOnlyInsideFrames(t *testing.T) {
300+ o, _, commits := testOutput(t)
301+ o.vrrOn = true
302+ o.composedSince = time.Now().Add(-time.Second)
303+ if o.wantVRR(false) {
304+ t.Fatal("VRR kept after vrrHold of composition")
305+ }
306+ o.composedSince = time.Now()
307+ if !o.wantVRR(false) {
308+ t.Fatal("VRR dropped before vrrHold")
309+ }
310+ if err := o.commitState(false); err != nil {
311+ t.Fatal(err)
312+ }
313+ if v, ok := (*commits)[0].req.value(tCrtc, pVRR); !ok || v != 0 || (*commits)[0].flags&flipEventFlag == 0 {
314+ t.Fatalf("vrr off commit %+v", (*commits)[0])
315+ }
316+}
317+
318+func TestReadPlanes(t *testing.T) {
319+ k := newMockkms(t)
320+ k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1}, {id: 2, possible: 2}, {id: 3, possible: 3}}, nil)
321+ k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}, "FB_ID": {21, 0}, "IN_FORMATS": {22, 5}}, nil)
322+ k.EXPECT().objProps(uint32(3), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planeCursor}}, nil)
323+ k.EXPECT().getBlob(uint32(5)).Return(nil, nil)
324+ ps, err := readPlanes(k, 0)
325+ if err != nil || len(ps) != 2 || ps[0].typ != planePrimary || ps[0].prop("FB_ID") != 21 || ps[1].typ != planeCursor {
326+ t.Fatalf("planes %+v %v", ps, err)
327+ }
328+}
329+
330+func TestVRRRefusedIsDisabled(t *testing.T) {
331+ o, _, commits := testOutput(t, unix.EINVAL)
332+ if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
333+ t.Fatal(err)
334+ }
335+ if len(*commits) != 2 || o.vrrProp != 0 || !o.pending {
336+ t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
337+ }
338+ if v, _ := (*commits)[1].req.value(tCrtc, pVRR); v != 0 {
339+ t.Fatal("retry kept VRR on")
340+ }
341+}
342+
343+func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) {
344+ o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
345+ o.clientFBs[9] = &clientFB{fbID: 80}
346+ ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
347+ if ok || err != nil || o.vrrProp == 0 || o.clientFBs[9].failed != "flip_refused" {
348+ t.Fatalf("ok=%v err=%v vrrProp=%d failed=%q", ok, err, o.vrrProp, o.clientFBs[9].failed)
349+ }
350+}
351+
352+// Run: while a frame is in flight its buffers may still be read by the
353+// GPU, so a newer content is reported only after the flip event; every
354+// frame fence is closed.
355+func TestRunReportsSeenAfterFlip(t *testing.T) {
356+ o, k, commits, commitMu := testOutputMu(t)
357+ o.cursor, o.tearing = nil, false
358+ flips := make(chan flipEvent, 1)
359+ o.flipped = flips
360+ r := portsmocks.NewMockRenderer(t)
361+ var mu sync.Mutex
362+ var fences []*os.File
363+ pipeBuf := func() ports.DMABuf {
364+ f, w, _ := os.Pipe()
365+ w.Close()
366+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
367+ }
368+ r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
369+ r.EXPECT().UseTarget(mock.Anything).Return()
370+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
371+ f, w, _ := os.Pipe()
372+ _, _ = w.Write([]byte{1}) // readable: signalled
373+ w.Close()
374+ mu.Lock()
375+ fences = append(fences, f)
376+ mu.Unlock()
377+ return f, nil
378+ })
379+ r.EXPECT().Close().Return().Once()
380+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
381+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
382+ k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
383+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
384+ frameCommits := func() int {
385+ commitMu.Lock()
386+ defer commitMu.Unlock()
387+ n := 0
388+ for _, c := range *commits {
389+ if c.user&3 == userFrame {
390+ n++
391+ }
392+ }
393+ return n
394+ }
395+ scenes := make(chan ports.Scene, 1)
396+ contents := make(chan ports.SurfaceContent, 1)
397+ presented := make(chan ports.OutputPresented, 8)
398+ ctx, cancel := context.WithCancel(context.Background())
399+ done := make(chan error, 1)
400+ go func() {
401+ done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, presented)
402+ }()
403+ scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
404+ waitFor(t, func() bool { return frameCommits() == 1 })
405+ // Frame 1 is in flight: content 3 replaces what it may be reading.
406+ contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}
407+ select {
408+ case p := <-presented:
409+ t.Fatalf("report while a frame is in flight: %+v", p)
410+ case <-time.After(30 * time.Millisecond):
411+ }
412+ commitMu.Lock()
413+ last := (*commits)[len(*commits)-1]
414+ commitMu.Unlock()
415+ flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
416+ select {
417+ case p := <-presented:
418+ if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 {
419+ t.Fatalf("report %+v", p)
420+ }
421+ case <-time.After(2 * time.Second):
422+ t.Fatal("no report after the flip")
423+ }
424+ cancel()
425+ if err := <-done; err != nil {
426+ t.Fatal(err)
427+ }
428+ mu.Lock()
429+ defer mu.Unlock()
430+ if len(fences) < 3 {
431+ t.Fatalf("%d renders", len(fences))
432+ }
433+ for _, f := range fences {
434+ if _, err := f.Stat(); err == nil {
435+ t.Fatal("frame fence left open")
436+ }
437+ }
438+}
439+
440+func waitFor(t *testing.T, cond func() bool) {
441+ t.Helper()
442+ deadline := time.Now().Add(2 * time.Second)
443+ for !cond() {
444+ if time.Now().After(deadline) {
445+ t.Fatal("timed out")
446+ }
447+ time.Sleep(2 * time.Millisecond)
448+ }
449+}
A internal/adapters/drm/overlay.go +223 -0
@@ -0,0 +1,223 @@
1+package drm
2+
3+import (
4+ "errors"
5+ "os"
6+ "time"
7+
8+ "github.com/bnema/nefertty/internal/ports"
9+ "golang.org/x/sys/unix"
10+)
11+
12+// Overlay plane: a window drawn from one opaque dmabuf, with nothing but
13+// the cursor above it, goes on an overlay plane at its place on screen;
14+// the rest of the scene is composed into the primary plane without it.
15+// The client buffer is shown with no GPU pass, like direct scanout, but
16+// the window need not cover the output. A refusal (TEST_ONLY) keeps the
17+// window composed and is cached per buffer.
18+
19+// pickOverlay takes an overlay plane above the primary one (zpos), when
20+// the output has one.
21+func (o *Output) pickOverlay() {
22+ pz, ok := o.primary.zpos()
23+ if !ok {
24+ return
25+ }
26+ for _, p := range o.stray {
27+ if p.typ != planeOverlay {
28+ continue
29+ }
30+ if z, ok := p.zpos(); ok && z > pz {
31+ o.overlay = p
32+ return
33+ }
34+ }
35+}
36+
37+// zpos is the plane's zpos value when known.
38+func (p *plane) zpos() (uint64, bool) {
39+ if p.props["zpos"] == 0 {
40+ return 0, false
41+ }
42+ return p.zposValue, true
43+}
44+
45+// overlayWin is the window on the overlay plane: its buffer and where it
46+// shows, in physical pixels.
47+type overlayWin struct {
48+ id ports.WindowID
49+ fb uint32
50+ buf uint64
51+ rect ports.Rect // CRTC rect
52+ w, h int // buffer size
53+ // acquire is the client's explicit-sync fence, a duplicate owned by
54+ // the frame loop (closed after the commit); nil without one.
55+ acquire *os.File
56+}
57+
58+// overlayCandidate finds the one window the overlay can show: an opaque
59+// dmabuf at integer physical coordinates, drawn 1:1, with no other window
60+// or layer above it. reason is why none.
61+func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (ports.SceneWindow, ports.SurfaceContent, string) {
62+ scale := s.Scale
63+ if scale <= 0 {
64+ scale = 1
65+ }
66+ var pick *ports.SceneWindow
67+ for i := range s.Windows {
68+ w := &s.Windows[i]
69+ if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
70+ continue
71+ }
72+ if w.Popup {
73+ // Popups draw after every window, wherever they are listed.
74+ return ports.SceneWindow{}, ports.SurfaceContent{}, "window_above"
75+ }
76+ c := surfaces[w.ID]
77+ if c.DMABuf != nil && c.Opaque && len(c.Children) == 0 {
78+ pick = w
79+ continue
80+ }
81+ if pick != nil {
82+ // Something is drawn above the candidate.
83+ return ports.SceneWindow{}, ports.SurfaceContent{}, "window_above"
84+ }
85+ }
86+ if pick == nil {
87+ return ports.SceneWindow{}, ports.SurfaceContent{}, "no_candidate"
88+ }
89+ for _, l := range s.Layers {
90+ if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 && !(pick.Fullscreen && l.Layer == ports.LayerTop) {
91+ return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above"
92+ }
93+ }
94+ if !pick.Fullscreen && !pick.Borderless && s.Border.Width > 0 {
95+ // The overlay shows the buffer alone; its border would go with it.
96+ return ports.SceneWindow{}, ports.SurfaceContent{}, "border"
97+ }
98+ c := surfaces[pick.ID]
99+ pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale
100+ if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) {
101+ return ports.SceneWindow{}, ports.SurfaceContent{}, "scaled"
102+ }
103+ x, y := float64(pick.Rect.X)*scale, float64(pick.Rect.Y)*scale
104+ if x != float64(int(x)) || y != float64(int(y)) {
105+ return ports.SceneWindow{}, ports.SurfaceContent{}, "fractional_position"
106+ }
107+ return *pick, c, ""
108+}
109+
110+// overlayFrame decides the overlay of a frame: the window, and the scene
111+// the renderer composes (the window left out). A zero overlayWin means
112+// none; the reason is logged on change.
113+func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (overlayWin, ports.Scene) {
114+ reason := "no_plane"
115+ var ov overlayWin
116+ if o.overlay != nil && o.scanout {
117+ var w ports.SceneWindow
118+ var c ports.SurfaceContent
119+ w, c, reason = overlayCandidate(s, surfaces)
120+ if reason == "" {
121+ var fb uint32
122+ if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" {
123+ scale := s.Scale
124+ if scale <= 0 {
125+ scale = 1
126+ }
127+ ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height,
128+ rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height},
129+ acquire: dupFence(c.Acquire)}
130+ }
131+ }
132+ }
133+ if reason != o.overlayReason {
134+ o.log.Info().Str("component", "render").Bool("overlay", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("overlay")
135+ o.overlayReason = reason
136+ }
137+ if ov.fb == 0 {
138+ return overlayWin{}, s
139+ }
140+ // The composed frame leaves the window out: the overlay shows it.
141+ rest := s
142+ rest.Windows = make([]ports.SceneWindow, 0, len(s.Windows))
143+ for _, w := range s.Windows {
144+ if w.ID != ov.id {
145+ rest.Windows = append(rest.Windows, w)
146+ }
147+ }
148+ return ov, rest
149+}
150+
151+// overlayProps puts ov on the overlay plane, or turns it off.
152+func (o *Output) overlayProps(req *atomicReq, ov overlayWin) {
153+ p := o.overlay
154+ if p == nil {
155+ return
156+ }
157+ if ov.fb == 0 {
158+ req.set(p.id, p.prop("FB_ID"), 0)
159+ req.set(p.id, p.prop("CRTC_ID"), 0)
160+ return
161+ }
162+ req.set(p.id, p.prop("FB_ID"), uint64(ov.fb))
163+ req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
164+ req.set(p.id, p.prop("SRC_X"), 0)
165+ req.set(p.id, p.prop("SRC_Y"), 0)
166+ req.set(p.id, p.prop("SRC_W"), uint64(ov.w)<<16)
167+ req.set(p.id, p.prop("SRC_H"), uint64(ov.h)<<16)
168+ req.set(p.id, p.prop("CRTC_X"), uint64(int64(ov.rect.X)))
169+ req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))
170+ req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))
171+ req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))
172+ if ov.acquire != nil {
173+ // KMS waits for the client's GPU work before scanning it out.
174+ req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd()))
175+ }
176+}
177+
178+// close releases the frame's duplicate of the acquire fence.
179+func (ov overlayWin) close() {
180+ if ov.acquire != nil {
181+ ov.acquire.Close()
182+ }
183+}
184+
185+// testOverlay asks KMS whether it takes the frame with ov on the overlay,
186+// the cursor at its current place included. A refusal is cached on the
187+// buffer.
188+func (o *Output) testOverlay(fb uint32, ov overlayWin) bool {
189+ req := &atomicReq{}
190+ o.primaryProps(req, fb)
191+ o.overlayProps(req, ov)
192+ if o.cursor != nil {
193+ o.cursor.props(req, o.crtc, o.cursor.desired())
194+ }
195+ err := o.k.commit(req, atomicTestOnly, 0)
196+ if err == nil {
197+ return true
198+ }
199+ if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) {
200+ cfb.overlayFailed = "overlay_refused"
201+ }
202+ o.log.Info().Str("component", "render").Err(err).Str("connector", o.conn.name).Msg("overlay refused")
203+ return false
204+}
205+
206+// errOverlayDropped: a commit was refused with the overlay on; the frame is
207+// composed again without it.
208+var errOverlayDropped = errors.New("overlay dropped")
209+
210+// overlayConflict handles a commit refused with buffer buf on the overlay:
211+// the buffer leaves the overlay for good (e.g. the cursor moved where the
212+// plane forbids it) and the frame is composed. It reports whether it did.
213+func (o *Output) overlayConflict(err error, buf uint64) bool {
214+ if buf == 0 || !errors.Is(err, unix.EINVAL) {
215+ return false
216+ }
217+ if cfb := o.clientFBs[buf]; cfb != nil {
218+ cfb.overlayFailed = "cursor_conflict"
219+ }
220+ o.log.Info().Str("component", "render").Bool("overlay", false).Str("reason", "cursor_conflict").Str("connector", o.conn.name).Msg("overlay")
221+ o.overlayReason = "cursor_conflict"
222+ return true
223+}
A internal/adapters/drm/overlay_test.go +182 -0
@@ -0,0 +1,182 @@
1+package drm
2+
3+import (
4+ "os"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/nefertty/internal/ports"
9+ "github.com/stretchr/testify/mock"
10+ "golang.org/x/sys/unix"
11+)
12+
13+const pZpos = 30
14+
15+// overlayOutput is a test output with an overlay plane above the primary.
16+func overlayOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) {
17+ o, k, commits := testOutput(t, errs...)
18+ props := map[string]uint32{"zpos": pZpos}
19+ for n, v := range planeProps {
20+ props[n] = v
21+ }
22+ o.primary.props = props
23+ o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2}}
24+ o.primary.formats = nil // the overlay's own formats decide
25+ o.pickOverlay()
26+ if o.overlay == nil {
27+ t.Fatal("no overlay picked")
28+ }
29+ return o, k, commits
30+}
31+
32+// A 1:1 opaque dmabuf window with nothing above goes on the overlay.
33+func overlayScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) {
34+ s := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
35+ {ID: 1, Rect: ports.Rect{W: 100, H: 100}},
36+ {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}},
37+ }}
38+ return s, map[ports.WindowID]ports.SurfaceContent{
39+ 2: {ID: 2, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Width: 100, Height: 100, Format: fourccXRGB}},
40+ }
41+}
42+
43+func TestOverlayCandidate(t *testing.T) {
44+ s, c := overlayScene()
45+ if w, _, reason := overlayCandidate(s, c); reason != "" || w.ID != 2 {
46+ t.Fatalf("candidate %v %q", w.ID, reason)
47+ }
48+ s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{W: 10, H: 10}})
49+ if _, _, reason := overlayCandidate(s, c); reason != "window_above" {
50+ t.Fatalf("reason %q", reason)
51+ }
52+ s, _ = overlayScene()
53+ s.Scale = 1.5
54+ if _, _, reason := overlayCandidate(s, c); reason != "scaled" {
55+ t.Fatalf("reason %q", reason)
56+ }
57+ s, _ = overlayScene()
58+ s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 5, H: 5}}}
59+ if _, _, reason := overlayCandidate(s, c); reason != "layer_above" {
60+ t.Fatalf("reason %q", reason)
61+ }
62+}
63+
64+// The overlay is chosen and the composed scene leaves its window out;
65+// the frame commit carries the plane at the window's place.
66+func TestOverlayChosen(t *testing.T) {
67+ o, k, commits := overlayOutput(t)
68+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
69+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
70+ s, c := overlayScene()
71+ ov, rest := o.overlayFrame(s, c)
72+ if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 {
73+ t.Fatalf("overlay %+v rest %+v", ov, rest.Windows)
74+ }
75+ if !o.testOverlay(70, ov) {
76+ t.Fatal("test refused")
77+ }
78+ if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
79+ t.Fatal(err)
80+ }
81+ last := (*commits)[len(*commits)-1]
82+ if v, _ := last.req.value(tOverlay, pFB); v != 88 {
83+ t.Fatalf("overlay fb %d", v)
84+ }
85+ if v, _ := last.req.value(tOverlay, 14); v != 100 {
86+ t.Fatalf("overlay x %d", v)
87+ }
88+ if o.overlayOn != 9 {
89+ t.Fatalf("overlayOn %d", o.overlayOn)
90+ }
91+}
92+
93+// A TEST_ONLY refusal composes the window and is cached on the buffer.
94+func TestOverlayTestRefusedFallsBack(t *testing.T) {
95+ o, k, _ := overlayOutput(t, unix.EINVAL)
96+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
97+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
98+ s, c := overlayScene()
99+ ov, _ := o.overlayFrame(s, c)
100+ if o.testOverlay(70, ov) {
101+ t.Fatal("refusal taken")
102+ }
103+ if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 {
104+ t.Fatal("refused buffer tried again")
105+ }
106+ // The overlay's refusal does not bar direct scanout of the buffer.
107+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
108+ if fb, reason := o.scanoutFB(c[2].DMABuf, time.Now()); fb != 88 || reason != "" {
109+ t.Fatalf("scanout after overlay refusal: %d %q", fb, reason)
110+ }
111+}
112+
113+// Popups are drawn above every window: one listed first still blocks the
114+// overlay; a bordered window does too.
115+func TestOverlayPopupAndBorder(t *testing.T) {
116+ s, c := overlayScene()
117+ s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{W: 5, H: 5}}}, s.Windows...)
118+ if _, _, reason := overlayCandidate(s, c); reason != "window_above" {
119+ t.Fatalf("popup: %q", reason)
120+ }
121+ s, _ = overlayScene()
122+ s.Border.Width = 2
123+ if _, _, reason := overlayCandidate(s, c); reason != "border" {
124+ t.Fatalf("border: %q", reason)
125+ }
126+}
127+
128+// A cursor commit refused with the overlay on drops the overlay: the
129+// next frame is composed.
130+func TestOverlayCursorConflictDropsOverlay(t *testing.T) {
131+ o, k, _ := overlayOutput(t, nil, unix.EINVAL)
132+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
133+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
134+ s, c := overlayScene()
135+ ov, _ := o.overlayFrame(s, c)
136+ if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil {
137+ t.Fatal(err)
138+ }
139+ o.pending = false
140+ o.cursor.image = true
141+ o.cursor.Move(150, 50)
142+ if err := o.commitState(false); err != errOverlayDropped {
143+ t.Fatalf("err %v", err)
144+ }
145+ if ov2, _ := o.overlayFrame(s, c); ov2.fb != 0 || o.overlayReason != "cursor_conflict" {
146+ t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason)
147+ }
148+}
149+
150+// An explicit-sync window on the overlay: the plane carries a duplicate of
151+// the client's acquire fence, closed once the frame is committed.
152+func TestOverlayCarriesAcquireFence(t *testing.T) {
153+ o, k, commits := overlayOutput(t)
154+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
155+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
156+ s, c := overlayScene()
157+ r, w, err := os.Pipe()
158+ if err != nil {
159+ t.Fatal(err)
160+ }
161+ defer r.Close()
162+ defer w.Close()
163+ win := c[2]
164+ win.Acquire = r
165+ c[2] = win
166+ ov, _ := o.overlayFrame(s, c)
167+ if ov.acquire == nil || ov.acquire.Fd() == r.Fd() {
168+ t.Fatal("overlay has no duplicate of the acquire fence")
169+ }
170+ fd := ov.acquire.Fd()
171+ if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
172+ t.Fatal(err)
173+ }
174+ ov.close()
175+ last := (*commits)[len(*commits)-1]
176+ if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) {
177+ t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd)
178+ }
179+ if _, err := ov.acquire.Stat(); err == nil {
180+ t.Fatal("duplicate left open")
181+ }
182+}
M internal/adapters/drm/props.go +4 -16
@@ -6,12 +6,11 @@ import (
66 "golang.org/x/sys/unix"
77 )
88
9-// KMS object properties, read and set with the legacy property ioctls.
9+// KMS object properties, read by name; atomic commits set them.
1010
1111 const (
1212 ioctlObjGetProps = 0xC02064B9 // DRM_IOWR('d', 0xB9, struct drm_mode_obj_get_properties)
1313 ioctlGetProp = 0xC04064AA // DRM_IOWR('d', 0xAA, struct drm_mode_get_property)
14- ioctlObjSetProp = 0xC01864BA // DRM_IOWR('d', 0xBA, struct drm_mode_obj_set_property)
1514
1615 objCrtc = 0xcccccccc
1716 objConnector = 0xc0c0c0c0
@@ -30,12 +29,6 @@ type getProp struct {
3029 countValues, countEn uint32
3130 }
3231
33-type objSetProp struct {
34- value uint64
35- propID, objID, objType uint32
36- _ uint32
37-}
38-
3932 // objProps returns an object's property IDs and values by name.
4033 func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) {
4134 q := objGetProps{objID: obj, objType: typ}
@@ -64,19 +57,14 @@ func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) {
6457
6558 // vrrProperty returns the CRTC's VRR_ENABLED property when the connector
6659 // is VRR capable, else 0.
67-func vrrProperty(fd int, conn, crtc uint32) uint32 {
68- cp, err := objProps(fd, conn, objConnector)
60+func vrrProperty(k kms, conn, crtc uint32) uint32 {
61+ cp, err := k.objProps(conn, objConnector)
6962 if err != nil || cp["vrr_capable"][1] != 1 {
7063 return 0
7164 }
72- rp, err := objProps(fd, crtc, objCrtc)
65+ rp, err := k.objProps(crtc, objCrtc)
7366 if err != nil {
7467 return 0
7568 }
7669 return uint32(rp["VRR_ENABLED"][0])
7770 }
78-
79-func setProp(fd int, obj, typ, prop uint32, v uint64) error {
80- s := objSetProp{value: v, propID: prop, objID: obj, objType: typ}
81- return ioctl(fd, ioctlObjSetProp, unsafe.Pointer(&s))
82-}
M internal/adapters/drm/scanout.go +176 -47
@@ -2,10 +2,13 @@ package drm
22
33 import (
44 "errors"
5+ "os"
6+ "slices"
57 "time"
68 "unsafe"
79
810 "github.com/bnema/nefertty/internal/ports"
11+ "golang.org/x/sys/unix"
912 )
1013
1114 // Direct scanout (ADR 006): when one fullscreen window covers the output
@@ -23,7 +26,6 @@ const (
2326
2427 fbModifiers = 1 << 1
2528 capAddFB2Modifiers = 0x10
26- capAsyncPageFlip = 0x7
2729 modInvalid = 0x00ffffffffffffff
2830 scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused
2931 )
@@ -43,6 +45,7 @@ type fbCmd2 struct {
4345
4446 // scanoutCandidate returns the window content the output can scan out
4547 // directly, or a reason why it cannot. w, h are the output's physical size.
48+// The plane's formats are checked by scanoutFB.
4649 func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) {
4750 var full *ports.SceneWindow
4851 for i := range s.Windows {
@@ -70,14 +73,10 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
7073 return c, "not_dmabuf"
7174 case len(c.Children) > 0:
7275 return c, "subsurfaces"
73- case len(c.DMABuf.Planes) != 1:
74- return c, "multi_plane"
7576 case c.Width != w || c.Height != h:
7677 return c, "size_mismatch"
7778 case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H:
7879 return c, "logical_mismatch"
79- case !scanoutable(c.DMABuf.Format):
80- return c, "format"
8180 case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH):
8281 return c, "geometry_crop"
8382 }
@@ -92,65 +91,132 @@ type clientFB struct {
9291 failed string
9392 // noAsync: KMS refused an async flip to it; it flips at vblank.
9493 noAsync bool
94+ // overlayFailed is why the overlay plane refused it; kept apart from
95+ // failed, so a refusal on one plane does not bar the other.
96+ overlayFailed string
97+ importErr bool
9598 }
9699
97-// scanoutFB returns the framebuffer of a client buffer, importing it on
98-// first use. A buffer KMS refuses is remembered with its reason.
100+// scanoutFB returns the framebuffer of a client buffer for the primary
101+// plane, importing it on first use. A buffer KMS refuses is remembered
102+// with its reason.
99103 func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
100- if fb := o.clientFBs[b.ID]; fb != nil {
101- fb.last = now
102- return fb.fbID, fb.failed
104+ return o.planeFB(o.primary, b, now, func(fb *clientFB) *string { return &fb.failed })
105+}
106+
107+// overlayFB is scanoutFB for the overlay plane, with its own formats and
108+// refusal reason.
109+func (o *Output) overlayFB(b *ports.DMABuf, now time.Time) (uint32, string) {
110+ return o.planeFB(o.overlay, b, now, func(fb *clientFB) *string { return &fb.overlayFailed })
111+}
112+
113+// planeFB imports b once as a framebuffer; reason is the plane's refusal
114+// field. Both planes share the framebuffer: they show the same format.
115+func (o *Output) planeFB(p *plane, b *ports.DMABuf, now time.Time, reason func(*clientFB) *string) (uint32, string) {
116+ fb := o.clientFBs[b.ID]
117+ if fb == nil {
118+ fb = &clientFB{}
119+ o.clientFBs[b.ID] = fb
103120 }
104- fb := &clientFB{last: now}
105- o.clientFBs[b.ID] = fb
106- id, err := o.card.addFB(b)
107- if err != nil {
108- o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed")
109- fb.failed = "import_failed"
110- return 0, fb.failed
121+ fb.last = now
122+ why := reason(fb)
123+ if *why != "" {
124+ return 0, *why
125+ }
126+ format, ok := planeFormat(p, b)
127+ if !ok {
128+ *why = "format"
129+ return 0, *why
130+ }
131+ if fb.fbID == 0 {
132+ if fb.importErr {
133+ *why = "import_failed"
134+ return 0, *why
135+ }
136+ id, err := o.k.addFB(b, format)
137+ if err != nil {
138+ o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed")
139+ fb.importErr, *why = true, "import_failed"
140+ return 0, *why
141+ }
142+ fb.fbID = id
111143 }
112- fb.fbID = id
113- return id, ""
144+ return fb.fbID, ""
114145 }
115146
116-// scanoutFormat is the framebuffer format for a client format. The primary
117-// plane shows no alpha, and a legacy page flip keeps the format of the
118-// composed image (XRGB8888): alpha formats are scanned out as their X
119-// variant, others are refused.
120-func scanoutFormat(f uint32) (uint32, bool) {
121- switch f {
122- case fourccXRGB, fourccARGB:
123- return fourccXRGB, true
147+// scanoutFormat is the framebuffer format a client buffer is scanned out
148+// as on the primary plane (see planeFormat).
149+func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { return planeFormat(o.primary, b) }
150+
151+// planeFormat is the framebuffer format a client buffer shows as on p,
152+// when p lists it with the buffer's modifier. Planes here show no alpha:
153+// an alpha format goes as its X variant when the plane has that one.
154+func planeFormat(p *plane, b *ports.DMABuf) (uint32, bool) {
155+ has := func(f uint32) bool {
156+ return slices.Contains(p.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier})
157+ }
158+ if x, ok := opaqueVariant[b.Format]; ok && has(x) {
159+ return x, true
124160 }
125- return 0, false
161+ return b.Format, has(b.Format)
162+}
163+
164+// opaqueVariant maps alpha formats to the same layout without alpha.
165+var opaqueVariant = map[uint32]uint32{
166+ fourccARGB: fourccXRGB,
167+ fourccABGR: fourccXBGR,
168+ fourccAR30: fourccXR30,
169+ fourccAB30: fourccXB30,
170+ fourccAR4H: fourccXR4H,
171+ fourccAB4H: fourccXB4H,
172+}
173+
174+// scanoutFormats are the formats a buffer can be scanned out with: in the
175+// primary plane's IN_FORMATS (directly or as its opaque variant) and
176+// composable by the renderer.
177+func (o *Output) scanoutFormats(sampled []ports.DMABufFormat) []ports.DMABufFormat {
178+ var out []ports.DMABufFormat
179+ for _, f := range sampled {
180+ b := ports.DMABuf{Format: f.Format, Modifier: f.Modifier}
181+ if _, ok := o.scanoutFormat(&b); ok {
182+ out = append(out, f)
183+ }
184+ }
185+ return out
126186 }
127187
128188 const (
129189 fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
130190 fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
191+ fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
192+ fourccABGR = 'A' | 'B'<<8 | '2'<<16 | '4'<<24
193+ fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
194+ fourccAR30 = 'A' | 'R'<<8 | '3'<<16 | '0'<<24
195+ fourccXB30 = 'X' | 'B'<<8 | '3'<<16 | '0'<<24
196+ fourccAB30 = 'A' | 'B'<<8 | '3'<<16 | '0'<<24
197+ fourccXR4H = 'X' | 'R'<<8 | '4'<<16 | 'H'<<24
198+ fourccAR4H = 'A' | 'R'<<8 | '4'<<16 | 'H'<<24
199+ fourccXB4H = 'X' | 'B'<<8 | '4'<<16 | 'H'<<24
200+ fourccAB4H = 'A' | 'B'<<8 | '4'<<16 | 'H'<<24
131201 )
132202
133-// addFB imports a dmabuf as a framebuffer: client buffers and the
134-// renderer's exported output images. The GEM handle belongs to the card fd
203+// addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's
204+// output images and cursor images. The GEM handle belongs to the card fd
135205 // and is shared by every import of the same buffer, so import, ADDFB2 and
136-// close run under the card lock and the handle is closed right away: the
206+// close run under the lock and the handle is closed right away: the
137207 // framebuffer keeps its own reference to the buffer.
138-func (c *Card) addFB(b *ports.DMABuf) (uint32, error) {
139- format, ok := scanoutFormat(b.Format)
140- if !ok {
141- return 0, errors.New("format not scanned out")
142- }
208+func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
143209 raw, err := b.Planes[0].File.SyscallConn()
144210 if err != nil {
145211 return 0, err
146212 }
147- c.gemMu.Lock()
148- defer c.gemMu.Unlock()
213+ k.gemMu.Lock()
214+ defer k.gemMu.Unlock()
149215 var p primeHandle
150216 var ioErr error
151217 if err := raw.Control(func(fd uintptr) {
152218 p.fd = int32(fd)
153- ioErr = ioctl(c.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
219+ ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
154220 }); err != nil {
155221 return 0, err
156222 }
@@ -159,19 +225,68 @@ func (c *Card) addFB(b *ports.DMABuf) (uint32, error) {
159225 }
160226 defer func() {
161227 g := gemClose{handle: p.handle}
162- _ = ioctl(c.fd, ioctlGemClose, unsafe.Pointer(&g))
228+ _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
163229 }()
164230 cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format}
165- cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = p.handle, b.Planes[0].Stride, b.Planes[0].Offset
166- if b.Modifier != modInvalid && (b.Modifier != 0 || c.modifiers) {
167- cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier
231+ useMod := b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers)
232+ if useMod {
233+ cmd.flags = fbModifiers
234+ }
235+ // Planes in the same dmabuf share its GEM handle: each distinct
236+ // handle is closed once.
237+ closing := map[uint32]bool{p.handle: true}
238+ for i, pl := range b.Planes {
239+ if i >= len(cmd.handles) {
240+ break
241+ }
242+ h := p.handle
243+ if i > 0 {
244+ var err error
245+ if h, err = k.primeImport(pl.File); err != nil {
246+ return 0, err
247+ }
248+ if !closing[h] {
249+ closing[h] = true
250+ defer k.gemClose(h)
251+ }
252+ }
253+ cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset
254+ if useMod {
255+ cmd.modifiers[i] = b.Modifier
256+ }
168257 }
169- if err := ioctl(c.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
258+ if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
170259 return 0, errors.Join(errors.New("add fb2"), err)
171260 }
172261 return cmd.fbID, nil
173262 }
174263
264+// primeImport returns the GEM handle of a dmabuf file. The caller holds
265+// gemMu and closes the handle.
266+func (k kmsDevice) primeImport(f *os.File) (uint32, error) {
267+ raw, err := f.SyscallConn()
268+ if err != nil {
269+ return 0, err
270+ }
271+ var p primeHandle
272+ var ioErr error
273+ if err := raw.Control(func(fd uintptr) {
274+ p.fd = int32(fd)
275+ ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
276+ }); err != nil {
277+ return 0, err
278+ }
279+ if ioErr != nil {
280+ return 0, errors.Join(errors.New("prime import"), ioErr)
281+ }
282+ return p.handle, nil
283+}
284+
285+func (k kmsDevice) gemClose(h uint32) {
286+ g := gemClose{handle: h}
287+ _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
288+}
289+
175290 // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all
176291 // of them, except those on screen or queued.
177292 func (o *Output) dropClientFBs(now time.Time, all bool) {
@@ -183,11 +298,25 @@ func (o *Output) dropClientFBs(now time.Time, all bool) {
183298 continue
184299 }
185300 if fb.fbID != 0 {
186- v := fb.fbID
187- _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v))
301+ _ = o.k.rmFB(fb.fbID)
188302 }
189303 delete(o.clientFBs, id)
190304 }
191305 }
192306
193-func scanoutable(f uint32) bool { _, ok := scanoutFormat(f); return ok }
307+// dupFence duplicates a sync file under its SyscallConn, so a concurrent
308+// Close cannot hand us a reused fd; nil when f is nil or closed.
309+func dupFence(f *os.File) *os.File {
310+ if f == nil {
311+ return nil
312+ }
313+ raw, err := f.SyscallConn()
314+ if err != nil {
315+ return nil
316+ }
317+ fd := -1
318+ if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 {
319+ return nil
320+ }
321+ return os.NewFile(uintptr(fd), "acquire-fence")
322+}
M internal/adapters/drm/scanout_test.go +95 -10
@@ -2,8 +2,10 @@ package drm
22
33 import (
44 "testing"
5+ "time"
56
67 "github.com/bnema/nefertty/internal/ports"
8+ "github.com/stretchr/testify/mock"
79 )
810
911 func TestScanoutCandidate(t *testing.T) {
@@ -38,11 +40,6 @@ func TestScanoutCandidate(t *testing.T) {
3840 {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"},
3941 {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"},
4042 {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "logical_mismatch"},
41- {"unsupported format", nil, func(c *ports.SurfaceContent) {
42- d := *c.DMABuf
43- d.Format = 'N' | 'V'<<8 | '1'<<16 | '2'<<24
44- c.DMABuf = &d
45- }, "format"},
4643 {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""},
4744 } {
4845 s := scene
@@ -60,11 +57,99 @@ func TestScanoutCandidate(t *testing.T) {
6057 }
6158 }
6259
63-func TestScanoutFormat(t *testing.T) {
64- for f, want := range map[uint32]bool{fourccXRGB: true, fourccARGB: true, 'A' | 'B'<<8 | '2'<<16 | '4'<<24: false} {
65- got, ok := scanoutFormat(f)
66- if ok != want || ok && got != fourccXRGB {
67- t.Errorf("%x: %x %v", f, got, ok)
60+// A buffer scans out when the primary plane lists its format (or the
61+// opaque variant) with its modifier; the tranche offers only formats the
62+// renderer also composes.
63+func TestScanoutFormatFollowsInFormats(t *testing.T) {
64+ const tiled = 0x0200000000000001
65+ o, k, _ := testOutput(t)
66+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0}, {Format: fourccXR30, Modifier: tiled}}
67+ nv12 := uint32('N' | 'V'<<8 | '1'<<16 | '2'<<24)
68+ for _, tc := range []struct {
69+ name string
70+ format uint32
71+ mod uint64
72+ as uint32
73+ ok bool
74+ }{
75+ {"argb as xrgb", fourccARGB, tiled, fourccXRGB, true},
76+ {"xrgb linear", fourccXRGB, 0, fourccXRGB, true},
77+ {"10 bit alpha as opaque", fourccAR30, tiled, fourccXR30, true},
78+ {"modifier not listed", fourccXRGB, 0x0200000000000002, 0, false},
79+ {"format not listed", nv12, tiled, 0, false},
80+ } {
81+ got, ok := o.scanoutFormat(&ports.DMABuf{Format: tc.format, Modifier: tc.mod})
82+ if ok != tc.ok || ok && got != tc.as {
83+ t.Errorf("%s: %#x %v", tc.name, got, ok)
84+ }
85+ }
86+ sampled := []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0x0200000000000002}, {Format: nv12, Modifier: tiled}}
87+ if got := o.scanoutFormats(sampled); len(got) != 1 || got[0] != sampled[0] {
88+ t.Fatalf("tranche %v", got)
89+ }
90+ // A buffer not in IN_FORMATS is refused before any import.
91+ o.clientFBs = map[uint64]*clientFB{}
92+ if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 4, Format: nv12, Modifier: tiled}, time.Now()); fb != 0 || reason != "format" {
93+ t.Fatalf("fb %d reason %q", fb, reason)
94+ }
95+ // A listed one is imported with every plane (addFB builds ADDFB2).
96+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
97+ if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 5, Format: fourccARGB, Modifier: tiled, Planes: []ports.DMABufPlane{{}, {}}}, time.Now()); fb != 88 || reason != "" {
98+ t.Fatalf("fb %d reason %q", fb, reason)
99+ }
100+}
101+
102+// Each modeset reports the formats clients may allocate for scanout;
103+// with direct scanout off the list is empty.
104+func TestModesetSendsScanoutFormats(t *testing.T) {
105+ const tiled = 0x0200000000000001
106+ for _, on := range []bool{true, false} {
107+ o, k, _ := testOutput(t)
108+ o.cursor = nil
109+ o.scanout, o.device = on, 9
110+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}}
111+ o.sampled = []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0}}
112+ ch := make(chan ports.OutputFormats, 1)
113+ o.formats = ch
114+ k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
115+ if err := o.modeset(); err != nil {
116+ t.Fatal(err)
68117 }
118+ f := <-ch
119+ if f.Output != "DP-1" || f.Device != 9 {
120+ t.Fatalf("report %+v", f)
121+ }
122+ if on != (len(f.Formats) == 1 && f.Formats[0] == o.sampled[0]) || !on && len(f.Formats) != 0 {
123+ t.Fatalf("scanout=%v formats %v", on, f.Formats)
124+ }
125+ }
126+}
127+
128+// A plane without IN_FORMATS (driver without modifiers) scans out its
129+// GETPLANE formats, linear.
130+func TestPlaneWithoutInFormatsIsLinear(t *testing.T) {
131+ k := newMockkms(t)
132+ k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1, formats: []uint32{fourccXRGB}}}, nil)
133+ k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}}, nil)
134+ ps, err := readPlanes(k, 0)
135+ if err != nil || len(ps) != 1 {
136+ t.Fatalf("%v %v", ps, err)
137+ }
138+ o := &Output{primary: ps[0]}
139+ if f, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB}); !ok || f != fourccXRGB {
140+ t.Fatalf("linear argb: %#x %v", f, ok)
141+ }
142+ if _, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB, Modifier: 0x0200000000000001}); ok {
143+ t.Fatal("tiled accepted without IN_FORMATS")
144+ }
145+}
146+
147+// A flip shows only the windows its scene draws: presentation feedback of
148+// a window on another workspace or hidden is discarded, not presented.
149+func TestShownByScene(t *testing.T) {
150+ s := ports.Scene{Windows: []ports.SceneWindow{{ID: 1}, {ID: 2, Hidden: true}}, Layers: []ports.SceneLayer{{ID: 5}}}
151+ got := shownBy(s, map[ports.WindowID]uint64{1: 3, 2: 4, 5: 6, 9: 7})
152+ if len(got) != 2 || got[1] != 3 || got[5] != 6 {
153+ t.Fatalf("shows %v", got)
69154 }
70155 }
A internal/adapters/drm/stuck_test.go +244 -0
@@ -0,0 +1,244 @@
1+package drm
2+
3+import (
4+ "context"
5+ "os"
6+ "sync"
7+ "sync/atomic"
8+ "testing"
9+ "time"
10+
11+ portsmocks "github.com/bnema/nefertty/internal/mocks/ports"
12+ "github.com/bnema/nefertty/internal/ports"
13+ "github.com/stretchr/testify/mock"
14+ "golang.org/x/sys/unix"
15+)
16+
17+// runStuck runs an output whose commits return errs (after the start
18+// modeset test and modeset) and whose CRTC never sends an event. It
19+// returns the recorded commits once n were made, failing after 2s.
20+func runStuck(t *testing.T, n int, errs ...error) []commitRec {
21+ t.Helper()
22+ h := startStuck(t, nil, errs...)
23+ h.scenes <- ports.Scene{Seq: 1}
24+ waitFor(t, func() bool { return h.count() >= n })
25+ return h.stop()
26+}
27+
28+// stuckRun is an output run by startStuck.
29+type stuckRun struct {
30+ t *testing.T
31+ scenes chan ports.Scene
32+ contents chan ports.SurfaceContent
33+ active chan bool
34+ presented chan ports.OutputPresented
35+ count func() int
36+ stop func() []commitRec
37+}
38+
39+// startStuck runs an output whose commits return errs (after the start
40+// modeset test and modeset) and whose CRTC never sends an event. render
41+// makes each frame's fence once the output started (nil: none).
42+func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
43+ t.Helper()
44+ o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...)
45+ o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
46+ o.stuckAfter = 200 * time.Millisecond
47+ o.flipped = make(chan flipEvent) // no event ever
48+ r := portsmocks.NewMockRenderer(t)
49+ buf := func() ports.DMABuf {
50+ f, w, _ := os.Pipe()
51+ w.Close()
52+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
53+ }
54+ r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
55+ r.EXPECT().UseTarget(mock.Anything).Return()
56+ var started atomic.Bool
57+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
58+ if render == nil || !started.Load() {
59+ return nil, nil
60+ }
61+ return render(), nil
62+ })
63+ r.EXPECT().Close().Return().Once()
64+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
65+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
66+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
67+ k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
68+ h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: make(chan ports.OutputPresented, 64)}
69+ ctx, cancel := context.WithCancel(context.Background())
70+ done := make(chan error, 1)
71+ go func() {
72+ done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.presented)
73+ }()
74+ h.count = func() int {
75+ commitMu.Lock()
76+ defer commitMu.Unlock()
77+ return len(*commits)
78+ }
79+ h.stop = func() []commitRec {
80+ cancel()
81+ if err := <-done; err != nil {
82+ t.Fatal(err)
83+ }
84+ commitMu.Lock()
85+ defer commitMu.Unlock()
86+ return append([]commitRec(nil), (*commits)...)
87+ }
88+ waitFor(t, func() bool { return h.count() == 2 })
89+ started.Store(true)
90+ return h
91+}
92+
93+// A commit whose event never comes must not stop the output: it is
94+// abandoned with a modeset, then the scene is committed again.
95+func TestMissingEventRecoversWithModeset(t *testing.T) {
96+ c := runStuck(t, 5)
97+ if c[2].user&3 != userFrame {
98+ t.Fatalf("commit 2 is not a frame: %+v", c[2])
99+ }
100+ if c[3].flags&atomicAllowModes == 0 {
101+ t.Fatalf("no recovery modeset after the missing event: flags %#x", c[3].flags)
102+ }
103+ if c[4].user&3 != userFrame || c[4].user>>userKindBits <= c[2].user>>userKindBits {
104+ t.Fatalf("scene not committed again after the modeset: %+v", c[4])
105+ }
106+}
107+
108+// EBUSY with no commit of ours in flight gets no event: the commit is
109+// retried without one.
110+func TestBusyWithoutEventRetries(t *testing.T) {
111+ c := runStuck(t, 4, unix.EBUSY)
112+ if c[2].user&3 != userFrame || c[3].user&3 != userFrame || c[3].flags&atomicAllowModes != 0 {
113+ t.Fatalf("busy frame not retried: %+v", c[2:])
114+ }
115+}
116+
117+// EBUSY that lasts past the limit ends in a modeset, and a modeset
118+// refused as busy is retried too.
119+func TestLastingBusyModesets(t *testing.T) {
120+ errs := make([]error, 8)
121+ for i := range errs {
122+ errs[i] = unix.EBUSY
123+ }
124+ c := runStuck(t, 2+len(errs)+1, errs...)
125+ found := false
126+ for _, r := range c[2:] {
127+ if r.flags&atomicAllowModes != 0 {
128+ found = true
129+ }
130+ }
131+ if !found {
132+ t.Fatal("no modeset while EBUSY lasted")
133+ }
134+}
135+
136+// fencePipe is a fence that signals when its write end is written.
137+func fencePipe(t *testing.T) (fence, signal *os.File) {
138+ t.Helper()
139+ r, w, err := os.Pipe()
140+ if err != nil {
141+ t.Fatal(err)
142+ }
143+ t.Cleanup(func() { r.Close(); w.Close() })
144+ return r, w
145+}
146+
147+// A frame whose GPU fence has not signalled is slow, not lost: it keeps
148+// waiting past the limit; once the fence signalled and still no event
149+// came, the output modesets.
150+func TestUnsignalledFenceDelaysRecovery(t *testing.T) {
151+ var mu sync.Mutex
152+ var signals []*os.File
153+ h := startStuck(t, func() *os.File {
154+ f, w := fencePipe(t)
155+ mu.Lock()
156+ signals = append(signals, w)
157+ mu.Unlock()
158+ return f
159+ })
160+ h.scenes <- ports.Scene{Seq: 1}
161+ waitFor(t, func() bool { return h.count() == 3 })
162+ time.Sleep(3 * 200 * time.Millisecond)
163+ if n := h.count(); n != 3 {
164+ t.Fatalf("%d commits while the frame's fence is unsignalled", n)
165+ }
166+ mu.Lock()
167+ _, _ = signals[0].Write([]byte{1})
168+ mu.Unlock()
169+ waitFor(t, func() bool { return h.count() >= 4 })
170+ if c := h.stop(); c[3].flags&atomicAllowModes == 0 {
171+ t.Fatalf("no modeset once the fence signalled: %#x", c[3].flags)
172+ }
173+}
174+
175+// A frame refused with EBUSY may still be read by the GPU: what was seen
176+// is reported only once its fence signalled.
177+func TestSeenWaitsForRefusedFrameFence(t *testing.T) {
178+ var mu sync.Mutex
179+ var signals []*os.File
180+ h := startStuck(t, func() *os.File {
181+ f, w := fencePipe(t)
182+ mu.Lock()
183+ signals = append(signals, w)
184+ mu.Unlock()
185+ return f
186+ }, unix.EBUSY, unix.EACCES)
187+ h.scenes <- ports.Scene{Seq: 1}
188+ // EBUSY, then the retry loses DRM master: nothing is pending.
189+ waitFor(t, func() bool { return h.count() == 4 })
190+ h.contents <- ports.SurfaceContent{ID: 1, Seq: 7}
191+ select {
192+ case p := <-h.presented:
193+ t.Fatalf("seen reported while refused frames may be read: %+v", p)
194+ case <-time.After(50 * time.Millisecond):
195+ }
196+ mu.Lock()
197+ for _, w := range signals {
198+ _, _ = w.Write([]byte{1})
199+ }
200+ mu.Unlock()
201+ select {
202+ case p := <-h.presented:
203+ if p.Seen[1] != 7 {
204+ t.Fatalf("report %+v", p)
205+ }
206+ case <-time.After(2 * time.Second):
207+ t.Fatal("seen not reported once the fences signalled")
208+ }
209+ h.stop()
210+}
211+
212+// An EBUSY run ends when commits stop being refused: a later EBUSY is
213+// retried, not taken as the same run lasting past the limit.
214+func TestLaterBusyStartsNewRun(t *testing.T) {
215+ o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY)
216+ o.stuckAfter = 100 * time.Millisecond
217+ enabled := true
218+ o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled)
219+ time.Sleep(busyRetry)
220+ if o.expire() {
221+ t.Fatal("first EBUSY retry modesets")
222+ }
223+ time.Sleep(2 * o.stuckAfter)
224+ o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled)
225+ time.Sleep(busyRetry)
226+ if o.expire() {
227+ t.Fatal("a new EBUSY taken as the old run: modeset")
228+ }
229+}
230+
231+// A modeset that fails keeps what is on screen: buffers still shown are
232+// not released under the plane.
233+func TestFailedModesetKeepsShown(t *testing.T) {
234+ o, k, _ := testOutput(t, unix.EACCES)
235+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
236+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
237+ o.shown, o.queued = 5, 6
238+ if err := o.modeset(); err == nil {
239+ t.Fatal("modeset succeeded")
240+ }
241+ if o.shown != 5 || o.queued != 6 {
242+ t.Fatalf("shown %d queued %d after a failed modeset", o.shown, o.queued)
243+ }
244+}
M internal/adapters/headless/output.go +45 -5
@@ -3,6 +3,7 @@ package headless
33 import (
44 "context"
55 "fmt"
6+ "golang.org/x/sys/unix"
67 "image"
78 "image/png"
89 "maps"
@@ -10,6 +11,7 @@ import (
1011 "path/filepath"
1112 "time"
1213
14+ "github.com/bnema/nefertty/internal/adapters/syncfile"
1315 "github.com/bnema/nefertty/internal/ports"
1416 "github.com/bnema/zerowrap"
1517 )
@@ -118,11 +120,24 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
118120 }
119121 }
120122 start := time.Now()
121- if err := r.Render(scene, surfaces); err != nil {
123+ done, err := r.Render(scene, surfaces)
124+ if err != nil {
122125 return fmt.Errorf("render frame: %w", err)
123126 }
127+ if done != nil {
128+ // No screen paces headless frames: wait for the GPU before
129+ // reporting that its buffers were read.
130+ err = syncfile.Wait(ctx, done)
131+ done.Close()
132+ if err != nil {
133+ if ctx.Err() != nil {
134+ return nil
135+ }
136+ return fmt.Errorf("frame fence: %w", err)
137+ }
138+ }
124139 frame++
125- pending = opts.report(pending, seen)
140+ pending = opts.flipped(pending, seen, scene, surfaces)
126141 if opts.ScreenshotDir != "" {
127142 shot := r.Pixels()
128143 if opts.Cursor != nil {
@@ -155,10 +170,8 @@ func writePNG(path string, img *image.RGBA) error {
155170 return os.Rename(f.Name(), path)
156171 }
157172
158-// report sends what the output read; a full channel drops it.
159173 // report sends what the output read, or returns it to retry when the
160-// channel is full. Headless frames are not paced by a screen: Flip stays
161-// false and frame callbacks follow the refresh timer.
174+// channel is full. Frames that were not drawn carry no flip.
162175 func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented {
163176 if opts.Presented == nil {
164177 return nil
@@ -166,6 +179,33 @@ func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uin
166179 return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)})
167180 }
168181
182+// flipped reports a drawn frame as a flip at the current CLOCK_MONOTONIC
183+// time (software clock, refresh unknown), so presentation feedback and
184+// frame callbacks follow headless frames. A report still waiting is
185+// sent first.
186+func (opts Options) flipped(pending *ports.OutputPresented, seen map[ports.WindowID]uint64, scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) *ports.OutputPresented {
187+ if opts.Presented == nil {
188+ return nil
189+ }
190+ var merged int
191+ if pending != nil && opts.send(pending) != nil {
192+ // The reader is behind: this frame's report replaces the waiting
193+ // one, and its flip counts as merged (its feedback is discarded).
194+ if pending.Flip != nil {
195+ merged = 1 + pending.Flip.Merged
196+ }
197+ }
198+ var ts unix.Timespec
199+ _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
200+ shows := map[ports.WindowID]uint64{}
201+ for id, c := range surfaces {
202+ if scene.Shows(id) {
203+ shows[id] = c.Seq
204+ }
205+ }
206+ return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen), Flip: &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows, Merged: merged}})
207+}
208+
169209 func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented {
170210 select {
171211 case opts.Presented <- *r:
M internal/adapters/headless/output_test.go +2 -2
@@ -35,12 +35,12 @@ func (f *frames) snapshot() ([]ports.Scene, []map[ports.WindowID]ports.SurfaceCo
3535 func recordingRenderer(t *testing.T, err error) (*portsmocks.MockRenderer, *frames) {
3636 r := portsmocks.NewMockRenderer(t)
3737 f := &frames{}
38- r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) error {
38+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
3939 f.mu.Lock()
4040 defer f.mu.Unlock()
4141 f.scenes = append(f.scenes, s)
4242 f.contents = append(f.contents, maps.Clone(c))
43- return err
43+ return nil, err
4444 }).Maybe()
4545 r.EXPECT().Pixels().Return(image.NewRGBA(image.Rect(0, 0, 2, 2))).Maybe()
4646 r.EXPECT().Close().Return().Once()
M internal/adapters/libinput/forward.go +19 -1
@@ -6,6 +6,7 @@ import (
66 "time"
77
88 "github.com/bnema/nefertty/internal/ports"
9+ "github.com/bnema/zerowrap"
910 )
1011
1112 // forwarder decouples reading libinput from core. libinput keeps reading
@@ -17,8 +18,15 @@ type forwarder struct {
1718 queue []ports.InputEvent
1819 wake chan struct{}
1920 stats forwardStats
21+ // The queue is unbounded (no event is ever dropped); long is set
22+ // while it is past forwardWarn entries, warned once each way.
23+ log zerowrap.Logger
24+ long bool
2025 }
2126
27+// forwardWarn is the queue length that means core has stalled.
28+const forwardWarn = 4096
29+
2230 // forwardStats is reset by take.
2331 type forwardStats struct {
2432 Motions int // pointer motions read
@@ -27,7 +35,9 @@ type forwardStats struct {
2735 MaxBlock time.Duration // longest wait for core to accept one event
2836 }
2937
30-func newForwarder() *forwarder { return &forwarder{wake: make(chan struct{}, 1)} }
38+func newForwarder(log zerowrap.Logger) *forwarder {
39+ return &forwarder{wake: make(chan struct{}, 1), log: log}
40+}
3141
3242 // push queues ev without blocking.
3343 func (f *forwarder) push(ev ports.InputEvent) {
@@ -49,6 +59,10 @@ func (f *forwarder) push(ev ports.InputEvent) {
4959 }
5060 }
5161 f.queue = append(f.queue, ev)
62+ if len(f.queue) > forwardWarn && !f.long {
63+ f.long = true
64+ f.log.Warn().Str("component", "input").Int("queued", len(f.queue)).Msg("core is not reading input; events queue up")
65+ }
5266 f.mu.Unlock()
5367 select {
5468 case f.wake <- struct{}{}:
@@ -65,6 +79,10 @@ func (f *forwarder) pop() (ports.InputEvent, bool) {
6579 ev := f.queue[0]
6680 f.queue[0] = nil
6781 f.queue = f.queue[1:]
82+ if f.long && len(f.queue) < forwardWarn {
83+ f.long = false
84+ f.log.Warn().Str("component", "input").Int("queued", len(f.queue)).Msg("input queue drained")
85+ }
6886 return ev, true
6987 }
7088
M internal/adapters/libinput/forward_test.go +63 -3
@@ -1,17 +1,22 @@
11 package libinput
22
33 import (
4+ "bytes"
45 "context"
6+ "strings"
7+ "sync"
58 "testing"
69 "time"
710
811 "github.com/bnema/nefertty/internal/ports"
12+ "github.com/bnema/zerowrap"
13+ "github.com/rs/zerolog"
914 )
1015
1116 // A slow consumer must not block push, and stale motion must collapse into
1217 // the latest position while keys and buttons keep their order.
1318 func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) {
14- f := newForwarder()
19+ f := newForwarder(zerowrap.Default())
1520 input := make(chan ports.InputEvent) // unbuffered: core is busy
1621 start := time.Now()
1722 for i := range 1000 {
@@ -56,7 +61,7 @@ func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) {
5661 }
5762
5863 func TestForwarderStopsOnCancel(t *testing.T) {
59- f := newForwarder()
64+ f := newForwarder(zerowrap.Default())
6065 f.push(ports.PointerMotion{X: 1})
6166 ctx, cancel := context.WithCancel(context.Background())
6267 done := make(chan struct{})
@@ -71,7 +76,7 @@ func TestForwarderStopsOnCancel(t *testing.T) {
7176
7277 // Coalesced motions keep the sum of their relative deltas.
7378 func TestForwarderSumsDeltas(t *testing.T) {
74- f := newForwarder()
79+ f := newForwarder(zerowrap.Default())
7580 f.push(ports.PointerMotion{X: 1, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4})
7681 f.push(ports.PointerMotion{X: 2, DX: 10, DY: 20, UnaccelDX: 30, UnaccelDY: 40})
7782 ev, _ := f.pop()
@@ -79,3 +84,58 @@ func TestForwarderSumsDeltas(t *testing.T) {
7984 t.Fatalf("got %+v, want %+v", ev, want)
8085 }
8186 }
87+
88+// 10 000 motions with core stalled are one event with exact delta sums.
89+func TestForwarderManyMotionsOneEvent(t *testing.T) {
90+ f := newForwarder(zerowrap.Default())
91+ for i := range 10000 {
92+ f.push(ports.PointerMotion{X: float64(i), DX: 0.5, UnaccelDY: 1})
93+ }
94+ if len(f.queue) != 1 {
95+ t.Fatalf("%d queued", len(f.queue))
96+ }
97+ ev, _ := f.pop()
98+ if m := ev.(ports.PointerMotion); m.X != 9999 || m.DX != 5000 || m.UnaccelDY != 10000 {
99+ t.Fatalf("%+v", m)
100+ }
101+}
102+
103+// Past forwardWarn queued events the forwarder warns once, and once more
104+// when the queue drains below it; nothing is dropped.
105+func TestForwarderWarnsOnceEachWay(t *testing.T) {
106+ var buf lockedBuffer
107+ f := newForwarder(zerowrap.Logger{Logger: zerolog.New(&buf)})
108+ for range forwardWarn + 100 {
109+ f.push(ports.PointerButton{Button: 272})
110+ }
111+ if n := strings.Count(buf.String(), "events queue up"); n != 1 {
112+ t.Fatalf("%d warnings", n)
113+ }
114+ for range 200 {
115+ f.pop()
116+ }
117+ if n := strings.Count(buf.String(), "input queue drained"); n != 1 {
118+ t.Fatalf("%d drained logs", n)
119+ }
120+ if len(f.queue) != forwardWarn-100 {
121+ t.Fatalf("dropped: %d left", len(f.queue))
122+ }
123+}
124+
125+// lockedBuffer is an io.Writer for a logger.
126+type lockedBuffer struct {
127+ mu sync.Mutex
128+ b bytes.Buffer
129+}
130+
131+func (l *lockedBuffer) Write(p []byte) (int, error) {
132+ l.mu.Lock()
133+ defer l.mu.Unlock()
134+ return l.b.Write(p)
135+}
136+
137+func (l *lockedBuffer) String() string {
138+ l.mu.Lock()
139+ defer l.mu.Unlock()
140+ return l.b.String()
141+}
M internal/adapters/libinput/libinput.go +7 -2
@@ -160,6 +160,9 @@ type Options struct {
160160 // the physical position on it.
161161 MoveCursor func(output string, x, y float64)
162162 Log zerowrap.Logger
163+ // LogMotion logs every pointer motion (--debug=input-motion); motions
164+ // are otherwise only counted in the input stats.
165+ LogMotion bool
163166 }
164167
165168 // statsEvery is how often Run logs input throughput.
@@ -192,7 +195,7 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
192195 p := newPointer(opts.Layout)
193196 p.moved(opts.MoveCursor)
194197 fd := getFD(li)
195- fwd := newForwarder()
198+ fwd := newForwarder(opts.Log)
196199 fwdCtx, stopFwd := context.WithCancel(ctx)
197200 var fwdDone sync.WaitGroup
198201 fwdDone.Go(func() { fwd.run(fwdCtx, input) })
@@ -278,7 +281,9 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) {
278281 m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe)
279282 m.TimeMsec, m.TimeUsec = now, pointerUsec(pe)
280283 p.moved(opts.MoveCursor)
281- log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer")
284+ if opts.LogMotion {
285+ log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer")
286+ }
282287 return m, nil
283288 case evPointerAbs:
284289 // Absolute devices (tablets, VMs) map to the whole layout.
A internal/adapters/syncfile/syncfile.go +38 -0
@@ -0,0 +1,38 @@
1+// Package syncfile waits for Linux sync files (dma-fence fds), such as the
2+// GPU fence a frame render returns.
3+package syncfile
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "os"
10+
11+ "golang.org/x/sys/unix"
12+)
13+
14+// pollSlice bounds one poll so a canceled ctx is seen.
15+const pollSlice = 100 // ms
16+
17+// Wait blocks until f signals or ctx ends. A sync file is signalled when
18+// it polls readable; an error or invalid fd is reported, not taken as
19+// signalled.
20+func Wait(ctx context.Context, f *os.File) error {
21+ fds := []unix.PollFd{{Fd: int32(f.Fd()), Events: unix.POLLIN}}
22+ for {
23+ n, err := unix.Poll(fds, pollSlice)
24+ switch {
25+ case errors.Is(err, unix.EINTR):
26+ continue
27+ case err != nil:
28+ return fmt.Errorf("poll sync file: %w", err)
29+ case n > 0 && fds[0].Revents&unix.POLLIN != 0:
30+ return nil
31+ case n > 0:
32+ return fmt.Errorf("sync file poll events %#x", fds[0].Revents)
33+ }
34+ if err := ctx.Err(); err != nil {
35+ return err
36+ }
37+ }
38+}
A internal/adapters/syncfile/syncfile_test.go +53 -0
@@ -0,0 +1,53 @@
1+package syncfile
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "testing"
8+ "time"
9+)
10+
11+// A pipe stands in for a sync file: readable means signalled.
12+func TestWaitReturnsWhenReadable(t *testing.T) {
13+ r, w, err := os.Pipe()
14+ if err != nil {
15+ t.Fatal(err)
16+ }
17+ defer r.Close()
18+ go func() {
19+ time.Sleep(10 * time.Millisecond)
20+ _, _ = w.Write([]byte{1})
21+ w.Close()
22+ }()
23+ if err := Wait(context.Background(), r); err != nil {
24+ t.Fatal(err)
25+ }
26+}
27+
28+func TestWaitStopsOnCancel(t *testing.T) {
29+ r, w, err := os.Pipe()
30+ if err != nil {
31+ t.Fatal(err)
32+ }
33+ defer r.Close()
34+ defer w.Close()
35+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
36+ defer cancel()
37+ if err := Wait(ctx, r); !errors.Is(err, context.DeadlineExceeded) {
38+ t.Fatalf("err %v", err)
39+ }
40+}
41+
42+// POLLERR alone (a write end whose reader is gone) is not a signal.
43+func TestWaitReportsErrorEvents(t *testing.T) {
44+ r, w, err := os.Pipe()
45+ if err != nil {
46+ t.Fatal(err)
47+ }
48+ defer w.Close()
49+ r.Close()
50+ if err := Wait(context.Background(), w); err == nil {
51+ t.Fatal("POLLERR taken as signalled")
52+ }
53+}
D internal/adapters/vulkan/blend.go +0 -190
@@ -1,190 +0,0 @@
1-package vulkan
2-
3-import (
4- _ "embed"
5- "encoding/binary"
6- "unsafe"
7-
8- vk "github.com/bnema/purego-vulkan/vulkan"
9-)
10-
11-// Translucent client pixels are blended over what is below them; opaque
12-// ones keep the plain copy. The blend pipeline draws one quad per upload
13-// and its fragment shader reads the staged pixels straight from the
14-// staging buffer: no texture, no sampler. Wayland buffers are
15-// premultiplied, hence ONE, ONE_MINUS_SRC_ALPHA.
16-//
17-// Limits: dmabuf buffers are still blitted, alpha ignored (blending them
18-// needs a sampled-image variant). The opaque region is not honoured, so
19-// an ARGB buffer blends even where it is opaque.
20-
21-//go:generate glslc -O --target-env=vulkan1.3 shaders/blend.vert -o shaders/blend.vert.spv
22-//go:generate glslc -O --target-env=vulkan1.3 shaders/blend.frag -o shaders/blend.frag.spv
23-
24-var (
25- //go:embed shaders/blend.vert.spv
26- blendVert []byte
27- //go:embed shaders/blend.frag.spv
28- blendFrag []byte
29-)
30-
31-// drawSize is one entry of the draw table (shaders: struct Draw).
32-const drawSize = 32
33-
34-// blendPipeline is the graphics state for blended uploads.
35-type blendPipeline struct {
36- setLayout vk.DescriptorSetLayout
37- layout vk.PipelineLayout
38- pipeline vk.Pipeline
39- pool vk.DescriptorPool
40- set vk.DescriptorSet
41-}
42-
43-func (r *Renderer) createBlend() error {
44- d, b := r.dd, &r.blend
45- bindings := []vk.DescriptorSetLayoutBinding{
46- {Binding: 0, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
47- {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit},
48- }
49- sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]}
50- if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &b.setLayout)); err != nil {
51- return err
52- }
53- pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &b.setLayout}
54- if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &b.layout)); err != nil {
55- return err
56- }
57- size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 2}
58- dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size}
59- if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &b.pool)); err != nil {
60- return err
61- }
62- dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &b.setLayout}
63- if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &b.set)); err != nil {
64- return err
65- }
66- vert, err := r.shaderModule(blendVert)
67- if err != nil {
68- return err
69- }
70- defer d.DestroyShaderModule(r.device, vert, nil)
71- frag, err := r.shaderModule(blendFrag)
72- if err != nil {
73- return err
74- }
75- defer d.DestroyShaderModule(r.device, frag, nil)
76- main := []byte("main\x00")
77- stages := []vk.PipelineShaderStageCreateInfo{
78- {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]},
79- {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]},
80- }
81- vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
82- assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
83- // The target size is fixed for the renderer's life: static viewport.
84- viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1}
85- scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}
86- viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
87- raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1}
88- multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit}
89- attachment := vk.PipelineColorBlendAttachmentState{
90- BlendEnable: 1,
91- SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd,
92- SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd,
93- ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit,
94- }
95- blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment}
96- format := vk.Format(vk.FormatB8g8r8a8Unorm)
97- rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
98- gpi := vk.GraphicsPipelineCreateInfo{
99- SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering),
100- StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly,
101- ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample,
102- ColorBlendState: &blendState, Layout: b.layout, BasePipelineIndex: -1,
103- }
104- return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &b.pipeline))
105-}
106-
107-func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) {
108- // SPIR-V is read as words: copy into a uint32-aligned slice.
109- words := make([]uint32, len(code)/4)
110- for i := range words {
111- words[i] = binary.LittleEndian.Uint32(code[i*4:])
112- }
113- info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]}
114- var m vk.ShaderModule
115- return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m))
116-}
117-
118-func (r *Renderer) destroyBlend() {
119- d, b := r.dd, &r.blend
120- if b.pipeline != 0 {
121- d.DestroyPipeline(r.device, b.pipeline, nil)
122- }
123- if b.pool != 0 {
124- d.DestroyDescriptorPool(r.device, b.pool, nil)
125- }
126- if b.layout != 0 {
127- d.DestroyPipelineLayout(r.device, b.layout, nil)
128- }
129- if b.setLayout != 0 {
130- d.DestroyDescriptorSetLayout(r.device, b.setLayout, nil)
131- }
132- *b = blendPipeline{}
133-}
134-
135-// bindStaging points the set at the staging buffer: pixels blended
136-// pixel bytes from 0, and the draw table of n entries at tableOffset.
137-func (r *Renderer) bindStaging(pixels, tableOffset, n int) {
138- infos := []vk.DescriptorBufferInfo{
139- {Buffer: r.staging, Range: vk.DeviceSize(max(pixels, 4))},
140- {Buffer: r.staging, Offset: vk.DeviceSize(tableOffset), Range: vk.DeviceSize(n * drawSize)},
141- }
142- writes := []vk.WriteDescriptorSet{
143- {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[0]},
144- {SType: vk.StructureTypeWriteDescriptorSet, DstSet: r.blend.set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &infos[1]},
145- }
146- r.dd.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil)
147-}
148-
149-// putDraw writes draw entry i of table.
150-func (r *Renderer) putDraw(table []byte, i int, u *upload) {
151- e := table[i*drawSize : (i+1)*drawSize]
152- for j, v := range []uint32{
153- uint32(u.rect.Min.X), uint32(u.rect.Min.Y), uint32(u.rect.Max.X), uint32(u.rect.Max.Y),
154- uint32(u.offset / 4), uint32(u.rect.Dx()), uint32(r.width), uint32(r.height),
155- } {
156- binary.LittleEndian.PutUint32(e[j*4:], v)
157- }
158-}
159-
160-// drawBlended draws uploads[first:last], all blended, into view: one
161-// instanced draw. The image is TRANSFER_DST before and after.
162-func (r *Renderer) drawBlended(image vk.Image, view vk.ImageView, first, last int) {
163- d := r.dd
164- rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
165- b := vk.ImageMemoryBarrier{
166- SType: vk.StructureTypeImageMemoryBarrier, Image: image, SubresourceRange: rangeInfo,
167- SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessColorAttachmentReadBit | vk.AccessColorAttachmentWriteBit,
168- OldLayout: vk.ImageLayoutTransferDstOptimal, NewLayout: vk.ImageLayoutColorAttachmentOptimal,
169- }
170- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
171- attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpLoad, StoreOp: vk.AttachmentStoreOpStore}
172- info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment}
173- d.CmdBeginRendering(r.command, &info)
174- d.CmdBindPipeline(r.command, vk.PipelineBindPointGraphics, r.blend.pipeline)
175- d.CmdBindDescriptorSets(r.command, vk.PipelineBindPointGraphics, r.blend.layout, 0, 1, &r.blend.set, 0, nil)
176- d.CmdDraw(r.command, 6, uint32(last-first), 0, uint32(first))
177- d.CmdEndRendering(r.command)
178- b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferWriteBit|vk.AccessTransferReadBit
179- b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferDstOptimal
180- d.CmdPipelineBarrier(r.command, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
181-}
182-
183-// createView makes the color attachment view of a target image.
184-func (r *Renderer) createView(t *target) error {
185- info := vk.ImageViewCreateInfo{
186- SType: vk.StructureTypeImageViewCreateInfo, Image: t.image, ViewType: vk.ImageViewType2d, Format: vk.FormatB8g8r8a8Unorm,
187- SubresourceRange: vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1},
188- }
189- return checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &t.view))
190-}
A internal/adapters/vulkan/compose.go +312 -0
@@ -0,0 +1,312 @@
1+package vulkan
2+
3+import (
4+ _ "embed"
5+ "encoding/binary"
6+ "image"
7+ "math"
8+ "os"
9+ "unsafe"
10+
11+ vk "github.com/bnema/purego-vulkan/vulkan"
12+)
13+
14+// Composition is one graphics pipeline drawing every piece of a frame as
15+// a quad, in paint order, inside one dynamic rendering pass: solid fills,
16+// client dmabufs sampled as images, and client wl_shm pixels read from a
17+// GPU buffer (shm.go). Push constants carry each draw; the descriptor set
18+// holds its image and buffer. Client pixels are premultiplied, hence the
19+// blend ONE, ONE_MINUS_SRC_ALPHA; opaque draws force alpha to 1.
20+
21+//go:generate glslc -O --target-env=vulkan1.3 shaders/compose.vert -o shaders/compose.vert.spv
22+//go:generate glslc -O --target-env=vulkan1.3 shaders/compose.frag -o shaders/compose.frag.spv
23+
24+var (
25+ //go:embed shaders/compose.vert.spv
26+ composeVert []byte
27+ //go:embed shaders/compose.frag.spv
28+ composeFrag []byte
29+)
30+
31+// Draw modes and flags (shaders: modeSolid, modeImage, flag*).
32+const (
33+ modeSolid = 0
34+ modeImage = 1
35+ modeBuffer = 2
36+
37+ flagOpaque = 1
38+ flagExact = 2
39+)
40+
41+// pushConstants is struct Draw of the shaders (std430 push constant block).
42+type pushConstants struct {
43+ rect [4]int32
44+ mapv [4]float32
45+ color [4]float32
46+ buf [4]uint32
47+ misc [4]uint32
48+}
49+
50+// draw is one quad of a frame.
51+type draw struct {
52+ pc pushConstants
53+ set vk.DescriptorSet
54+ im *imported // sampled client dmabuf, for ownership barriers
55+ // acquire is the content's explicit-sync fence (nil: implicit).
56+ acquire *os.File
57+}
58+
59+// composer is the pipeline state and the objects every draw can bind.
60+type composer struct {
61+ setLayout vk.DescriptorSetLayout
62+ layout vk.PipelineLayout
63+ pipeline vk.Pipeline
64+ sampler vk.Sampler
65+ // Placeholders for the binding a draw does not use: every binding
66+ // the shader declares must be valid.
67+ dummyImage vk.Image
68+ dummyMemory vk.DeviceMemory
69+ dummyView vk.ImageView
70+ dummyBuffer gpuBuffer
71+ dummyPool vk.DescriptorPool
72+ dummySet vk.DescriptorSet
73+}
74+
75+func (r *Renderer) createComposer() error {
76+ d, c := r.dd, &r.compose
77+ bindings := []vk.DescriptorSetLayoutBinding{
78+ {Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
79+ {Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
80+ }
81+ sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 2, Bindings: &bindings[0]}
82+ if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &c.setLayout)); err != nil {
83+ return err
84+ }
85+ pcr := vk.PushConstantRange{StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit, Size: uint32(unsafe.Sizeof(pushConstants{}))}
86+ pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &c.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &pcr}
87+ if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &c.layout)); err != nil {
88+ return err
89+ }
90+ si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterLinear, MinFilter: vk.FilterLinear, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge}
91+ if err := checked("vkCreateSampler", d.CreateSampler(r.device, &si, nil, &c.sampler)); err != nil {
92+ return err
93+ }
94+ if err := r.createPipeline(); err != nil {
95+ return err
96+ }
97+ return r.createDummies()
98+}
99+
100+func (r *Renderer) createPipeline() error {
101+ d, c := r.dd, &r.compose
102+ vert, err := r.shaderModule(composeVert)
103+ if err != nil {
104+ return err
105+ }
106+ defer d.DestroyShaderModule(r.device, vert, nil)
107+ frag, err := r.shaderModule(composeFrag)
108+ if err != nil {
109+ return err
110+ }
111+ defer d.DestroyShaderModule(r.device, frag, nil)
112+ main := []byte("main\x00")
113+ stages := []vk.PipelineShaderStageCreateInfo{
114+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]},
115+ {SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]},
116+ }
117+ vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
118+ assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
119+ // The target size is fixed for the renderer's life: static viewport.
120+ viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1}
121+ scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}
122+ viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
123+ raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1}
124+ multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit}
125+ attachment := vk.PipelineColorBlendAttachmentState{
126+ BlendEnable: 1,
127+ SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd,
128+ SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd,
129+ ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit,
130+ }
131+ blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment}
132+ format := vk.Format(vk.FormatB8g8r8a8Unorm)
133+ rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
134+ gpi := vk.GraphicsPipelineCreateInfo{
135+ SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering),
136+ StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly,
137+ ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample,
138+ ColorBlendState: &blendState, Layout: c.layout, BasePipelineIndex: -1,
139+ }
140+ return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, &c.pipeline))
141+}
142+
143+// createDummies makes the placeholder image (1×1, shader-read layout) and
144+// buffer, and the set fills bind.
145+func (r *Renderer) createDummies() error {
146+ d, c := r.dd, &r.compose
147+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: 1, Height: 1, Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
148+ if err := checked("vkCreateImage(dummy)", d.CreateImage(r.device, &ii, nil, &c.dummyImage)); err != nil {
149+ return err
150+ }
151+ var req vk.MemoryRequirements
152+ d.GetImageMemoryRequirements(r.device, c.dummyImage, &req)
153+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
154+ if err != nil {
155+ return err
156+ }
157+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
158+ if err := checked("vkAllocateMemory(dummy)", d.AllocateMemory(r.device, &alloc, nil, &c.dummyMemory)); err != nil {
159+ return err
160+ }
161+ if err := checked("vkBindImageMemory(dummy)", d.BindImageMemory(r.device, c.dummyImage, c.dummyMemory, 0)); err != nil {
162+ return err
163+ }
164+ if c.dummyView, err = r.imageView(c.dummyImage, vk.FormatB8g8r8a8Unorm); err != nil {
165+ return err
166+ }
167+ if err := r.newGPUBuffer(&c.dummyBuffer, 16); err != nil {
168+ return err
169+ }
170+ if c.dummyPool, c.dummySet, err = r.newSet(c.dummyView, c.dummyBuffer.buffer); err != nil {
171+ return err
172+ }
173+ // Move the placeholder image to the layout every draw declares.
174+ return r.oneShot(func(cmd vk.CommandBuffer) {
175+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, DstAccessMask: vk.AccessShaderReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: c.dummyImage, SubresourceRange: colorRange}
176+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
177+ })
178+}
179+
180+// newSet allocates a descriptor set from its own pool (the bindings lack
181+// vkFreeDescriptorSets: the pool is destroyed with the object).
182+func (r *Renderer) newSet(view vk.ImageView, buffer vk.Buffer) (vk.DescriptorPool, vk.DescriptorSet, error) {
183+ d := r.dd
184+ sizes := []vk.DescriptorPoolSize{{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}, {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1}}
185+ dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 2, PoolSizes: &sizes[0]}
186+ var pool vk.DescriptorPool
187+ if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &pool)); err != nil {
188+ return 0, 0, err
189+ }
190+ var set vk.DescriptorSet
191+ dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout}
192+ if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &set)); err != nil {
193+ d.DestroyDescriptorPool(r.device, pool, nil)
194+ return 0, 0, err
195+ }
196+ img := vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
197+ buf := vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize}
198+ writes := []vk.WriteDescriptorSet{
199+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img},
200+ {SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &buf},
201+ }
202+ d.UpdateDescriptorSets(r.device, 2, &writes[0], 0, nil)
203+ return pool, set, nil
204+}
205+
206+func (r *Renderer) destroyComposer() {
207+ d, c := r.dd, &r.compose
208+ if c.dummyPool != 0 {
209+ d.DestroyDescriptorPool(r.device, c.dummyPool, nil)
210+ }
211+ r.freeGPUBuffer(&c.dummyBuffer)
212+ if c.dummyView != 0 {
213+ d.DestroyImageView(r.device, c.dummyView, nil)
214+ }
215+ if c.dummyImage != 0 {
216+ d.DestroyImage(r.device, c.dummyImage, nil)
217+ }
218+ if c.dummyMemory != 0 {
219+ d.FreeMemory(r.device, c.dummyMemory, nil)
220+ }
221+ if c.sampler != 0 {
222+ d.DestroySampler(r.device, c.sampler, nil)
223+ }
224+ if c.pipeline != 0 {
225+ d.DestroyPipeline(r.device, c.pipeline, nil)
226+ }
227+ if c.layout != 0 {
228+ d.DestroyPipelineLayout(r.device, c.layout, nil)
229+ }
230+ if c.setLayout != 0 {
231+ d.DestroyDescriptorSetLayout(r.device, c.setLayout, nil)
232+ }
233+ *c = composer{}
234+}
235+
236+func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) {
237+ // SPIR-V is read as words: copy into a uint32-aligned slice.
238+ words := make([]uint32, len(code)/4)
239+ for i := range words {
240+ words[i] = binary.LittleEndian.Uint32(code[i*4:])
241+ }
242+ info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]}
243+ var m vk.ShaderModule
244+ return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m))
245+}
246+
247+var colorRange = vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
248+
249+// imageView makes a 2D color view of an image.
250+func (r *Renderer) imageView(img vk.Image, format vk.Format) (vk.ImageView, error) {
251+ info := vk.ImageViewCreateInfo{SType: vk.StructureTypeImageViewCreateInfo, Image: img, ViewType: vk.ImageViewType2d, Format: format, SubresourceRange: colorRange}
252+ var v vk.ImageView
253+ return v, checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &v))
254+}
255+
256+// createView makes the color attachment view of a target image.
257+func (r *Renderer) createView(t *target) (err error) {
258+ t.view, err = r.imageView(t.image, vk.FormatB8g8r8a8Unorm)
259+ return err
260+}
261+
262+// fillDraw is a solid rect of c.
263+func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw {
264+ dr := draw{set: r.compose.dummySet}
265+ dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
266+ dr.pc.color = [4]float32{float32(c[0]) / 255, float32(c[1]) / 255, float32(c[2]) / 255, 1}
267+ dr.pc.misc = [4]uint32{modeSolid, 0, uint32(r.width), uint32(r.height)}
268+ return dr
269+}
270+
271+// contentDraw draws the visible rect of a w×h buffer mapped onto full
272+// (target pixels). exact: the buffer is drawn at its size.
273+func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, mode uint32, opaque bool) draw {
274+ var dr draw
275+ sx, sy := float32(w)/float32(full.Dx()), float32(h)/float32(full.Dy())
276+ dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
277+ dr.pc.mapv = [4]float32{-float32(full.Min.X) * sx, -float32(full.Min.Y) * sy, sx, sy}
278+ flags := uint32(0)
279+ if opaque {
280+ flags |= flagOpaque
281+ }
282+ if full.Dx() == w && full.Dy() == h {
283+ flags |= flagExact
284+ }
285+ dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}
286+ return dr
287+}
288+
289+// recordDraws draws ds in one rendering pass that clears view to bg, or
290+// keeps its contents (keep).
291+func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw, keep bool) {
292+ d, c := r.dd, &r.compose
293+ clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)}
294+ attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear}
295+ if keep {
296+ attachment.LoadOp = vk.AttachmentLoadOpLoad
297+ }
298+ info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment}
299+ d.CmdBeginRendering(cmd, &info)
300+ d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, c.pipeline)
301+ var bound vk.DescriptorSet
302+ for i := range ds {
303+ dr := &ds[i]
304+ if dr.set != bound {
305+ d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, c.layout, 0, 1, &dr.set, 0, nil)
306+ bound = dr.set
307+ }
308+ d.CmdPushConstants(cmd, c.layout, vk.ShaderStageVertexBit|vk.ShaderStageFragmentBit, 0, uint32(unsafe.Sizeof(dr.pc)), unsafe.Pointer(&dr.pc))
309+ d.CmdDraw(cmd, 6, 1, 0, 0)
310+ }
311+ d.CmdEndRendering(cmd)
312+}
A internal/adapters/vulkan/compose_test.go +215 -0
@@ -0,0 +1,215 @@
1+package vulkan
2+
3+import (
4+ "context"
5+ "image/color"
6+ "os"
7+ "slices"
8+ "testing"
9+ "time"
10+
11+ "github.com/bnema/nefertty/internal/adapters/syncfile"
12+ "github.com/bnema/nefertty/internal/ports"
13+)
14+
15+// near reports whether two colors are within tol per channel.
16+func near(a, b color.RGBA, tol int) bool {
17+ d := func(x, y uint8) bool { v := int(x) - int(y); return v >= -tol && v <= tol }
18+ return d(a.R, b.R) && d(a.G, b.G) && d(a.B, b.B)
19+}
20+
21+// Scaled wl_shm pixels filter linearly: halfway between a black and a
22+// white texel is grey, not either one (nearest neighbour).
23+func TestRendererScaledSHMFiltersLinearly(t *testing.T) {
24+ r, err := New(64, 16)
25+ if err != nil {
26+ t.Skipf("Vulkan unavailable: %v", err)
27+ }
28+ defer r.Close()
29+ // 2×1 buffer: black, white; drawn 64×16, texel centres at x=16, x=48.
30+ px := []byte{0, 0, 0, 255, 255, 255, 255, 255}
31+ c := shmContent(t, 2, 1, 8, px)
32+ c.LogicalW, c.LogicalH = 64, 16
33+ scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}, Borderless: true}}}
34+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
35+ t.Fatal(err)
36+ }
37+ got := r.Pixels().RGBAAt(32, 8)
38+ if !near(got, color.RGBA{128, 128, 128, 255}, 8) {
39+ t.Fatalf("midpoint %v, want grey", got)
40+ }
41+ if got := r.Pixels().RGBAAt(2, 8); !near(got, color.RGBA{0, 0, 0, 255}, 2) {
42+ t.Fatalf("left edge %v, want black (clamped)", got)
43+ }
44+}
45+
46+// Scaled dmabufs are sampled with the linear sampler.
47+func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {
48+ r, err := New(128, 4)
49+ if err != nil {
50+ t.Skipf("Vulkan unavailable: %v", err)
51+ }
52+ defer r.Close()
53+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
54+ if !slices.Contains(r.DMABuf().Formats, linear) {
55+ t.Skipf("linear ARGB8888 not importable")
56+ }
57+ // 64×1: left half black, right half white, drawn twice as wide.
58+ f := udmabuf(t, 64, 1, func(x, _ int) [4]byte {
59+ if x < 32 {
60+ return [4]byte{0, 0, 0, 255}
61+ }
62+ return [4]byte{255, 255, 255, 255}
63+ })
64+ buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
65+ scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}, Borderless: true}}}
66+ c := ports.SurfaceContent{ID: 1, Width: 64, Height: 1, LogicalW: 128, LogicalH: 4, DMABuf: buf}
67+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
68+ t.Fatal(err)
69+ }
70+ // Target x=64 (centre 64.5) maps to buffer 32.25: between texel 31
71+ // (black, centre 31.5) and 32 (white, centre 32.5), 75% white.
72+ if got := r.Pixels().RGBAAt(64, 2); !near(got, color.RGBA{191, 191, 191, 255}, 10) {
73+ t.Fatalf("boundary %v, want a linear mix", got)
74+ }
75+}
76+
77+// Render returns a sync file that becomes readable when the GPU is done.
78+func TestRendererReturnsDoneFence(t *testing.T) {
79+ r, err := New(32, 32)
80+ if err != nil {
81+ t.Skipf("Vulkan unavailable: %v", err)
82+ }
83+ defer r.Close()
84+ if !r.syncFD {
85+ t.Skip("device cannot export sync files")
86+ }
87+ done, err := r.Render(ports.Scene{Background: "#102030"}, nil)
88+ if err != nil {
89+ t.Fatal(err)
90+ }
91+ if done == nil {
92+ t.Fatal("no done fence")
93+ }
94+ defer done.Close()
95+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
96+ defer cancel()
97+ if err := syncfile.Wait(ctx, done); err != nil {
98+ t.Fatal(err)
99+ }
100+}
101+
102+// An unchanged wl_shm window is not copied again; a new content is.
103+func TestRendererSHMCopiedOncePerContent(t *testing.T) {
104+ r, err := New(32, 32)
105+ if err != nil {
106+ t.Skipf("Vulkan unavailable: %v", err)
107+ }
108+ defer r.Close()
109+ c := solidContent(t, 16, 16, color.RGBA{1, 2, 3, 255})
110+ c.ID, c.Seq = 1, 1
111+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}}
112+ contents := map[ports.WindowID]ports.SurfaceContent{1: c}
113+ render := func() {
114+ t.Helper()
115+ done, err := r.Render(scene, contents)
116+ if done != nil {
117+ done.Close()
118+ }
119+ if err != nil {
120+ t.Fatal(err)
121+ }
122+ }
123+ render()
124+ if r.copied != 16*16*4 {
125+ t.Fatalf("first frame copied %d", r.copied)
126+ }
127+ for range 3 {
128+ render()
129+ }
130+ if r.copied != 16*16*4 {
131+ t.Fatalf("unchanged content copied again: %d", r.copied)
132+ }
133+ c.Seq = 2
134+ contents[1] = c
135+ render()
136+ if r.copied != 2*16*16*4 {
137+ t.Fatalf("new content copied %d", r.copied)
138+ }
139+}
140+
141+// render draws a frame and closes its fence.
142+func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
143+ done, err := r.Render(s, contents)
144+ if done != nil {
145+ done.Close()
146+ }
147+ return err
148+}
149+
150+// A wl_shm window's GPU buffer is rewritten only after every frame that
151+// read it completed: contents 1, 2, 3 back to back alternate two buffers,
152+// and the third copy waits for the frame that drew the first (the slot
153+// ring and shmCopyFor's waitFrame both guarantee it).
154+func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) {
155+ r, err := New(32, 32)
156+ if err != nil {
157+ t.Skipf("Vulkan unavailable: %v", err)
158+ }
159+ defer r.Close()
160+ if !r.syncFD {
161+ t.Skip("frames are finished on return without sync files")
162+ }
163+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}}
164+ var readers []uint64
165+ for seq := uint64(1); seq <= 3; seq++ {
166+ c := solidContent(t, 16, 16, color.RGBA{uint8(seq * 60), 0, 0, 255})
167+ c.ID, c.Seq = 1, seq
168+ if seq == 3 {
169+ // The buffer about to be reused was last read by frame 1.
170+ if r.completed >= readers[0] {
171+ t.Fatalf("frame %d already known done before the reuse", readers[0])
172+ }
173+ }
174+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
175+ t.Fatal(err)
176+ }
177+ readers = append(readers, r.frame)
178+ if seq == 3 && r.completed < readers[0] {
179+ t.Fatalf("buffer of frame %d rewritten with completed=%d", readers[0], r.completed)
180+ }
181+ }
182+ if got := r.Pixels().RGBAAt(4, 4); got != (color.RGBA{180, 0, 0, 255}) {
183+ t.Fatalf("pixel %v", got)
184+ }
185+}
186+
187+// A content's explicit acquire fence becomes a wait semaphore; a file
188+// that is not a sync file is refused (no semaphore) without failing the
189+// frame, and the caller's file stays open.
190+func TestRendererImportsAcquireFence(t *testing.T) {
191+ r, err := New(8, 8)
192+ if err != nil {
193+ t.Skipf("Vulkan unavailable: %v", err)
194+ }
195+ defer r.Close()
196+ done, err := r.Render(ports.Scene{Background: "#000000"}, nil)
197+ if err != nil || done == nil {
198+ t.Skipf("no sync file: %v", err)
199+ }
200+ defer done.Close()
201+ sem := r.importFence(done)
202+ if sem == 0 {
203+ t.Fatal("sync file not imported")
204+ }
205+ r.dd.DestroySemaphore(r.device, sem, nil)
206+ if _, err := done.Stat(); err != nil {
207+ t.Fatal("caller's fence closed")
208+ }
209+ f, w, _ := os.Pipe()
210+ defer f.Close()
211+ defer w.Close()
212+ if sem := r.importFence(f); sem != 0 {
213+ t.Fatal("pipe imported as a fence")
214+ }
215+}
A internal/adapters/vulkan/cursor.go +149 -0
@@ -0,0 +1,149 @@
1+package vulkan
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "os"
7+ "unsafe"
8+
9+ "github.com/bnema/nefertty/internal/ports"
10+ vk "github.com/bnema/purego-vulkan/vulkan"
11+)
12+
13+// Cursor images for the display's cursor plane: two linear B8G8R8A8
14+// images in host-visible memory, exported as dmabufs. The CPU writes a
15+// cursor image only when the cursor changes, never per frame.
16+
17+type cursorImage struct {
18+ image vk.Image
19+ memory vk.DeviceMemory
20+ mapped unsafe.Pointer
21+ pitch int
22+ size int
23+}
24+
25+// fourccARGB is DRM_FORMAT_ARGB8888: B8G8R8A8 in memory.
26+const fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
27+
28+// CursorBuffers allocates the two cursor images, replacing earlier ones.
29+func (r *Renderer) CursorBuffers(size int) ([2]ports.DMABuf, error) {
30+ var out [2]ports.DMABuf
31+ r.dropCursors()
32+ if r.dd.GetMemoryFdKHR == nil || len(r.dmabuf.Formats) == 0 || size <= 0 || size > 512 {
33+ return out, errors.New("device cannot export cursor images")
34+ }
35+ for i := range r.cursors {
36+ c, buf, err := r.cursorImage(size)
37+ if err != nil {
38+ r.dropCursors()
39+ for _, b := range out[:i] {
40+ b.Planes[0].File.Close()
41+ }
42+ return [2]ports.DMABuf{}, err
43+ }
44+ r.cursors[i], out[i] = c, buf
45+ }
46+ return out, nil
47+}
48+
49+func (r *Renderer) cursorImage(size int) (*cursorImage, ports.DMABuf, error) {
50+ d := r.dd
51+ c := &cursorImage{size: size}
52+ ok := false
53+ defer func() {
54+ if !ok {
55+ r.freeCursor(c)
56+ }
57+ }()
58+ // Exported images take an explicit DRM modifier: linear, as cursor
59+ // planes scan out. The GPU never touches them (the CPU writes, KMS reads).
60+ linear := uint64(0)
61+ list := vk.ImageDrmFormatModifierListCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierListCreateInfoEXT, DrmFormatModifierCount: 1, DrmFormatModifiers: &linear}
62+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&list), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
63+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(size), Height: uint32(size), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
64+ if err := checked("vkCreateImage(cursor)", d.CreateImage(r.device, &ii, nil, &c.image)); err != nil {
65+ return nil, ports.DMABuf{}, err
66+ }
67+ var req vk.MemoryRequirements
68+ d.GetImageMemoryRequirements(r.device, c.image, &req)
69+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
70+ if err != nil {
71+ return nil, ports.DMABuf{}, err
72+ }
73+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: c.image}
74+ export := vk.ExportMemoryAllocateInfo{SType: vk.StructureTypeExportMemoryAllocateInfo, Next: unsafe.Pointer(&dedicated), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
75+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&export), AllocationSize: req.Size, MemoryTypeIndex: kind}
76+ if err := checked("vkAllocateMemory(cursor)", d.AllocateMemory(r.device, &alloc, nil, &c.memory)); err != nil {
77+ return nil, ports.DMABuf{}, err
78+ }
79+ if err := checked("vkBindImageMemory(cursor)", d.BindImageMemory(r.device, c.image, c.memory, 0)); err != nil {
80+ return nil, ports.DMABuf{}, err
81+ }
82+ var layout vk.SubresourceLayout
83+ sub := vk.ImageSubresource{AspectMask: vk.ImageAspectMemoryPlane0BitEXT}
84+ d.GetImageSubresourceLayout(r.device, c.image, &sub, &layout)
85+ c.pitch = int(layout.RowPitch)
86+ // Cursor planes scan out packed rows from the start of the buffer.
87+ if c.pitch != size*4 || layout.Offset != 0 {
88+ return nil, ports.DMABuf{}, fmt.Errorf("cursor image layout pitch %d offset %d, want pitch %d offset 0", c.pitch, layout.Offset, size*4)
89+ }
90+ if err := checked("vkMapMemory(cursor)", d.MapMemory(r.device, c.memory, 0, vk.DeviceSize(req.Size), 0, &c.mapped)); err != nil {
91+ return nil, ports.DMABuf{}, err
92+ }
93+ c.mapped = unsafe.Add(c.mapped, layout.Offset)
94+ clear(unsafe.Slice((*byte)(c.mapped), c.pitch*size))
95+ var fd int32
96+ get := vk.MemoryGetFdInfoKHR{SType: vk.StructureTypeMemoryGetFDInfoKHR, Memory: c.memory, HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
97+ if err := checked("vkGetMemoryFdKHR(cursor)", d.GetMemoryFdKHR(r.device, &get, &fd)); err != nil {
98+ return nil, ports.DMABuf{}, err
99+ }
100+ f := os.NewFile(uintptr(fd), "nefertty-cursor")
101+ if f == nil {
102+ return nil, ports.DMABuf{}, fmt.Errorf("invalid exported fd %d", fd)
103+ }
104+ ok = true
105+ return c, ports.DMABuf{Width: size, Height: size, Format: fourccARGB, Modifier: 0, Planes: []ports.DMABufPlane{{File: f, Offset: uint32(layout.Offset), Stride: uint32(layout.RowPitch)}}}, nil
106+}
107+
108+// WriteCursor copies premultiplied ARGB8888 pixels (w×h, w*4 per row)
109+// into cursor image i at its row pitch, cropped and cleared around.
110+func (r *Renderer) WriteCursor(i int, pixels []byte, w, h int) error {
111+ if i < 0 || i >= len(r.cursors) || r.cursors[i] == nil {
112+ return errors.New("no cursor image")
113+ }
114+ if w < 0 || h < 0 || len(pixels) < w*h*4 {
115+ return errors.New("short cursor image")
116+ }
117+ c := r.cursors[i]
118+ mem := unsafe.Slice((*byte)(c.mapped), c.pitch*c.size)
119+ clear(mem)
120+ cw := min(w, c.size) * 4
121+ for y := range min(h, c.size) {
122+ copy(mem[y*c.pitch:y*c.pitch+cw], pixels[y*w*4:y*w*4+cw])
123+ }
124+ return nil
125+}
126+
127+func (r *Renderer) freeCursor(c *cursorImage) {
128+ if c.mapped != nil {
129+ r.dd.UnmapMemory(r.device, c.memory)
130+ c.mapped = nil
131+ }
132+ if c.image != 0 {
133+ r.dd.DestroyImage(r.device, c.image, nil)
134+ c.image = 0
135+ }
136+ if c.memory != 0 {
137+ r.dd.FreeMemory(r.device, c.memory, nil)
138+ c.memory = 0
139+ }
140+}
141+
142+func (r *Renderer) dropCursors() {
143+ for i, c := range r.cursors {
144+ if c != nil {
145+ r.freeCursor(c)
146+ r.cursors[i] = nil
147+ }
148+ }
149+}
A internal/adapters/vulkan/cursor_test.go +48 -0
@@ -0,0 +1,48 @@
1+package vulkan
2+
3+import (
4+ "testing"
5+ "unsafe"
6+)
7+
8+// Cursor images are packed (pitch = size*4), exported, and WriteCursor
9+// crops to the image and clears around the pixels.
10+func TestCursorBuffersWriteCursor(t *testing.T) {
11+ r, err := New(64, 48)
12+ if err != nil {
13+ t.Skipf("Vulkan unavailable: %v", err)
14+ }
15+ defer r.Close()
16+ bufs, err := r.CursorBuffers(64)
17+ if err != nil {
18+ t.Skipf("no exportable cursor images: %v", err)
19+ }
20+ for _, b := range bufs {
21+ if p := b.Planes[0]; p.Stride != 64*4 || p.Offset != 0 || b.Format != fourccARGB {
22+ t.Fatalf("layout %+v format %#x", p, b.Format)
23+ }
24+ b.Planes[0].File.Close()
25+ }
26+ mem := func() []byte { c := r.cursors[1]; return unsafe.Slice((*byte)(c.mapped), c.pitch*c.size) }
27+ // A first image fills the corner; a second, smaller one must clear it.
28+ big := make([]byte, 100*100*4)
29+ for i := range big {
30+ big[i] = 0xff
31+ }
32+ if err := r.WriteCursor(1, big, 100, 100); err != nil { // cropped to 64
33+ t.Fatal(err)
34+ }
35+ if m := mem(); m[63*256+63*4] != 0xff {
36+ t.Fatal("cropped image not written")
37+ }
38+ if err := r.WriteCursor(1, []byte{1, 2, 3, 4}, 1, 1); err != nil {
39+ t.Fatal(err)
40+ }
41+ m := mem()
42+ if m[0] != 1 || m[3] != 4 || m[4] != 0 || m[256] != 0 || m[63*256+63*4] != 0 {
43+ t.Fatal("image not cleared around the new pixels")
44+ }
45+ if err := r.WriteCursor(1, []byte{1}, 1, 1); err == nil {
46+ t.Fatal("short image accepted")
47+ }
48+}
A internal/adapters/vulkan/damage.go +146 -0
@@ -0,0 +1,146 @@
1+package vulkan
2+
3+import (
4+ "image"
5+ "math"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+)
9+
10+// Damage: an output target keeps what it holds (scene Seq, each window's
11+// content Seq). When the scene is the same, only the region changed since
12+// then is redrawn: every quad is clipped to it on the CPU and the pass
13+// keeps the rest of the image. A changed scene, a target never drawn or
14+// a history too short redraws everything.
15+
16+// damageRegion collects the region of a target to redraw this frame and
17+// what the target will hold after it: the windows drawn, their content
18+// Seq and rect.
19+type damageRegion struct {
20+ held *target // nil: redraw everything
21+ area image.Rectangle
22+ seen map[ports.WindowID]bool
23+ bound image.Rectangle
24+ drawn map[ports.WindowID]heldWindow
25+}
26+
27+// heldWindow is a window a target holds: its content Seq and rect.
28+type heldWindow struct {
29+ seq uint64
30+ rect image.Rectangle
31+}
32+
33+func newDamage(tg *target, s ports.Scene, bounds image.Rectangle) *damageRegion {
34+ d := &damageRegion{seen: map[ports.WindowID]bool{}, bound: bounds, drawn: map[ports.WindowID]heldWindow{}}
35+ // Seq 0 is a scene core did not number (setup, tests): redraw all.
36+ if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq {
37+ d.held = tg
38+ }
39+ return d
40+}
41+
42+// all reports whether the whole target is redrawn.
43+func (d *damageRegion) all() bool { return d.held == nil }
44+
45+func (d *damageRegion) add(r image.Rectangle) {
46+ if d.held != nil {
47+ d.area = d.area.Union(r.Intersect(d.bound))
48+ }
49+}
50+
51+// window records a window drawn in rect (physical): if its content changed
52+// and no finer damage comes (content), the whole rect is redrawn.
53+func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect image.Rectangle) {
54+ if d.seen[id] {
55+ return
56+ }
57+ d.seen[id] = true
58+ d.drawn[id] = heldWindow{seq: c.Seq, rect: rect}
59+ if d.held == nil {
60+ return
61+ }
62+ hw, ok := d.held.windows[id]
63+ held := hw.seq
64+ if ok && held == c.Seq {
65+ return
66+ }
67+ if !ok || c.Empty() || len(c.Children) > 0 {
68+ d.add(rect)
69+ return
70+ }
71+ rects, known := c.DamageSince(held)
72+ if !known {
73+ d.add(rect)
74+ return
75+ }
76+ // Mapped by content, called next: nothing more here.
77+ _ = rects
78+}
79+
80+// content maps a root surface's buffer damage onto full (where its w×h
81+// buffer is drawn), clipped to dst. Scaled buffers filter linearly: one
82+// more pixel around each rect.
83+func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, dst image.Rectangle) {
84+ if d.held == nil {
85+ return
86+ }
87+ hw, ok := d.held.windows[id]
88+ held := hw.seq
89+ if !ok || held == c.Seq || len(c.Children) > 0 {
90+ return
91+ }
92+ rects, known := c.DamageSince(held)
93+ if !known || c.Width <= 0 || c.Height <= 0 {
94+ return
95+ }
96+ // Linear filtering spreads a texel over half a buffer pixel around
97+ // it: scale/2 target pixels, plus one for rounding.
98+ pad := 0
99+ if full.Dx() != c.Width || full.Dy() != c.Height {
100+ s := max(float64(full.Dx())/float64(c.Width), float64(full.Dy())/float64(c.Height))
101+ pad = int(math.Ceil(s/2)) + 1
102+ }
103+ for _, r := range rects {
104+ x0 := full.Min.X + r.X*full.Dx()/c.Width - pad
105+ y0 := full.Min.Y + r.Y*full.Dy()/c.Height - pad
106+ x1 := full.Min.X + ((r.X+r.W)*full.Dx()+c.Width-1)/c.Width + pad
107+ y1 := full.Min.Y + ((r.Y+r.H)*full.Dy()+c.Height-1)/c.Height + pad
108+ d.add(image.Rect(x0, y0, x1, y1).Intersect(dst))
109+ }
110+}
111+
112+// finish adds the rects of windows the target holds that this frame does
113+// not draw (e.g. a window that moved to an overlay plane and back).
114+func (d *damageRegion) finish() {
115+ if d.held == nil {
116+ return
117+ }
118+ for id, hw := range d.held.windows {
119+ if _, ok := d.drawn[id]; !ok {
120+ d.add(hw.rect)
121+ }
122+ }
123+}
124+
125+// clip limits the draws to the region; draws outside it are dropped.
126+func (d *damageRegion) clip(ds []draw) []draw {
127+ if d.held == nil {
128+ return ds
129+ }
130+ out := ds[:0]
131+ for _, dr := range ds {
132+ r := image.Rect(int(dr.pc.rect[0]), int(dr.pc.rect[1]), int(dr.pc.rect[2]), int(dr.pc.rect[3])).Intersect(d.area)
133+ if r.Empty() {
134+ continue
135+ }
136+ dr.pc.rect = [4]int32{int32(r.Min.X), int32(r.Min.Y), int32(r.Max.X), int32(r.Max.Y)}
137+ out = append(out, dr)
138+ }
139+ return out
140+}
141+
142+// hold records what the target holds after this frame: only the windows
143+// it drew.
144+func (tg *target) hold(s ports.Scene, d *damageRegion) {
145+ tg.valid, tg.sceneSeq, tg.windows = true, s.Seq, d.drawn
146+}
A internal/adapters/vulkan/damage_test.go +136 -0
@@ -0,0 +1,136 @@
1+package vulkan
2+
3+import (
4+ "bytes"
5+ "image/color"
6+ "os"
7+ "testing"
8+
9+ "github.com/bnema/nefertty/internal/ports"
10+)
11+
12+// damagedContent writes px into the content's pool (the client drawing
13+// into the same buffer) and records the damage of the new Seq.
14+func damagedContent(t *testing.T, f *os.File, base ports.SurfaceContent, seq uint64, px []byte, hist []ports.SeqDamage) ports.SurfaceContent {
15+ t.Helper()
16+ if _, err := f.WriteAt(px, 0); err != nil {
17+ t.Fatal(err)
18+ }
19+ c := base
20+ c.Seq, c.DamageHistory = seq, hist
21+ return c
22+}
23+
24+// Partial redraws are pixel-identical to full ones: with both targets
25+// ages (A then B, then one render) and with shm double buffers
26+// (render, skip, render).
27+func TestRendererDamageMatchesFullRedraw(t *testing.T) {
28+ const w, h = 32, 32
29+ newR := func() *Renderer {
30+ r, err := New(64, 48)
31+ if err != nil {
32+ t.Skipf("Vulkan unavailable: %v", err)
33+ }
34+ t.Cleanup(r.Close)
35+ return r
36+ }
37+ damaged, full := newR(), newR()
38+ bufsD, err := damaged.ExportTargets(2, nil)
39+ if err != nil {
40+ t.Skipf("no exportable targets: %v", err)
41+ }
42+ for _, b := range bufsD {
43+ b.Planes[0].File.Close()
44+ }
45+ px := func(c color.RGBA, x0, y0, x1, y1 int, into []byte) []byte {
46+ for y := y0; y < y1; y++ {
47+ for x := x0; x < x1; x++ {
48+ copy(into[(y*w+x)*4:], []byte{c.B, c.G, c.R, 255})
49+ }
50+ }
51+ return into
52+ }
53+ pixels := px(color.RGBA{10, 20, 30, 255}, 0, 0, w, h, make([]byte, w*h*4))
54+ base := shmContent(t, w, h, w*4, pixels)
55+ base.ID = 1
56+ f := base.SHM.File
57+ scene := ports.Scene{Seq: 5, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: w, H: h}, Borderless: true}}}
58+ hist := []ports.SeqDamage{{Seq: 1, Full: true}}
59+ c := damagedContent(t, f, *base, 1, pixels, hist)
60+ step := func(i int, c ports.SurfaceContent) {
61+ t.Helper()
62+ damaged.UseTarget(i % 2)
63+ contents := map[ports.WindowID]ports.SurfaceContent{1: c}
64+ if err := render(damaged, scene, contents); err != nil {
65+ t.Fatal(err)
66+ }
67+ if err := render(full, ports.Scene{Background: scene.Background, Windows: scene.Windows}, contents); err != nil {
68+ t.Fatal(err)
69+ }
70+ if a, b := damaged.Pixels(), full.Pixels(); !bytes.Equal(a.Pix, b.Pix) {
71+ t.Fatalf("frame %d differs from a full redraw", i)
72+ }
73+ }
74+ step(0, c)
75+ step(1, c) // target 1 drawn whole once
76+ before := damaged.redrawn
77+ // A: a red square; B: a green one elsewhere. Target 0 holds Seq 1,
78+ // so its next frame redraws the union of both.
79+ for i, d := range []struct {
80+ col color.RGBA
81+ rect ports.Rect
82+ }{{color.RGBA{255, 0, 0, 255}, ports.Rect{X: 2, Y: 2, W: 4, H: 4}}, {color.RGBA{0, 255, 0, 255}, ports.Rect{X: 20, Y: 10, W: 3, H: 5}}} {
83+ seq := uint64(2 + i)
84+ px(d.col, d.rect.X, d.rect.Y, d.rect.X+d.rect.W, d.rect.Y+d.rect.H, pixels)
85+ hist = append(hist, ports.SeqDamage{Seq: seq, Rects: []ports.Rect{d.rect}})
86+ c = damagedContent(t, f, *base, seq, pixels, hist)
87+ }
88+ step(2, c)
89+ if got := damaged.redrawn - before; got >= 64*48 {
90+ t.Fatalf("partial frame redrew %d pixels", got)
91+ }
92+ // Render, skip (no frame), render: target 1 is two contents behind.
93+ px(color.RGBA{0, 0, 255, 255}, 0, 30, 2, 32, pixels)
94+ hist = append(hist, ports.SeqDamage{Seq: 4, Rects: []ports.Rect{{Y: 30, W: 2, H: 2}}})
95+ c = damagedContent(t, f, *base, 4, pixels, hist)
96+ step(3, c)
97+ step(4, c)
98+}
99+
100+// A window left out of a frame (on an overlay plane) and drawn again in
101+// the same scene is redrawn whole; one held but no longer drawn is
102+// cleared.
103+func TestRendererDamageWindowLeftOutAndBack(t *testing.T) {
104+ newR := func() *Renderer {
105+ r, err := New(64, 48)
106+ if err != nil {
107+ t.Skipf("Vulkan unavailable: %v", err)
108+ }
109+ t.Cleanup(r.Close)
110+ return r
111+ }
112+ damaged, full := newR(), newR()
113+ c := solidContent(t, 16, 16, color.RGBA{200, 10, 10, 255})
114+ c.ID, c.Seq = 1, 1
115+ contents := map[ports.WindowID]ports.SurfaceContent{1: c}
116+ with := ports.Scene{Seq: 9, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 16, H: 16}, Borderless: true}}}
117+ without := with
118+ without.Windows = nil
119+ check := func(s ports.Scene, what string) {
120+ t.Helper()
121+ if err := render(damaged, s, contents); err != nil {
122+ t.Fatal(err)
123+ }
124+ plain := s
125+ plain.Seq = 0
126+ if err := render(full, plain, contents); err != nil {
127+ t.Fatal(err)
128+ }
129+ if !bytes.Equal(damaged.Pixels().Pix, full.Pixels().Pix) {
130+ t.Fatalf("%s differs from a full redraw", what)
131+ }
132+ }
133+ check(without, "overlay frame")
134+ check(with, "overlay refused, window composed")
135+ check(without, "window on the overlay again")
136+}
M internal/adapters/vulkan/dmabuf.go +47 -14
@@ -13,7 +13,7 @@ import (
1313
1414 // dmabuf import (linux-dmabuf clients): each client buffer becomes a
1515 // VkImage bound to its imported memory, cached by buffer ID while the
16-// client keeps attaching it, and blitted into the output image. Sync is
16+// client keeps attaching it, and sampled by the compose shader. Sync is
1717 // implicit: before sampling, the renderer waits on the fences the client's
1818 // driver attached to the buffer (DMA_BUF_IOCTL_EXPORT_SYNC_FILE), imported
1919 // as a semaphore.
@@ -86,7 +86,7 @@ func cstring(b []byte) string {
8686 }
8787
8888 // probeDMABuf lists the fourcc/modifier pairs the device imports as
89-// single-plane blit sources, and the render node.
89+// single-plane sampled images with linear filtering, and the render node.
9090 func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
9191 var sup ports.DMABufSupport
9292 drm := vk.PhysicalDeviceDrmPropertiesEXT{SType: vk.StructureTypePhysicalDeviceDRMPropertiesEXT}
@@ -107,8 +107,9 @@ func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
107107 r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp)
108108 for _, m := range mods[:list.DrmFormatModifierCount] {
109109 // Multi-plane modifiers (compression metadata) are left to a
110- // later step; the blit needs a source.
111- if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&vk.FormatFeatureBlitSrcBit == 0 {
110+ // later step.
111+ need := vk.FormatFeatureFlags(vk.FormatFeatureSampledImageBit | vk.FormatFeatureSampledImageFilterLinearBit)
112+ if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&need != need {
112113 continue
113114 }
114115 if r.importable(physical, f.format, m.DrmFormatModifier) {
@@ -124,7 +125,7 @@ func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
124125 func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
125126 mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
126127 ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
127- info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit}
128+ info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit}
128129 extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
129130 out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
130131 if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
@@ -136,10 +137,14 @@ func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modi
136137 // DMABuf reports the formats this renderer imports.
137138 func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf }
138139
139-// imported is a client dmabuf bound to a VkImage.
140+// imported is a client dmabuf bound to a VkImage, with the view and set
141+// the compose shader samples it through.
140142 type imported struct {
141143 image vk.Image
142144 memory vk.DeviceMemory
145+ view vk.ImageView
146+ pool vk.DescriptorPool
147+ set vk.DescriptorSet
143148 fd int // our duplicate of plane 0, for implicit sync
144149 // last is the frame that drew it, for eviction.
145150 last uint64
@@ -177,7 +182,7 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
177182 layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)}
178183 explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout}
179184 external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
180- ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(b.Width), Height: uint32(b.Height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
185+ ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(b.Width), Height: uint32(b.Height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
181186 if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil {
182187 return nil, err
183188 }
@@ -206,6 +211,12 @@ func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
206211 if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil {
207212 return nil, err
208213 }
214+ if im.view, err = r.imageView(im.image, format); err != nil {
215+ return nil, err
216+ }
217+ if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer); err != nil {
218+ return nil, err
219+ }
209220 ok = true
210221 r.imports[b.ID] = im
211222 return im, nil
@@ -229,6 +240,12 @@ func dupFile(f *os.File) (int, error) {
229240 }
230241
231242 func (r *Renderer) release(im *imported) {
243+ if im.pool != 0 {
244+ r.dd.DestroyDescriptorPool(r.device, im.pool, nil)
245+ }
246+ if im.view != 0 {
247+ r.dd.DestroyImageView(r.device, im.view, nil)
248+ }
232249 if im.image != 0 {
233250 r.dd.DestroyImage(r.device, im.image, nil)
234251 }
@@ -240,13 +257,13 @@ func (r *Renderer) release(im *imported) {
240257 }
241258 }
242259
243-// dropUnused frees imports not drawn for importTTL frames: the client moved
244-// on to other buffers or went away. Called after a frame completes.
260+// dropUnused frees imports not drawn for importTTL frames (the client
261+// moved on to other buffers or went away), once the last frame that drew
262+// them completed.
245263 func (r *Renderer) dropUnused() {
246- r.frame++
247264 for id, im := range r.imports {
248265 if r.frame-im.last > importTTL {
249- r.release(im)
266+ r.retire(im.last, func() { r.release(im) })
250267 delete(r.imports, id)
251268 }
252269 }
@@ -270,15 +287,31 @@ func (r *Renderer) readFence(im *imported) vk.Semaphore {
270287 if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 {
271288 return 0
272289 }
290+ return r.importSyncFD(int(arg.fd))
291+}
292+
293+// importFence makes a wait semaphore of a sync file. The file stays the
294+// caller's: Vulkan takes a duplicate, made while the file is held open.
295+func (r *Renderer) importFence(f *os.File) vk.Semaphore {
296+ fd, err := dupFile(f)
297+ if err != nil {
298+ return 0
299+ }
300+ return r.importSyncFD(fd)
301+}
302+
303+// importSyncFD imports a sync file fd as a temporary semaphore payload;
304+// the fd is Vulkan's on success and closed on failure.
305+func (r *Renderer) importSyncFD(fd int) vk.Semaphore {
273306 var sem vk.Semaphore
274307 si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
275308 if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success {
276- unix.Close(int(arg.fd))
309+ unix.Close(fd)
277310 return 0
278311 }
279- imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: arg.fd}
312+ imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: int32(fd)}
280313 if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success {
281- unix.Close(int(arg.fd))
314+ unix.Close(fd)
282315 r.dd.DestroySemaphore(r.device, sem, nil)
283316 return 0
284317 }
M internal/adapters/vulkan/dmabuf_test.go +31 -2
@@ -1,6 +1,7 @@
11 package vulkan
22
33 import (
4+ "image"
45 "image/color"
56 "os"
67 "slices"
@@ -81,7 +82,7 @@ func TestRendererImportsDMABuf(t *testing.T) {
8182 buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
8283 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}
8384 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}}
84- if err := r.Render(scene, contents); err != nil {
85+ if err := render(r, scene, contents); err != nil {
8586 t.Fatal(err)
8687 }
8788 px := r.Pixels()
@@ -98,7 +99,7 @@ func TestRendererImportsDMABuf(t *testing.T) {
9899 }
99100 // Frames without the buffer: its import is released after importTTL.
100101 for range importTTL + 1 {
101- if err := r.Render(scene, nil); err != nil {
102+ if err := render(r, scene, nil); err != nil {
102103 t.Fatal(err)
103104 }
104105 }
@@ -106,3 +107,31 @@ func TestRendererImportsDMABuf(t *testing.T) {
106107 t.Fatalf("import kept: %d", len(r.imports))
107108 }
108109 }
110+
111+// A subsurface dmabuf is read after its own acquire fence, not the root's.
112+func TestRendererSubsurfaceAcquire(t *testing.T) {
113+ r, err := New(80, 32)
114+ if err != nil {
115+ t.Skipf("Vulkan unavailable: %v", err)
116+ }
117+ defer r.Close()
118+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
119+ if !slices.Contains(r.DMABuf().Formats, linear) {
120+ t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf())
121+ }
122+ f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} })
123+ fence, w, _ := os.Pipe()
124+ defer fence.Close()
125+ defer w.Close()
126+ child := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, Acquire: fence,
127+ DMABuf: &ports.DMABuf{ID: 8, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}}
128+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
129+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, Children: []ports.Subsurface{{SurfaceContent: child}}}}
130+ ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 80, 32)))
131+ for _, d := range ds {
132+ if d.im != nil && d.acquire == fence {
133+ return
134+ }
135+ }
136+ t.Fatal("subsurface draw has no acquire fence")
137+}
A internal/adapters/vulkan/frame.go +365 -0
@@ -0,0 +1,365 @@
1+package vulkan
2+
3+import (
4+ "fmt"
5+ "image"
6+ "math"
7+ "os"
8+ "runtime"
9+ "slices"
10+ "unsafe"
11+
12+ "github.com/bnema/nefertty/internal/ports"
13+ vk "github.com/bnema/purego-vulkan/vulkan"
14+)
15+
16+// Frames are recorded into a ring of slots, one per frame in flight. The
17+// CPU never waits for a frame it just submitted: Render returns a sync
18+// file the display waits on (KMS IN_FENCE_FD). A slot's fence is waited
19+// only when the slot is reused, and objects a frame read (imports, GPU
20+// buffers, client semaphores) are freed only once that frame completed.
21+
22+// frameSlots is how many frames may be in flight: one per output target.
23+const frameSlots = 2
24+
25+// frameSlot is the command buffer and sync objects of one frame in flight.
26+type frameSlot struct {
27+ cmd vk.CommandBuffer
28+ fence vk.Fence
29+ // done is signalled with the frame and exported as its sync file.
30+ done vk.Semaphore
31+ frame uint64 // the frame recorded last, 0 before any
32+ busy bool // submitted, fence not yet seen signalled
33+}
34+
35+// deferredFree runs once frame completed.
36+type deferredFree struct {
37+ frame uint64
38+ free func()
39+}
40+
41+func (r *Renderer) createSlots() error {
42+ d := r.dd
43+ cmds := make([]vk.CommandBuffer, frameSlots)
44+ ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: frameSlots}
45+ if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmds[0])); err != nil {
46+ return err
47+ }
48+ for i := range r.slots {
49+ s := &r.slots[i]
50+ s.cmd = cmds[i]
51+ fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
52+ if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil {
53+ return err
54+ }
55+ if err := r.newDone(s); err != nil {
56+ return err
57+ }
58+ }
59+ return nil
60+}
61+
62+// newDone creates a slot's frame semaphore, exportable as a sync file when
63+// the device allows it; a device that refuses falls back to waiting for
64+// frames on the CPU (syncFD off).
65+func (r *Renderer) newDone(s *frameSlot) error {
66+ d := r.dd
67+ si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
68+ export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
69+ if r.syncFD {
70+ si.Next = unsafe.Pointer(&export)
71+ if d.CreateSemaphore(r.device, &si, nil, &s.done) == vk.Success {
72+ return nil
73+ }
74+ r.syncFD = false
75+ si.Next = nil
76+ }
77+ return checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done))
78+}
79+
80+func (r *Renderer) destroySlots() {
81+ d := r.dd
82+ for i := range r.slots {
83+ s := &r.slots[i]
84+ if s.fence != 0 {
85+ d.DestroyFence(r.device, s.fence, nil)
86+ }
87+ if s.done != 0 {
88+ d.DestroySemaphore(r.device, s.done, nil)
89+ }
90+ if s.cmd != 0 {
91+ d.FreeCommandBuffers(r.device, r.pool, 1, &s.cmd)
92+ }
93+ *s = frameSlot{}
94+ }
95+}
96+
97+// waitFrame blocks until frame n completed. Frames not submitted yet and
98+// frames known done return at once. Every slot holding a frame up to n
99+// is waited and reset, so its fence is unsignalled before reuse.
100+func (r *Renderer) waitFrame(n uint64) error {
101+ n = min(n, r.submitted)
102+ if n <= r.completed {
103+ return nil
104+ }
105+ for i := range r.slots {
106+ s := &r.slots[i]
107+ if !s.busy || s.frame > n {
108+ continue
109+ }
110+ if err := checked("vkWaitForFences", r.dd.WaitForFences(r.device, 1, &s.fence, 1, math.MaxUint64)); err != nil {
111+ return err
112+ }
113+ if err := checked("vkResetFences", r.dd.ResetFences(r.device, 1, &s.fence)); err != nil {
114+ return err
115+ }
116+ s.busy = false
117+ }
118+ r.completed = n
119+ r.runDeferred()
120+ return nil
121+}
122+
123+// retire frees an object once the frame that last used it completed.
124+func (r *Renderer) retire(frame uint64, free func()) {
125+ if frame <= r.completed {
126+ free()
127+ return
128+ }
129+ r.deferred = append(r.deferred, deferredFree{frame: frame, free: free})
130+}
131+
132+func (r *Renderer) runDeferred() {
133+ r.deferred = slices.DeleteFunc(r.deferred, func(f deferredFree) bool {
134+ if f.frame <= r.completed {
135+ f.free()
136+ return true
137+ }
138+ return false
139+ })
140+}
141+
142+// idle records that the device finished every submitted frame.
143+func (r *Renderer) idle() {
144+ for i := range r.slots {
145+ s := &r.slots[i]
146+ if s.busy {
147+ _ = r.dd.ResetFences(r.device, 1, &s.fence)
148+ s.busy = false
149+ }
150+ }
151+ // Frames recorded but never submitted have no GPU work either.
152+ r.completed = r.frame
153+ r.runDeferred()
154+}
155+
156+// Render draws a frame into the current target. done is signalled when
157+// the GPU finished it; nil when the device cannot export a sync file,
158+// then the frame is finished on return.
159+func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {
160+ // A frame that fails before its submit keeps its number: objects it
161+ // touched are freed once a later frame completes (never early).
162+ r.frame++
163+ slot := &r.slots[r.frame%frameSlots]
164+ // The slot's previous frame must be done before its command buffer
165+ // is recorded again.
166+ if slot.busy {
167+ if err := r.waitFrame(slot.frame); err != nil {
168+ return nil, err
169+ }
170+ }
171+ tg := r.target()
172+ dmg := newDamage(tg, s, image.Rect(0, 0, r.width, r.height))
173+ ds := r.draws(s, contents, dmg)
174+ dmg.finish()
175+ r.dropPools()
176+ partial := !dmg.all()
177+ if partial {
178+ // The pass keeps the image: repaint the background under the
179+ // region, then everything clipped to it.
180+ ds = dmg.clip(append([]draw{r.fillDraw(dmg.area, parseColor(s.Background))}, ds...))
181+ r.redrawn += dmg.area.Dx() * dmg.area.Dy()
182+ } else {
183+ r.redrawn += r.width * r.height
184+ }
185+ d := r.dd
186+ cmd := slot.cmd
187+ if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil {
188+ return nil, err
189+ }
190+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
191+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
192+ return nil, err
193+ }
194+ // A full redraw clears the target: its old contents never matter
195+ // (UNDEFINED). A partial one keeps them.
196+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange}
197+ if partial {
198+ b.OldLayout = tg.layout
199+ b.DstAccessMask |= vk.AccessColorAttachmentReadBit
200+ }
201+ if tg.exported {
202+ // Acquire from the display (foreign queue family).
203+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
204+ }
205+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
206+ // Client buffers come from the foreign queue family (their driver)
207+ // and go back to it after the frame, so the next frame acquires them.
208+ var dmas []*imported
209+ acquires := map[*imported]*os.File{}
210+ for _, dr := range ds {
211+ if dr.im != nil && !slices.Contains(dmas, dr.im) {
212+ dmas = append(dmas, dr.im)
213+ dr.im.last = r.frame
214+ }
215+ if dr.acquire != nil {
216+ acquires[dr.im] = dr.acquire
217+ }
218+ }
219+ r.ownership(cmd, dmas, true)
220+ r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial)
221+ r.ownership(cmd, dmas, false)
222+ b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal
223+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored
224+ if tg.exported {
225+ // Release to the display: KMS scans it out after the fence.
226+ b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0
227+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
228+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
229+ } else {
230+ b.NewLayout, b.DstAccessMask = vk.ImageLayoutTransferSrcOptimal, vk.AccessTransferReadBit
231+ d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
232+ }
233+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
234+ return nil, err
235+ }
236+ // Wait for the client's GPU writes to each buffer: its explicit
237+ // acquire fence, else the buffer's implicit fences.
238+ var waits []vk.Semaphore
239+ for _, im := range dmas {
240+ sem := vk.Semaphore(0)
241+ if f := acquires[im]; f != nil {
242+ sem = r.importFence(f)
243+ } else {
244+ sem = r.readFence(im)
245+ }
246+ if sem != 0 {
247+ waits = append(waits, sem)
248+ }
249+ }
250+ frame := r.frame
251+ for _, sem := range waits {
252+ r.retire(frame, func() { d.DestroySemaphore(r.device, sem, nil) })
253+ }
254+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
255+ stages := make([]vk.PipelineStageFlags, len(waits))
256+ if len(waits) > 0 {
257+ for i := range stages {
258+ stages[i] = vk.PipelineStageFragmentShaderBit
259+ }
260+ submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
261+ }
262+ if r.syncFD {
263+ submit.SignalSemaphoreCount, submit.SignalSemaphores = 1, &slot.done
264+ }
265+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, slot.fence)); err != nil {
266+ return nil, err
267+ }
268+ runtime.KeepAlive(stages)
269+ runtime.KeepAlive(waits)
270+ slot.frame, slot.busy, r.submitted = frame, true, frame
271+ tg.layout = b.NewLayout
272+ tg.hold(s, dmg)
273+ r.last, r.readBack = tg, false
274+ r.dropUnused()
275+ r.dropShm()
276+ if !r.syncFD {
277+ return nil, r.waitFrame(frame)
278+ }
279+ var fd int32
280+ get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
281+ if d.GetSemaphoreFdKHR(r.device, &get, &fd) != vk.Success {
282+ // The frame is drawn: finish it on the CPU. The semaphore stays
283+ // signalled: replace it, so the slot's next submit signals a
284+ // fresh one.
285+ if err := r.waitFrame(frame); err != nil {
286+ return nil, err
287+ }
288+ d.DestroySemaphore(r.device, slot.done, nil)
289+ slot.done = 0
290+ return nil, r.newDone(slot)
291+ }
292+ if fd < 0 {
293+ // Already signalled: the driver may return -1.
294+ return nil, r.waitFrame(frame)
295+ }
296+ f := os.NewFile(uintptr(fd), "nefertty-frame")
297+ if f == nil {
298+ return nil, fmt.Errorf("invalid frame sync file %d", fd)
299+ }
300+ return f, nil
301+}
302+
303+// ownership moves client images between their driver (foreign queue
304+// family, GENERAL: their contents survive) and the fragment shader.
305+func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) {
306+ for _, im := range dmas {
307+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange}
308+ if acquire {
309+ b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal
310+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
311+ b.DstAccessMask = vk.AccessShaderReadBit
312+ // The frame waits on the client's fence at the fragment
313+ // stage: the transfer must come after that wait.
314+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
315+ } else {
316+ b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral
317+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
318+ b.SrcAccessMask = vk.AccessShaderReadBit
319+ r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
320+ }
321+ }
322+}
323+
324+// oneShot records and runs commands outside the frame ring and waits for
325+// them (setup and screenshots only, never per frame).
326+func (r *Renderer) oneShot(record func(vk.CommandBuffer)) error {
327+ d := r.dd
328+ var cmd vk.CommandBuffer
329+ ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}
330+ if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmd)); err != nil {
331+ return err
332+ }
333+ defer d.FreeCommandBuffers(r.device, r.pool, 1, &cmd)
334+ var fence vk.Fence
335+ fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
336+ if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &fence)); err != nil {
337+ return err
338+ }
339+ defer d.DestroyFence(r.device, fence, nil)
340+ begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
341+ if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
342+ return err
343+ }
344+ record(cmd)
345+ if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
346+ return err
347+ }
348+ submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
349+ if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, fence)); err != nil {
350+ return err
351+ }
352+ return checked("vkWaitForFences", d.WaitForFences(r.device, 1, &fence, 1, math.MaxUint64))
353+}
354+
355+// Clear draws a background-only frame and waits for it.
356+func (r *Renderer) Clear(rgb [3]uint8) error {
357+ done, err := r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
358+ if done != nil {
359+ done.Close()
360+ }
361+ if err != nil {
362+ return err
363+ }
364+ return r.waitFrame(r.frame)
365+}
M internal/adapters/vulkan/renderer.go +114 -469
@@ -6,7 +6,6 @@ import (
66 "math"
77 "os"
88 "runtime"
9- "slices"
109 "strconv"
1110 "unsafe"
1211
@@ -30,42 +29,44 @@ type Renderer struct {
3029 current int
3130 renderMods []uint64
3231 // last is the target of the last frame; readback copies it into
33- // buffer only when Pixels or CopyBGRX asks (stale until then).
34- last *target
35- readBack bool
36- buffer vk.Buffer
37- bufferMemory vk.DeviceMemory
38- staging vk.Buffer
39- stagingMemory vk.DeviceMemory
40- stagingMapped unsafe.Pointer
41- stagingSize int
42- mapped unsafe.Pointer
43- pool vk.CommandPool
44- command vk.CommandBuffer
45- fence vk.Fence
46- memory vk.PhysicalDeviceMemoryProperties
47- family uint32
32+ // buffer only when Pixels asks (stale until then).
33+ last *target
34+ readBack bool
35+ buffer vk.Buffer
36+ bufferMemory vk.DeviceMemory
37+ mapped unsafe.Pointer
38+ pool vk.CommandPool
39+ memory vk.PhysicalDeviceMemoryProperties
40+ family uint32
41+ // Frames in flight (frame.go): frame is the last frame recorded,
42+ // submitted the last submitted, completed the last known finished.
43+ slots [frameSlots]frameSlot
44+ frame, submitted, completed uint64
45+ deferred []deferredFree
46+ // syncFD: frames export a sync file (else Render waits for them).
47+ syncFD bool
4848 // dmabuf is what clients may send; imports are their buffers by ID.
4949 dmabuf ports.DMABufSupport
5050 imports map[uint64]*imported
5151 // pools are client wl_shm pools mapped by pool ID.
5252 pools map[uint64]*mapping
53- // retired are pool mappings replaced this frame, unmapped after staging.
53+ // retired are pool mappings replaced this frame, unmapped once the
54+ // frame's copies are done.
5455 retired [][]byte
55- frame uint64
56- // fillRegions is reused by Render for the bands of solid fills.
57- fillRegions []vk.BufferImageCopy
58- // blend draws translucent client pixels (blend.go); align is the
59- // device's storage buffer offset alignment.
60- blend blendPipeline
61- align int
62- maxRange int // bytes of blended pixels one frame may bind
56+ // shm are the GPU copies of wl_shm surfaces (shm.go); copied counts
57+ // the bytes copied into them, for tests.
58+ shm map[shmKey]*shmSurface
59+ copied int
60+ // redrawn counts the target pixels drawn, for tests.
61+ redrawn int
62+ // cursors are the exported cursor images (CursorBuffers).
63+ cursors [2]*cursorImage
64+ // compose draws every frame (compose.go); maxRange bounds one
65+ // storage buffer binding.
66+ compose composer
67+ maxRange int
6368 }
6469
65-// fillRows is the height of the strip staged for a solid fill: the CPU
66-// writes it once, the GPU copies it down the rect.
67-const fillRows = 64
68-
6970 func checked(name string, result vk.Result) error {
7071 if err := vk.Check(result); err != nil {
7172 return fmt.Errorf("%s: %w", name, err)
@@ -77,7 +78,7 @@ func New(width, height int) (r *Renderer, err error) {
7778 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
7879 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
7980 }
80- r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}}
81+ r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}, shm: map[shmKey]*shmSurface{}}
8182 defer func() {
8283 if err != nil {
8384 r.Close()
@@ -153,7 +154,6 @@ func New(width, height int) (r *Renderer, err error) {
153154 r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)
154155 var properties vk.PhysicalDeviceProperties
155156 r.id.GetPhysicalDeviceProperties(physical, &properties)
156- r.align = int(max(properties.Limits.MinStorageBufferOffsetAlignment, 4))
157157 r.maxRange = int(properties.Limits.MaxStorageBufferRange)
158158 r.family = family
159159 priority := float32(1)
@@ -187,6 +187,9 @@ func New(width, height int) (r *Renderer, err error) {
187187 r.dmabuf = sup
188188 r.probeRenderModifiers(physical)
189189 }
190+ // VK_KHR_external_semaphore_fd (enabled above): frames export
191+ // their fence as a sync file.
192+ r.syncFD = r.dd.GetSemaphoreFdKHR != nil
190193 }
191194 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
192195 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
@@ -214,7 +217,7 @@ func New(width, height int) (r *Renderer, err error) {
214217 return
215218 }
216219 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)
217- // Readback (screenshots, fallback) reads this buffer: uncached memory is ~100x slower.
220+ // Readback (screenshots) reads this buffer: uncached memory is ~100x slower.
218221 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)
219222 if err != nil {
220223 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
@@ -232,44 +235,17 @@ func New(width, height int) (r *Renderer, err error) {
232235 if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil {
233236 return
234237 }
235- // Start with two frame-sized regions; later frames grow staging on demand.
236- stagingSize := size * 2
237- r.stagingSize = int(stagingSize)
238- stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
239- if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil {
240- return
241- }
242- r.dd.GetBufferMemoryRequirements(r.device, r.staging, &req)
243- kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
244- if err != nil {
245- return
246- }
247- alloc = vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
248- if err = checked("vkAllocateMemory(staging)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.stagingMemory)); err != nil {
249- return
250- }
251- if err = checked("vkBindBufferMemory(staging)", r.dd.BindBufferMemory(r.device, r.staging, r.stagingMemory, 0)); err != nil {
252- return
253- }
254- if err = checked("vkMapMemory(staging)", r.dd.MapMemory(r.device, r.stagingMemory, 0, stagingSize, 0, &r.stagingMapped)); err != nil {
255- return
256- }
257238 pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family}
258239 if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil {
259240 return
260241 }
261- ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}
262- if err = checked("vkAllocateCommandBuffers", r.dd.AllocateCommandBuffers(r.device, &ai, &r.command)); err != nil {
242+ if err = r.createSlots(); err != nil {
263243 return
264244 }
265245 if err = r.createView(&r.own); err != nil {
266246 return
267247 }
268- if err = r.createBlend(); err != nil {
269- return
270- }
271- fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
272- if err = checked("vkCreateFence", r.dd.CreateFence(r.device, &fi, nil, &r.fence)); err != nil {
248+ if err = r.createComposer(); err != nil {
273249 return
274250 }
275251 runtime.KeepAlive(priority)
@@ -323,111 +299,9 @@ func windowColor(id ports.WindowID) [3]uint8 {
323299 return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
324300 }
325301
326-// ensureStaging grows the mapped transfer buffer before recording any commands.
327-func (r *Renderer) ensureStaging(size int) error {
328- if size <= r.stagingSize {
329- return nil
330- }
331- d := r.dd
332- capacity := size
333- info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: stagingUsage, SharingMode: vk.SharingModeExclusive}
334- var buffer vk.Buffer
335- if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil {
336- return err
337- }
338- var memory vk.DeviceMemory
339- defer func() {
340- if buffer != 0 {
341- d.DestroyBuffer(r.device, buffer, nil)
342- }
343- if memory != 0 {
344- d.FreeMemory(r.device, memory, nil)
345- }
346- }()
347- var req vk.MemoryRequirements
348- d.GetBufferMemoryRequirements(r.device, buffer, &req)
349- kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
350- if err != nil {
351- return err
352- }
353- alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
354- if err := checked("vkAllocateMemory(staging)", d.AllocateMemory(r.device, &alloc, nil, &memory)); err != nil {
355- return err
356- }
357- if err := checked("vkBindBufferMemory(staging)", d.BindBufferMemory(r.device, buffer, memory, 0)); err != nil {
358- return err
359- }
360- var mapped unsafe.Pointer
361- if err := checked("vkMapMemory(staging)", d.MapMemory(r.device, memory, 0, vk.DeviceSize(capacity), 0, &mapped)); err != nil {
362- return err
363- }
364- d.UnmapMemory(r.device, r.stagingMemory)
365- d.DestroyBuffer(r.device, r.staging, nil)
366- d.FreeMemory(r.device, r.stagingMemory, nil)
367- r.staging, r.stagingMemory, r.stagingMapped, r.stagingSize = buffer, memory, mapped, capacity
368- buffer, memory = 0, 0
369- return nil
370-}
371-
372-func (r *Renderer) Clear(rgb [3]uint8) error {
373- return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
374-}
375-
376-// upload is one piece of a frame: a solid fill, client pixels staged from
377-// a wl_shm pool, or a dmabuf blitted in place.
378-type upload struct {
379- rect image.Rectangle
380- offset int
381- pixels []byte
382- stride int
383- // dst covers src; they differ in size when the buffer is not drawn at
384- // the physical size (integer-scale clients, rounding).
385- dst, src image.Rectangle
386- color [3]uint8
387- opaque bool
388- // blend draws the staged pixels over the target instead of copying.
389- blend bool
390- // dma is a client GPU buffer, blitted instead of copied from staging.
391- dma *imported
392- // fill is a solid color: staging holds one strip of rows.
393- fill bool
394-}
395-
396-// size is the staging bytes of an upload: a strip for a fill.
397-func (u *upload) size() int {
398- if u.fill {
399- return u.rect.Dx() * min(u.rect.Dy(), fillRows) * 4
400- }
401- return u.rect.Dx() * u.rect.Dy() * 4
402-}
403-
404-// stage copies the visible pixels of a client buffer into dst, resampling
405-// when the buffer is not drawn at its size.
406-func (u *upload) stage(dst []byte) {
407- for row := 0; row < u.rect.Dy(); row++ {
408- line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
409- y := u.rect.Min.Y + row
410- if u.dst.Size() == u.src.Size() {
411- start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
412- copy(line, u.pixels[start:start+len(line)])
413- } else {
414- // Nearest-neighbour resample, sampling pixel centres.
415- sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
416- src := u.pixels[sy*u.stride:]
417- for i := 0; i < u.rect.Dx(); i++ {
418- sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
419- copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
420- }
421- }
422- if u.opaque {
423- for i := 3; i < len(line); i += 4 {
424- line[i] = 255
425- }
426- }
427- }
428-}
429-
430-func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
302+// draws walks the scene into quads in paint order, copying new wl_shm
303+// content into GPU buffers on the way.
304+func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw {
431305 // Scene rects are logical; everything below works in physical pixels.
432306 scale := s.Scale
433307 if scale <= 0 {
@@ -435,21 +309,20 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
435309 }
436310 phys := func(v int) int { return int(math.Round(float64(v) * scale)) }
437311 physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }
438- var uploads []upload
312+ var draws []draw
439313 bounds := image.Rect(0, 0, r.width, r.height)
440314 add := func(rect image.Rectangle, c [3]uint8) {
441315 rect = rect.Intersect(bounds)
442316 if rect.Empty() {
443317 return
444318 }
445- // A fill stages a strip of up to fillRows rows; the GPU copies the
446- // strip down the rect.
447- uploads = append(uploads, upload{rect: rect, color: c, fill: true})
319+ draws = append(draws, r.fillDraw(rect, c))
448320 }
449- // addContent maps src (buffer pixels) onto dst (physical pixels).
450- addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) {
321+ // addContent draws the part of a buffer mapped onto full (physical
322+ // pixels) that dst shows. key names the surface, seq its content.
323+ addContent := func(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
451324 rect := dst.Intersect(bounds)
452- if rect.Empty() || src.Empty() {
325+ if rect.Empty() {
453326 return
454327 }
455328 if content.DMABuf != nil {
@@ -458,7 +331,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
458331 // The window shows its background until a buffer imports.
459332 return
460333 }
461- uploads = append(uploads, upload{rect: rect, dst: dst, src: src, dma: im})
334+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
335+ dr.set, dr.im = im.set, im
336+ dr.acquire = content.Acquire
337+ draws = append(draws, dr)
462338 return
463339 }
464340 b := content.SHM
@@ -466,11 +342,24 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
466342 if err != nil {
467343 return
468344 }
469- uploads = append(uploads, upload{rect: rect, pixels: pixels[b.Offset:], stride: b.Stride, opaque: content.Opaque, blend: !content.Opaque, dst: dst, src: src})
345+ st := shmState{w: content.Width, h: content.Height, seq: seq}
346+ // Damage history is the root surface's: children copy in full.
347+ var damage func(uint64) ([]ports.Rect, bool)
348+ if key.index == 0 {
349+ damage = content.DamageSince
350+ }
351+ c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
352+ if err != nil {
353+ return
354+ }
355+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
356+ dr.set = c.set
357+ dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
358+ draws = append(draws, dr)
470359 }
471360 // drawSurface draws one surface buffer with its origin at (x, y)
472361 // logical, clipped to clip (logical).
473- drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle) {
362+ drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64) {
474363 lw, lh := content.LogicalW, content.LogicalH
475364 if lw <= 0 || lh <= 0 {
476365 lw, lh = content.Width, content.Height
@@ -492,26 +381,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
492381 if dst.Empty() {
493382 return
494383 }
495- // The visible part of dst in buffer pixels.
496- bx := func(v int) int { return (v - full.Min.X) * content.Width / full.Dx() }
497- by := func(v int) int { return (v - full.Min.Y) * content.Height / full.Dy() }
498- src := image.Rect(bx(dst.Min.X), by(dst.Min.Y), bx(dst.Max.X), by(dst.Max.Y))
499- addContent(dst, src, content)
384+ if key.index == 0 {
385+ dmg.content(key.win, content, full, dst)
386+ }
387+ addContent(dst, full, content, key, seq)
500388 }
501389 // place draws a surface tree with its window geometry at (x, y)
502390 // logical, clipped to w×h logical: client shadows fall outside.
503- place := func(content *ports.SurfaceContent, x, y, w, h int) {
391+ // Every surface of the tree is keyed by its window and place in it;
392+ // the tree's Seq changes with any of them.
393+ place := func(id ports.WindowID, content *ports.SurfaceContent, x, y, w, h int) {
504394 clip := image.Rect(x, y, x+w, y+h)
505395 ox, oy := x-content.Geometry.X, y-content.Geometry.Y
506396 for i := range content.Children {
507397 if ch := &content.Children[i]; ch.Below {
508- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
398+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
509399 }
510400 }
511- drawSurface(content, ox, oy, clip)
401+ drawSurface(content, ox, oy, clip, shmKey{id, 0}, content.Seq)
512402 for i := range content.Children {
513403 if ch := &content.Children[i]; !ch.Below {
514- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
404+ drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
515405 }
516406 }
517407 }
@@ -527,7 +417,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
527417 continue
528418 }
529419 content := contents[layer.ID]
530- place(&content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
420+ dmg.window(layer.ID, content, physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H))
421+ place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
531422 }
532423 }
533424 addLayers(false)
@@ -547,11 +438,12 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
547438 cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b
548439 body := physRect(cx, cy, cw, ch)
549440 content := contents[w.ID]
441+ dmg.window(w.ID, content, physRect(x, y, w.Rect.W, w.Rect.H))
550442 if content.Empty() {
551443 add(body, windowColor(w.ID))
552444 } else {
553445 add(body, parseColor(s.Background))
554- place(&content, cx, cy, cw, ch)
446+ place(w.ID, &content, cx, cy, cw, ch)
555447 }
556448 borderColor := s.Border.Inactive
557449 if w.Focused {
@@ -576,225 +468,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
576468 for _, w := range s.Windows {
577469 if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {
578470 content := contents[w.ID]
579- place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
471+ dmg.window(w.ID, content, physRect(w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H))
472+ place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
580473 }
581474 }
582475 }
583476 addPopups(false)
584477 addLayers(true)
585478 addPopups(true)
586- // Blended pixels are staged first, so the shaders bind only them,
587- // within the device's storage buffer range; past it they are copied.
588- // The draw table follows them, then the other uploads.
589- blends, used := 0, 0
590- for k := range uploads {
591- u := &uploads[k]
592- if u.blend && used+u.size() > r.maxRange {
593- u.blend = false
594- }
595- if u.blend {
596- u.offset = used
597- used += u.size()
598- blends++
599- }
600- }
601- pixelBytes := used
602- tableOffset := (used + r.align - 1) / r.align * r.align
603- used = tableOffset + blends*drawSize
604- for k := range uploads {
605- if u := &uploads[k]; !u.blend && u.dma == nil {
606- u.offset = used
607- used += u.size()
608- }
609- }
610- if err := r.ensureStaging(used); err != nil {
611- return err
612- }
613- data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)
614- if blends > 0 {
615- r.bindStaging(pixelBytes, tableOffset, blends)
616- i := 0
617- for k := range uploads {
618- if uploads[k].blend {
619- r.putDraw(data[tableOffset:], i, &uploads[k])
620- i++
621- }
622- }
623- }
624- for _, u := range uploads {
625- if u.dma != nil {
626- continue
627- }
628- if u.fill {
629- line := data[u.offset : u.offset+u.rect.Dx()*min(u.rect.Dy(), fillRows)*4]
630- for i := 0; i < len(line); i += 4 {
631- line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255
632- }
633- continue
634- }
635- dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
636- // The client may shrink its pool under the mapping: its window
637- // then shows garbage for this frame, never a crash.
638- copyGuarded(func() { u.stage(dst) })
639- }
640- rgb := parseColor(s.Background)
641- d := r.dd
642- if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
643- return err
644- }
645- begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
646- if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
647- return err
648- }
649- rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
650- tg := r.target()
651- srcStage := vk.PipelineStageFlags(vk.PipelineStageTopOfPipeBit)
652- srcAccess := vk.AccessFlags(0)
653- if tg.layout == vk.ImageLayoutTransferSrcOptimal {
654- srcStage = vk.PipelineStageTransferBit
655- srcAccess = vk.AccessTransferReadBit
656- }
657- // The whole target is cleared: its old contents never matter.
658- barrier := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: srcAccess, DstAccessMask: vk.AccessTransferWriteBit, OldLayout: tg.layout, NewLayout: vk.ImageLayoutTransferDstOptimal, Image: tg.image, SubresourceRange: rangeInfo}
659- if tg.exported {
660- // Acquire from the display (foreign queue family).
661- barrier.OldLayout = vk.ImageLayoutUndefined
662- barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
663- srcStage, barrier.SrcAccessMask = vk.PipelineStageTopOfPipeBit, 0
664- }
665- d.CmdPipelineBarrier(r.command, srcStage, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
666- barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = 0, 0
667- // VkClearColorValue is a union; UNORM formats read it as float32.
668- unorm := func(v uint8) uint32 { return math.Float32bits(float32(v) / 255) }
669- color := vk.ClearColorValue{unorm(rgb[0]), unorm(rgb[1]), unorm(rgb[2]), math.Float32bits(1)}
670- d.CmdClearColorImage(r.command, tg.image, vk.ImageLayoutTransferDstOptimal, &color, 1, &rangeInfo)
671- barrier.SrcAccessMask = vk.AccessTransferWriteBit
672- barrier.DstAccessMask = vk.AccessTransferWriteBit
673- barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
674- barrier.NewLayout = vk.ImageLayoutTransferDstOptimal
675- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
676- // Client buffers come from the foreign queue family (their driver) and
677- // go back to it after the frame, so the next frame acquires them again.
678- var dmas []*imported
679- for _, u := range uploads {
680- if u.dma != nil && !slices.Contains(dmas, u.dma) {
681- dmas = append(dmas, u.dma)
682- }
683- }
684- ownership := func(acquire bool) {
685- for _, im := range dmas {
686- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: rangeInfo}
687- if acquire {
688- // GENERAL, never UNDEFINED: the client's contents must survive.
689- b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutTransferSrcOptimal
690- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
691- b.DstAccessMask = vk.AccessTransferReadBit
692- } else {
693- b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
694- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
695- b.SrcAccessMask = vk.AccessTransferReadBit
696- }
697- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
698- }
699- }
700- ownership(true)
701- drawn := 0 // blended uploads drawn so far: their index in the draw table
702- for i := 0; i < len(uploads); i++ {
703- f := uploads[i]
704- if i > 0 && !f.blend {
705- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
706- }
707- if f.blend {
708- // Consecutive blended uploads share one draw: blending follows
709- // primitive order within it.
710- n := 1
711- for i+n < len(uploads) && uploads[i+n].blend {
712- n++
713- }
714- r.drawBlended(tg.image, tg.view, drawn, drawn+n)
715- drawn += n
716- i += n - 1
717- continue
718- }
719- rect := f.rect
720- if f.dma != nil {
721- // Map the visible part of dst back onto the buffer.
722- sx := func(x int) int32 { return int32(f.src.Min.X + (x-f.dst.Min.X)*f.src.Dx()/f.dst.Dx()) }
723- sy := func(y int) int32 { return int32(f.src.Min.Y + (y-f.dst.Min.Y)*f.src.Dy()/f.dst.Dy()) }
724- layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
725- blit := vk.ImageBlit{
726- SrcSubresource: layers,
727- SrcOffsets: [2]vk.Offset3D{{X: sx(rect.Min.X), Y: sy(rect.Min.Y)}, {X: sx(rect.Max.X), Y: sy(rect.Max.Y), Z: 1}},
728- DstSubresource: layers,
729- DstOffsets: [2]vk.Offset3D{{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, {X: int32(rect.Max.X), Y: int32(rect.Max.Y), Z: 1}},
730- }
731- d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
732- continue
733- }
734- layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
735- if f.fill {
736- // Every band reads the same staged strip.
737- r.fillRegions = r.fillRegions[:0]
738- for y := rect.Min.Y; y < rect.Max.Y; y += fillRows {
739- h := min(fillRows, rect.Max.Y-y)
740- r.fillRegions = append(r.fillRegions, vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(h), Depth: 1}})
741- }
742- d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0])
743- continue
744- }
745- region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
746- d.CmdCopyBufferToImage(r.command, r.staging, tg.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
747- }
748- ownership(false)
749- barrier.SrcAccessMask = vk.AccessTransferWriteBit
750- barrier.DstAccessMask = vk.AccessTransferReadBit
751- barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
752- barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal
753- if tg.exported {
754- // Release to the display: KMS scans it out after the fence.
755- barrier.NewLayout = vk.ImageLayoutGeneral
756- barrier.DstAccessMask = 0
757- barrier.SrcQueueFamilyIndex, barrier.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
758- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &barrier)
759- } else {
760- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
761- }
762- if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
763- return err
764- }
765- // Implicit sync: wait for the client's GPU writes to each buffer.
766- var waits []vk.Semaphore
767- for _, im := range dmas {
768- if sem := r.readFence(im); sem != 0 {
769- waits = append(waits, sem)
770- }
771- }
772- defer func() {
773- for _, sem := range waits {
774- d.DestroySemaphore(r.device, sem, nil)
775- }
776- }()
777- submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
778- stages := make([]vk.PipelineStageFlags, len(waits))
779- if len(waits) > 0 {
780- for i := range stages {
781- stages[i] = vk.PipelineStageTransferBit
782- }
783- submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
784- }
785- if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
786- return err
787- }
788- runtime.KeepAlive(stages)
789- tg.layout = barrier.NewLayout
790- r.last, r.readBack = tg, false
791- // The fence wait is the GPU completion ADR 014 requires before a flip.
792- if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
793- return err
794- }
795- r.dropUnused()
796- r.dropPools()
797- return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
479+ return draws
798480 }
799481
800482 func (r *Renderer) Pixels() *image.RGBA {
@@ -810,71 +492,50 @@ func (r *Renderer) Pixels() *image.RGBA {
810492 return out
811493 }
812494
813-// CopyBGRX writes the last frame into an XRGB8888 buffer with the given pitch.
814-// The image is B8G8R8A8, which is already XRGB8888 in memory.
815-func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
816- if r.mapped == nil || r.readback() != nil {
817- return
818- }
819- row := r.width * 4
820- src := unsafe.Slice((*byte)(r.mapped), row*r.height)
821- for y := 0; y < r.height; y++ {
822- copy(dst[y*pitch:y*pitch+row], src[y*row:(y+1)*row])
823- }
824-}
825-
826495 // Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE.
827496 const (
828497 queueFamilyIgnored = ^uint32(0)
829498 wholeSize = ^vk.DeviceSize(0)
830499 )
831500
832-// readback copies the last frame into the host buffer, once per frame.
501+// readback copies the last frame into the host buffer, once per frame
502+// (screenshots only).
833503 func (r *Renderer) readback() error {
834504 if r.readBack || r.last == nil || r.last.image == 0 {
835505 return nil
836506 }
837- d := r.dd
838- if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
839- return err
840- }
841- begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo}
842- if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(r.command, &begin)); err != nil {
507+ // The copy reads the frame: wait for it, and keep the ring ordered.
508+ if err := r.waitFrame(r.submitted); err != nil {
843509 return err
844510 }
845511 t := r.last
846- rangeInfo := vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
847- b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, Image: t.image, SubresourceRange: rangeInfo}
848- if t.exported {
849- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
850- b.SrcAccessMask = 0
851- }
852- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
853- region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
854- d.CmdCopyImageToBuffer(r.command, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
855- // Make the copy visible to host reads of the mapped buffer.
856- hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}
857- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
858- if t.exported {
859- b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
860- b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
861- b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
862- d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
863- } else {
864- t.layout = vk.ImageLayoutTransferSrcOptimal
865- }
866- if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
867- return err
868- }
869- submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
870- if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
512+ err := r.oneShot(func(cmd vk.CommandBuffer) {
513+ d := r.dd
514+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessTransferReadBit, OldLayout: t.layout, NewLayout: vk.ImageLayoutTransferSrcOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: t.image, SubresourceRange: colorRange}
515+ if t.exported {
516+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
517+ }
518+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
519+ region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
520+ d.CmdCopyImageToBuffer(cmd, t.image, vk.ImageLayoutTransferSrcOptimal, r.buffer, 1, &region)
521+ // Make the copy visible to host reads of the mapped buffer.
522+ hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: r.buffer, Size: wholeSize}
523+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
524+ if t.exported {
525+ b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
526+ b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
527+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
528+ d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
529+ }
530+ })
531+ if err != nil {
871532 return err
872533 }
873- if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
874- return err
534+ if !t.exported {
535+ t.layout = vk.ImageLayoutTransferSrcOptimal
875536 }
876537 r.readBack = true
877- return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
538+ return nil
878539 }
879540
880541 func (r *Renderer) Close() {
@@ -885,38 +546,22 @@ func (r *Renderer) Close() {
885546 d := r.dd
886547 if r.device != 0 {
887548 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
549+ r.idle()
888550 }
889551 r.dropTargets()
552+ r.dropCursors()
890553 for id, im := range r.imports {
891554 r.release(im)
892555 delete(r.imports, id)
893556 }
894557 r.frame += importTTL + 1
895558 r.dropPools()
896- if r.fence != 0 {
897- d.DestroyFence(r.device, r.fence, nil)
898- r.fence = 0
899- }
900- if r.command != 0 {
901- d.FreeCommandBuffers(r.device, r.pool, 1, &r.command)
902- r.command = 0
903- }
559+ r.dropShm()
560+ r.destroySlots()
904561 if r.pool != 0 {
905562 d.DestroyCommandPool(r.device, r.pool, nil)
906563 r.pool = 0
907564 }
908- if r.stagingMapped != nil {
909- d.UnmapMemory(r.device, r.stagingMemory)
910- r.stagingMapped = nil
911- }
912- if r.staging != 0 {
913- d.DestroyBuffer(r.device, r.staging, nil)
914- r.staging = 0
915- }
916- if r.stagingMemory != 0 {
917- d.FreeMemory(r.device, r.stagingMemory, nil)
918- r.stagingMemory = 0
919- }
920565 if r.mapped != nil {
921566 d.UnmapMemory(r.device, r.bufferMemory)
922567 r.mapped = nil
@@ -929,7 +574,7 @@ func (r *Renderer) Close() {
929574 d.FreeMemory(r.device, r.bufferMemory, nil)
930575 r.bufferMemory = 0
931576 }
932- r.destroyBlend()
577+ r.destroyComposer()
933578 r.freeTarget(&r.own)
934579 d.DestroyDevice(r.device, nil)
935580 r.device = 0
M internal/adapters/vulkan/renderer_test.go +14 -15
@@ -48,7 +48,7 @@ func TestRendererRender(t *testing.T) {
4848 bg := color.RGBA{16, 32, 48, 255}
4949 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
5050 t.Helper()
51- if err := r.Render(s, nil); err != nil {
51+ if err := render(r, s, nil); err != nil {
5252 t.Fatal(err)
5353 }
5454 for p, want := range expected {
@@ -92,7 +92,7 @@ func TestRendererContents(t *testing.T) {
9292 if c != nil {
9393 contents = map[ports.WindowID]ports.SurfaceContent{1: *c}
9494 }
95- if err := r.Render(scene, contents); err != nil {
95+ if err := render(r, scene, contents); err != nil {
9696 t.Fatal(err)
9797 }
9898 }
@@ -127,7 +127,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
127127 {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},
128128 {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},
129129 }}
130- if err := r.Render(scene, nil); err != nil {
130+ if err := render(r, scene, nil); err != nil {
131131 t.Fatal(err)
132132 }
133133 c := windowColor(2)
@@ -143,7 +143,7 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) {
143143 opaque := shmContent(t, 16, 16, 64, px)
144144 opaque.Opaque = true
145145 contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque}
146- if err := r.Render(scene, contents); err != nil {
146+ if err := render(r, scene, contents); err != nil {
147147 t.Fatal(err)
148148 }
149149 for _, tc := range []struct {
@@ -174,7 +174,7 @@ func TestRendererLayers(t *testing.T) {
174174 window := color.RGBA{wc[0], wc[1], wc[2], 255}
175175 check := func(want color.RGBA) {
176176 t.Helper()
177- if err := r.Render(scene, contents); err != nil {
177+ if err := render(r, scene, contents); err != nil {
178178 t.Fatal(err)
179179 }
180180 if got := r.Pixels().RGBAAt(5, 2); got != want {
@@ -218,7 +218,7 @@ func TestRendererScale(t *testing.T) {
218218 scaled := shmContent(t, 20, 20, 80, solid(20, 20, red))
219219 scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10
220220 content := map[ports.WindowID]ports.SurfaceContent{1: *scaled}
221- if err := r.Render(s, content); err != nil {
221+ if err := render(r, s, content); err != nil {
222222 t.Fatal(err)
223223 }
224224 px := r.Pixels()
@@ -236,7 +236,7 @@ func TestRendererScale(t *testing.T) {
236236 // Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20
237237 // buffer is 10x10 logical and downscaled to 15x15 physical at (6,6).
238238 s.Scale = 1.5
239- if err := r.Render(s, content); err != nil {
239+ if err := render(r, s, content); err != nil {
240240 t.Fatal(err)
241241 }
242242 px = r.Pixels()
@@ -252,7 +252,7 @@ func TestRendererScale(t *testing.T) {
252252 odd := shmContent(t, 16, 15, 64, solid(16, 15, red))
253253 odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10
254254 content[1] = *odd
255- if err := r.Render(s, content); err != nil {
255+ if err := render(r, s, content); err != nil {
256256 t.Fatal(err)
257257 }
258258 if got := r.Pixels().At(21, 10); got != (color.RGBA{0, 0, 0, 255}) {
@@ -309,7 +309,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
309309 {X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)},
310310 {X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)},
311311 }
312- if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
312+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
313313 t.Fatal(err)
314314 }
315315 px := r.Pixels()
@@ -327,7 +327,7 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
327327 }
328328 // A root with no buffer of its own still shows its children.
329329 only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}}
330- if err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
330+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
331331 t.Fatal(err)
332332 }
333333 if got := r.Pixels().At(20, 15); got != blue {
@@ -335,17 +335,16 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
335335 }
336336 }
337337
338-// A fill taller than one staged strip is copied down in bands: every row
339-// through the last one gets the color.
338+// A tall fill covers every row through the last one.
340339 func TestRendererTallFill(t *testing.T) {
341- h := fillRows*2 + 7
340+ h := 135
342341 r, err := New(8, h)
343342 if err != nil {
344343 t.Skipf("Vulkan unavailable: %v", err)
345344 }
346345 defer r.Close()
347346 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
348- if err := r.Render(scene, nil); err != nil {
347+ if err := render(r, scene, nil); err != nil {
349348 t.Fatal(err)
350349 }
351350 c := windowColor(1)
@@ -403,7 +402,7 @@ func TestRendererAlphaBlend(t *testing.T) {
403402 {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}},
404403 {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}},
405404 }}
406- if err := r.Render(scene, contents); err != nil {
405+ if err := render(r, scene, contents); err != nil {
407406 t.Fatal(err)
408407 }
409408 px := r.Pixels()
D internal/adapters/vulkan/shaders/blend.frag +0 -22
@@ -1,22 +0,0 @@
1-#version 450
2-// Reads the staged B8G8R8A8 pixel under the fragment: premultiplied, as
3-// Wayland ARGB buffers are, so the pipeline blends ONE, ONE_MINUS_SRC_ALPHA.
4-
5-struct Draw {
6- ivec4 rect;
7- uvec4 pixels;
8-};
9-
10-layout(std430, set = 0, binding = 0) readonly buffer Pixels { uint pixels[]; };
11-layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; };
12-
13-layout(location = 0) flat in uint draw;
14-layout(location = 0) out vec4 color;
15-
16-void main() {
17- Draw d = draws[draw];
18- ivec2 p = ivec2(gl_FragCoord.xy) - d.rect.xy;
19- // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
20- vec4 bgra = unpackUnorm4x8(pixels[d.pixels.x + uint(p.y) * d.pixels.y + uint(p.x)]);
21- color = bgra.zyxw;
22-}
D internal/adapters/vulkan/shaders/blend.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

D internal/adapters/vulkan/shaders/blend.vert +0 -21
@@ -1,21 +0,0 @@
1-#version 450
2-// One quad per instance: the instance indexes the draw table.
3-
4-struct Draw {
5- ivec4 rect; // x0, y0, x1, y1 in target pixels
6- uvec4 pixels; // first pixel word in staging, row width, target w, target h
7-};
8-
9-layout(std430, set = 0, binding = 1) readonly buffer Draws { Draw draws[]; };
10-
11-layout(location = 0) flat out uint draw;
12-
13-void main() {
14- Draw d = draws[gl_InstanceIndex];
15- // Two triangles: 0,1,2 and 2,1,3 of the corners.
16- const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1));
17- ivec2 c = corners[gl_VertexIndex];
18- vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w);
19- gl_Position = vec4(p / vec2(d.pixels.zw) * 2.0 - 1.0, 0.0, 1.0);
20- draw = uint(gl_InstanceIndex);
21-}
D internal/adapters/vulkan/shaders/blend.vert.spv +0 -0 binary file: no text diff

binary file: no text diff

A internal/adapters/vulkan/shaders/compose.frag +55 -0
@@ -0,0 +1,55 @@
1+#version 450
2+// Draws a solid fill, a client dmabuf (sampled image) or a client wl_shm
3+// buffer (B8G8R8A8 words in a storage buffer). Client pixels are
4+// premultiplied, as Wayland buffers are: the pipeline blends ONE,
5+// ONE_MINUS_SRC_ALPHA. Scaled draws filter linearly; 1:1 draws read the
6+// texel under the fragment, pixel exact.
7+
8+layout(push_constant) uniform Draw {
9+ ivec4 rect;
10+ vec4 map;
11+ vec4 color;
12+ uvec4 buf;
13+ uvec4 misc;
14+} d;
15+
16+layout(set = 0, binding = 0) uniform sampler2D tex;
17+layout(std430, set = 0, binding = 1) readonly buffer Pixels { uint pixels[]; };
18+
19+layout(location = 0) out vec4 color;
20+
21+const uint modeSolid = 0u;
22+const uint modeImage = 1u;
23+const uint flagOpaque = 1u;
24+const uint flagExact = 2u;
25+
26+// texel reads buffer pixel p, clamped to the buffer (clamp to edge).
27+vec4 texel(ivec2 p) {
28+ p = clamp(p, ivec2(0), ivec2(d.buf.yz) - 1);
29+ // Little-endian B,G,R,A bytes: unpack yields (B, G, R, A).
30+ return unpackUnorm4x8(pixels[d.buf.x + uint(p.y) * d.buf.y + uint(p.x)]).zyxw;
31+}
32+
33+void main() {
34+ vec2 src = d.map.xy + gl_FragCoord.xy * d.map.zw;
35+ bool exact = (d.misc.y & flagExact) != 0u;
36+ vec4 c;
37+ if (d.misc.x == modeSolid) {
38+ c = d.color;
39+ } else if (d.misc.x == modeImage) {
40+ ivec2 size = textureSize(tex, 0);
41+ c = exact ? texelFetch(tex, clamp(ivec2(floor(src)), ivec2(0), size - 1), 0) : texture(tex, src / vec2(size));
42+ } else if (exact) {
43+ c = texel(ivec2(floor(src)));
44+ } else {
45+ // Bilinear: four texel reads around the sample point.
46+ vec2 p = src - 0.5;
47+ ivec2 i = ivec2(floor(p));
48+ vec2 f = p - vec2(i);
49+ c = mix(mix(texel(i), texel(i + ivec2(1, 0)), f.x), mix(texel(i + ivec2(0, 1)), texel(i + ivec2(1, 1)), f.x), f.y);
50+ }
51+ if ((d.misc.y & flagOpaque) != 0u) {
52+ c.a = 1.0;
53+ }
54+ color = c;
55+}
A internal/adapters/vulkan/shaders/compose.frag.spv +0 -0 binary file: no text diff

binary file: no text diff

A internal/adapters/vulkan/shaders/compose.vert +17 -0
@@ -0,0 +1,17 @@
1+#version 450
2+// One quad per draw: two triangles over the visible target rect.
3+
4+layout(push_constant) uniform Draw {
5+ ivec4 rect; // visible x0, y0, x1, y1 in target pixels
6+ vec4 map; // source texel = map.xy + fragment centre * map.zw
7+ vec4 color; // solid fills: premultiplied RGBA
8+ uvec4 buf; // buffer draws: first word, row width, height, unused
9+ uvec4 misc; // mode, flags, target width, target height
10+} d;
11+
12+void main() {
13+ const ivec2 corners[6] = ivec2[](ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(0, 1), ivec2(1, 0), ivec2(1, 1));
14+ ivec2 c = corners[gl_VertexIndex];
15+ vec2 p = vec2(c.x == 0 ? d.rect.x : d.rect.z, c.y == 0 ? d.rect.y : d.rect.w);
16+ gl_Position = vec4(p / vec2(d.misc.zw) * 2.0 - 1.0, 0.0, 1.0);
17+}
A internal/adapters/vulkan/shaders/compose.vert.spv +0 -0 binary file: no text diff

binary file: no text diff

M internal/adapters/vulkan/shm.go +198 -7
@@ -3,15 +3,20 @@ package vulkan
33 import (
44 "fmt"
55 "runtime/debug"
6+ "unsafe"
67
78 "github.com/bnema/nefertty/internal/ports"
9+ vk "github.com/bnema/purego-vulkan/vulkan"
810 "golang.org/x/sys/unix"
911 )
1012
11-// wl_shm buffers are read in place: each client pool is mapped once, read
12-// only, and the visible pixels are copied from it into staging when a frame
13-// is drawn. Mappings follow the dmabuf import rules: kept while drawn,
14-// dropped after importTTL frames without use.
13+// wl_shm buffers: each client pool is mapped once, read only. A surface's
14+// pixels are copied once per new content into a persistently mapped,
15+// host-visible GPU buffer (device-local when the GPU exposes it, ReBAR)
16+// that the fragment shader reads directly: one memcpy per row, no staging
17+// copy, no resample, no alpha fix-up on the CPU. Each surface has two GPU
18+// buffers used in turn, so the CPU never writes one a frame in flight
19+// reads. Mappings and buffers are dropped after importTTL frames unused.
1520
1621 // mapping is a client pool mapped read-only.
1722 type mapping struct {
@@ -27,8 +32,8 @@ func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) {
2732 m.last = r.frame
2833 return m.data, nil
2934 }
30- // Uploads collected earlier this frame may still slice the old
31- // mapping: it is unmapped once staging is done.
35+ // Draws collected earlier this frame may still read the old
36+ // mapping: it is unmapped once the frame is recorded.
3237 r.retired = append(r.retired, m.data)
3338 delete(r.pools, b.Pool)
3439 }
@@ -53,7 +58,8 @@ func (r *Renderer) shmPixels(b *ports.SHMBuffer, end int) ([]byte, error) {
5358 }
5459
5560 // dropPools unmaps replaced mappings and pools not drawn for importTTL
56-// frames.
61+// frames. The GPU never reads a mapping: this runs once the CPU copies of
62+// a frame are done.
5763 func (r *Renderer) dropPools() {
5864 for _, data := range r.retired {
5965 _ = unix.Munmap(data)
@@ -80,3 +86,188 @@ func copyGuarded(copy func()) (ok bool) {
8086 copy()
8187 return true
8288 }
89+
90+// gpuBuffer is a host-visible storage buffer, mapped for its whole life.
91+type gpuBuffer struct {
92+ buffer vk.Buffer
93+ memory vk.DeviceMemory
94+ mapped unsafe.Pointer
95+ size int
96+ // last is the frame that last read it: it is written or freed only
97+ // once that frame completed.
98+ last uint64
99+}
100+
101+func (r *Renderer) newGPUBuffer(b *gpuBuffer, size int) error {
102+ d := r.dd
103+ ok := false
104+ defer func() {
105+ if !ok {
106+ r.freeGPUBuffer(b)
107+ }
108+ }()
109+ info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(size), Usage: vk.BufferUsageStorageBufferBit, SharingMode: vk.SharingModeExclusive}
110+ if err := checked("vkCreateBuffer(shm)", d.CreateBuffer(r.device, &info, nil, &b.buffer)); err != nil {
111+ return err
112+ }
113+ var req vk.MemoryRequirements
114+ d.GetBufferMemoryRequirements(r.device, b.buffer, &req)
115+ host := vk.MemoryPropertyFlags(vk.MemoryPropertyHostVisibleBit | vk.MemoryPropertyHostCoherentBit)
116+ kind, err := r.findMemoryType(req.MemoryTypeBits, host|vk.MemoryPropertyDeviceLocalBit)
117+ if err != nil {
118+ kind, err = r.findMemoryType(req.MemoryTypeBits, host)
119+ }
120+ if err != nil {
121+ return err
122+ }
123+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
124+ if err := checked("vkAllocateMemory(shm)", d.AllocateMemory(r.device, &alloc, nil, &b.memory)); err != nil {
125+ return err
126+ }
127+ if err := checked("vkBindBufferMemory(shm)", d.BindBufferMemory(r.device, b.buffer, b.memory, 0)); err != nil {
128+ return err
129+ }
130+ if err := checked("vkMapMemory(shm)", d.MapMemory(r.device, b.memory, 0, vk.DeviceSize(size), 0, &b.mapped)); err != nil {
131+ return err
132+ }
133+ b.size, ok = size, true
134+ return nil
135+}
136+
137+func (r *Renderer) freeGPUBuffer(b *gpuBuffer) {
138+ d := r.dd
139+ if b.mapped != nil {
140+ d.UnmapMemory(r.device, b.memory)
141+ }
142+ if b.buffer != 0 {
143+ d.DestroyBuffer(r.device, b.buffer, nil)
144+ }
145+ if b.memory != 0 {
146+ d.FreeMemory(r.device, b.memory, nil)
147+ }
148+ *b = gpuBuffer{}
149+}
150+
151+// shmKey is a surface of a window: index 0 is the root, i+1 its child i.
152+type shmKey struct {
153+ win ports.WindowID
154+ index int
155+}
156+
157+// shmState is what a GPU buffer holds: a w×h client buffer at a content
158+// Seq.
159+type shmState struct {
160+ w, h int
161+ seq uint64
162+}
163+
164+// shmCopy is one of a surface's two GPU buffers and the set binding it.
165+type shmCopy struct {
166+ gpu gpuBuffer
167+ pool vk.DescriptorPool
168+ set vk.DescriptorSet
169+ holds shmState
170+ valid bool
171+}
172+
173+// shmSurface is the GPU copy of one surface's wl_shm content.
174+type shmSurface struct {
175+ bufs [2]*shmCopy
176+ last uint64 // frame that drew it
177+}
178+
179+// shmCopyFor returns the GPU buffer holding a surface's content st, copying
180+// the client pixels (stride bytes per row from offset) into one when no
181+// buffer holds them yet. A buffer holding an older content of the same
182+// size gets only what changed since (damage, in buffer pixels; false:
183+// everything).
184+func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, stride int, damage func(since uint64) ([]ports.Rect, bool)) (*shmCopy, error) {
185+ s := r.shm[key]
186+ if s == nil {
187+ s = &shmSurface{}
188+ r.shm[key] = s
189+ }
190+ s.last = r.frame
191+ for _, c := range s.bufs {
192+ if c != nil && c.valid && c.holds == st {
193+ c.gpu.last = r.frame
194+ return c, nil
195+ }
196+ }
197+ // The older buffer takes the new content.
198+ i := 0
199+ if s.bufs[0] != nil && (s.bufs[1] == nil || s.bufs[1].gpu.last < s.bufs[0].gpu.last) {
200+ i = 1
201+ }
202+ size := st.w * st.h * 4
203+ if size > r.maxRange {
204+ return nil, fmt.Errorf("shm buffer of %d bytes past the storage buffer range", size)
205+ }
206+ c := s.bufs[i]
207+ if c != nil && c.gpu.size != size {
208+ r.retire(c.gpu.last, func() { r.freeShmCopy(c) })
209+ s.bufs[i], c = nil, nil
210+ }
211+ if c == nil {
212+ c = &shmCopy{}
213+ if err := r.newGPUBuffer(&c.gpu, size); err != nil {
214+ return nil, err
215+ }
216+ var err error
217+ if c.pool, c.set, err = r.newSet(r.compose.dummyView, c.gpu.buffer); err != nil {
218+ r.freeGPUBuffer(&c.gpu)
219+ return nil, err
220+ }
221+ s.bufs[i] = c
222+ }
223+ if err := r.waitFrame(c.gpu.last); err != nil {
224+ return nil, err
225+ }
226+ dst := unsafe.Slice((*byte)(c.gpu.mapped), size)
227+ row := st.w * 4
228+ rects := []ports.Rect{{W: st.w, H: st.h}}
229+ if c.valid && c.holds.w == st.w && c.holds.h == st.h && damage != nil {
230+ if d, ok := damage(c.holds.seq); ok {
231+ rects = d
232+ }
233+ }
234+ // The client may shrink its pool under the mapping: its window then
235+ // shows garbage for this frame, never a crash, and is copied again.
236+ c.valid = copyGuarded(func() {
237+ for _, rc := range rects {
238+ x0, y0 := max(rc.X, 0), max(rc.Y, 0)
239+ x1, y1 := min(rc.X+rc.W, st.w), min(rc.Y+rc.H, st.h)
240+ for y := y0; y < y1; y++ {
241+ d := y*row + x0*4
242+ s := offset + y*stride + x0*4
243+ n := (x1 - x0) * 4
244+ copy(dst[d:d+n], pixels[s:s+n])
245+ r.copied += n
246+ }
247+ }
248+ })
249+ c.holds, c.gpu.last = st, r.frame
250+ return c, nil
251+}
252+
253+func (r *Renderer) freeShmCopy(c *shmCopy) {
254+ if c.pool != 0 {
255+ r.dd.DestroyDescriptorPool(r.device, c.pool, nil)
256+ c.pool = 0
257+ }
258+ r.freeGPUBuffer(&c.gpu)
259+}
260+
261+// dropShm frees the GPU buffers of surfaces not drawn for importTTL frames.
262+func (r *Renderer) dropShm() {
263+ for key, s := range r.shm {
264+ if r.frame-s.last > importTTL {
265+ for _, c := range s.bufs {
266+ if c != nil {
267+ r.retire(c.gpu.last, func() { r.freeShmCopy(c) })
268+ }
269+ }
270+ delete(r.shm, key)
271+ }
272+ }
273+}
M internal/adapters/vulkan/targets.go +14 -15
@@ -17,7 +17,7 @@ import (
1717 // turns into KMS framebuffers. Render then composes straight into the
1818 // selected target and the output flips to it; nothing is copied by the
1919 // CPU. The internal image is the target until then (headless, tests).
20-// Readback into host memory happens only for Pixels and CopyBGRX.
20+// Readback into host memory happens only for Pixels (screenshots).
2121
2222 // target is an image frames are drawn into.
2323 type target struct {
@@ -28,6 +28,11 @@ type target struct {
2828 // exported images are shared with the display: ownership goes to the
2929 // foreign queue family after each frame, and KMS reads them.
3030 exported bool
31+ // What the image holds (damage.go): the scene Seq and each window's
32+ // content Seq; valid once a frame was drawn whole.
33+ valid bool
34+ sceneSeq uint64
35+ windows map[ports.WindowID]heldWindow
3136 }
3237
3338 // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored.
@@ -105,9 +110,8 @@ func (r *Renderer) probeRenderModifiers(physical vk.PhysicalDevice) {
105110 mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
106111 list.DrmFormatModifierProperties = &mods[0]
107112 r.id.GetPhysicalDeviceFormatProperties2(physical, vk.FormatB8g8r8a8Unorm, &fp)
108- need := vk.FormatFeatureFlags(formatFeatureTransferDst | formatFeatureTransferSrc | vk.FormatFeatureBlitDstBit | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit)
109- // Targets are drawn on too (blend.go): a transfer-only modifier is
110- // not a target, and without any the display falls back to readback.
113+ need := vk.FormatFeatureFlags(formatFeatureTransferSrc | vk.FormatFeatureColorAttachmentBit | vk.FormatFeatureColorAttachmentBlendBit)
114+ // Targets are drawn on (compose.go) and read back for screenshots.
111115 for _, m := range mods[:list.DrmFormatModifierCount] {
112116 if m.DrmFormatModifierPlaneCount == 1 && m.DrmFormatModifierTilingFeatures&need == need && r.exportable(physical, m.DrmFormatModifier) {
113117 r.renderMods = append(r.renderMods, m.DrmFormatModifier)
@@ -127,18 +131,12 @@ func (r *Renderer) exportable(physical vk.PhysicalDevice, modifier uint64) bool
127131 return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureExportableBit != 0
128132 }
129133
130-// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT / _DST_BIT (Vulkan 1.1), missing from
131-// the generated bindings.
132-const (
133- formatFeatureTransferSrc = 1 << 14
134- formatFeatureTransferDst = 1 << 15
135-)
136-
137-// Targets are copied into and drawn on (blend.go).
138-const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit | vk.ImageUsageColorAttachmentBit
134+// VK_FORMAT_FEATURE_TRANSFER_SRC_BIT (Vulkan 1.1), missing from the
135+// generated bindings.
136+const formatFeatureTransferSrc = 1 << 14
139137
140-// Staging feeds copies and, as a storage buffer, the blend shaders.
141-const stagingUsage = vk.BufferUsageTransferSrcBit | vk.BufferUsageStorageBufferBit
138+// Targets are drawn on (compose.go) and copied from for screenshots.
139+const targetUsage = vk.ImageUsageTransferSrcBit | vk.ImageUsageColorAttachmentBit
142140
143141 // exportTarget creates one exported image with a modifier from mods.
144142 func (r *Renderer) exportTarget(mods []uint64) (*target, ports.DMABuf, error) {
@@ -213,6 +211,7 @@ func (r *Renderer) freeTarget(t *target) {
213211 func (r *Renderer) dropTargets() {
214212 if len(r.targets) > 0 && r.device != 0 {
215213 _ = checked("vkDeviceWaitIdle", r.dd.DeviceWaitIdle(r.device))
214+ r.idle()
216215 }
217216 for _, t := range r.targets {
218217 if r.last == t {
M internal/adapters/vulkan/targets_test.go +2 -2
@@ -31,7 +31,7 @@ func TestExportTargets(t *testing.T) {
3131 colors := []string{"#ff0000", "#0000ff"}
3232 for i, c := range colors {
3333 r.UseTarget(i)
34- if err := r.Render(ports.Scene{Background: c}, nil); err != nil {
34+ if err := render(r, ports.Scene{Background: c}, nil); err != nil {
3535 t.Fatal(err)
3636 }
3737 }
@@ -55,7 +55,7 @@ func TestExportTargets(t *testing.T) {
5555 }
5656 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}}
5757 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}}
58- if err := viewer.Render(scene, contents); err != nil {
58+ if err := render(viewer, scene, contents); err != nil {
5959 t.Fatal(err)
6060 }
6161 if got := viewer.Pixels().At(10, 10); got != (color.RGBA{255, 0, 0, 255}) {
A internal/adapters/wayland/damage_test.go +109 -0
@@ -0,0 +1,109 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/wayland"
9+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
10+ "golang.org/x/sys/unix"
11+)
12+
13+// Damage reaches the renderer in buffer pixels, per content, with the
14+// window's recent history; a new buffer without damage is a full change.
15+func TestDamageHistory(t *testing.T) {
16+ s, events, _, contents, dir := contentServer(t)
17+ c := protocolClient(t, s, dir)
18+ comp := bindVersion(t, c, "wl_compositor", 4)
19+ wm := bindProtocol(t, c, "xdg_wm_base")
20+ shm := bindProtocol(t, c, "wl_shm")
21+ registerProtocol(t, c, shm)
22+ surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID()
23+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
24+ registerProtocol(t, c, surf)
25+ requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
26+ serials := make(chan uint32, 8)
27+ xp := &configureProxy{serial: serials}
28+ xp.SetID(xdg)
29+ c.Context().Register(xp)
30+ registerProtocol(t, c, top)
31+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
32+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
33+ if err := c.Roundtrip(); err != nil {
34+ t.Fatal(err)
35+ }
36+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials)
37+ fd, err := unix.MemfdCreate("damage", 0)
38+ if err != nil {
39+ t.Fatal(err)
40+ }
41+ defer unix.Close(fd)
42+ if err := unix.Ftruncate(fd, 2*16*16*4); err != nil {
43+ t.Fatal(err)
44+ }
45+ pool := c.AllocateID()
46+ registerProtocol(t, c, pool)
47+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(2*16*16*4)); err != nil {
48+ t.Fatal(err)
49+ }
50+ bufs := [2]uint32{c.AllocateID(), c.AllocateID()}
51+ for i, b := range bufs {
52+ registerProtocol(t, c, b)
53+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, b, int32(i*16*16*4), int32(16), int32(16), int32(64), uint32(0))
54+ }
55+ next := func() ports.SurfaceContent {
56+ t.Helper()
57+ if err := c.Roundtrip(); err != nil {
58+ t.Fatal(err)
59+ }
60+ deadline := time.After(2 * time.Second)
61+ for {
62+ select {
63+ case got := <-contents:
64+ if got.Width == 16 {
65+ return got
66+ }
67+ case <-deadline:
68+ t.Fatal("no content")
69+ }
70+ }
71+ }
72+ // A new size: full.
73+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0))
74+ requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(0), int32(0), int32(4), int32(4))
75+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
76+ mapped(t, events, 2*time.Second)
77+ first := next()
78+ if h := first.DamageHistory; len(h) == 0 || h[len(h)-1].Seq != first.Seq || !h[len(h)-1].Full {
79+ t.Fatalf("resize: %+v", h)
80+ }
81+ // Buffer and surface damage, clipped to the buffer.
82+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0))
83+ requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(2), int32(3), int32(4), int32(5))
84+ requestProtocol(t, c, surf, wayland.SurfaceRequestDamage, int32(14), int32(14), int32(1<<30), int32(1<<30))
85+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
86+ second := next()
87+ got, ok := second.DamageSince(first.Seq)
88+ want := []ports.Rect{{X: 2, Y: 3, W: 4, H: 5}, {X: 14, Y: 14, W: 2, H: 2}}
89+ if !ok || len(got) != 2 || got[0] != want[0] || got[1] != want[1] {
90+ t.Fatalf("damage %v %v", got, ok)
91+ }
92+ // Same buffer size, new logical size (buffer scale): full.
93+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0))
94+ requestProtocol(t, c, surf, wayland.SurfaceRequestSetBufferScale, int32(2))
95+ requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(0), int32(0), int32(1), int32(1))
96+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
97+ scaled := next()
98+ if _, ok := scaled.DamageSince(second.Seq); ok {
99+ t.Fatal("logical resize kept partial damage")
100+ }
101+ second = scaled
102+ // No damage with a new buffer: full.
103+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0))
104+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
105+ third := next()
106+ if _, ok := third.DamageSince(second.Seq); ok {
107+ t.Fatal("undamaged new buffer taken as unchanged")
108+ }
109+}
M internal/adapters/wayland/dmabuf.go +99 -13
@@ -15,7 +15,11 @@ import (
1515
1616 // linux-dmabuf v4: clients hand GPU buffers to the renderer without copies.
1717 // The formats are those the renderer imports (ports.DMABufSupport); without
18-// any, the global is not advertised and clients use wl_shm.
18+// any, the global is not advertised and clients use wl_shm. Surface
19+// feedback also offers, first, a scanout tranche while the surface's
20+// window is fullscreen on an output that reported scanout formats
21+// (ports.OutputFormats): a buffer allocated from it can be flipped to the
22+// display with no composition.
1923
2024 const dmabufVersion = 4
2125
@@ -30,7 +34,7 @@ func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error
3034 if err != nil {
3135 return err
3236 }
33- s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table}
37+ s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table, scanout: map[string]ports.OutputFormats{}, feedbacks: map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface{}}
3438 return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {
3539 r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)
3640 if err != nil || v >= 4 {
@@ -77,6 +81,11 @@ type dmabufGlobal struct {
7781 support ports.DMABufSupport
7882 table *os.File
7983 nextID uint64
84+ // scanout are the outputs' direct scanout formats by output name.
85+ scanout map[string]ports.OutputFormats
86+ // feedbacks are the live feedback objects and their surface (nil:
87+ // default feedback).
88+ feedbacks map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface
8089 }
8190
8291 func (g *dmabufGlobal) close() {
@@ -98,35 +107,112 @@ func (g *dmabufGlobal) CreateParams(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32)
98107 }
99108
100109 func (g *dmabufGlobal) GetDefaultFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
101- g.feedback(r, id)
110+ g.feedback(r, id, nil)
102111 }
103112
104-// GetSurfaceFeedback sends the default feedback: one renderer, one tranche.
105-// Scanout tranches come with direct scanout.
106-func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, _ *wayland.Surface) {
107- g.feedback(r, id)
113+// GetSurfaceFeedback follows the surface: a scanout tranche comes first
114+// while it can be scanned out (see scanoutFor).
115+func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *wayland.Surface) {
116+ g.feedback(r, id, g.server.surfaceOf(surf))
108117 }
109118
110-func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
119+func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *surface) {
111120 fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{})
112121 if err != nil {
113122 return
114123 }
115- dev := make([]byte, 8)
116- binary.NativeEndian.PutUint64(dev, g.support.Device)
124+ g.feedbacks[fb] = surf
125+ fb.OnDestroy = func() { delete(g.feedbacks, fb) }
126+ g.send(fb, surf)
127+}
128+
129+// devBytes is a dev_t as the protocol's array.
130+func devBytes(dev uint64) []byte {
131+ b := make([]byte, 8)
132+ binary.NativeEndian.PutUint64(b, dev)
133+ return b
134+}
135+
136+// send sends the whole feedback: table, main device, the scanout tranche
137+// when the surface has one, then the renderer tranche.
138+func (g *dmabufGlobal) send(fb *linuxdmabuf.ZwpLinuxDmabufFeedbackV1, surf *surface) {
139+ fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))
140+ main := devBytes(g.support.Device)
141+ fb.SendMainDevice(main)
142+ if out, ok := g.scanoutFor(surf); ok {
143+ var indices []byte
144+ for _, f := range out.Formats {
145+ if i := slices.Index(g.support.Formats, f); i >= 0 {
146+ indices = binary.NativeEndian.AppendUint16(indices, uint16(i))
147+ }
148+ }
149+ if len(indices) > 0 {
150+ fb.SendTrancheTargetDevice(devBytes(out.Device))
151+ fb.SendTrancheFlags(uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout))
152+ fb.SendTrancheFormats(indices)
153+ fb.SendTrancheDone()
154+ }
155+ }
117156 indices := make([]byte, 2*len(g.support.Formats))
118157 for i := range g.support.Formats {
119158 binary.NativeEndian.PutUint16(indices[i*2:], uint16(i))
120159 }
121- fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))
122- fb.SendMainDevice(dev)
123- fb.SendTrancheTargetDevice(dev)
160+ fb.SendTrancheTargetDevice(main)
124161 fb.SendTrancheFlags(0)
125162 fb.SendTrancheFormats(indices)
126163 fb.SendTrancheDone()
127164 fb.SendDone()
128165 }
129166
167+// scanoutFor is the scanout offer for a surface: its window is fullscreen
168+// on an output that reported scanout formats.
169+func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) {
170+ if surf == nil || surf.destroyed {
171+ return ports.OutputFormats{}, false
172+ }
173+ root := surf.root()
174+ if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen {
175+ return ports.OutputFormats{}, false
176+ }
177+ out, ok := g.scanout[root.xdg.window.last.Output]
178+ return out, ok && len(out.Formats) > 0
179+}
180+
181+// setOutputFormats records an output's scanout formats and sends the
182+// feedback of the surfaces fullscreen on it again. Outputs offer only
183+// formats the renderer samples (a refused buffer is composed), so they
184+// are all in the table.
185+func (g *dmabufGlobal) setOutputFormats(f ports.OutputFormats) {
186+ if old, ok := g.scanout[f.Output]; ok && old.Device == f.Device && slices.Equal(old.Formats, f.Formats) {
187+ return
188+ }
189+ if len(f.Formats) == 0 {
190+ delete(g.scanout, f.Output)
191+ } else {
192+ g.scanout[f.Output] = f
193+ }
194+ for fb, surf := range g.feedbacks {
195+ if surf != nil && !surf.destroyed {
196+ if root := surf.root(); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Fullscreen && root.xdg.window.last.Output == f.Output {
197+ g.send(fb, surf)
198+ }
199+ }
200+ }
201+}
202+
203+// resendSurface sends the feedback of a window's surfaces again, when its
204+// fullscreen state or output changed.
205+func (g *dmabufGlobal) resendSurface(root *surface) {
206+ if g == nil {
207+ return
208+ }
209+ for fb, surf := range g.feedbacks {
210+ if surf != nil && surf.root() == root {
211+ g.send(fb, surf)
212+ }
213+ }
214+}
215+
130216 type feedback struct{}
131217
132218 func (feedback) Destroy(*linuxdmabuf.ZwpLinuxDmabufFeedbackV1) {}
M internal/adapters/wayland/dmabuf_test.go +115 -0
@@ -172,3 +172,118 @@ func TestWestonDMABuf(t *testing.T) {
172172 }
173173 }
174174 }
175+
176+// feedbackProxy records the tranches of each feedback round (done).
177+type feedbackProxy struct {
178+ wlturbo.BaseProxy
179+ rounds chan []tranche
180+ cur []tranche
181+ open tranche
182+}
183+
184+type tranche struct {
185+ flags uint32
186+ indices []uint16
187+}
188+
189+func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {
190+ switch uint32(e.Opcode) {
191+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable:
192+ if fd := e.Fd(); fd != 0 {
193+ unix.Close(int(fd))
194+ }
195+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags:
196+ p.open.flags = e.Uint32()
197+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats:
198+ a := e.Array()
199+ for i := 0; i+1 < len(a); i += 2 {
200+ p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8)
201+ }
202+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheDone:
203+ p.cur, p.open = append(p.cur, p.open), tranche{}
204+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone:
205+ p.rounds <- p.cur
206+ p.cur = nil
207+ }
208+}
209+
210+// A fullscreen surface on an output with scanout formats gets a scanout
211+// tranche first; leaving fullscreen sends the renderer tranche alone.
212+func TestDMABufScanoutTranche(t *testing.T) {
213+ tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
214+ sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
215+ dir := t.TempDir()
216+ events := make(chan ports.ClientEvent, 16)
217+ commands := make(chan ports.ClientCommand, 16)
218+ formats := make(chan ports.OutputFormats, 1)
219+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: commands, OutputFormats: formats}, logging.For(context.Background(), "wayland"))
220+ if err != nil {
221+ t.Fatal(err)
222+ }
223+ ctx, cancel := context.WithCancel(context.Background())
224+ done := make(chan error, 1)
225+ go func() { done <- s.Run(ctx) }()
226+ t.Cleanup(func() { cancel(); <-done })
227+ c := protocolClient(t, s, dir)
228+ w, surf, xdg := surfaceMapper(t, c, events)()
229+ // Our configures must not block the mapper's one-slot proxy.
230+ registerProtocol(t, c, xdg)
231+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
232+ fb := c.AllocateID()
233+ proxy := &feedbackProxy{rounds: make(chan []tranche, 8)}
234+ proxy.SetID(fb)
235+ c.Context().Register(proxy)
236+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, fb, surf)
237+ round := func(what string) []tranche {
238+ t.Helper()
239+ deadline := time.After(2 * time.Second)
240+ for {
241+ if err := c.Roundtrip(); err != nil {
242+ t.Fatal(err)
243+ }
244+ select {
245+ case r := <-proxy.rounds:
246+ return r
247+ case <-deadline:
248+ t.Fatalf("no feedback: %s", what)
249+ default:
250+ time.Sleep(5 * time.Millisecond)
251+ }
252+ }
253+ }
254+ scanout := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout)
255+ if r := round("initial"); len(r) != 1 || r[0].flags != 0 || len(r[0].indices) != 2 {
256+ t.Fatalf("tiled window: %+v", r)
257+ }
258+ formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
259+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"}
260+ r := round("fullscreen")
261+ for len(r) == 1 { // the formats may land after the configure
262+ r = round("fullscreen with formats")
263+ }
264+ if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 {
265+ t.Fatalf("fullscreen: %+v", r)
266+ }
267+ // Fullscreen on another output: its formats (none) apply at once.
268+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "OTHER-1"}
269+ if r := round("other output"); len(r) != 1 {
270+ t.Fatalf("moved output: %+v", r)
271+ }
272+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1"}
273+ if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 {
274+ t.Fatalf("left fullscreen: %+v", r)
275+ }
276+ // A destroyed feedback gets nothing more (a protocol error would
277+ // kill the connection) and leaves the server's list.
278+ requestProtocol(t, c, fb, linuxdmabuf.ZwpLinuxDmabufFeedbackV1RequestDestroy)
279+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"}
280+ formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7}
281+ if err := c.Roundtrip(); err != nil {
282+ t.Fatal(err)
283+ }
284+ var n int
285+ s.display.Do(func() { n = len(s.dmabuf.feedbacks) })
286+ if n != 0 {
287+ t.Fatalf("%d feedbacks kept", n)
288+ }
289+}
M internal/adapters/wayland/fifo.go +42 -5
@@ -7,6 +7,7 @@ import (
77 "github.com/bnema/purego-libwayland/protocol/committiming"
88 "github.com/bnema/purego-libwayland/protocol/contenttype"
99 "github.com/bnema/purego-libwayland/protocol/fifo"
10+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
1011 "github.com/bnema/purego-libwayland/protocol/wayland"
1112 "github.com/bnema/purego-libwayland/server"
1213 "golang.org/x/sys/unix"
@@ -24,7 +25,7 @@ import (
2425 // queued; the pacer (frames.go) applies queued updates in order when they
2526 // are ready. wp_content_type_v1 is recorded and logged only.
2627
27-func registerPresentation(d *server.Display, s *Server) error {
28+func registerPresentationConstraints(d *server.Display, s *Server) error {
2829 if err := fifo.NewWpFifoManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
2930 _, _ = fifo.NewWpFifoManagerV1(c, int32(v), id, fifoManager{s})
3031 }); err != nil {
@@ -216,6 +217,10 @@ type update struct {
216217 barrier bool
217218 wait bool
218219 at time.Time
220+ feedback []*presentationtime.WpPresentationFeedback
221+ sync *commitSync
222+ damage []ports.Rect
223+ bufDamage []ports.Rect
219224 }
220225
221226 type childMove struct {
@@ -226,8 +231,10 @@ type childMove struct {
226231 // takePending moves the pending state into an update: one-shot state is
227232 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
228233 func (s *surface) takePending() update {
229- u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime}
234+ u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback, sync: s.pendingSync}
230235 s.attached, s.pending, s.callbacks = false, nil, nil
236+ s.pendingFeedback, s.pendingSync = nil, nil
237+ s.pendingDamage, s.pendingBufDamage = nil, nil
231238 s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
232239 if v := s.viewport; v != nil {
233240 u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet
@@ -256,6 +263,8 @@ func (s *surface) putPending(u update) {
256263 s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks
257264 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
258265 s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at
266+ s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage
267+ s.pendingFeedback, s.pendingSync = u.feedback, u.sync
259268 if u.vp != nil && s.viewport == u.vp {
260269 u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet
261270 }
@@ -277,13 +286,31 @@ func (s *surface) putPending(u update) {
277286
278287 // mustWait reports whether the pending commit cannot apply now.
279288 func (s *surface) mustWait(now time.Time) bool {
280- return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now)
289+ return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now) || !s.syncReady(s.pendingSync)
281290 }
282291
283292 // tooEarly reports whether content applied now would show before at:
284293 // it reaches the screen one refresh later.
285294 func (s *surface) tooEarly(at, now time.Time) bool {
286- return !at.IsZero() && at.After(now.Add(s.server.outputPeriod(s.server.fifoOutput(s))))
295+ return !at.IsZero() && at.After(s.server.nextRefresh(s.server.fifoOutput(s), now))
296+}
297+
298+// nextRefresh is when the output's next frame reaches the screen: the
299+// vblank after now counted from its last flip (kernel time, refresh
300+// period), or one period from now without flips or while VRR is on.
301+func (s *Server) nextRefresh(name string, now time.Time) time.Time {
302+ p := s.outputPeriod(name)
303+ f, ok := s.flips[name]
304+ if !ok || f.When <= 0 || f.Refresh <= 0 {
305+ return now.Add(p)
306+ }
307+ mono := monotonic(now)
308+ since := mono - f.When
309+ if since < 0 || since > time.Second {
310+ return now.Add(p)
311+ }
312+ n := since/f.Refresh + 1
313+ return now.Add(f.When + n*f.Refresh - mono)
287314 }
288315
289316 // queueUpdate queues the pending commit until it can apply.
@@ -318,6 +345,13 @@ func (s *surface) dropQueue() {
318345 } else if len(u.callbacks) > 0 {
319346 s.server.queueFrames(s.server.frameOutput(s), u.callbacks)
320347 }
348+ for _, fb := range u.feedback {
349+ discard(fb)
350+ }
351+ s.dropSync(u.sync)
352+ if u.sync != nil {
353+ continue // explicit sync: the release point replaced release
354+ }
321355 if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
322356 released[u.buffer.Resource] = true
323357 u.buffer.SendRelease()
@@ -384,8 +418,11 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
384418 if u.wait && surf.barrier {
385419 break
386420 }
421+ if !surf.syncReady(u.sync) {
422+ break
423+ }
387424 if surf.tooEarly(u.at, now) {
388- wait = min(wait, u.at.Add(-p).Sub(now))
425+ wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond)
389426 break
390427 }
391428 surf.queue = surf.queue[1:]
M internal/adapters/wayland/frames.go +44 -5
@@ -2,6 +2,7 @@ package wayland
22
33 import (
44 "context"
5+ "golang.org/x/sys/unix"
56 "slices"
67 "time"
78
@@ -34,7 +35,7 @@ func (s *Server) pace(ctx context.Context) {
3435 case <-s.display.Stopped():
3536 return
3637 case p := <-presented:
37- flipped[p.Output] = flipped[p.Output] || p.Flip
38+ flipped[p.Output] = flipped[p.Output] || paces(p)
3839 reports = append(reports, p)
3940 case <-s.frameReady:
4041 case <-timer.C:
@@ -45,7 +46,7 @@ func (s *Server) pace(ctx context.Context) {
4546 for {
4647 select {
4748 case p := <-presented:
48- flipped[p.Output] = flipped[p.Output] || p.Flip
49+ flipped[p.Output] = flipped[p.Output] || paces(p)
4950 reports = append(reports, p)
5051 default:
5152 break drain
@@ -54,10 +55,28 @@ func (s *Server) pace(ctx context.Context) {
5455 wait, idle := defaultFramePeriod, false
5556 if !s.display.Do(func() {
5657 if ctx.Err() == nil {
58+ // Each flip answers its own presentation feedbacks, in
59+ // order: flips are never merged here.
60+ for _, p := range reports {
61+ if p.Flip != nil {
62+ s.flips[p.Output] = *p.Flip
63+ s.presentFlip(p.Output, p.Flip)
64+ }
65+ }
66+ s.dropFeedbacks(time.Now())
67+ if len(s.feedbacks) > 0 {
68+ wait = feedbackTimeout
69+ }
5770 var due []string
58- due, wait, idle = s.dueFrames(time.Now(), flipped)
71+ var dueWait time.Duration
72+ due, dueWait, idle = s.dueFrames(time.Now(), flipped)
73+ if len(s.feedbacks) > 0 {
74+ // Unanswered feedbacks time out: keep ticking.
75+ dueWait, idle = min(dueWait, feedbackTimeout), false
76+ }
77+ wait = dueWait
5978 for _, name := range due {
60- s.sendFrames(name)
79+ s.sendFrames(name, flipped[name])
6180 }
6281 if s.releaseHeld(time.Now(), reports) && idle {
6382 // Held buffers wait for a flip or heldTimeout.
@@ -167,13 +186,24 @@ func (s *Server) dueFrames(now time.Time, flipped map[string]bool) (fire []strin
167186 return fire, max(wait, time.Millisecond), !waiting
168187 }
169188
170-func (s *Server) sendFrames(name string) {
189+// paces reports whether a report paces frame callbacks: a flip of a real
190+// display. Software flips (headless) answer presentation feedback, but
191+// callbacks keep the output's refresh period.
192+func paces(p ports.OutputPresented) bool { return p.Flip != nil && p.Flip.HardwareClock }
193+
194+// sendFrames fires an output's callbacks; flipped: a flip of it fired them.
195+func (s *Server) sendFrames(name string, flipped bool) {
171196 callbacks := s.awaiting[name]
172197 delete(s.awaiting, name)
173198 if len(callbacks) == 0 {
174199 return
175200 }
201+ // done carries the flip's time when the output flips (the frame the
202+ // client drew for is on screen), else now.
176203 ms := uint32(time.Since(s.started).Milliseconds())
204+ if f, ok := s.flips[name]; ok && flipped && f.When > 0 {
205+ ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds())
206+ }
177207 for _, cb := range callbacks {
178208 if !cb.Resource.Alive() {
179209 continue
@@ -191,3 +221,12 @@ func framePeriod(refreshMilli int) time.Duration {
191221 }
192222 return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli))
193223 }
224+
225+// monotonic is t on CLOCK_MONOTONIC, the clock of flip timestamps.
226+func monotonic(t time.Time) time.Duration {
227+ var ts unix.Timespec
228+ if unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) != nil {
229+ return 0
230+ }
231+ return time.Duration(ts.Nano()) - time.Since(t)
232+}
M internal/adapters/wayland/globals.go +3 -0
@@ -18,7 +18,10 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
1818 func() error { return registerDMABuf(d, s, o.DMABuf) },
1919 func() error { return registerPointerConstraints(d, s) },
2020 func() error { return registerTearing(d, s) },
21+ func() error { return registerPresentationConstraints(d, s) },
2122 func() error { return registerPresentation(d, s) },
23+ func() error { return registerSyncobj(d, s) },
24+ func() error { return registerInhibit(d, s) },
2225 func() error { return registerActivation(d, s) },
2326 func() error { return registerVirtualKeyboard(d, s) },
2427 func() error {
A internal/adapters/wayland/inhibit.go +163 -0
@@ -0,0 +1,163 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/nefertty/internal/ports"
5+ "github.com/bnema/purego-libwayland/protocol/idleinhibit"
6+ "github.com/bnema/purego-libwayland/protocol/keyboardshortcutsinhibit"
7+ "github.com/bnema/purego-libwayland/protocol/wayland"
8+ "github.com/bnema/purego-libwayland/server"
9+)
10+
11+// Inhibitors. keyboard-shortcuts-inhibit: a window asks for every key,
12+// compositor binds included, while it has keyboard focus; core decides
13+// when it is in effect (ports.ShortcutsInhibitState) and the client hears
14+// active/inactive. idle-inhibit: a window keeps the session awake while
15+// it is mapped. Both are reported to core per window.
16+
17+// inhibitor is one inhibitor object on a surface.
18+type inhibitor struct {
19+ surf *surface
20+ shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle
21+ window ports.WindowID // reported to core (0: not yet)
22+ active bool // shortcuts: last state sent
23+}
24+
25+func registerInhibit(d *server.Display, s *Server) error {
26+ if err := keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
27+ _, _ = keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitManagerV1(c, int32(v), id, shortcutsManager{s})
28+ }); err != nil {
29+ return err
30+ }
31+ return idleinhibit.NewZwpIdleInhibitManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
32+ _, _ = idleinhibit.NewZwpIdleInhibitManagerV1(c, int32(v), id, idleManager{s})
33+ })
34+}
35+
36+type shortcutsManager struct{ server *Server }
37+
38+func (shortcutsManager) Destroy(*keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1) {}
39+
40+// InhibitShortcuts adds a surface's inhibitor; one per surface and seat
41+// (there is one seat).
42+func (m shortcutsManager) InhibitShortcuts(r *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1, id uint32, surf *wayland.Surface, _ *wayland.Seat) {
43+ state := m.server.surfaceOf(surf)
44+ if state == nil {
45+ return
46+ }
47+ for _, in := range m.server.inhibitors {
48+ if in.surf == state && in.shortcut != nil {
49+ r.PostError(uint32(keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited), "surface already inhibits shortcuts")
50+ return
51+ }
52+ }
53+ res, err := keyboardshortcutsinhibit.NewZwpKeyboardShortcutsInhibitorV1(r.Client(), r.Version(), id, shortcutsInhibitor{})
54+ if err != nil {
55+ return
56+ }
57+ in := &inhibitor{surf: state, shortcut: res}
58+ m.server.addInhibitor(in)
59+ res.OnDestroy = func() { m.server.removeInhibitor(in) }
60+}
61+
62+type shortcutsInhibitor struct{}
63+
64+func (shortcutsInhibitor) Destroy(*keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1) {}
65+
66+type idleManager struct{ server *Server }
67+
68+func (idleManager) Destroy(*idleinhibit.ZwpIdleInhibitManagerV1) {}
69+
70+// CreateInhibitor adds an idle inhibitor; a surface may have several.
71+func (m idleManager) CreateInhibitor(r *idleinhibit.ZwpIdleInhibitManagerV1, id uint32, surf *wayland.Surface) {
72+ state := m.server.surfaceOf(surf)
73+ if state == nil {
74+ return
75+ }
76+ res, err := idleinhibit.NewZwpIdleInhibitorV1(r.Client(), r.Version(), id, idleInhibitor{})
77+ if err != nil {
78+ return
79+ }
80+ in := &inhibitor{surf: state}
81+ m.server.addInhibitor(in)
82+ res.OnDestroy = func() { m.server.removeInhibitor(in) }
83+}
84+
85+type idleInhibitor struct{}
86+
87+func (idleInhibitor) Destroy(*idleinhibit.ZwpIdleInhibitorV1) {}
88+
89+func (s *Server) addInhibitor(in *inhibitor) {
90+ s.inhibitors = append(s.inhibitors, in)
91+ s.syncInhibitors()
92+}
93+
94+func (s *Server) removeInhibitor(in *inhibitor) {
95+ for i, x := range s.inhibitors {
96+ if x == in {
97+ s.inhibitors = append(s.inhibitors[:i], s.inhibitors[i+1:]...)
98+ break
99+ }
100+ }
101+ in.surf = nil
102+ s.syncInhibitors()
103+}
104+
105+// syncInhibitors tells core, per window, whether it has inhibitors of
106+// each kind, after any change: an inhibitor added or removed, a window
107+// mapped or unmapped.
108+func (s *Server) syncInhibitors() {
109+ shortcuts, idle := map[ports.WindowID]bool{}, map[ports.WindowID]bool{}
110+ for _, in := range s.inhibitors {
111+ in.window = 0
112+ if in.surf != nil && !in.surf.destroyed {
113+ in.window = in.surf.root().windowID()
114+ }
115+ if in.window == 0 {
116+ continue
117+ }
118+ if in.shortcut != nil {
119+ shortcuts[in.window] = true
120+ } else {
121+ idle[in.window] = true
122+ }
123+ }
124+ for id, on := range diffSets(s.shortcutWindows, shortcuts) {
125+ s.emit(ports.ShortcutsInhibit{Window: id, Active: on})
126+ }
127+ for id, on := range diffSets(s.idleWindows, idle) {
128+ s.emit(ports.IdleInhibit{Window: id, Active: on})
129+ }
130+ s.shortcutWindows, s.idleWindows = shortcuts, idle
131+}
132+
133+// diffSets is the membership changes from old to cur.
134+func diffSets(old, cur map[ports.WindowID]bool) map[ports.WindowID]bool {
135+ out := map[ports.WindowID]bool{}
136+ for id := range cur {
137+ if !old[id] {
138+ out[id] = true
139+ }
140+ }
141+ for id := range old {
142+ if !cur[id] {
143+ out[id] = false
144+ }
145+ }
146+ return out
147+}
148+
149+// setShortcutsInhibit applies core's decision to a window's shortcuts
150+// inhibitor.
151+func (s *Server) setShortcutsInhibit(c ports.ShortcutsInhibitState) {
152+ for _, in := range s.inhibitors {
153+ if in.shortcut == nil || in.window != c.Window || in.active == c.Active || !in.shortcut.Resource.Alive() {
154+ continue
155+ }
156+ in.active = c.Active
157+ if c.Active {
158+ in.shortcut.SendActive()
159+ } else {
160+ in.shortcut.SendInactive()
161+ }
162+ }
163+}
A internal/adapters/wayland/inhibit_test.go +118 -0
@@ -0,0 +1,118 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/idleinhibit"
9+ "github.com/bnema/purego-libwayland/protocol/keyboardshortcutsinhibit"
10+ "github.com/bnema/wlturbo"
11+)
12+
13+type inhibitorProxy struct {
14+ wlturbo.BaseProxy
15+ states chan bool
16+}
17+
18+func (p *inhibitorProxy) Dispatch(e *wlturbo.Event) {
19+ p.states <- uint32(e.Opcode) == keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1EventActive
20+}
21+
22+// clientEvent waits for the next client event of type T.
23+func clientEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T {
24+ t.Helper()
25+ deadline := time.After(2 * time.Second)
26+ for {
27+ select {
28+ case ev := <-events:
29+ if v, ok := ev.(T); ok {
30+ return v
31+ }
32+ case <-deadline:
33+ var zero T
34+ t.Fatalf("no %T", zero)
35+ return zero
36+ }
37+ }
38+}
39+
40+func TestShortcutsInhibitor(t *testing.T) {
41+ s, events, commands, _, dir := contentServer(t)
42+ c := protocolClient(t, s, dir)
43+ w, surf, _ := surfaceMapper(t, c, events)()
44+ seat := bindProtocol(t, c, "wl_seat")
45+ registerProtocol(t, c, seat)
46+ mgr := bindProtocol(t, c, "zwp_keyboard_shortcuts_inhibit_manager_v1")
47+ in := c.AllocateID()
48+ p := &inhibitorProxy{states: make(chan bool, 4)}
49+ p.SetID(in)
50+ c.Context().Register(p)
51+ requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, in, surf, seat)
52+ if v := clientEvent[ports.ShortcutsInhibit](t, events); v != (ports.ShortcutsInhibit{Window: w.ID, Active: true}) {
53+ t.Fatal(v)
54+ }
55+ // Core decides: active, then inactive.
56+ state := func(active bool) {
57+ t.Helper()
58+ commands <- ports.ShortcutsInhibitState{Window: w.ID, Active: active}
59+ deadline := time.Now().Add(2 * time.Second)
60+ for time.Now().Before(deadline) {
61+ if err := c.Roundtrip(); err != nil {
62+ t.Fatal(err)
63+ }
64+ select {
65+ case got := <-p.states:
66+ if got != active {
67+ t.Fatalf("state %v", got)
68+ }
69+ return
70+ case <-time.After(5 * time.Millisecond):
71+ }
72+ }
73+ t.Fatal("no active/inactive event")
74+ }
75+ state(true)
76+ state(false)
77+ // Destroying it tells core.
78+ requestProtocol(t, c, in, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1RequestDestroy)
79+ if v := clientEvent[ports.ShortcutsInhibit](t, events); v.Active {
80+ t.Fatal(v)
81+ }
82+ // A second inhibitor on the same surface is a protocol error.
83+ requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, c.AllocateID(), surf, seat)
84+ requestProtocol(t, c, mgr, keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1RequestInhibitShortcuts, c.AllocateID(), surf, seat)
85+ expectProtocolError(t, c, mgr, uint32(keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited))
86+}
87+
88+// Idle inhibitors: several per surface, the window is reported once and
89+// released when the last one goes.
90+func TestIdleInhibitors(t *testing.T) {
91+ s, events, _, _, dir := contentServer(t)
92+ c := protocolClient(t, s, dir)
93+ w, surf, _ := surfaceMapper(t, c, events)()
94+ mgr := bindProtocol(t, c, "zwp_idle_inhibit_manager_v1")
95+ a, b := c.AllocateID(), c.AllocateID()
96+ registerProtocol(t, c, a)
97+ registerProtocol(t, c, b)
98+ requestProtocol(t, c, mgr, idleinhibit.ZwpIdleInhibitManagerV1RequestCreateInhibitor, a, surf)
99+ requestProtocol(t, c, mgr, idleinhibit.ZwpIdleInhibitManagerV1RequestCreateInhibitor, b, surf)
100+ if v := clientEvent[ports.IdleInhibit](t, events); v != (ports.IdleInhibit{Window: w.ID, Active: true}) {
101+ t.Fatal(v)
102+ }
103+ requestProtocol(t, c, a, idleinhibit.ZwpIdleInhibitorV1RequestDestroy)
104+ requestProtocol(t, c, b, idleinhibit.ZwpIdleInhibitorV1RequestDestroy)
105+ if v := clientEvent[ports.IdleInhibit](t, events); v.Active {
106+ t.Fatal(v)
107+ }
108+ if err := c.Roundtrip(); err != nil {
109+ t.Fatal(err)
110+ }
111+ select {
112+ case ev := <-events:
113+ if _, ok := ev.(ports.IdleInhibit); ok {
114+ t.Fatalf("extra %+v", ev)
115+ }
116+ default:
117+ }
118+}
M internal/adapters/wayland/layer.go +1 -1
@@ -167,7 +167,7 @@ func (l *layerSurface) unmap() {
167167 l.acked = false
168168 l.serials = nil
169169 l.shell.server.layerChanged()
170- l.shell.server.emitContent(ports.SurfaceContent{ID: l.id})
170+ l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}, damage{full: true})
171171 }
172172
173173 // close tells the client its output is gone; the surface stays unmapped
A internal/adapters/wayland/presentation.go +193 -0
@@ -0,0 +1,193 @@
1+package wayland
2+
3+import (
4+ "time"
5+
6+ "github.com/bnema/nefertty/internal/ports"
7+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
8+ "github.com/bnema/purego-libwayland/protocol/wayland"
9+ "github.com/bnema/purego-libwayland/server"
10+ "golang.org/x/sys/unix"
11+)
12+
13+// wp_presentation: a client learns when each commit reached the screen.
14+// A feedback belongs to the commit it was requested before; on commit it
15+// waits for the flip that shows its window's content (FlipInfo.Shows)
16+// and gets that flip's kernel timestamp, vblank counter and refresh.
17+// A content replaced before any flip showed it, a destroyed surface or a
18+// surface on no output is discarded.
19+
20+const presentationVersion = 2
21+
22+// feedbackWait is a committed feedback: presented once a flip on the
23+// surface's output shows content seq of window win.
24+type feedbackWait struct {
25+ fb *presentationtime.WpPresentationFeedback
26+ surf *surface
27+ win ports.WindowID
28+ seq uint64
29+ at time.Time
30+ // replaced is the window content Seq of the surface's next commit (0:
31+ // none yet): a flip showing it or later shows the replacement.
32+ replaced uint64
33+}
34+
35+func registerPresentation(d *server.Display, s *Server) error {
36+ return presentationtime.NewWpPresentationGlobal(d, presentationVersion, func(c server.Client, v, id uint32) {
37+ r, err := presentationtime.NewWpPresentation(c, int32(v), id, presentation{s})
38+ if err == nil {
39+ r.SendClockId(unix.CLOCK_MONOTONIC)
40+ }
41+ })
42+}
43+
44+type presentation struct{ server *Server }
45+
46+func (presentation) Destroy(*presentationtime.WpPresentation) {}
47+
48+// Feedback attaches a feedback to the surface's next commit.
49+func (p presentation) Feedback(r *presentationtime.WpPresentation, surf *wayland.Surface, id uint32) {
50+ fb, err := presentationtime.NewWpPresentationFeedback(r.Client(), r.Version(), id, feedbackHandler{})
51+ if err != nil {
52+ return
53+ }
54+ state := p.server.surfaceOf(surf)
55+ if state == nil || state.destroyed {
56+ fb.SendDiscarded()
57+ fb.Destroy()
58+ return
59+ }
60+ state.pendingFeedback = append(state.pendingFeedback, fb)
61+}
62+
63+type feedbackHandler struct{}
64+
65+// Destroy is not a request of wp_presentation_feedback; the handler
66+// satisfies the generated interface.
67+func (feedbackHandler) Destroy(*presentationtime.WpPresentationFeedback) {}
68+
69+// commitFeedback moves a surface's pending feedbacks to its window's
70+// content just emitted, or discards them when the surface shows nowhere.
71+// A commit with a new buffer (fresh) replaces the surface's earlier
72+// waiting feedbacks.
73+func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedback, fresh bool) {
74+ srv := s.server
75+ root := s.root()
76+ win := root.windowID()
77+ if win != 0 && fresh {
78+ srv.contentMu.Lock()
79+ seq := srv.contentSeq[win]
80+ srv.contentMu.Unlock()
81+ for i := range srv.feedbacks {
82+ if w := &srv.feedbacks[i]; w.surf == s && w.replaced == 0 && seq > w.seq {
83+ w.replaced = seq
84+ }
85+ }
86+ }
87+ if len(pending) == 0 {
88+ return
89+ }
90+ output := srv.frameOutput(s)
91+ if win == 0 || output == "" {
92+ for _, fb := range pending {
93+ discard(fb)
94+ }
95+ return
96+ }
97+ srv.contentMu.Lock()
98+ seq := srv.contentSeq[win]
99+ srv.contentMu.Unlock()
100+ for _, fb := range pending {
101+ srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()})
102+ }
103+}
104+
105+func discard(fb *presentationtime.WpPresentationFeedback) {
106+ if fb.Resource.Alive() {
107+ fb.SendDiscarded()
108+ fb.Destroy()
109+ }
110+}
111+
112+// presentFlip answers the feedbacks a flip of output answers: a window
113+// content at or after a feedback's is shown; the feedback is presented,
114+// or discarded when its surface committed again by then.
115+func (s *Server) presentFlip(output string, f *ports.FlipInfo) {
116+ if f == nil || len(s.feedbacks) == 0 {
117+ return
118+ }
119+ o := s.outputByNameExact(output)
120+ kept := s.feedbacks[:0]
121+ for _, w := range s.feedbacks {
122+ shown, ok := f.Shows[w.win]
123+ switch {
124+ case !w.fb.Resource.Alive():
125+ case w.surf.destroyed:
126+ discard(w.fb)
127+ case s.frameOutput(w.surf) != output || !ok || shown < w.seq:
128+ kept = append(kept, w)
129+ case f.Merged > 0:
130+ // The reader fell behind: which flip showed it is unknown.
131+ discard(w.fb)
132+ default:
133+ // Shown, unless the surface's next commit is shown too.
134+ s.sendPresented(w, o, f, w.replaced == 0 || shown < w.replaced)
135+ }
136+ }
137+ clear(s.feedbacks[len(kept):])
138+ s.feedbacks = kept
139+}
140+
141+// sendPresented sends presented for the flip; exact is false when a
142+// later commit of the surface replaced it before the flip (then it is
143+// discarded).
144+func (s *Server) sendPresented(w feedbackWait, o *output, f *ports.FlipInfo, exact bool) {
145+ if !exact {
146+ discard(w.fb)
147+ return
148+ }
149+ if o != nil {
150+ for _, r := range o.resources {
151+ if r.Resource.Alive() && r.Client() == w.fb.Client() {
152+ w.fb.SendSyncOutput(r)
153+ }
154+ }
155+ }
156+ flags := uint32(0)
157+ // A software clock (headless) claims nothing about the display.
158+ if !f.Async && f.HardwareClock {
159+ flags |= uint32(presentationtime.WpPresentationFeedbackKindVsync)
160+ }
161+ if f.HardwareClock {
162+ flags |= uint32(presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion)
163+ }
164+ if f.ZeroCopy != 0 && f.ZeroCopy == w.win {
165+ flags |= uint32(presentationtime.WpPresentationFeedbackKindZeroCopy)
166+ }
167+ sec, nsec := uint64(f.When/time.Second), uint32(f.When%time.Second)
168+ w.fb.SendPresented(uint32(sec>>32), uint32(sec), nsec, uint32(f.Refresh), uint32(f.Seq>>32), uint32(f.Seq), flags)
169+ w.fb.Destroy()
170+}
171+
172+// feedbackTimeout discards a feedback no flip answered: its window is
173+// hidden, on an output that does not flip (headless, switched away), or
174+// was never drawn.
175+const feedbackTimeout = time.Second
176+
177+// dropFeedbacks discards the feedbacks of destroyed surfaces, of outputs
178+// that are gone, and those older than feedbackTimeout.
179+func (s *Server) dropFeedbacks(now time.Time) {
180+ kept := s.feedbacks[:0]
181+ for _, w := range s.feedbacks {
182+ if !w.fb.Resource.Alive() {
183+ continue
184+ }
185+ if w.surf.destroyed || s.frameOutput(w.surf) == "" || now.Sub(w.at) > feedbackTimeout {
186+ discard(w.fb)
187+ continue
188+ }
189+ kept = append(kept, w)
190+ }
191+ clear(s.feedbacks[len(kept):])
192+ s.feedbacks = kept
193+}
A internal/adapters/wayland/presentation_test.go +284 -0
@@ -0,0 +1,284 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/nefertty/internal/logging"
10+ "github.com/bnema/nefertty/internal/ports"
11+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
12+ "github.com/bnema/purego-libwayland/protocol/wayland"
13+ "github.com/bnema/wlturbo"
14+ "golang.org/x/sys/unix"
15+)
16+
17+// presentedEvent is one feedback answer: presented (with its fields) or
18+// discarded.
19+type presentedEvent struct {
20+ discarded bool
21+ when time.Duration
22+ refresh uint32
23+ seq uint64
24+ flags uint32
25+ syncOutputs int
26+}
27+
28+type feedbackEvents struct {
29+ wlturbo.BaseProxy
30+ out chan presentedEvent
31+ sync int
32+}
33+
34+func (p *feedbackEvents) Dispatch(e *wlturbo.Event) {
35+ switch uint32(e.Opcode) {
36+ case presentationtime.WpPresentationFeedbackEventSyncOutput:
37+ p.sync++
38+ case presentationtime.WpPresentationFeedbackEventPresented:
39+ d := e.Data()
40+ u := func(i int) uint32 { return binary.NativeEndian.Uint32(d[i*4:]) }
41+ sec := uint64(u(0))<<32 | uint64(u(1))
42+ p.out <- presentedEvent{when: time.Duration(sec)*time.Second + time.Duration(u(2)), refresh: u(3), seq: uint64(u(4))<<32 | uint64(u(5)), flags: u(6), syncOutputs: p.sync}
43+ case presentationtime.WpPresentationFeedbackEventDiscarded:
44+ p.out <- presentedEvent{discarded: true}
45+ }
46+}
47+
48+// presentServer is a server whose flips the test sends.
49+func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, chan ports.OutputPresented, string) {
50+ t.Helper()
51+ dir := t.TempDir()
52+ events := make(chan ports.ClientEvent, 16)
53+ commands := make(chan ports.ClientCommand, 16)
54+ contents := make(chan ports.SurfaceContent, 64)
55+ presented := make(chan ports.OutputPresented, 16)
56+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents, Presented: presented}, logging.For(context.Background(), "wayland"))
57+ if err != nil {
58+ t.Fatal(err)
59+ }
60+ ctx, cancel := context.WithCancel(context.Background())
61+ done := make(chan error, 1)
62+ go func() { done <- s.Run(ctx) }()
63+ t.Cleanup(func() { cancel(); <-done })
64+ return s, events, commands, contents, presented, dir
65+}
66+
67+func TestPresentationFeedback(t *testing.T) {
68+ s, events, commands, contents, presented, dir := presentServer(t)
69+ c := protocolClient(t, s, dir)
70+ w, surf, xdg := surfaceMapper(t, c, events)()
71+ registerProtocol(t, c, xdg)
72+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1"}
73+ for deadline := time.Now().Add(2 * time.Second); ; {
74+ var out string
75+ s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) })
76+ if out != "" {
77+ break
78+ }
79+ if time.Now().After(deadline) {
80+ t.Fatal("configure not applied")
81+ }
82+ time.Sleep(2 * time.Millisecond)
83+ }
84+ registerProtocol(t, c, bindProtocol(t, c, "wl_output"))
85+ pres := bindProtocol(t, c, "wp_presentation")
86+ registerProtocol(t, c, pres)
87+ // Drain contents so seqs are known.
88+ lastSeq := func() uint64 {
89+ t.Helper()
90+ var seq uint64
91+ deadline := time.After(2 * time.Second)
92+ for {
93+ select {
94+ case got := <-contents:
95+ if got.ID == w.ID {
96+ seq = got.Seq
97+ }
98+ case <-time.After(50 * time.Millisecond):
99+ if seq != 0 {
100+ return seq
101+ }
102+ case <-deadline:
103+ t.Fatal("no content")
104+ }
105+ }
106+ }
107+ feedback := func() *feedbackEvents {
108+ t.Helper()
109+ fb := c.AllocateID()
110+ p := &feedbackEvents{out: make(chan presentedEvent, 4)}
111+ p.SetID(fb)
112+ c.Context().Register(p)
113+ requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb)
114+ return p
115+ }
116+ answer := func(p *feedbackEvents, what string) presentedEvent {
117+ t.Helper()
118+ deadline := time.Now().Add(2 * time.Second)
119+ for time.Now().Before(deadline) {
120+ if err := c.Roundtrip(); err != nil {
121+ t.Fatal(err)
122+ }
123+ select {
124+ case ev := <-p.out:
125+ return ev
126+ case <-time.After(5 * time.Millisecond):
127+ }
128+ }
129+ t.Fatalf("no answer: %s", what)
130+ return presentedEvent{}
131+ }
132+ // A fresh 1×1 buffer per commit: every commit is a new content.
133+ shm := bindProtocol(t, c, "wl_shm")
134+ registerProtocol(t, c, shm)
135+ fd, err := unix.MemfdCreate("present", 0)
136+ if err != nil {
137+ t.Fatal(err)
138+ }
139+ defer unix.Close(fd)
140+ if err := unix.Ftruncate(fd, 4); err != nil {
141+ t.Fatal(err)
142+ }
143+ pool := c.AllocateID()
144+ registerProtocol(t, c, pool)
145+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
146+ t.Fatal(err)
147+ }
148+ commit := func() {
149+ buf := c.AllocateID()
150+ registerProtocol(t, c, buf)
151+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
152+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
153+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
154+ if err := c.Roundtrip(); err != nil {
155+ t.Fatal(err)
156+ }
157+ }
158+ flip := func(when time.Duration, seq uint64, shows uint64, zero ports.WindowID) {
159+ presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}}
160+ }
161+ // Presented with the flip's time, counter and flags.
162+ a := feedback()
163+ commit()
164+ seqA := lastSeq()
165+ flip(5*time.Second+7, 42, seqA, w.ID)
166+ ev := answer(a, "presented")
167+ if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 {
168+ t.Fatalf("presented %+v", ev)
169+ }
170+ want := uint32(presentationtime.WpPresentationFeedbackKindVsync | presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion | presentationtime.WpPresentationFeedbackKindZeroCopy)
171+ if ev.flags != want {
172+ t.Fatalf("flags %#x want %#x", ev.flags, want)
173+ }
174+ // Two flips in one drain: two presented events, each with its time.
175+ b := feedback()
176+ commit()
177+ seqB := lastSeq()
178+ d := feedback()
179+ commit()
180+ seqD := lastSeq()
181+ flip(6*time.Second, 43, seqB, w.ID+1) // another window on the overlay
182+ flip(7*time.Second, 44, seqD, 0)
183+ if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 || ev.flags&uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) != 0 {
184+ t.Fatalf("first %+v", ev)
185+ }
186+ if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 {
187+ t.Fatalf("second %+v", ev)
188+ }
189+ // Replaced before shown: discarded when the flip shows the newer one.
190+ old := feedback()
191+ commit()
192+ lastSeq()
193+ commit()
194+ seqNew := lastSeq()
195+ flip(8*time.Second, 45, seqNew, 0)
196+ if ev := answer(old, "replaced"); !ev.discarded {
197+ t.Fatalf("replaced: %+v", ev)
198+ }
199+ // A subsurface's feedback follows its root: presented with the root's
200+ // content that the flip shows.
201+ comp := bindProtocol(t, c, "wl_compositor")
202+ sub := bindProtocol(t, c, "wl_subcompositor")
203+ child, subsurf := c.AllocateID(), c.AllocateID()
204+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
205+ registerProtocol(t, c, child)
206+ requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, subsurf, child, surf)
207+ registerProtocol(t, c, subsurf)
208+ cfb := c.AllocateID()
209+ cp := &feedbackEvents{out: make(chan presentedEvent, 1)}
210+ cp.SetID(cfb)
211+ c.Context().Register(cp)
212+ requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, cfb)
213+ requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
214+ commit() // the parent applies the subsurface
215+ seqC := lastSeq()
216+ flip(8500*time.Millisecond, 50, seqC, 0)
217+ if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 {
218+ t.Fatalf("subsurface %+v", ev)
219+ }
220+ // Surface destroyed: discarded.
221+ gone := feedback()
222+ commit()
223+ lastSeq()
224+ requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
225+ flip(9*time.Second, 46, 0, 0)
226+ if ev := answer(gone, "destroyed"); !ev.discarded {
227+ t.Fatalf("destroyed: %+v", ev)
228+ }
229+}
230+
231+// A hidden window (no output) gets discarded at once.
232+func TestPresentationHiddenDiscarded(t *testing.T) {
233+ s, events, _, _, _, dir := presentServer(t)
234+ c := protocolClient(t, s, dir)
235+ _, surf, _ := surfaceMapper(t, c, events)()
236+ pres := bindProtocol(t, c, "wp_presentation")
237+ registerProtocol(t, c, pres)
238+ fb := c.AllocateID()
239+ p := &feedbackEvents{out: make(chan presentedEvent, 1)}
240+ p.SetID(fb)
241+ c.Context().Register(p)
242+ requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb)
243+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
244+ if err := c.Roundtrip(); err != nil {
245+ t.Fatal(err)
246+ }
247+ if err := c.Roundtrip(); err != nil {
248+ t.Fatal(err)
249+ }
250+ select {
251+ case ev := <-p.out:
252+ if !ev.discarded {
253+ t.Fatalf("%+v", ev)
254+ }
255+ default:
256+ t.Fatal("not discarded")
257+ }
258+}
259+
260+// Commit timing: a commit targeted two refreshes ahead of the last flip
261+// is not applied on the next one.
262+func TestNextRefreshFollowsFlips(t *testing.T) {
263+ s := &Server{flips: map[string]ports.FlipInfo{}}
264+ s.outputs = []*output{{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "A", RefreshMilli: 60000}}}}
265+ now := time.Now()
266+ mono := monotonic(now)
267+ period := time.Second / 60
268+ s.flips["A"] = ports.FlipInfo{When: mono - period/4, Refresh: period}
269+ next := s.nextRefresh("A", now)
270+ if d := next.Sub(now); d < period*3/4-time.Millisecond || d > period*3/4+time.Millisecond {
271+ t.Fatalf("next refresh in %v, want %v", d, period*3/4)
272+ }
273+}
274+
275+// Software flips (headless) answer feedback without flags and do not pace
276+// frame callbacks.
277+func TestSoftwareFlipsDoNotPace(t *testing.T) {
278+ if paces(ports.OutputPresented{Flip: &ports.FlipInfo{}}) {
279+ t.Fatal("software flip paces callbacks")
280+ }
281+ if !paces(ports.OutputPresented{Flip: &ports.FlipInfo{HardwareClock: true}}) {
282+ t.Fatal("display flip does not pace")
283+ }
284+}
M internal/adapters/wayland/release.go +39 -11
@@ -21,7 +21,10 @@ import (
2121 const heldTimeout = 100 * time.Millisecond
2222
2323 type heldBuffer struct {
24- res *wayland.Buffer
24+ res *wayland.Buffer
25+ // sync is the buffer's explicit-sync state: its release point is
26+ // signalled instead of wl_buffer.release.
27+ sync syncHold
2528 window ports.WindowID
2629 id uint64 // DMABuf ID, 0 for wl_shm
2730 after uint64 // the window's content Seq when the buffer was replaced
@@ -30,17 +33,14 @@ type heldBuffer struct {
3033
3134 // releaseBuffer releases a replaced buffer, or holds it until the output
3235 // showing its window no longer reads it.
33-func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {
34- if !b.Resource.Alive() {
35- return
36- }
36+func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer, sync syncHold) {
3737 window, name := surf.root().windowID(), s.frameOutput(surf)
38- if window == 0 || name == "" {
38+ if !b.Resource.Alive() || window == 0 || name == "" {
3939 // Not drawn (unmapped, hidden, a cursor): nothing reads it.
40- b.SendRelease()
40+ s.release(b, sync)
4141 return
4242 }
43- h := heldBuffer{res: b, window: window, at: time.Now()}
43+ h := heldBuffer{res: b, sync: sync, window: window, at: time.Now()}
4444 if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok {
4545 h.id = d.buf.ID
4646 }
@@ -55,6 +55,36 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {
5555 }
5656 }
5757
58+// release gives a buffer back: its release point is signalled when it
59+// has one (explicit sync), else wl_buffer.release.
60+func (s *Server) release(b *wayland.Buffer, sync syncHold) {
61+ if sync.release.set() || sync.acquire != nil {
62+ s.releaseSync(sync)
63+ return
64+ }
65+ if b != nil && b.Resource.Alive() {
66+ b.SendRelease()
67+ }
68+}
69+
70+// releaseBufferSync holds only a buffer's explicit-sync points: the same
71+// buffer committed again with new points (clients reuse one buffer).
72+func (s *Server) releaseBufferSync(surf *surface, sync syncHold) {
73+ if !sync.release.set() && sync.acquire == nil {
74+ return
75+ }
76+ window := surf.root().windowID()
77+ if window == 0 || s.frameOutput(surf) == "" {
78+ s.releaseSync(sync)
79+ return
80+ }
81+ s.contentMu.Lock()
82+ after := s.contentSeq[window]
83+ s.contentMu.Unlock()
84+ s.held = append(s.held, heldBuffer{sync: sync, window: window, after: after, at: time.Now()})
85+ s.wakePacer()
86+}
87+
5888 // releaseHeld records the output reports and releases the buffers no
5989 // output reads. Every output reads every window's content, so a buffer
6090 // waits until no live output shows or queues it and all of them have seen
@@ -79,9 +109,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
79109 kept = append(kept, h)
80110 continue
81111 }
82- if h.res.Resource.Alive() {
83- h.res.SendRelease()
84- }
112+ s.release(h.res, h.sync)
85113 }
86114 clear(s.held[len(kept):])
87115 s.held = kept
M internal/adapters/wayland/server.go +117 -15
@@ -6,6 +6,7 @@ import (
66 "fmt"
77 "github.com/bnema/purego-libwayland/protocol/relativepointer"
88 "os"
9+ "slices"
910 "sync"
1011 "time"
1112
@@ -23,9 +24,15 @@ type Options struct {
2324 RuntimeDir string
2425 Outputs ports.Layout
2526 // DMABuf is what the renderer imports; empty disables linux-dmabuf.
26- DMABuf ports.DMABufSupport
27+ DMABuf ports.DMABufSupport
28+ // SyncobjNode is the render node (/dev/dri/renderD*) timeline
29+ // syncobjs are imported on; empty or without timeline support, no
30+ // explicit sync is offered.
31+ SyncobjNode string
2732 Keymap string
2833 RepeatRate, RepeatDelay int
34+ // syncDev replaces the render node's syncobj interface (tests).
35+ syncDev syncobjDevice
2936 }
3037
3138 // Channels carries client notifications and commands. Events may be unbuffered.
@@ -38,6 +45,9 @@ type Channels struct {
3845 // Presented paces frame callbacks on the outputs' page flips; outputs
3946 // that do not flip (idle, headless) are paced at their refresh rate.
4047 Presented <-chan ports.OutputPresented
48+ // OutputFormats are the outputs' direct scanout formats, offered in
49+ // dmabuf feedback to fullscreen surfaces.
50+ OutputFormats <-chan ports.OutputFormats
4151 }
4252 type Server struct {
4353 display *server.Display
@@ -98,17 +108,32 @@ type Server struct {
98108 // Events, which happens only at shutdown (core owns the receiving end). A
99109 // stalled core is a bug surfaced by ctx cancellation, not a reason to block
100110 // the display goroutine.
101- eventMu sync.Mutex
102- events []ports.ClientEvent
103- eventReady chan struct{}
104- contentMu sync.Mutex
105- contents map[ports.WindowID]ports.SurfaceContent
106- contentSeq map[ports.WindowID]uint64
107- contentNotify chan struct{}
108- contentReady chan struct{}
109- outputs []*output
110- focusedOutput string
111- fractions map[*surface]*fractionalscale.WpFractionalScaleV1
111+ eventMu sync.Mutex
112+ events []ports.ClientEvent
113+ eventReady chan struct{}
114+ contentMu sync.Mutex
115+ contents map[ports.WindowID]ports.SurfaceContent
116+ contentSeq map[ports.WindowID]uint64
117+ // damage is each window's recent damage (contentMu).
118+ damage map[ports.WindowID][]ports.SeqDamage
119+ // feedbacks wait for the flip that shows their content (presentation.go).
120+ feedbacks []feedbackWait
121+ // Explicit sync (syncobj.go): the render node, imported timelines by
122+ // resource, and the acquire point waiter.
123+ syncDev syncobjDevice
124+ timelines map[*server.Resource]*timeline
125+ syncWait *syncWaiter
126+ // flips is each output's latest flip (pacer, under Do).
127+ flips map[string]ports.FlipInfo
128+ // inhibitors are the live inhibitor objects (inhibit.go);
129+ // shortcutWindows and idleWindows what core was told.
130+ inhibitors []*inhibitor
131+ shortcutWindows, idleWindows map[ports.WindowID]bool
132+ contentNotify chan struct{}
133+ contentReady chan struct{}
134+ outputs []*output
135+ focusedOutput string
136+ fractions map[*surface]*fractionalscale.WpFractionalScaleV1
112137 // cursorSurface is the wl_pointer.set_cursor surface in use.
113138 cursorSurface *surface
114139 // cursorMu guards only the latest cursor change for forwardCursors.
@@ -159,9 +184,30 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
159184 d.Close()
160185 return nil, err
161186 }
162- s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
187+ s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
163188 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
164189 s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
190+ s.flips = map[string]ports.FlipInfo{}
191+ s.timelines = map[*server.Resource]*timeline{}
192+ var node *renderNode
193+ if opts.syncDev != nil {
194+ s.syncDev = opts.syncDev
195+ if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
196+ cleanup()
197+ d.Close()
198+ return nil, err
199+ }
200+ } else if opts.SyncobjNode != "" {
201+ if node, err = openSyncobj(opts.SyncobjNode); err != nil {
202+ log.Info().Str("component", "wayland").Err(err).Msg("explicit sync off")
203+ node = nil
204+ } else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
205+ node.close()
206+ node = nil
207+ } else {
208+ s.syncDev = node
209+ }
210+ }
165211 s.tokens = map[string]activationToken{}
166212 if opts.Keymap != "" {
167213 fd, size, e := keymapFile(opts.Keymap)
@@ -177,6 +223,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
177223 unix.Close(s.keymapFD)
178224 }
179225 s.dmabuf.close()
226+ if s.syncWait != nil {
227+ s.syncWait.close()
228+ }
229+ if node != nil {
230+ node.close()
231+ }
180232 cleanup()
181233 }
182234 if err = registerGlobals(d, opts, s); err != nil {
@@ -196,7 +248,7 @@ func (s *Server) Run(ctx context.Context) error {
196248 s.started = time.Now()
197249 s.ctx = ctx
198250 var wg sync.WaitGroup
199- wg.Add(5)
251+ wg.Add(6)
200252 go func() { defer wg.Done(); s.forward(ctx) }()
201253 go func() { defer wg.Done(); s.forwardCursors(ctx) }()
202254 go func() { defer wg.Done(); s.forwardContents(ctx) }()
@@ -233,11 +285,44 @@ func (s *Server) Run(ctx context.Context) error {
233285 }
234286 }()
235287 go func() { defer wg.Done(); s.pace(ctx) }()
288+ go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
289+ if s.syncWait != nil {
290+ wg.Add(1)
291+ go func() { defer wg.Done(); s.syncWait.run(ctx) }()
292+ }
236293 err := s.display.Run(ctx)
237294 wg.Wait()
238295 return err
239296 }
240297
298+// damageHistory is how many contents a window's damage history covers:
299+// enough for a renderer target a few frames old.
300+const damageHistory = 4
301+
302+// forwardOutputFormats applies the outputs' scanout formats on the
303+// display goroutine.
304+func (s *Server) forwardOutputFormats(ctx context.Context) {
305+ for {
306+ select {
307+ case <-ctx.Done():
308+ return
309+ case <-s.display.Stopped():
310+ return
311+ case f, ok := <-s.channels.OutputFormats:
312+ if !ok {
313+ return
314+ }
315+ if !s.display.Do(func() {
316+ if s.dmabuf != nil {
317+ s.dmabuf.setOutputFormats(f)
318+ }
319+ }) {
320+ return
321+ }
322+ }
323+ }
324+}
325+
241326 func (s *Server) emit(ev ports.ClientEvent) {
242327 if s.channels.Events == nil {
243328 return
@@ -275,13 +360,24 @@ func (s *Server) forward(ctx context.Context) {
275360 }
276361 }
277362
278-func (s *Server) emitContent(c ports.SurfaceContent) {
363+func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
279364 if s.channels.Contents == nil {
280365 return
281366 }
282367 s.contentMu.Lock()
283368 s.contentSeq[c.ID]++
284369 c.Seq = s.contentSeq[c.ID]
370+ // The window's damage history: its last damageHistory contents.
371+ h := append(s.damage[c.ID], ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects})
372+ if len(h) > damageHistory {
373+ h = h[len(h)-damageHistory:]
374+ }
375+ if c.Empty() {
376+ delete(s.damage, c.ID)
377+ } else {
378+ s.damage[c.ID] = h
379+ }
380+ c.DamageHistory = slices.Clone(h)
285381 s.contents[c.ID] = c
286382 close(s.contentNotify)
287383 s.contentNotify = make(chan struct{})
@@ -473,8 +569,12 @@ func (s *Server) apply(cmd ports.ClientCommand) {
473569 return
474570 }
475571 s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure")
572+ scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output
476573 w.last, w.hasLast = c, true
477574 w.sendConfigure()
575+ if scanoutChanged && w.xdg.surface != nil {
576+ s.dmabuf.resendSurface(w.xdg.surface)
577+ }
478578 if surf := w.xdg.surface; surf != nil && c.Output != "" {
479579 surf.sendTreeScale()
480580 }
@@ -482,6 +582,8 @@ func (s *Server) apply(cmd ports.ClientCommand) {
482582 s.setOutputs(c)
483583 case ports.SlotsPending:
484584 s.slotsPending = c.Pending
585+ case ports.ShortcutsInhibitState:
586+ s.setShortcutsInhibit(c)
485587 case ports.CloseWindow:
486588 w := s.windows[c.ID]
487589 if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() {
M internal/adapters/wayland/surface.go +126 -11
@@ -1,6 +1,8 @@
11 package wayland
22
33 import (
4+ "github.com/bnema/purego-libwayland/protocol/presentationtime"
5+ "image"
46 "time"
57
68 "github.com/bnema/nefertty/internal/ports"
@@ -60,8 +62,36 @@ type surface struct {
6062 // contentType is the wp_content_type_v1; contentKind the committed type.
6163 contentType *contentTypeHandler
6264 contentKind, pendingKind uint32
65+ // Damage requested since the last commit: in surface (logical) and
66+ // buffer pixels; committed is what the last commit changed, in buffer
67+ // pixels (full: everything), read by the window's damage history.
68+ pendingDamage, pendingBufDamage []ports.Rect
69+ committed damage
70+ // pendingFeedback are wp_presentation feedbacks for the next commit.
71+ pendingFeedback []*presentationtime.WpPresentationFeedback
72+ // Explicit sync (syncobj.go): the surface's syncobj object; the
73+ // committed points of the pending commit (pendingSync, with the wait
74+ // for its acquire point); and the current buffer's hold.
75+ sync *syncState
76+ pendingSync *commitSync
77+ hold syncHold
6378 }
6479
80+// commitSync is a commit's explicit-sync points and acquire wait.
81+type commitSync struct {
82+ acquire, release syncPoint
83+ wait *syncWait
84+}
85+
86+// damage is what one commit changed in a surface's buffer.
87+type damage struct {
88+ full bool
89+ rects []ports.Rect
90+}
91+
92+// maxDamageRects bounds one commit's rects: past it the commit is full.
93+const maxDamageRects = 32
94+
6595 // subState is the subsurface tree: parent is set on subsurfaces, children
6696 // are ordered bottom to top, below marks children under their parent.
6797 type subState struct {
@@ -127,11 +157,16 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
127157 }
128158 }
129159
130-// redraw sends the tree of a mapped root to the outputs.
160+// redraw sends the tree of a mapped root to the outputs. Only a commit
161+// of the root's own buffer with no other change keeps partial damage.
131162 func (s *surface) redraw() {
132163 r := s.root()
133164 if id := r.windowID(); id != 0 && s.server.channels.Contents != nil {
134- s.server.emitContent(r.tree(id))
165+ d := damage{full: true}
166+ if s == r && len(r.sub.children) == 0 {
167+ d = r.committed
168+ }
169+ s.server.emitContent(r.tree(id), d)
135170 }
136171 }
137172
@@ -154,6 +189,10 @@ func (s *surface) detach() {
154189
155190 func (s *surface) Destroy(*wayland.Surface) {
156191 s.destroyed = true
192+ for _, fb := range s.pendingFeedback {
193+ discard(fb)
194+ }
195+ s.pendingFeedback = nil
157196 s.tearing = nil // the control becomes inert
158197 s.dropQueue()
159198 if s.server.cursorSurface == s {
@@ -171,8 +210,11 @@ func (s *surface) Destroy(*wayland.Surface) {
171210 }
172211 s.callbacks = nil
173212 // The client may reuse the buffer on another surface.
213+ s.dropSync(s.pendingSync)
214+ s.pendingSync = nil
174215 if s.current != nil {
175- s.server.releaseBuffer(s, s.current)
216+ s.server.releaseBuffer(s, s.current, s.hold)
217+ s.hold = syncHold{}
176218 }
177219 s.current, s.pending = nil, nil
178220 if s.role != nil {
@@ -201,7 +243,12 @@ func (s *surface) Commit(*wayland.Surface) {
201243 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
202244 return
203245 }
204- // A commit behind a fifo barrier or a future timestamp waits (fifo.go).
246+ if !s.checkSyncCommit() {
247+ return
248+ }
249+ s.takeSyncPoints()
250+ // A commit behind a fifo barrier, a future timestamp or an acquire
251+ // point without a fence yet waits (fifo.go).
205252 if s.mustWait(time.Now()) {
206253 s.queueUpdate()
207254 return
@@ -229,14 +276,22 @@ func (s *surface) applyCommit() {
229276 if s.viewport != nil {
230277 s.viewport.commit()
231278 }
279+ cs := s.pendingSync
280+ s.pendingSync = nil
232281 if s.attached {
233282 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
234- s.server.releaseBuffer(s, s.current)
283+ s.server.releaseBuffer(s, s.current, s.hold)
284+ } else if cs != nil || s.pending == nil {
285+ // Same buffer again, or detached: the old points are done
286+ // with once the new ones take over.
287+ s.server.releaseBufferSync(s, s.hold)
235288 }
289+ s.hold = syncHold{}
236290 s.current = s.pending
237291 s.pending = nil
238292 s.attached = false
239293 }
294+ s.applySync(cs)
240295 if len(s.callbacks) > 0 {
241296 s.server.queueFrames(s.server.frameOutput(s), s.callbacks)
242297 }
@@ -248,10 +303,13 @@ func (s *surface) applyCommit() {
248303 if state, ok := s.server.buffers[s.current.Resource]; ok {
249304 if c, ok := state.content(0); ok {
250305 c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
306+ resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH
307+ s.commitDamage(true, resized, c.Width, c.Height)
251308 if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) {
252309 s.lastW, s.lastH = c.Width, c.Height
253310 s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")
254311 }
312+ c.Acquire = s.hold.acquire
255313 s.content, s.has = c, true
256314 } else if shm, ok := state.(*buffer); ok {
257315 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
@@ -260,6 +318,9 @@ func (s *surface) applyCommit() {
260318 // replaces them.
261319 }
262320 }
321+ if !fresh {
322+ s.commitDamage(false, false, 0, 0)
323+ }
263324 if s.current == nil {
264325 s.content, s.has = ports.SurfaceContent{}, false
265326 }
@@ -279,15 +340,69 @@ func (s *surface) applyCommit() {
279340 if s.xdg != nil && s.xdg.window != nil {
280341 s.xdg.window.afterCommit()
281342 }
282- if fresh || moved || geometry || hinted || s.sub.parent != nil {
343+ drawn := fresh || moved || geometry || hinted || s.sub.parent != nil
344+ if drawn {
345+ if moved || geometry {
346+ s.committed = damage{full: true}
347+ }
283348 s.redraw()
284349 }
350+ fb := s.pendingFeedback
351+ s.pendingFeedback = nil
352+ s.commitFeedback(fb, fresh)
353+}
354+func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) {
355+ if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects {
356+ s.pendingDamage = append(s.pendingDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
357+ }
358+}
359+func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) {
360+ if w > 0 && h > 0 && len(s.pendingBufDamage) <= maxDamageRects {
361+ s.pendingBufDamage = append(s.pendingBufDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
362+ }
363+}
364+
365+// commitDamage turns the requested damage into buffer pixels of a bw×bh
366+// buffer. Surface damage scales by the buffer/logical ratio, rounded out.
367+// A new size, too many rects or none at all with a new buffer (clients
368+// that do not report damage) is a full change.
369+func (s *surface) commitDamage(fresh, resized bool, bw, bh int) {
370+ surf, buf := s.pendingDamage, s.pendingBufDamage
371+ s.pendingDamage, s.pendingBufDamage = nil, nil
372+ if !fresh {
373+ s.committed = damage{}
374+ return
375+ }
376+ if resized || len(surf)+len(buf) == 0 || len(surf)+len(buf) > maxDamageRects {
377+ s.committed = damage{full: true}
378+ return
379+ }
380+ lw, lh := s.logicalSize(bw, bh)
381+ d := damage{rects: make([]ports.Rect, 0, len(surf)+len(buf))}
382+ bounds := image.Rect(0, 0, bw, bh)
383+ add := func(r image.Rectangle) {
384+ if r = r.Intersect(bounds); !r.Empty() {
385+ d.rects = append(d.rects, ports.Rect{X: r.Min.X, Y: r.Min.Y, W: r.Dx(), H: r.Dy()})
386+ }
387+ }
388+ for _, r := range buf {
389+ add(image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H))
390+ }
391+ for _, r := range surf {
392+ if lw <= 0 || lh <= 0 {
393+ s.committed = damage{full: true}
394+ return
395+ }
396+ // Clients may damage INT32_MAX-sized rects: clamp before scaling.
397+ x0, y0 := max(r.X, 0), max(r.Y, 0)
398+ x1, y1 := min(r.X+min(r.W, lw), lw), min(r.Y+min(r.H, lh), lh)
399+ add(image.Rect(x0*bw/lw, y0*bh/lh, (x1*bw+lw-1)/lw, (y1*bh+lh-1)/lh))
400+ }
401+ s.committed = d
285402 }
286-func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}
287-func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
288-func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
289-func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {}
290-func (*surface) SetBufferTransform(*wayland.Surface, int32) {}
403+func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
404+func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {}
405+func (*surface) SetBufferTransform(*wayland.Surface, int32) {}
291406 func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
292407 if v < 1 {
293408 r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive")
A internal/adapters/wayland/syncdev.go +128 -0
@@ -0,0 +1,128 @@
1+package wayland
2+
3+import (
4+ "fmt"
5+ "os"
6+ "runtime"
7+ "unsafe"
8+
9+ "golang.org/x/sys/unix"
10+)
11+
12+// syncobjDevice is the DRM syncobj interface of a render node: explicit
13+// sync timelines clients import (wp_linux_drm_syncobj_v1).
14+type syncobjDevice interface {
15+ // fdToHandle imports a timeline syncobj fd.
16+ fdToHandle(fd int) (uint32, error)
17+ // exportSyncFile returns a sync file of a timeline point's fence.
18+ exportSyncFile(h uint32, point uint64) (*os.File, error)
19+ // signal signals a timeline point (a release point).
20+ signal(h uint32, point uint64) error
21+ // eventfd writes efd once the point has a fence (WAIT_AVAILABLE).
22+ eventfd(h uint32, point uint64, efd int) error
23+ destroy(h uint32) error
24+}
25+
26+const (
27+ ioctlSyncobjDestroy = 0xC00864C0 // DRM_IOWR(0xC0, struct drm_syncobj_destroy)
28+ ioctlSyncobjHandleToFD = 0xC01864C1 // DRM_IOWR(0xC1, struct drm_syncobj_handle)
29+ ioctlSyncobjFDToHandle = 0xC01864C2 // DRM_IOWR(0xC2, struct drm_syncobj_handle)
30+ ioctlSyncobjTimelineSignal = 0xC01864CD // DRM_IOWR(0xCD, struct drm_syncobj_timeline_array)
31+ ioctlSyncobjEventfd = 0xC01864CF // DRM_IOWR(0xCF, struct drm_syncobj_eventfd)
32+ ioctlGetCap = 0xC010640C // DRM_IOWR(0x0C, struct drm_get_cap)
33+
34+ syncobjExportSyncFile = 1 << 0
35+ syncobjTimeline = 1 << 1
36+ syncobjWaitAvailable = 1 << 2
37+ capSyncobjTimeline = 0x14
38+)
39+
40+type syncobjHandle struct {
41+ handle, flags uint32
42+ fd int32
43+ pad uint32
44+ point uint64
45+}
46+
47+type syncobjDestroy struct{ handle, pad uint32 }
48+
49+type syncobjTimelineArray struct {
50+ handles, points uint64
51+ count, flags uint32
52+}
53+
54+type syncobjEventfd struct {
55+ handle, flags uint32
56+ point uint64
57+ fd int32
58+ pad uint32
59+}
60+
61+type drmCap struct{ capability, value uint64 }
62+
63+// renderNode is a DRM render node opened for syncobj ioctls.
64+type renderNode struct{ fd int }
65+
66+// openSyncobj opens a render node that supports timeline syncobjs.
67+func openSyncobj(path string) (*renderNode, error) {
68+ fd, err := unix.Open(path, unix.O_RDWR|unix.O_CLOEXEC, 0)
69+ if err != nil {
70+ return nil, err
71+ }
72+ c := drmCap{capability: capSyncobjTimeline}
73+ if ioctl(fd, ioctlGetCap, unsafe.Pointer(&c)) != nil || c.value == 0 {
74+ unix.Close(fd)
75+ return nil, fmt.Errorf("%s: no timeline syncobjs", path)
76+ }
77+ return &renderNode{fd: fd}, nil
78+}
79+
80+func (n *renderNode) close() { unix.Close(n.fd) }
81+
82+func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
83+ for {
84+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
85+ if errno == unix.EINTR || errno == unix.EAGAIN {
86+ continue
87+ }
88+ if errno != 0 {
89+ return errno
90+ }
91+ return nil
92+ }
93+}
94+
95+func (n *renderNode) fdToHandle(fd int) (uint32, error) {
96+ a := syncobjHandle{fd: int32(fd)}
97+ if err := ioctl(n.fd, ioctlSyncobjFDToHandle, unsafe.Pointer(&a)); err != nil {
98+ return 0, err
99+ }
100+ return a.handle, nil
101+}
102+
103+func (n *renderNode) exportSyncFile(h uint32, point uint64) (*os.File, error) {
104+ a := syncobjHandle{handle: h, flags: syncobjExportSyncFile | syncobjTimeline, fd: -1, point: point}
105+ if err := ioctl(n.fd, ioctlSyncobjHandleToFD, unsafe.Pointer(&a)); err != nil {
106+ return nil, err
107+ }
108+ return os.NewFile(uintptr(a.fd), "acquire-fence"), nil
109+}
110+
111+func (n *renderNode) signal(h uint32, point uint64) error {
112+ handles, points := [1]uint32{h}, [1]uint64{point}
113+ a := syncobjTimelineArray{handles: uint64(uintptr(unsafe.Pointer(&handles))), points: uint64(uintptr(unsafe.Pointer(&points))), count: 1}
114+ err := ioctl(n.fd, ioctlSyncobjTimelineSignal, unsafe.Pointer(&a))
115+ runtime.KeepAlive(&handles)
116+ runtime.KeepAlive(&points)
117+ return err
118+}
119+
120+func (n *renderNode) eventfd(h uint32, point uint64, efd int) error {
121+ a := syncobjEventfd{handle: h, flags: syncobjWaitAvailable, point: point, fd: int32(efd)}
122+ return ioctl(n.fd, ioctlSyncobjEventfd, unsafe.Pointer(&a))
123+}
124+
125+func (n *renderNode) destroy(h uint32) error {
126+ a := syncobjDestroy{handle: h}
127+ return ioctl(n.fd, ioctlSyncobjDestroy, unsafe.Pointer(&a))
128+}
A internal/adapters/wayland/syncobj.go +422 -0
@@ -0,0 +1,422 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "sync"
8+ "sync/atomic"
9+
10+ "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"
11+ "github.com/bnema/purego-libwayland/protocol/wayland"
12+ "github.com/bnema/purego-libwayland/server"
13+ "golang.org/x/sys/unix"
14+)
15+
16+// wp_linux_drm_syncobj_v1: explicit sync. A client names, per commit, the
17+// timeline point its GPU signals when the buffer is drawn (acquire) and
18+// the point we signal once nothing reads the buffer any more (release).
19+// A commit waits (like a fifo barrier) until its acquire point has a
20+// fence; the fence then travels with the content as a sync file the
21+// renderer and the display wait on, so no CPU waits for the GPU. The
22+// release point replaces wl_buffer.release.
23+
24+const syncobjVersion = 1
25+
26+// timeline is an imported client timeline.
27+type timeline struct {
28+ dev syncobjDevice
29+ handle uint32
30+ alive bool
31+ // uses counts committed points still to signal or wait on.
32+ uses int
33+}
34+
35+// syncPoint is a point on a timeline.
36+type syncPoint struct {
37+ tl *timeline
38+ point uint64
39+}
40+
41+func (p syncPoint) set() bool { return p.tl != nil }
42+
43+// syncState is a surface's wp_linux_drm_syncobj_surface_v1 state.
44+type syncState struct {
45+ resource *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1
46+ acquire, release syncPoint // pending, for the next commit
47+}
48+
49+func registerSyncobj(d *server.Display, s *Server) error {
50+ if s.syncDev == nil {
51+ return nil
52+ }
53+ return linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1Global(d, syncobjVersion, func(c server.Client, v, id uint32) {
54+ _, _ = linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1(c, int32(v), id, syncManager{s})
55+ })
56+}
57+
58+type syncManager struct{ server *Server }
59+
60+func (syncManager) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1) {}
61+
62+func (m syncManager) GetSurface(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, surf *wayland.Surface) {
63+ state := m.server.surfaceOf(surf)
64+ if state == nil {
65+ return
66+ }
67+ if state.sync != nil {
68+ r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists), "surface already has a syncobj surface")
69+ return
70+ }
71+ st := &syncState{}
72+ res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjSurfaceV1(r.Client(), r.Version(), id, syncSurface{state, st})
73+ if err != nil {
74+ return
75+ }
76+ st.resource = res
77+ state.sync = st
78+ res.OnDestroy = func() {
79+ if state.sync == st {
80+ state.sync = nil
81+ }
82+ }
83+}
84+
85+func (m syncManager) ImportTimeline(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, fd int) {
86+ defer unix.Close(fd)
87+ h, err := m.server.syncDev.fdToHandle(fd)
88+ if err != nil {
89+ r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline), "invalid timeline fd")
90+ return
91+ }
92+ tl := &timeline{dev: m.server.syncDev, handle: h, alive: true}
93+ res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjTimelineV1(r.Client(), r.Version(), id, timelineHandler{})
94+ if err != nil {
95+ _ = tl.dev.destroy(h)
96+ return
97+ }
98+ m.server.timelines[res.Resource] = tl
99+ res.OnDestroy = func() {
100+ delete(m.server.timelines, res.Resource)
101+ // Points already committed keep the syncobj: destroyed once no
102+ // held buffer or waiting commit uses it (timeline.drop).
103+ tl.alive = false
104+ m.server.dropTimeline(tl)
105+ }
106+}
107+
108+type timelineHandler struct{}
109+
110+func (timelineHandler) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1) {}
111+
112+type syncSurface struct {
113+ surf *surface
114+ st *syncState
115+}
116+
117+func (syncSurface) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1) {}
118+
119+func (h syncSurface) point(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) (syncPoint, bool) {
120+ if h.surf.destroyed {
121+ r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface), "surface destroyed")
122+ return syncPoint{}, false
123+ }
124+ t := h.surf.server.timelines[tl.Resource]
125+ if t == nil {
126+ return syncPoint{}, false
127+ }
128+ return syncPoint{tl: t, point: uint64(hi)<<32 | uint64(lo)}, true
129+}
130+
131+func (h syncSurface) SetAcquirePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) {
132+ if p, ok := h.point(r, tl, hi, lo); ok {
133+ h.st.acquire = p
134+ }
135+}
136+
137+func (h syncSurface) SetReleasePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) {
138+ if p, ok := h.point(r, tl, hi, lo); ok {
139+ h.st.release = p
140+ }
141+}
142+
143+// checkSyncCommit applies the protocol rules to a commit with sync
144+// points; false means a protocol error was posted.
145+func (s *surface) checkSyncCommit() bool {
146+ st := s.sync
147+ if st == nil {
148+ return true
149+ }
150+ a, r := st.acquire, st.release
151+ // Points belong only to a commit that attaches a non-null buffer.
152+ hasBuffer := s.attached && s.pending != nil
153+ res := st.resource
154+ switch {
155+ case !a.set() && !r.set():
156+ if hasBuffer {
157+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point")
158+ return false
159+ }
160+ return true
161+ case !hasBuffer:
162+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer), "points without a buffer")
163+ case !a.set():
164+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "no acquire point")
165+ case !r.set():
166+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint), "no release point")
167+ case a.tl == r.tl && a.point >= r.point:
168+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints), "release point not after acquire point")
169+ case s.pendingIsSHM():
170+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer), "explicit sync on a wl_shm buffer")
171+ default:
172+ return true
173+ }
174+ return false
175+}
176+
177+// pendingIsSHM reports whether the buffer of the next commit is wl_shm.
178+func (s *surface) pendingIsSHM() bool {
179+ b := s.pending
180+ if !s.attached {
181+ b = s.current
182+ }
183+ if b == nil {
184+ return false
185+ }
186+ _, dma := s.server.buffers[b.Resource].(*dmabufBuffer)
187+ return !dma
188+}
189+
190+// syncWaiter watches acquire points: each gets an eventfd the kernel
191+// writes once the point has a fence, and the pacer is woken to apply the
192+// commits that waited for it.
193+type syncWaiter struct {
194+ mu sync.Mutex
195+ ready map[*syncWait]bool
196+ wake func()
197+ pipeR *os.File
198+ pipeW *os.File
199+ added []*syncWait // new waits for run (under mu)
200+}
201+
202+// syncWait is one acquire point a queued commit waits for.
203+type syncWait struct {
204+ efd int
205+ done bool // the eventfd fired or the wait was cancelled (pacer, under Do)
206+ cancelled atomic.Bool // run stops polling it and closes efd
207+}
208+
209+func newSyncWaiter(wake func()) (*syncWaiter, error) {
210+ r, w, err := os.Pipe()
211+ if err != nil {
212+ return nil, err
213+ }
214+ return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w}, nil
215+}
216+
217+// watch registers an eventfd for p; the wait is ready once it fires.
218+func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) {
219+ efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
220+ if err != nil {
221+ return nil, err
222+ }
223+ if err := p.tl.dev.eventfd(p.tl.handle, p.point, efd); err != nil {
224+ unix.Close(efd)
225+ return nil, err
226+ }
227+ w := &syncWait{efd: efd}
228+ sw.mu.Lock()
229+ sw.added = append(sw.added, w)
230+ sw.mu.Unlock()
231+ _, _ = sw.pipeW.Write([]byte{0})
232+ return w, nil
233+}
234+
235+// fired reports whether w's point has a fence, and forgets w once it has.
236+func (sw *syncWaiter) fired(w *syncWait) bool {
237+ sw.mu.Lock()
238+ defer sw.mu.Unlock()
239+ if sw.ready[w] {
240+ delete(sw.ready, w)
241+ w.done = true
242+ }
243+ return w.done
244+}
245+
246+// cancel stops watching w (the commit was dropped).
247+func (sw *syncWaiter) cancel(w *syncWait) {
248+ w.cancelled.Store(true)
249+ sw.mu.Lock()
250+ delete(sw.ready, w)
251+ sw.mu.Unlock()
252+ w.done = true
253+ _, _ = sw.pipeW.Write([]byte{0}) // run closes its eventfd
254+}
255+
256+// run polls every watched eventfd; it owns and closes them.
257+func (sw *syncWaiter) run(ctx context.Context) {
258+ defer sw.pipeR.Close()
259+ var waits []*syncWait
260+ defer func() {
261+ sw.mu.Lock()
262+ waits = append(waits, sw.added...)
263+ sw.added = nil
264+ sw.mu.Unlock()
265+ for _, w := range waits {
266+ unix.Close(w.efd)
267+ }
268+ }()
269+ for ctx.Err() == nil {
270+ live := waits[:0]
271+ for _, w := range waits {
272+ if w.cancelled.Load() {
273+ unix.Close(w.efd)
274+ continue
275+ }
276+ live = append(live, w)
277+ }
278+ waits = live
279+ fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}}
280+ for _, w := range waits {
281+ fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN})
282+ }
283+ n, err := unix.Poll(fds, 100)
284+ if err != nil && !errors.Is(err, unix.EINTR) {
285+ return
286+ }
287+ if n <= 0 {
288+ continue
289+ }
290+ fired := false
291+ kept := waits[:0]
292+ for i, w := range waits {
293+ if w.cancelled.Load() {
294+ unix.Close(w.efd)
295+ continue
296+ }
297+ if fds[i+1].Revents == 0 {
298+ kept = append(kept, w)
299+ continue
300+ }
301+ unix.Close(w.efd)
302+ sw.mu.Lock()
303+ sw.ready[w] = true
304+ sw.mu.Unlock()
305+ fired = true
306+ }
307+ waits = kept
308+ // New waits join the next poll.
309+ if fds[0].Revents != 0 {
310+ var b [64]byte
311+ _, _ = sw.pipeR.Read(b[:])
312+ sw.mu.Lock()
313+ waits = append(waits, sw.added...)
314+ sw.added = nil
315+ sw.mu.Unlock()
316+ }
317+ if fired {
318+ sw.wake()
319+ }
320+ }
321+}
322+
323+// close stops the waiter's input side.
324+func (sw *syncWaiter) close() { sw.pipeW.Close() }
325+
326+// syncHold is a buffer's explicit-sync state while it is on screen: the
327+// acquire fence handed to readers and the release point to signal.
328+type syncHold struct {
329+ acquire *os.File
330+ release syncPoint
331+}
332+
333+// releaseSync signals the release point once nothing reads the buffer,
334+// and closes the acquire fence.
335+func (s *Server) releaseSync(h syncHold) {
336+ if h.acquire != nil {
337+ h.acquire.Close()
338+ }
339+ if h.release.set() {
340+ if err := h.release.tl.dev.signal(h.release.tl.handle, h.release.point); err != nil {
341+ s.log.Warn().Str("component", "wayland").Err(err).Msg("release point")
342+ }
343+ h.release.tl.uses--
344+ s.dropTimeline(h.release.tl)
345+ }
346+}
347+
348+// dropTimeline destroys a timeline the client destroyed once no held
349+// buffer still needs to signal it.
350+func (s *Server) dropTimeline(tl *timeline) {
351+ if !tl.alive && tl.uses <= 0 && tl.handle != 0 {
352+ _ = tl.dev.destroy(tl.handle)
353+ tl.handle = 0
354+ }
355+}
356+
357+// takeSyncPoints moves the surface's requested points to the commit and
358+// starts waiting for the acquire point's fence.
359+func (s *surface) takeSyncPoints() {
360+ st := s.sync
361+ if st == nil || !st.acquire.set() {
362+ return
363+ }
364+ cs := &commitSync{acquire: st.acquire, release: st.release}
365+ st.acquire, st.release = syncPoint{}, syncPoint{}
366+ cs.acquire.tl.uses++
367+ cs.release.tl.uses++
368+ w, err := s.server.syncWait.watch(cs.acquire)
369+ if err != nil {
370+ // The point cannot be watched: apply now, readers wait on the
371+ // fence (or its absence) when they read the buffer.
372+ s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire point")
373+ } else {
374+ cs.wait = w
375+ }
376+ s.pendingSync = cs
377+}
378+
379+// syncReady reports whether a commit's acquire point has its fence.
380+func (s *surface) syncReady(cs *commitSync) bool {
381+ return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait)
382+}
383+
384+// dropSync forgets a commit that will never apply. Its release point is
385+// signalled only once the acquire point has a fence: signalling earlier
386+// could move a shared timeline past a point the client has not submitted.
387+func (s *surface) dropSync(cs *commitSync) {
388+ if cs == nil {
389+ return
390+ }
391+ ready := cs.wait != nil && s.server.syncWait.fired(cs.wait)
392+ if cs.wait != nil {
393+ s.server.syncWait.cancel(cs.wait)
394+ }
395+ cs.acquire.tl.uses--
396+ s.server.dropTimeline(cs.acquire.tl)
397+ if ready {
398+ s.server.releaseSync(syncHold{release: cs.release})
399+ return
400+ }
401+ cs.release.tl.uses--
402+ s.server.dropTimeline(cs.release.tl)
403+}
404+
405+// applySync makes a commit's points the current buffer's: its acquire
406+// fence goes with the content, its release point waits for release.
407+func (s *surface) applySync(cs *commitSync) {
408+ if cs == nil {
409+ return
410+ }
411+ if cs.wait != nil {
412+ s.server.syncWait.cancel(cs.wait)
413+ }
414+ f, err := cs.acquire.tl.dev.exportSyncFile(cs.acquire.tl.handle, cs.acquire.point)
415+ if err != nil {
416+ s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire fence")
417+ f = nil
418+ }
419+ cs.acquire.tl.uses--
420+ s.server.dropTimeline(cs.acquire.tl)
421+ s.hold = syncHold{acquire: f, release: cs.release}
422+}
A internal/adapters/wayland/syncobj_device_mock_test.go +346 -0
@@ -0,0 +1,346 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package wayland
6+
7+import (
8+ "os"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// newMocksyncobjDevice creates a new instance of mocksyncobjDevice. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func newMocksyncobjDevice(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *mocksyncobjDevice {
19+ if helper, ok := t.(interface{ Helper() }); ok {
20+ helper.Helper()
21+ }
22+
23+ mock := &mocksyncobjDevice{}
24+ mock.Mock.Test(t)
25+
26+ t.Cleanup(func() {
27+ if helper, ok := t.(interface{ Helper() }); ok {
28+ helper.Helper()
29+ }
30+ mock.AssertExpectations(t)
31+ })
32+
33+ return mock
34+}
35+
36+// mocksyncobjDevice is an autogenerated mock type for the syncobjDevice type
37+type mocksyncobjDevice struct {
38+ mock.Mock
39+}
40+
41+type mocksyncobjDevice_Expecter struct {
42+ mock *mock.Mock
43+}
44+
45+func (_m *mocksyncobjDevice) EXPECT() *mocksyncobjDevice_Expecter {
46+ return &mocksyncobjDevice_Expecter{mock: &_m.Mock}
47+}
48+
49+// destroy provides a mock function for the type mocksyncobjDevice
50+func (_mock *mocksyncobjDevice) destroy(h uint32) error {
51+ ret := _mock.Called(h)
52+
53+ if len(ret) == 0 {
54+ panic("no return value specified for destroy")
55+ }
56+
57+ var r0 error
58+ if returnFunc, ok := ret.Get(0).(func(uint32) error); ok {
59+ r0 = returnFunc(h)
60+ } else {
61+ r0 = ret.Error(0)
62+ }
63+ return r0
64+}
65+
66+// mocksyncobjDevice_destroy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroy'
67+type mocksyncobjDevice_destroy_Call struct {
68+ *mock.Call
69+}
70+
71+// destroy is a helper method to define mock.On call
72+// - h uint32
73+func (_e *mocksyncobjDevice_Expecter) destroy(h any) *mocksyncobjDevice_destroy_Call {
74+ return &mocksyncobjDevice_destroy_Call{Call: _e.mock.On("destroy", h)}
75+}
76+
77+func (_c *mocksyncobjDevice_destroy_Call) Run(run func(h uint32)) *mocksyncobjDevice_destroy_Call {
78+ _c.Call.Run(func(args mock.Arguments) {
79+ var arg0 uint32
80+ if args[0] != nil {
81+ arg0 = args[0].(uint32)
82+ }
83+ run(
84+ arg0,
85+ )
86+ })
87+ return _c
88+}
89+
90+func (_c *mocksyncobjDevice_destroy_Call) Return(err error) *mocksyncobjDevice_destroy_Call {
91+ _c.Call.Return(err)
92+ return _c
93+}
94+
95+func (_c *mocksyncobjDevice_destroy_Call) RunAndReturn(run func(h uint32) error) *mocksyncobjDevice_destroy_Call {
96+ _c.Call.Return(run)
97+ return _c
98+}
99+
100+// eventfd provides a mock function for the type mocksyncobjDevice
101+func (_mock *mocksyncobjDevice) eventfd(h uint32, point uint64, efd int) error {
102+ ret := _mock.Called(h, point, efd)
103+
104+ if len(ret) == 0 {
105+ panic("no return value specified for eventfd")
106+ }
107+
108+ var r0 error
109+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64, int) error); ok {
110+ r0 = returnFunc(h, point, efd)
111+ } else {
112+ r0 = ret.Error(0)
113+ }
114+ return r0
115+}
116+
117+// mocksyncobjDevice_eventfd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'eventfd'
118+type mocksyncobjDevice_eventfd_Call struct {
119+ *mock.Call
120+}
121+
122+// eventfd is a helper method to define mock.On call
123+// - h uint32
124+// - point uint64
125+// - efd int
126+func (_e *mocksyncobjDevice_Expecter) eventfd(h any, point any, efd any) *mocksyncobjDevice_eventfd_Call {
127+ return &mocksyncobjDevice_eventfd_Call{Call: _e.mock.On("eventfd", h, point, efd)}
128+}
129+
130+func (_c *mocksyncobjDevice_eventfd_Call) Run(run func(h uint32, point uint64, efd int)) *mocksyncobjDevice_eventfd_Call {
131+ _c.Call.Run(func(args mock.Arguments) {
132+ var arg0 uint32
133+ if args[0] != nil {
134+ arg0 = args[0].(uint32)
135+ }
136+ var arg1 uint64
137+ if args[1] != nil {
138+ arg1 = args[1].(uint64)
139+ }
140+ var arg2 int
141+ if args[2] != nil {
142+ arg2 = args[2].(int)
143+ }
144+ run(
145+ arg0,
146+ arg1,
147+ arg2,
148+ )
149+ })
150+ return _c
151+}
152+
153+func (_c *mocksyncobjDevice_eventfd_Call) Return(err error) *mocksyncobjDevice_eventfd_Call {
154+ _c.Call.Return(err)
155+ return _c
156+}
157+
158+func (_c *mocksyncobjDevice_eventfd_Call) RunAndReturn(run func(h uint32, point uint64, efd int) error) *mocksyncobjDevice_eventfd_Call {
159+ _c.Call.Return(run)
160+ return _c
161+}
162+
163+// exportSyncFile provides a mock function for the type mocksyncobjDevice
164+func (_mock *mocksyncobjDevice) exportSyncFile(h uint32, point uint64) (*os.File, error) {
165+ ret := _mock.Called(h, point)
166+
167+ if len(ret) == 0 {
168+ panic("no return value specified for exportSyncFile")
169+ }
170+
171+ var r0 *os.File
172+ var r1 error
173+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64) (*os.File, error)); ok {
174+ return returnFunc(h, point)
175+ }
176+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64) *os.File); ok {
177+ r0 = returnFunc(h, point)
178+ } else {
179+ if ret.Get(0) != nil {
180+ r0 = ret.Get(0).(*os.File)
181+ }
182+ }
183+ if returnFunc, ok := ret.Get(1).(func(uint32, uint64) error); ok {
184+ r1 = returnFunc(h, point)
185+ } else {
186+ r1 = ret.Error(1)
187+ }
188+ return r0, r1
189+}
190+
191+// mocksyncobjDevice_exportSyncFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'exportSyncFile'
192+type mocksyncobjDevice_exportSyncFile_Call struct {
193+ *mock.Call
194+}
195+
196+// exportSyncFile is a helper method to define mock.On call
197+// - h uint32
198+// - point uint64
199+func (_e *mocksyncobjDevice_Expecter) exportSyncFile(h any, point any) *mocksyncobjDevice_exportSyncFile_Call {
200+ return &mocksyncobjDevice_exportSyncFile_Call{Call: _e.mock.On("exportSyncFile", h, point)}
201+}
202+
203+func (_c *mocksyncobjDevice_exportSyncFile_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_exportSyncFile_Call {
204+ _c.Call.Run(func(args mock.Arguments) {
205+ var arg0 uint32
206+ if args[0] != nil {
207+ arg0 = args[0].(uint32)
208+ }
209+ var arg1 uint64
210+ if args[1] != nil {
211+ arg1 = args[1].(uint64)
212+ }
213+ run(
214+ arg0,
215+ arg1,
216+ )
217+ })
218+ return _c
219+}
220+
221+func (_c *mocksyncobjDevice_exportSyncFile_Call) Return(file *os.File, err error) *mocksyncobjDevice_exportSyncFile_Call {
222+ _c.Call.Return(file, err)
223+ return _c
224+}
225+
226+func (_c *mocksyncobjDevice_exportSyncFile_Call) RunAndReturn(run func(h uint32, point uint64) (*os.File, error)) *mocksyncobjDevice_exportSyncFile_Call {
227+ _c.Call.Return(run)
228+ return _c
229+}
230+
231+// fdToHandle provides a mock function for the type mocksyncobjDevice
232+func (_mock *mocksyncobjDevice) fdToHandle(fd int) (uint32, error) {
233+ ret := _mock.Called(fd)
234+
235+ if len(ret) == 0 {
236+ panic("no return value specified for fdToHandle")
237+ }
238+
239+ var r0 uint32
240+ var r1 error
241+ if returnFunc, ok := ret.Get(0).(func(int) (uint32, error)); ok {
242+ return returnFunc(fd)
243+ }
244+ if returnFunc, ok := ret.Get(0).(func(int) uint32); ok {
245+ r0 = returnFunc(fd)
246+ } else {
247+ r0 = ret.Get(0).(uint32)
248+ }
249+ if returnFunc, ok := ret.Get(1).(func(int) error); ok {
250+ r1 = returnFunc(fd)
251+ } else {
252+ r1 = ret.Error(1)
253+ }
254+ return r0, r1
255+}
256+
257+// mocksyncobjDevice_fdToHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'fdToHandle'
258+type mocksyncobjDevice_fdToHandle_Call struct {
259+ *mock.Call
260+}
261+
262+// fdToHandle is a helper method to define mock.On call
263+// - fd int
264+func (_e *mocksyncobjDevice_Expecter) fdToHandle(fd any) *mocksyncobjDevice_fdToHandle_Call {
265+ return &mocksyncobjDevice_fdToHandle_Call{Call: _e.mock.On("fdToHandle", fd)}
266+}
267+
268+func (_c *mocksyncobjDevice_fdToHandle_Call) Run(run func(fd int)) *mocksyncobjDevice_fdToHandle_Call {
269+ _c.Call.Run(func(args mock.Arguments) {
270+ var arg0 int
271+ if args[0] != nil {
272+ arg0 = args[0].(int)
273+ }
274+ run(
275+ arg0,
276+ )
277+ })
278+ return _c
279+}
280+
281+func (_c *mocksyncobjDevice_fdToHandle_Call) Return(v uint32, err error) *mocksyncobjDevice_fdToHandle_Call {
282+ _c.Call.Return(v, err)
283+ return _c
284+}
285+
286+func (_c *mocksyncobjDevice_fdToHandle_Call) RunAndReturn(run func(fd int) (uint32, error)) *mocksyncobjDevice_fdToHandle_Call {
287+ _c.Call.Return(run)
288+ return _c
289+}
290+
291+// signal provides a mock function for the type mocksyncobjDevice
292+func (_mock *mocksyncobjDevice) signal(h uint32, point uint64) error {
293+ ret := _mock.Called(h, point)
294+
295+ if len(ret) == 0 {
296+ panic("no return value specified for signal")
297+ }
298+
299+ var r0 error
300+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64) error); ok {
301+ r0 = returnFunc(h, point)
302+ } else {
303+ r0 = ret.Error(0)
304+ }
305+ return r0
306+}
307+
308+// mocksyncobjDevice_signal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'signal'
309+type mocksyncobjDevice_signal_Call struct {
310+ *mock.Call
311+}
312+
313+// signal is a helper method to define mock.On call
314+// - h uint32
315+// - point uint64
316+func (_e *mocksyncobjDevice_Expecter) signal(h any, point any) *mocksyncobjDevice_signal_Call {
317+ return &mocksyncobjDevice_signal_Call{Call: _e.mock.On("signal", h, point)}
318+}
319+
320+func (_c *mocksyncobjDevice_signal_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_signal_Call {
321+ _c.Call.Run(func(args mock.Arguments) {
322+ var arg0 uint32
323+ if args[0] != nil {
324+ arg0 = args[0].(uint32)
325+ }
326+ var arg1 uint64
327+ if args[1] != nil {
328+ arg1 = args[1].(uint64)
329+ }
330+ run(
331+ arg0,
332+ arg1,
333+ )
334+ })
335+ return _c
336+}
337+
338+func (_c *mocksyncobjDevice_signal_Call) Return(err error) *mocksyncobjDevice_signal_Call {
339+ _c.Call.Return(err)
340+ return _c
341+}
342+
343+func (_c *mocksyncobjDevice_signal_Call) RunAndReturn(run func(h uint32, point uint64) error) *mocksyncobjDevice_signal_Call {
344+ _c.Call.Return(run)
345+ return _c
346+}
A internal/adapters/wayland/syncobj_test.go +461 -0
@@ -0,0 +1,461 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os"
6+ "strings"
7+ "sync"
8+ "testing"
9+ "time"
10+ "unsafe"
11+
12+ "github.com/bnema/nefertty/internal/logging"
13+ "github.com/bnema/nefertty/internal/ports"
14+ "github.com/bnema/purego-libwayland/protocol/linuxdmabuf"
15+ "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"
16+ "github.com/bnema/purego-libwayland/protocol/wayland"
17+ "github.com/bnema/wlturbo"
18+ "github.com/stretchr/testify/mock"
19+ "golang.org/x/sys/unix"
20+)
21+
22+func TestSyncobjStructSizes(t *testing.T) {
23+ for _, tc := range []struct {
24+ name string
25+ got uintptr
26+ req uintptr
27+ }{
28+ {"destroy", unsafe.Sizeof(syncobjDestroy{}), ioctlSyncobjDestroy},
29+ {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD},
30+ {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle},
31+ {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal},
32+ {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd},
33+ {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap},
34+ } {
35+ if want := (tc.req >> 16) & 0x3fff; tc.got != want {
36+ t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)
37+ }
38+ }
39+}
40+
41+// syncHarness is a server with a mocked syncobj device and a client with
42+// a mapped window, a dmabuf buffer and the syncobj objects.
43+type syncHarness struct {
44+ t *testing.T
45+ s *Server
46+ c *wlturbo.Display
47+ dev *mocksyncobjDevice
48+ contents chan ports.SurfaceContent
49+ presented chan ports.OutputPresented
50+ surf uint32
51+ syncSurf uint32
52+ timeline uint32
53+ win ports.WindowID
54+ mu sync.Mutex
55+ efds map[uint64]int // eventfd by acquire point
56+ signals []uint64
57+ fences []*os.File
58+ releases map[uint32]*syncReleaseProxy
59+ mgr uint32
60+}
61+
62+func newSyncHarness(t *testing.T) *syncHarness {
63+ h := &syncHarness{t: t, efds: map[uint64]int{}}
64+ h.dev = newMocksyncobjDevice(t)
65+ h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe()
66+ h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe()
67+ h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error {
68+ // Keep a duplicate: the waiter closes its own when it fires.
69+ dup, err := unix.Dup(efd)
70+ if err != nil {
71+ return err
72+ }
73+ h.mu.Lock()
74+ h.efds[point] = dup
75+ h.mu.Unlock()
76+ return nil
77+ }).Maybe()
78+ h.dev.EXPECT().exportSyncFile(uint32(7), mock.Anything).RunAndReturn(func(uint32, uint64) (*os.File, error) {
79+ r, w, err := os.Pipe()
80+ if err != nil {
81+ return nil, err
82+ }
83+ w.Close()
84+ h.mu.Lock()
85+ h.fences = append(h.fences, r)
86+ h.mu.Unlock()
87+ return r, nil
88+ }).Maybe()
89+ h.dev.EXPECT().signal(uint32(7), mock.Anything).RunAndReturn(func(_ uint32, point uint64) error {
90+ h.mu.Lock()
91+ h.signals = append(h.signals, point)
92+ h.mu.Unlock()
93+ return nil
94+ }).Maybe()
95+ dir := t.TempDir()
96+ events := make(chan ports.ClientEvent, 16)
97+ commands := make(chan ports.ClientCommand, 16)
98+ h.contents = make(chan ports.SurfaceContent, 64)
99+ h.presented = make(chan ports.OutputPresented, 16)
100+ sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}}
101+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands, Contents: h.contents, Presented: h.presented}, logging.For(context.Background(), "wayland"))
102+ if err != nil {
103+ t.Fatal(err)
104+ }
105+ ctx, cancel := context.WithCancel(context.Background())
106+ done := make(chan error, 1)
107+ go func() { done <- s.Run(ctx) }()
108+ t.Cleanup(func() {
109+ cancel()
110+ <-done
111+ h.mu.Lock()
112+ for _, fd := range h.efds {
113+ unix.Close(fd)
114+ }
115+ h.mu.Unlock()
116+ })
117+ h.s = s
118+ h.c = protocolClient(t, s, dir)
119+ w, surf, xdg := surfaceMapper(t, h.c, events)()
120+ registerProtocol(t, h.c, xdg)
121+ h.win, h.surf = w.ID, surf
122+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 64, Height: 16, Output: "HEADLESS-1"}
123+ mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
124+ h.syncSurf = h.c.AllocateID()
125+ requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.syncSurf, surf)
126+ registerProtocol(t, h.c, h.syncSurf)
127+ fd, err := unix.MemfdCreate("timeline", 0)
128+ if err != nil {
129+ t.Fatal(err)
130+ }
131+ defer unix.Close(fd)
132+ h.timeline = h.c.AllocateID()
133+ registerProtocol(t, h.c, h.timeline)
134+ if err := h.c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, h.timeline); err != nil {
135+ t.Fatal(err)
136+ }
137+ for deadline := time.Now().Add(2 * time.Second); ; {
138+ var out string
139+ s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) })
140+ if out != "" {
141+ break
142+ }
143+ if time.Now().After(deadline) {
144+ t.Fatal("configure not applied")
145+ }
146+ time.Sleep(2 * time.Millisecond)
147+ }
148+ return h
149+}
150+
151+// dmabuf makes a client dmabuf wl_buffer (a memfd stands in for it).
152+func (h *syncHarness) dmabuf() uint32 {
153+ t, c := h.t, h.c
154+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
155+ fd, err := unix.MemfdCreate("buf", 0)
156+ if err != nil {
157+ t.Fatal(err)
158+ }
159+ defer unix.Close(fd)
160+ if err := unix.Ftruncate(fd, 256*16); err != nil {
161+ t.Fatal(err)
162+ }
163+ p := c.AllocateID()
164+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
165+ registerProtocol(t, c, p)
166+ if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
167+ t.Fatal(err)
168+ }
169+ b := c.AllocateID()
170+ rel := &syncReleaseProxy{released: make(chan struct{}, 4)}
171+ rel.SetID(b)
172+ c.Context().Register(rel)
173+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, b, int32(64), int32(16), linearARGB.Format, uint32(0))
174+ h.releases[b] = rel
175+ return b
176+}
177+
178+type syncReleaseProxy struct {
179+ wlturbo.BaseProxy
180+ released chan struct{}
181+}
182+
183+func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) {
184+ if uint32(e.Opcode) == wayland.BufferEventRelease {
185+ p.released <- struct{}{}
186+ }
187+}
188+
189+func (h *syncHarness) setPoints(acquire, release uint64) {
190+ requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire))
191+ requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release))
192+}
193+
194+func (h *syncHarness) commit(buf uint32, acquire, release uint64) {
195+ t, c := h.t, h.c
196+ requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
197+ h.setPoints(acquire, release)
198+ requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit)
199+ if err := c.Roundtrip(); err != nil {
200+ t.Fatal(err)
201+ }
202+}
203+
204+// content waits for a DMABuf content of the window.
205+func (h *syncHarness) content(wait time.Duration) (ports.SurfaceContent, bool) {
206+ deadline := time.After(wait)
207+ for {
208+ select {
209+ case c := <-h.contents:
210+ if c.ID == h.win && c.DMABuf != nil {
211+ return c, true
212+ }
213+ case <-deadline:
214+ return ports.SurfaceContent{}, false
215+ }
216+ }
217+}
218+
219+// fire makes the kernel's eventfd for an acquire point readable.
220+func (h *syncHarness) fire(point uint64) {
221+ h.t.Helper()
222+ deadline := time.Now().Add(2 * time.Second)
223+ for {
224+ h.mu.Lock()
225+ fd, ok := h.efds[point]
226+ h.mu.Unlock()
227+ if ok {
228+ var one [8]byte
229+ one[0] = 1
230+ if _, err := unix.Write(fd, one[:]); err != nil {
231+ h.t.Fatal(err)
232+ }
233+ return
234+ }
235+ if time.Now().After(deadline) {
236+ h.t.Fatalf("no eventfd for point %d", point)
237+ }
238+ time.Sleep(2 * time.Millisecond)
239+ }
240+}
241+
242+func (h *syncHarness) signalled() []uint64 {
243+ h.mu.Lock()
244+ defer h.mu.Unlock()
245+ return append([]uint64(nil), h.signals...)
246+}
247+
248+// A commit waits for its acquire point; the content carries the fence;
249+// replacing it signals the release point once outputs moved on, and
250+// wl_buffer.release is never sent. The acquire fence closes only then.
251+func TestSyncobjAcquireAndRelease(t *testing.T) {
252+ h := newSyncHarness(t)
253+ h.releases = map[uint32]*syncReleaseProxy{}
254+ a, b := h.dmabuf(), h.dmabuf()
255+ h.commit(a, 1, 2)
256+ if _, ok := h.content(50 * time.Millisecond); ok {
257+ t.Fatal("content before the acquire point had a fence")
258+ }
259+ h.fire(1)
260+ first, ok := h.content(2 * time.Second)
261+ if !ok || first.Acquire == nil {
262+ t.Fatalf("content %+v", first)
263+ }
264+ h.commit(b, 3, 4)
265+ h.fire(3)
266+ second, ok := h.content(2 * time.Second)
267+ if !ok {
268+ t.Fatal("second content")
269+ }
270+ if _, err := first.Acquire.Stat(); err != nil {
271+ t.Fatal("acquire fence closed before release")
272+ }
273+ // The output read the second content: the first buffer is released.
274+ h.presented <- ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}}
275+ deadline := time.Now().Add(2 * time.Second)
276+ for len(h.signalled()) == 0 && time.Now().Before(deadline) {
277+ if err := h.c.Roundtrip(); err != nil {
278+ t.Fatal(err)
279+ }
280+ time.Sleep(2 * time.Millisecond)
281+ }
282+ if got := h.signalled(); len(got) != 1 || got[0] != 2 {
283+ t.Fatalf("signalled %v", got)
284+ }
285+ if _, err := first.Acquire.Stat(); err == nil {
286+ t.Fatal("acquire fence left open after release")
287+ }
288+ if err := h.c.Roundtrip(); err != nil {
289+ t.Fatal(err)
290+ }
291+ select {
292+ case <-h.releases[a].released:
293+ t.Fatal("wl_buffer.release sent under explicit sync")
294+ default:
295+ }
296+}
297+
298+func TestSyncobjProtocolErrors(t *testing.T) {
299+ for _, tc := range []struct {
300+ name string
301+ run func(h *syncHarness)
302+ obj func(h *syncHarness) uint32
303+ code uint32
304+ }{
305+ {"conflicting points", func(h *syncHarness) {
306+ b := h.dmabuf()
307+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
308+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
309+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
310+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
311+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints)},
312+ {"no release point", func(h *syncHarness) {
313+ b := h.dmabuf()
314+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
315+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
316+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
317+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint)},
318+ {"buffer without points", func(h *syncHarness) {
319+ b := h.dmabuf()
320+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
321+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
322+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
323+ {"no acquire point", func(h *syncHarness) {
324+ b := h.dmabuf()
325+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
326+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
327+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
328+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
329+ {"points without attach", func(h *syncHarness) {
330+ b := h.dmabuf()
331+ h.commit(b, 1, 2)
332+ h.fire(1)
333+ h.setPoints(3, 4)
334+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
335+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
336+ {"points with null attach", func(h *syncHarness) {
337+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
338+ h.setPoints(1, 2)
339+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
340+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
341+ {"wl_shm buffer", func(h *syncHarness) {
342+ b := shmBuffer(t, h.c)
343+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
344+ h.setPoints(1, 2)
345+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
346+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)},
347+ {"surface destroyed", func(h *syncHarness) {
348+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
349+ h.setPoints(1, 2)
350+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)},
351+ {"surface exists", func(h *syncHarness) {
352+ mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
353+ h.mgr = mgr
354+ requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.c.AllocateID(), h.surf)
355+ }, func(h *syncHarness) uint32 { return h.mgr }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists)},
356+ } {
357+ t.Run(tc.name, func(t *testing.T) {
358+ h := newSyncHarness(t)
359+ h.releases = map[uint32]*syncReleaseProxy{}
360+ h.t = t
361+ tc.run(h)
362+ expectProtocolError(t, h.c, tc.obj(h), tc.code)
363+ })
364+ }
365+}
366+
367+// An invalid timeline fd is a protocol error.
368+func TestSyncobjInvalidTimeline(t *testing.T) {
369+ dev := newMocksyncobjDevice(t)
370+ dev.EXPECT().fdToHandle(mock.Anything).Return(0, unix.EINVAL).Once()
371+ dir := t.TempDir()
372+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, syncDev: dev}, Channels{}, logging.For(context.Background(), "wayland"))
373+ if err != nil {
374+ t.Fatal(err)
375+ }
376+ ctx, cancel := context.WithCancel(context.Background())
377+ done := make(chan error, 1)
378+ go func() { done <- s.Run(ctx) }()
379+ t.Cleanup(func() { cancel(); <-done })
380+ c := protocolClient(t, s, dir)
381+ mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1")
382+ fd, _ := unix.MemfdCreate("bad", 0)
383+ defer unix.Close(fd)
384+ if err := c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, c.AllocateID()); err != nil {
385+ t.Fatal(err)
386+ }
387+ expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))
388+}
389+
390+// A commit dropped before its acquire point has a fence signals nothing
391+// (the client has not submitted that point) and sends no wl_buffer.release;
392+// the waiter closes its eventfd and no acquire file is left open.
393+func TestSyncobjDroppedCommit(t *testing.T) {
394+ h := newSyncHarness(t)
395+ h.releases = map[uint32]*syncReleaseProxy{}
396+ a := h.dmabuf()
397+ before := openFDs(t)
398+ h.commit(a, 1, 2)
399+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
400+ if err := h.c.Roundtrip(); err != nil {
401+ t.Fatal(err)
402+ }
403+ // The point never gets a fence: the waiter must still stop polling it.
404+ time.Sleep(250 * time.Millisecond)
405+ if err := h.c.Roundtrip(); err != nil {
406+ t.Fatal(err)
407+ }
408+ if got := h.signalled(); len(got) != 0 {
409+ t.Fatalf("signalled %v for a commit whose acquire point never arrived", got)
410+ }
411+ select {
412+ case <-h.releases[a].released:
413+ t.Fatal("wl_buffer.release sent under explicit sync")
414+ default:
415+ }
416+ // The harness keeps its own duplicate of the eventfd.
417+ if after := openFDs(t); after > before+1 {
418+ t.Fatalf("eventfds and sync files %d -> %d", before, after)
419+ }
420+}
421+
422+// A commit dropped after its acquire point fired signals its release point.
423+func TestSyncobjDroppedReadyCommitReleases(t *testing.T) {
424+ h := newSyncHarness(t)
425+ h.releases = map[uint32]*syncReleaseProxy{}
426+ a := h.dmabuf()
427+ h.commit(a, 1, 2)
428+ h.fire(1)
429+ if _, ok := h.content(2 * time.Second); !ok {
430+ t.Fatal("content")
431+ }
432+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
433+ deadline := time.Now().Add(2 * time.Second)
434+ for len(h.signalled()) == 0 && time.Now().Before(deadline) {
435+ if err := h.c.Roundtrip(); err != nil {
436+ t.Fatal(err)
437+ }
438+ time.Sleep(2 * time.Millisecond)
439+ }
440+ if got := h.signalled(); len(got) != 1 || got[0] != 2 {
441+ t.Fatalf("signalled %v", got)
442+ }
443+}
444+
445+// openFDs counts the eventfds and sync files (pipes stand in for them)
446+// the process holds.
447+func openFDs(t *testing.T) int {
448+ t.Helper()
449+ ents, err := os.ReadDir("/proc/self/fd")
450+ if err != nil {
451+ t.Fatal(err)
452+ }
453+ n := 0
454+ for _, e := range ents {
455+ l, _ := os.Readlink("/proc/self/fd/" + e.Name())
456+ if strings.Contains(l, "eventfd") || strings.HasPrefix(l, "pipe:") {
457+ n++
458+ }
459+ }
460+ return n
461+}
M internal/adapters/wayland/xdg.go +3 -1
@@ -182,7 +182,8 @@ func (w *window) unmap() {
182182 w.xdg.acked = false
183183 w.xdg.serials = nil
184184 w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})
185- w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id})
185+ w.xdg.server.syncInhibitors()
186+ w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true})
186187 }
187188 func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
188189 if x.window != nil {
@@ -215,6 +216,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
215216 // Its size comes with the commit, in WindowResized (afterCommit).
216217 w.floating = w.floats()
217218 x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, Slot: slot, PID: r.Client().PID(), Floating: w.floating, Width: w.floatW, Height: w.floatH})
219+ x.server.syncInhibitors()
218220 x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped")
219221 } else if !buffer && w.mapped {
220222 w.unmap()
M internal/app/app.go +18 -4
@@ -25,6 +25,7 @@ import (
2525 "github.com/bnema/nefertty/internal/logging"
2626 "github.com/bnema/nefertty/internal/ports"
2727 "github.com/bnema/zerowrap"
28+ "golang.org/x/sys/unix"
2829 )
2930
3031 type Options struct {
@@ -87,6 +88,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
8788 contents := make(chan ports.SurfaceContent, 64)
8889 cursorChanges := make(chan ports.CursorChange, 1)
8990 presented := make(chan ports.OutputPresented, 64)
91+ outputFormats := make(chan ports.OutputFormats, 8)
9092 ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal}
9193 c, err := core.New(opts.Config, ch)
9294 if err != nil {
@@ -101,7 +103,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
101103 // Every output renders on the same GPU: its formats are the clients'.
102104 dmabuf := vulkan.Probe()
103105 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
104- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented}, logging.For(ctx, "wayland"))
106+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, SyncobjNode: renderNode(dmabuf.Device), Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented, OutputFormats: outputFormats}, logging.For(ctx, "wayland"))
105107 if err != nil {
106108 km.Close()
107109 return err
@@ -159,7 +161,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
159161 return
160162 }
161163 done <- safe("input", func() error {
162- return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input")}, input)
164+ return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion")}, input)
163165 })
164166 return
165167 }
@@ -215,8 +217,12 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
215217 }
216218 if hw != nil {
217219 done <- safe("output", func() error {
218- want := func() drm.Want { return wantFromConfig(opts.Config) }
219- return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm"))
220+ want := func() drm.Want {
221+ w := wantFromConfig(opts.Config)
222+ w.Sampled = dmabuf.Formats
223+ return w
224+ }
225+ return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, outputFormats, curs, newRenderer, logging.For(ctx, "drm"))
220226 })
221227 return
222228 }
@@ -373,3 +379,11 @@ func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.
373379 log.Info().Str("DISPLAY", d.Name()).Msg("listening on X11 display")
374380 return d
375381 }
382+
383+// renderNode is the /dev/dri path of a render node dev_t, "" when none.
384+func renderNode(dev uint64) string {
385+ if dev == 0 {
386+ return ""
387+ }
388+ return fmt.Sprintf("/dev/dri/renderD%d", unix.Minor(dev))
389+}
M internal/app/drm.go +5 -2
@@ -109,12 +109,13 @@ func (b *drmBackend) close() {
109109 // one flip reader per card, and a udev watcher that rescans connectors on
110110 // hotplug. Core learns about outputs through events. It returns when ctx
111111 // ends, after every output is closed.
112-func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
112+func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, formats chan<- ports.OutputFormats, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
113113 ctx, cancel := context.WithCancel(ctx)
114114 defer cancel()
115115 var readers sync.WaitGroup
116116 readerErr := make(chan error, len(b.cards))
117117 for _, c := range b.cards {
118+ c.SetFormats(formats)
118119 readers.Go(func() {
119120 if err := safe("drm events", func() error { return c.ReadEvents(ctx) }); err != nil {
120121 readerErr <- fmt.Errorf("%s: %w", c.Path(), err)
@@ -141,7 +142,9 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, event
141142 stopping := map[string]bool{}
142143 scan := func() {
143144 for _, c := range b.cards {
144- c.SetWant(want())
145+ w := want()
146+ w.Device = c.Device()
147+ c.SetWant(w)
145148 added, removed, replaced, err := c.Scan()
146149 if err != nil {
147150 log.Warn().Err(err).Msg("scan connectors")
M internal/core/core.go +77 -5
@@ -89,6 +89,12 @@ type Core struct {
8989 // pointer moving to another output does not.
9090 layerFocus WindowID
9191 layerOver map[*screen]WindowID
92+ // inhibitors are the windows asking to keep compositor binds while
93+ // focused, inhibiting the one active now (0: none); idle are those
94+ // keeping the session awake.
95+ inhibitors map[WindowID]bool
96+ inhibiting WindowID
97+ idle map[WindowID]bool
9298 }
9399
94100 func keyName(s string) string {
@@ -271,7 +277,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) {
271277 return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1")
272278 }
273279 // A placeholder screen holds windows until the first output arrives.
274- c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}}
280+ c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}}
275281 if err := c.apply(cfg); err != nil {
276282 return nil, err
277283 }
@@ -448,12 +454,37 @@ func (c *Core) publish(ctx context.Context) error {
448454 }
449455 c.focus = focus
450456 }
457+ if err := c.updateInhibit(ctx); err != nil {
458+ return err
459+ }
451460 c.resolveConstraint()
452461 latest(c.ch.Scenes, scenes)
453462 c.publishState()
454463 return nil
455464 }
456465
466+// updateInhibit makes the shortcuts inhibitor of the keyboard focus the
467+// active one and tells wayland when it changes.
468+func (c *Core) updateInhibit(ctx context.Context) error {
469+ want := WindowID(0)
470+ if f := c.keyboardFocus(); f != 0 && c.inhibitors[f] {
471+ want = f
472+ }
473+ if want == c.inhibiting {
474+ return nil
475+ }
476+ if c.inhibiting != 0 {
477+ if err := c.command(ctx, ports.ShortcutsInhibitState{Window: c.inhibiting}); err != nil {
478+ return err
479+ }
480+ }
481+ c.inhibiting = want
482+ if want != 0 {
483+ return c.command(ctx, ports.ShortcutsInhibitState{Window: want, Active: true})
484+ }
485+ return nil
486+}
487+
457488 // resolveConstraint turns the active constraint into global coordinates
458489 // and sends it to input when it changed. A hidden window holds nothing.
459490 func (c *Core) resolveConstraint() {
@@ -616,6 +647,24 @@ func (c *Core) Run(ctx context.Context) error {
616647 if p := c.popups[v.ID]; p != nil {
617648 p.mapped = true
618649 }
650+ case ports.ShortcutsInhibit:
651+ if v.Active {
652+ c.inhibitors[v.Window] = true
653+ } else {
654+ delete(c.inhibitors, v.Window)
655+ }
656+ if err := c.updateInhibit(ctx); err != nil {
657+ return nil
658+ }
659+ continue
660+ case ports.IdleInhibit:
661+ if v.Active {
662+ c.idle[v.Window] = true
663+ } else {
664+ delete(c.idle, v.Window)
665+ }
666+ c.publishState()
667+ continue
619668 case ports.WindowAppID:
620669 if info, ok := c.clients[v.ID]; ok {
621670 info.AppID = v.AppID
@@ -631,6 +680,8 @@ func (c *Core) Run(ctx context.Context) error {
631680 return nil
632681 }
633682 delete(c.clients, v.ID)
683+ delete(c.inhibitors, v.ID)
684+ delete(c.idle, v.ID)
634685 if s, _ := c.screenOf(v.ID); s != nil {
635686 s.mon.RemoveWindow(v.ID)
636687 }
@@ -797,6 +848,21 @@ func (c *Core) Run(ctx context.Context) error {
797848 if !ok {
798849 continue
799850 }
851+ // A focused window inhibiting shortcuts gets every key:
852+ // no bind runs (emergency quit and VT switch are handled by
853+ // input before core).
854+ if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() {
855+ if held := heldKey(key); !key.Pressed && c.pressed[held] {
856+ // Its press ran a bind before inhibiting began: the
857+ // window never saw it.
858+ delete(c.pressed, held)
859+ continue
860+ }
861+ if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil {
862+ return nil
863+ }
864+ continue
865+ }
800866 name := keyName(key.Keysym)
801867 // A bind on the keysym wins; then the unshifted keysym (Cmd+Shift+1
802868 // prints exclam on US); then the physical key (code:N).
@@ -808,10 +874,7 @@ func (c *Core) Run(ctx context.Context) error {
808874 action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
809875 }
810876 // Track presses by physical key: Shift may be released before the key.
811- held := name
812- if key.Keycode != 0 {
813- held = "#" + strconv.FormatUint(uint64(key.Keycode), 10)
814- }
877+ held := heldKey(key)
815878 if !key.Pressed {
816879 consumed := c.pressed[held]
817880 delete(c.pressed, held)
@@ -902,3 +965,12 @@ func (c *Core) activate(ctx context.Context, id WindowID) error {
902965 c.focusScreen = c.screenIndex(s.name())
903966 return c.afterShow(ctx, before)
904967 }
968+
969+// heldKey names a key for press tracking: by physical key, since Shift
970+// may be released before the key.
971+func heldKey(key ports.KeyEvent) string {
972+ if key.Keycode != 0 {
973+ return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
974+ }
975+ return keyName(key.Keysym)
976+}
A internal/core/inhibit_test.go +127 -0
@@ -0,0 +1,127 @@
1+package core_test
2+
3+import (
4+ "context"
5+ "testing"
6+ "time"
7+
8+ "github.com/bnema/nefertty/internal/adapters/config"
9+ "github.com/bnema/nefertty/internal/core"
10+ "github.com/bnema/nefertty/internal/ports"
11+)
12+
13+// commandOf waits for the next command of type T, skipping others.
14+func commandOf[T ports.ClientCommand](t *testing.T, commands <-chan ports.ClientCommand) T {
15+ t.Helper()
16+ deadline := time.After(2 * time.Second)
17+ for {
18+ select {
19+ case c := <-commands:
20+ if v, ok := c.(T); ok {
21+ return v
22+ }
23+ case <-deadline:
24+ var zero T
25+ t.Fatalf("no %T", zero)
26+ return zero
27+ }
28+ }
29+}
30+
31+// A focused window inhibiting shortcuts gets bound keys; it stops when
32+// it loses focus, and binds run again.
33+func TestShortcutsInhibit(t *testing.T) {
34+ cfg := config.Defaults()
35+ input := make(chan ports.InputEvent, 4)
36+ client := make(chan ports.ClientEvent, 4)
37+ commands := make(chan ports.ClientCommand, 64)
38+ scenes := make(chan []ports.Scene, 1)
39+ spawn := make(chan ports.SpawnRequest, 4)
40+ state := make(chan ports.State, 1)
41+ output := make(chan ports.OutputEvent, 1)
42+ c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn, State: state})
43+ if err != nil {
44+ t.Fatal(err)
45+ }
46+ ctx, cancel := context.WithCancel(context.Background())
47+ done := make(chan error, 1)
48+ go func() { done <- c.Run(ctx) }()
49+ defer func() { cancel(); receive(t, done) }()
50+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
51+ scene(t, scenes)
52+ client <- ports.WindowMapped{ID: 1}
53+ scene(t, scenes)
54+ client <- ports.ShortcutsInhibit{Window: 1, Active: true}
55+ if v := commandOf[ports.ShortcutsInhibitState](t, commands); v != (ports.ShortcutsInhibitState{Window: 1, Active: true}) {
56+ t.Fatal(v)
57+ }
58+ bound := ports.KeyEvent{Keysym: "Return", Mods: ports.ModSuper, Pressed: true}
59+ input <- bound
60+ if v := commandOf[ports.ForwardKey](t, commands); v != (ports.ForwardKey{ID: 1, Key: bound}) {
61+ t.Fatal(v)
62+ }
63+ select {
64+ case r := <-spawn:
65+ t.Fatalf("bind ran: %v", r)
66+ case <-time.After(30 * time.Millisecond):
67+ }
68+ // A new window takes focus: the inhibitor goes inactive, binds run.
69+ client <- ports.WindowMapped{ID: 2}
70+ if v := commandOf[ports.ShortcutsInhibitState](t, commands); v != (ports.ShortcutsInhibitState{Window: 1}) {
71+ t.Fatal(v)
72+ }
73+ input <- bound
74+ receive(t, spawn)
75+ // Idle inhibitors show in the state.
76+ client <- ports.IdleInhibit{Window: 2, Active: true}
77+ deadline := time.After(2 * time.Second)
78+ for {
79+ select {
80+ case st := <-state:
81+ for _, w := range st.Windows {
82+ if w.ID == 2 && w.IdleInhibit {
83+ return
84+ }
85+ }
86+ case <-deadline:
87+ t.Fatal("idle inhibit not in state")
88+ }
89+ }
90+}
91+
92+// A key whose press ran a bind before inhibiting began is not released to
93+// the window: it never saw the press.
94+func TestShortcutsInhibitDropsBoundRelease(t *testing.T) {
95+ cfg := config.Defaults()
96+ input := make(chan ports.InputEvent, 4)
97+ client := make(chan ports.ClientEvent, 4)
98+ commands := make(chan ports.ClientCommand, 64)
99+ scenes := make(chan []ports.Scene, 1)
100+ spawn := make(chan ports.SpawnRequest, 4)
101+ output := make(chan ports.OutputEvent, 1)
102+ c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn})
103+ if err != nil {
104+ t.Fatal(err)
105+ }
106+ ctx, cancel := context.WithCancel(context.Background())
107+ done := make(chan error, 1)
108+ go func() { done <- c.Run(ctx) }()
109+ defer func() { cancel(); receive(t, done) }()
110+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
111+ scene(t, scenes)
112+ client <- ports.WindowMapped{ID: 1}
113+ scene(t, scenes)
114+ press := ports.KeyEvent{Keysym: "Return", Keycode: 36, Mods: ports.ModSuper, Pressed: true}
115+ input <- press
116+ receive(t, spawn)
117+ client <- ports.ShortcutsInhibit{Window: 1, Active: true}
118+ commandOf[ports.ShortcutsInhibitState](t, commands)
119+ release := press
120+ release.Pressed = false
121+ input <- release
122+ other := ports.KeyEvent{Keysym: "a", Keycode: 38, Pressed: true}
123+ input <- other
124+ if v := commandOf[ports.ForwardKey](t, commands); v.Key != other {
125+ t.Fatalf("forwarded %+v before %+v", v.Key, other)
126+ }
127+}
M internal/core/state.go +1 -1
@@ -40,7 +40,7 @@ func (c *Core) state() ports.State {
4040 n := indexOf(m.Workspaces, w) + 1
4141 for _, id := range w.windows() {
4242 info := c.clients[id]
43- st.Windows = append(st.Windows, ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: w == cur})
43+ st.Windows = append(st.Windows, ports.WindowState{ID: id, AppID: info.AppID, PID: info.PID, Output: s.name(), Workspace: n, Visible: w == cur, IdleInhibit: c.idle[id]})
4444 }
4545 }
4646 }
M internal/logging/logging.go +17 -1
@@ -20,6 +20,11 @@ var components = map[string]bool{
2020 "render": true, "sync": true, "config": true, "app": true,
2121 }
2222
23+// categories are debug switches narrower than a component, only on when
24+// named: "all" leaves them off (input-motion logs every pointer motion).
25+// Naming one turns on debug for its component.
26+var categories = map[string]string{"input-motion": "input"}
27+
2328 type debugKey struct{}
2429 type levelKey struct{}
2530 type debugSet map[string]bool
@@ -34,7 +39,7 @@ func ParseDebug(value string) (map[string]bool, error) {
3439 selected[name] = true
3540 continue
3641 }
37- if !components[name] {
42+ if _, ok := categories[name]; !components[name] && !ok {
3843 return nil, fmt.Errorf("invalid debug component %q", name)
3944 }
4045 selected[name] = true
@@ -104,6 +109,12 @@ func (w *consoleAndFile) Write(p []byte) (int, error) {
104109 return w.file.Write(p)
105110 }
106111
112+// Enabled reports whether a debug category was named in --debug.
113+func Enabled(ctx context.Context, category string) bool {
114+ selected, _ := ctx.Value(debugKey{}).(debugSet)
115+ return selected[category]
116+}
117+
107118 // For returns a component logger with debug enabled only for selected components.
108119 func For(ctx context.Context, component string) zerowrap.Logger {
109120 log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component)
@@ -120,6 +131,11 @@ func For(ctx context.Context, component string) zerowrap.Logger {
120131 if selected["all"] || selected[component] {
121132 level = zerolog.DebugLevel
122133 }
134+ for name, owner := range categories {
135+ if selected[name] && owner == component {
136+ level = zerolog.DebugLevel
137+ }
138+ }
123139 log = zerowrap.Logger{Logger: log.Level(level)}
124140 return log
125141 }
M internal/logging/logging_test.go +40 -0
@@ -18,6 +18,7 @@ func TestParseDebug(t *testing.T) {
1818 }{
1919 {"", false, nil}, {"core,app", false, []string{"core", "app"}},
2020 {"all", false, []string{"all"}}, {"unknown", true, nil}, {"core,", true, nil},
21+ {"all,input-motion", false, []string{"all", "input-motion"}},
2122 } {
2223 got, err := ParseDebug(tc.input)
2324 if (err != nil) != tc.wantError {
@@ -82,3 +83,42 @@ func splitLines(data []byte) [][]byte {
8283 }
8384 return lines
8485 }
86+
87+// A category alone raises its component to debug, so its lines are written.
88+func TestCategoryEnablesComponentDebug(t *testing.T) {
89+ t.Setenv("XDG_STATE_HOME", t.TempDir())
90+ ctx, closeLog, err := Open(context.Background(), "info", "input-motion")
91+ if err != nil {
92+ t.Fatal(err)
93+ }
94+ input, core := For(ctx, "input"), For(ctx, "core")
95+ input.Debug().Msg("motion")
96+ core.Debug().Msg("hidden")
97+ if err := closeLog(); err != nil {
98+ t.Fatal(err)
99+ }
100+ data, err := os.ReadFile(filepath.Join(os.Getenv("XDG_STATE_HOME"), "nefertty", "runs", "latest.log"))
101+ if err != nil {
102+ t.Fatal(err)
103+ }
104+ if lines := splitLines(data); len(lines) != 1 || !bytes.Contains(lines[0], []byte(`"motion"`)) {
105+ t.Fatalf("got %q", data)
106+ }
107+}
108+
109+// Narrow categories are on only when named, never through "all".
110+func TestEnabledCategories(t *testing.T) {
111+ for _, tc := range []struct {
112+ debug string
113+ want bool
114+ }{{"all", false}, {"input", false}, {"input-motion", true}, {"all,input-motion", true}} {
115+ sel, err := ParseDebug(tc.debug)
116+ if err != nil {
117+ t.Fatal(err)
118+ }
119+ ctx := context.WithValue(context.Background(), debugKey{}, debugSet(sel))
120+ if got := Enabled(ctx, "input-motion"); got != tc.want {
121+ t.Errorf("%q: %v", tc.debug, got)
122+ }
123+ }
124+}
M internal/mocks/ports/devices_mock.go +128 -31
@@ -6,6 +6,7 @@ package portsmocks
66
77 import (
88 "image"
9+ "os"
910
1011 "github.com/bnema/nefertty/internal/ports"
1112 mock "github.com/stretchr/testify/mock"
@@ -80,49 +81,65 @@ func (_c *MockRenderer_Close_Call) RunAndReturn(run func()) *MockRenderer_Close_
8081 return _c
8182 }
8283
83-// CopyBGRX provides a mock function for the type MockRenderer
84-func (_mock *MockRenderer) CopyBGRX(dst []byte, pitch int) {
85- _mock.Called(dst, pitch)
86- return
84+// CursorBuffers provides a mock function for the type MockRenderer
85+func (_mock *MockRenderer) CursorBuffers(size int) ([2]ports.DMABuf, error) {
86+ ret := _mock.Called(size)
87+
88+ if len(ret) == 0 {
89+ panic("no return value specified for CursorBuffers")
90+ }
91+
92+ var r0 [2]ports.DMABuf
93+ var r1 error
94+ if returnFunc, ok := ret.Get(0).(func(int) ([2]ports.DMABuf, error)); ok {
95+ return returnFunc(size)
96+ }
97+ if returnFunc, ok := ret.Get(0).(func(int) [2]ports.DMABuf); ok {
98+ r0 = returnFunc(size)
99+ } else {
100+ if ret.Get(0) != nil {
101+ r0 = ret.Get(0).([2]ports.DMABuf)
102+ }
103+ }
104+ if returnFunc, ok := ret.Get(1).(func(int) error); ok {
105+ r1 = returnFunc(size)
106+ } else {
107+ r1 = ret.Error(1)
108+ }
109+ return r0, r1
87110 }
88111
89-// MockRenderer_CopyBGRX_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CopyBGRX'
90-type MockRenderer_CopyBGRX_Call struct {
112+// MockRenderer_CursorBuffers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CursorBuffers'
113+type MockRenderer_CursorBuffers_Call struct {
91114 *mock.Call
92115 }
93116
94-// CopyBGRX is a helper method to define mock.On call
95-// - dst []byte
96-// - pitch int
97-func (_e *MockRenderer_Expecter) CopyBGRX(dst any, pitch any) *MockRenderer_CopyBGRX_Call {
98- return &MockRenderer_CopyBGRX_Call{Call: _e.mock.On("CopyBGRX", dst, pitch)}
117+// CursorBuffers is a helper method to define mock.On call
118+// - size int
119+func (_e *MockRenderer_Expecter) CursorBuffers(size any) *MockRenderer_CursorBuffers_Call {
120+ return &MockRenderer_CursorBuffers_Call{Call: _e.mock.On("CursorBuffers", size)}
99121 }
100122
101-func (_c *MockRenderer_CopyBGRX_Call) Run(run func(dst []byte, pitch int)) *MockRenderer_CopyBGRX_Call {
123+func (_c *MockRenderer_CursorBuffers_Call) Run(run func(size int)) *MockRenderer_CursorBuffers_Call {
102124 _c.Call.Run(func(args mock.Arguments) {
103- var arg0 []byte
125+ var arg0 int
104126 if args[0] != nil {
105- arg0 = args[0].([]byte)
106- }
107- var arg1 int
108- if args[1] != nil {
109- arg1 = args[1].(int)
127+ arg0 = args[0].(int)
110128 }
111129 run(
112130 arg0,
113- arg1,
114131 )
115132 })
116133 return _c
117134 }
118135
119-func (_c *MockRenderer_CopyBGRX_Call) Return() *MockRenderer_CopyBGRX_Call {
120- _c.Call.Return()
136+func (_c *MockRenderer_CursorBuffers_Call) Return(dMABufs [2]ports.DMABuf, err error) *MockRenderer_CursorBuffers_Call {
137+ _c.Call.Return(dMABufs, err)
121138 return _c
122139 }
123140
124-func (_c *MockRenderer_CopyBGRX_Call) RunAndReturn(run func(dst []byte, pitch int)) *MockRenderer_CopyBGRX_Call {
125- _c.Run(run)
141+func (_c *MockRenderer_CursorBuffers_Call) RunAndReturn(run func(size int) ([2]ports.DMABuf, error)) *MockRenderer_CursorBuffers_Call {
142+ _c.Call.Return(run)
126143 return _c
127144 }
128145
@@ -241,20 +258,31 @@ func (_c *MockRenderer_Pixels_Call) RunAndReturn(run func() *image.RGBA) *MockRe
241258 }
242259
243260 // Render provides a mock function for the type MockRenderer
244-func (_mock *MockRenderer) Render(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) error {
261+func (_mock *MockRenderer) Render(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
245262 ret := _mock.Called(scene, windowIDToSurfaceContent)
246263
247264 if len(ret) == 0 {
248265 panic("no return value specified for Render")
249266 }
250267
251- var r0 error
252- if returnFunc, ok := ret.Get(0).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error); ok {
268+ var r0 *os.File
269+ var r1 error
270+ if returnFunc, ok := ret.Get(0).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error)); ok {
271+ return returnFunc(scene, windowIDToSurfaceContent)
272+ }
273+ if returnFunc, ok := ret.Get(0).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) *os.File); ok {
253274 r0 = returnFunc(scene, windowIDToSurfaceContent)
254275 } else {
255- r0 = ret.Error(0)
276+ if ret.Get(0) != nil {
277+ r0 = ret.Get(0).(*os.File)
278+ }
256279 }
257- return r0
280+ if returnFunc, ok := ret.Get(1).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error); ok {
281+ r1 = returnFunc(scene, windowIDToSurfaceContent)
282+ } else {
283+ r1 = ret.Error(1)
284+ }
285+ return r0, r1
258286 }
259287
260288 // MockRenderer_Render_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Render'
@@ -287,12 +315,12 @@ func (_c *MockRenderer_Render_Call) Run(run func(scene ports.Scene, windowIDToSu
287315 return _c
288316 }
289317
290-func (_c *MockRenderer_Render_Call) Return(err error) *MockRenderer_Render_Call {
291- _c.Call.Return(err)
318+func (_c *MockRenderer_Render_Call) Return(done *os.File, err error) *MockRenderer_Render_Call {
319+ _c.Call.Return(done, err)
292320 return _c
293321 }
294322
295-func (_c *MockRenderer_Render_Call) RunAndReturn(run func(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) error) *MockRenderer_Render_Call {
323+func (_c *MockRenderer_Render_Call) RunAndReturn(run func(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) (*os.File, error)) *MockRenderer_Render_Call {
296324 _c.Call.Return(run)
297325 return _c
298326 }
@@ -337,6 +365,75 @@ func (_c *MockRenderer_UseTarget_Call) RunAndReturn(run func(i int)) *MockRender
337365 return _c
338366 }
339367
368+// WriteCursor provides a mock function for the type MockRenderer
369+func (_mock *MockRenderer) WriteCursor(i int, pixels []byte, w int, h int) error {
370+ ret := _mock.Called(i, pixels, w, h)
371+
372+ if len(ret) == 0 {
373+ panic("no return value specified for WriteCursor")
374+ }
375+
376+ var r0 error
377+ if returnFunc, ok := ret.Get(0).(func(int, []byte, int, int) error); ok {
378+ r0 = returnFunc(i, pixels, w, h)
379+ } else {
380+ r0 = ret.Error(0)
381+ }
382+ return r0
383+}
384+
385+// MockRenderer_WriteCursor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteCursor'
386+type MockRenderer_WriteCursor_Call struct {
387+ *mock.Call
388+}
389+
390+// WriteCursor is a helper method to define mock.On call
391+// - i int
392+// - pixels []byte
393+// - w int
394+// - h int
395+func (_e *MockRenderer_Expecter) WriteCursor(i any, pixels any, w any, h any) *MockRenderer_WriteCursor_Call {
396+ return &MockRenderer_WriteCursor_Call{Call: _e.mock.On("WriteCursor", i, pixels, w, h)}
397+}
398+
399+func (_c *MockRenderer_WriteCursor_Call) Run(run func(i int, pixels []byte, w int, h int)) *MockRenderer_WriteCursor_Call {
400+ _c.Call.Run(func(args mock.Arguments) {
401+ var arg0 int
402+ if args[0] != nil {
403+ arg0 = args[0].(int)
404+ }
405+ var arg1 []byte
406+ if args[1] != nil {
407+ arg1 = args[1].([]byte)
408+ }
409+ var arg2 int
410+ if args[2] != nil {
411+ arg2 = args[2].(int)
412+ }
413+ var arg3 int
414+ if args[3] != nil {
415+ arg3 = args[3].(int)
416+ }
417+ run(
418+ arg0,
419+ arg1,
420+ arg2,
421+ arg3,
422+ )
423+ })
424+ return _c
425+}
426+
427+func (_c *MockRenderer_WriteCursor_Call) Return(err error) *MockRenderer_WriteCursor_Call {
428+ _c.Call.Return(err)
429+ return _c
430+}
431+
432+func (_c *MockRenderer_WriteCursor_Call) RunAndReturn(run func(i int, pixels []byte, w int, h int) error) *MockRenderer_WriteCursor_Call {
433+ _c.Call.Return(run)
434+ return _c
435+}
436+
340437 // NewMockSeat creates a new instance of MockSeat. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
341438 // The first argument is typically a *testing.T value.
342439 func NewMockSeat(t interface {
A internal/ports/damage_test.go +37 -0
@@ -0,0 +1,37 @@
1+package ports
2+
3+import (
4+ "slices"
5+ "testing"
6+)
7+
8+func TestDamageSince(t *testing.T) {
9+ a, b := Rect{X: 1, W: 2, H: 2}, Rect{Y: 5, W: 1, H: 1}
10+ c := SurfaceContent{Seq: 7, DamageHistory: []SeqDamage{{Seq: 5, Rects: []Rect{a}}, {Seq: 6, Full: true}, {Seq: 7, Rects: []Rect{b}}}}
11+ for _, tc := range []struct {
12+ name string
13+ since uint64
14+ want []Rect
15+ ok bool
16+ }{
17+ {"up to date", 7, nil, true},
18+ {"one behind", 6, []Rect{b}, true},
19+ {"across a full change", 5, nil, false},
20+ {"history too short", 3, nil, false},
21+ {"first content", 0, nil, false},
22+ } {
23+ got, ok := c.DamageSince(tc.since)
24+ if ok != tc.ok || !slices.Equal(got, tc.want) {
25+ t.Errorf("%s: %v %v", tc.name, got, ok)
26+ }
27+ }
28+ c.DamageHistory[1] = SeqDamage{Seq: 6, Rects: []Rect{a}}
29+ if got, ok := c.DamageSince(4); !ok || !slices.Equal(got, []Rect{a, a, b}) {
30+ t.Fatalf("union %v %v", got, ok)
31+ }
32+ // A history not ending at the content is stale: redraw all.
33+ c.Seq = 8
34+ if _, ok := c.DamageSince(6); ok {
35+ t.Fatal("stale history used")
36+ }
37+}
M internal/ports/devices.go +14 -6
@@ -1,12 +1,17 @@
11 package ports
22
3-import "image"
3+import (
4+ "image"
5+ "os"
6+)
47
58 // Renderer draws scenes for an output. One goroutine owns it. Frames go
69 // to its own image until ExportTargets gives it scanout images; then each
7-// Render draws into the target chosen by UseTarget (ADR 014: no CPU copy).
10+// Render draws into the target chosen by UseTarget (ADR 014: no CPU pass).
811 type Renderer interface {
9- Render(Scene, map[WindowID]SurfaceContent) error
12+ // Render draws the scene. done is a sync file signalled when the GPU
13+ // finished the frame (nil: already finished); the caller closes it.
14+ Render(Scene, map[WindowID]SurfaceContent) (done *os.File, err error)
1015 // ExportTargets allocates n images of the renderer's size that the
1116 // display can scan out, with one of the given XRGB8888 modifiers
1217 // (none: any the device exports), and returns them as dmabufs.
@@ -14,11 +19,14 @@ type Renderer interface {
1419 ExportTargets(n int, modifiers []uint64) ([]DMABuf, error)
1520 // UseTarget selects the exported image the next Render draws into.
1621 UseTarget(i int)
22+ // CursorBuffers allocates two linear ARGB8888 images of size×size the
23+ // display can show on its cursor plane, and returns them as dmabufs.
24+ CursorBuffers(size int) ([2]DMABuf, error)
25+ // WriteCursor fills cursor image i with premultiplied ARGB8888 pixels
26+ // (w×h, w*4 per row), cropped to the image and cleared around it.
27+ WriteCursor(i int, pixels []byte, w, h int) error
1728 // Pixels reads the last frame back (headless screenshots, tests).
1829 Pixels() *image.RGBA
19- // CopyBGRX reads the last frame back as XRGB8888 rows of the given
20- // pitch: the logged fallback when no image can be exported.
21- CopyBGRX(dst []byte, pitch int)
2230 Close()
2331 }
2432
M internal/ports/messages.go +113 -6
@@ -1,6 +1,9 @@
11 package ports
22
3-import "os"
3+import (
4+ "os"
5+ "time"
6+)
47
58 // ClientEvent carries wayland → core notifications.
69 type ClientEvent interface{ clientEvent() }
@@ -31,6 +34,25 @@ type WindowUnmapped struct{ ID WindowID }
3134
3235 func (WindowUnmapped) clientEvent() {}
3336
37+// ShortcutsInhibit carries wayland → core that a window asks for (Active)
38+// or stops asking for all keys, compositor binds included, while it has
39+// keyboard focus (zwp_keyboard_shortcuts_inhibit_v1).
40+type ShortcutsInhibit struct {
41+ Window WindowID
42+ Active bool
43+}
44+
45+func (ShortcutsInhibit) clientEvent() {}
46+
47+// IdleInhibit carries wayland → core that a window keeps (Active) or
48+// stops keeping the session from going idle (zwp_idle_inhibit_v1).
49+type IdleInhibit struct {
50+ Window WindowID
51+ Active bool
52+}
53+
54+func (IdleInhibit) clientEvent() {}
55+
3456 // WindowFullscreenRequest carries wayland → core fullscreen requests.
3557 type WindowFullscreenRequest struct {
3658 ID WindowID
@@ -315,17 +337,47 @@ func (OutputAdded) outputEvent() {}
315337 // OutputPresented carries output → wayland what an output shows and reads.
316338 // Flip is set when a page flip completed: frame callbacks of the surfaces
317339 // on it are due. Shown and Queued are the DMABuf IDs scanned out directly
318-// (0: a composed image); Seen is the latest content Seq the output got per
319-// window. A replaced client buffer is released once every output that
320-// reports has seen a later content of its window and neither shows nor
321-// queues it.
340+// (0: a composed image); Seen is the latest content Seq per window the
341+// output finished reading (the GPU is done with it). A replaced client
342+// buffer is released once every output that reports has seen a later
343+// content of its window and neither shows nor queues it.
322344 type OutputPresented struct {
323345 Output string
324- Flip bool
346+ Flip *FlipInfo
325347 Shown, Queued uint64
326348 Seen map[WindowID]uint64
327349 }
328350
351+// OutputFormats carries output → wayland the dmabuf formats an output can
352+// scan out directly (its primary plane's, that the renderer also samples,
353+// so a refused buffer can still be composed). Device is the KMS device
354+// (dev_t) clients allocate scanout buffers for. Sent at start and after
355+// every modeset; empty Formats means no direct scanout.
356+type OutputFormats struct {
357+ Output string
358+ Device uint64
359+ Formats []DMABufFormat
360+}
361+
362+// FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time
363+// (hardware clock on DRM), Seq the output's vblank counter, Refresh the
364+// refresh period (0 while variable refresh is on). ZeroCopy is the window
365+// whose buffer was shown without composition (direct scanout or overlay
366+// plane; 0: none), Async set for a tearing flip.
367+// Shows is the content Seq per window the flipped frame shows. Merged
368+// counts earlier flips folded into this one when the reader fell behind:
369+// their presentation feedback is discarded.
370+type FlipInfo struct {
371+ When time.Duration
372+ Seq uint64
373+ Refresh time.Duration
374+ ZeroCopy WindowID
375+ Async bool
376+ HardwareClock bool
377+ Merged int
378+ Shows map[WindowID]uint64
379+}
380+
329381 // OutputRemoved carries output → core an unplugged display.
330382 type OutputRemoved struct{ Name string }
331383
@@ -337,6 +389,15 @@ type ConfigChanged struct{ Config Config }
337389 // ClientCommand carries core → wayland commands.
338390 type ClientCommand interface{ clientCommand() }
339391
392+// ShortcutsInhibitState carries core → wayland whether a window's
393+// shortcuts inhibitor is in effect (it has keyboard focus).
394+type ShortcutsInhibitState struct {
395+ Window WindowID
396+ Active bool
397+}
398+
399+func (ShortcutsInhibitState) clientCommand() {}
400+
340401 // ConfigureWindow carries core → wayland geometry and state. Output is the
341402 // connector showing the window, empty while it is hidden.
342403 type ConfigureWindow struct {
@@ -590,6 +651,50 @@ type SurfaceContent struct {
590651 // Async asks for tearing presentation (wp_tearing_control_v1); outputs
591652 // honour it only in direct scanout.
592653 Async bool
654+ // Acquire is the explicit-sync fence of this content's root buffer
655+ // (wp_linux_drm_syncobj_v1): readers wait on it before reading the
656+ // buffer, instead of the buffer's implicit fences. Wayland owns it and
657+ // closes it when the buffer is released; a reader that keeps it past
658+ // the call duplicates it under Acquire.SyscallConn. nil: implicit sync.
659+ // Wayland closes it only after every output reported a later content of
660+ // the window as seen, so a reader never gets it already closed.
661+ Acquire *os.File
662+ // DamageHistory is what changed in the root surface's buffer over the
663+ // window's last contents, oldest first, ending with this one (Seq).
664+ // A renderer holding an older content redraws the union of the
665+ // entries after it, or everything when the history does not reach it.
666+ DamageHistory []SeqDamage
667+}
668+
669+// SeqDamage is what content Seq changed from the one before: Rects in
670+// root buffer pixels, or everything when Full.
671+type SeqDamage struct {
672+ Seq uint64
673+ Full bool
674+ Rects []Rect
675+}
676+
677+// DamageSince is the union of changes after content seq up to this one,
678+// and false when the history does not reach back to seq (redraw all).
679+func (c SurfaceContent) DamageSince(seq uint64) ([]Rect, bool) {
680+ if seq >= c.Seq {
681+ return nil, true
682+ }
683+ h := c.DamageHistory
684+ if len(h) == 0 || h[0].Seq > seq+1 || h[len(h)-1].Seq != c.Seq {
685+ return nil, false
686+ }
687+ var out []Rect
688+ for _, d := range h {
689+ if d.Seq <= seq {
690+ continue
691+ }
692+ if d.Full {
693+ return nil, false
694+ }
695+ out = append(out, d.Rects...)
696+ }
697+ return out, true
593698 }
594699
595700 // Subsurface is a child surface at X, Y logical pixels from the root
@@ -695,4 +800,6 @@ type WindowState struct {
695800 // Visible means on the workspace on screen (it may be behind a
696801 // fullscreen window).
697802 Visible bool
803+ // IdleInhibit is set while the window keeps the session from idling.
804+ IdleInhibit bool `json:",omitempty"`
698805 }

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 23

Tags 10

↑↓ moveenter openesc close