1package drm
2
3import (
4 "errors"
5 "slices"
6 "sync"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
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.
16
17const (
18 ioctlGetCap = 0xC010640C // DRM_IOWR('d', 0x0C, struct drm_get_cap)
19 capCursorW = 0x8
20 capCursorH = 0x9
21)
22
23type getCap struct{ capability, value uint64 }
24
25// Cursor is the hardware cursor of one output.
26type 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 // imgs are the two images (nil: none), alloc says who allocated them
37 // (allocDumb or allocVulkan) and fallback makes the renderer's images
38 // when KMS refuses the dumb ones. fbs mirrors their framebuffers; cur
39 // is on screen or next.
40 imgs cursorImages
41 alloc string
42 fallback func() (cursorImages, error)
43 fbs [2]uint32
44 cur int
45 image bool // an image is loaded
46 hotX, hotY int
47 // last is a copy of the loaded image, written again into new images
48 // when the allocator changes.
49 last laterImage
50 applied cursorState // what the last commit put on the plane
51 commits int
52 // screen is the image the plane scans out (0: none); flight is what
53 // the pending commit puts on it when flying. An image is written only
54 // when it is neither, so the plane never shows a half-written image.
55 screen uint32
56 flight cursorState
57 flying bool
58 // later is an image that waits for a free slot (both on screen or in
59 // flight); off is set when KMS refused the cursor plane.
60 later *laterImage
61 off bool
62}
63
64type laterImage struct {
65 pixels []byte
66 w, h, hotX, hotY int
67}
68
69// cursorState is the cursor plane as committed: off, or fb at x, y.
70type cursorState struct {
71 on bool
72 fb uint32
73 x, y int
74}
75
76// CursorStats counts cursor work since the last TakeStats.
77type CursorStats struct {
78 Moves int // Move calls
79 Commits int // commits that moved or changed the cursor
80}
81
82func newCursor(p *plane, size int) *Cursor {
83 return &Cursor{plane: p, size: size, wake: make(chan struct{}, 1)}
84}
85
86// TakeStats returns and resets the counters. Output goroutine only.
87func (c *Cursor) TakeStats() CursorStats {
88 c.mu.Lock()
89 s := CursorStats{Moves: c.moves, Commits: c.commits}
90 c.moves, c.commits = 0, 0
91 c.mu.Unlock()
92 return s
93}
94
95// Limit is the largest image side the cursor plane accepts.
96func (c *Cursor) Limit() int { return c.size }
97
98// Move places the hotspot at physical (x, y). It never blocks.
99func (c *Cursor) Move(x, y float64) {
100 c.mu.Lock()
101 c.x, c.y = int(x), int(y)
102 c.away = false
103 c.moves++
104 c.mu.Unlock()
105 c.poke()
106}
107
108// Hide removes the cursor from the output until the next move (the pointer
109// is on another output).
110func (c *Cursor) Hide() {
111 c.mu.Lock()
112 c.away = true
113 c.mu.Unlock()
114 c.poke()
115}
116
117func (c *Cursor) poke() {
118 select {
119 case c.wake <- struct{}{}:
120 default: // a wake is queued; the output reads the latest state
121 }
122}
123
124// setup allocates the two cursor images and their framebuffers: KMS dumb
125// buffers first, then the renderer's images. dumbErr is why the dumb buffers
126// were not used (nil when they are); err is set when no images exist.
127func (c *Cursor) setup(k kms, r ports.Renderer) (dumbErr, err error) {
128 c.fallback = func() (cursorImages, error) { return newRendererCursor(k, r, c.size) }
129 d, dumbErr := newDumbCursor(k, c.size)
130 if dumbErr == nil {
131 c.install(d, allocDumb)
132 return nil, nil
133 }
134 imgs, err := c.fallback()
135 if err != nil {
136 return dumbErr, err
137 }
138 c.install(imgs, allocVulkan)
139 return dumbErr, nil
140}
141
142// install makes imgs the cursor images; none is on screen or in flight.
143func (c *Cursor) install(imgs cursorImages, alloc string) {
144 c.imgs, c.alloc = imgs, alloc
145 for i := range c.fbs {
146 c.fbs[i] = imgs.fb(i)
147 }
148 c.cur = 0
149 c.screen, c.flight, c.flying, c.applied = 0, cursorState{}, false, cursorState{}
150}
151
152// useFallback replaces refused dumb images with the renderer's and writes
153// the current image into them. It reports false when it cannot: no fallback,
154// already used, or no images either.
155func (c *Cursor) useFallback(hdrNits float64) (bool, error) {
156 if c.alloc != allocDumb || c.fallback == nil {
157 return false, nil
158 }
159 imgs, err := c.fallback()
160 if err != nil {
161 return false, err
162 }
163 c.imgs.close()
164 c.install(imgs, allocVulkan)
165 l := c.later
166 if l == nil && c.image {
167 l = &c.last
168 }
169 c.image, c.later = false, nil
170 if l == nil {
171 return true, nil
172 }
173 return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits)
174}
175
176// setImage loads premultiplied ARGB8888 pixels into an image neither on
177// screen nor in flight; when there is none it waits for the next commit
178// event (flushLater). An empty image hides the cursor (a client asked for
179// none).
180// hdrNits is the SDR white of an HDR output (0: SDR).
181func (c *Cursor) setImage(pixels []byte, w, h, hotX, hotY int, hdrNits float64) error {
182 c.later = nil
183 if w <= 0 || h <= 0 || len(pixels) < w*h*4 {
184 c.image = false
185 return nil
186 }
187 if c.imgs == nil {
188 return errors.New("no cursor images")
189 }
190 next := c.freeSlot()
191 if next < 0 {
192 c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY}
193 return nil
194 }
195 if err := c.imgs.write(next, pixels, w, h, hdrNits); err != nil {
196 return err
197 }
198 c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY
199 c.last = laterImage{pixels: append(c.last.pixels[:0], pixels[:w*h*4]...), w: w, h: h, hotX: hotX, hotY: hotY}
200 return nil
201}
202
203// freeSlot is an image the plane neither shows nor is about to show, -1
204// when both are busy.
205func (c *Cursor) freeSlot() int {
206 for i, fb := range c.fbs {
207 if fb != c.screen && !(c.flying && c.flight.on && c.flight.fb == fb) {
208 return i
209 }
210 }
211 return -1
212}
213
214// flushLater loads an image that waited for a free slot. It reports
215// whether the cursor changed.
216func (c *Cursor) flushLater(hdrNits float64) (bool, error) {
217 l := c.later
218 if l == nil || c.freeSlot() < 0 {
219 return false, nil
220 }
221 return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits)
222}
223
224// committed records that a commit carrying state s is in flight.
225func (c *Cursor) committed(s cursorState) {
226 if s != c.applied {
227 c.mu.Lock()
228 c.commits++
229 c.mu.Unlock()
230 }
231 c.applied, c.flight, c.flying = s, s, true
232}
233
234// landed records that the pending commit reached the screen.
235func (c *Cursor) landed() {
236 if c.flying {
237 c.screen, c.flying = 0, false
238 if c.flight.on {
239 c.screen = c.flight.fb
240 }
241 }
242}
243
244// desired is what the cursor plane should show now.
245func (c *Cursor) desired() cursorState {
246 c.mu.Lock()
247 x, y, away := c.x, c.y, c.away
248 c.mu.Unlock()
249 if !c.image || away || c.off || c.fbs[c.cur] == 0 {
250 return cursorState{}
251 }
252 return cursorState{on: true, fb: c.fbs[c.cur], x: x - c.hotX, y: y - c.hotY}
253}
254
255// props adds the cursor plane in state s to req.
256func (c *Cursor) props(req *atomicReq, crtc uint32, s cursorState) {
257 p := c.plane
258 if !s.on {
259 req.set(p.id, p.prop("FB_ID"), 0)
260 req.set(p.id, p.prop("CRTC_ID"), 0)
261 return
262 }
263 size := uint64(c.size)
264 req.set(p.id, p.prop("FB_ID"), uint64(s.fb))
265 req.set(p.id, p.prop("CRTC_ID"), uint64(crtc))
266 req.set(p.id, p.prop("SRC_X"), 0)
267 req.set(p.id, p.prop("SRC_Y"), 0)
268 req.set(p.id, p.prop("SRC_W"), size<<16)
269 req.set(p.id, p.prop("SRC_H"), size<<16)
270 req.set(p.id, p.prop("CRTC_X"), uint64(int64(s.x)))
271 req.set(p.id, p.prop("CRTC_Y"), uint64(int64(s.y)))
272 req.set(p.id, p.prop("CRTC_W"), size)
273 req.set(p.id, p.prop("CRTC_H"), size)
274}
275
276// free removes the framebuffers and releases the images.
277func (c *Cursor) free() {
278 if c.imgs != nil {
279 c.imgs.close()
280 c.imgs, c.alloc = nil, ""
281 }
282 c.fbs = [2]uint32{}
283 c.image, c.later = false, nil
284 c.screen, c.flying = 0, false
285}