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 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
52}
53
54type laterImage struct {
55 pixels []byte
56 w, h, hotX, hotY int
57}
58
59// cursorState is the cursor plane as committed: off, or fb at x, y.
60type cursorState struct {
61 on bool
62 fb uint32
63 x, y int
64}
65
66// CursorStats counts cursor work since the last TakeStats.
67type CursorStats struct {
68 Moves int // Move calls
69 Commits int // commits that moved or changed the cursor
70}
71
72func newCursor(p *plane, size int) *Cursor {
73 return &Cursor{plane: p, size: size, wake: make(chan struct{}, 1)}
74}
75
76// TakeStats returns and resets the counters. Output goroutine only.
77func (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}
84
85// Limit is the largest image side the cursor plane accepts.
86func (c *Cursor) Limit() int { return c.size }
87
88// Move places the hotspot at physical (x, y). It never blocks.
89func (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()
96}
97
98// Hide removes the cursor from the output until the next move (the pointer
99// is on another output).
100func (c *Cursor) Hide() {
101 c.mu.Lock()
102 c.away = true
103 c.mu.Unlock()
104 c.poke()
105}
106
107func (c *Cursor) poke() {
108 select {
109 case c.wake <- struct{}{}:
110 default: // a wake is queued; the output reads the latest state
111 }
112}
113
114// setup allocates the two cursor images and their framebuffers.
115func (c *Cursor) setup(k kms, r ports.Renderer) error {
116 bufs, err := r.CursorBuffers(c.size)
117 if err != nil {
118 return err
119 }
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 }
127 }
128 if err != nil {
129 c.free(k)
130 }
131 return err
132}
133
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).
138func (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
142 return nil
143 }
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
151 }
152 if err := r.WriteCursor(next, pixels, w, h); err != nil {
153 return err
154 }
155 c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY
156 return nil
157}
158
159// freeSlot is an image the plane neither shows nor is about to show, -1
160// when both are busy.
161func (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 }
166 }
167 return -1
168}
169
170// flushLater loads an image that waited for a free slot. It reports
171// whether the cursor changed.
172func (c *Cursor) flushLater(r ports.Renderer) (bool, error) {
173 l := c.later
174 if l == nil || c.freeSlot() < 0 {
175 return false, nil
176 }
177 return true, c.setImage(r, l.pixels, l.w, l.h, l.hotX, l.hotY)
178}
179
180// committed records that a commit carrying state s is in flight.
181func (c *Cursor) committed(s cursorState) {
182 if s != c.applied {
183 c.mu.Lock()
184 c.commits++
185 c.mu.Unlock()
186 }
187 c.applied, c.flight, c.flying = s, s, true
188}
189
190// landed records that the pending commit reached the screen.
191func (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 }
198}
199
200// desired is what the cursor plane should show now.
201func (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{}
207 }
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.
212func (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)
217 return
218 }
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
232func (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
241}