main f2d166b

cursor.go RawHistoryFolder

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}

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 25

Tags 10

↑↓ moveenter openesc close