feat/data-dnd e05cb15

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	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}

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