v0.3.0 cf370a0

session_lock_surface.go RawHistoryFolder

1package wayland
2
3import (
4	"cmp"
5	"slices"
6
7	"github.com/bnema/go-wayland-bindings/server/extsessionlock"
8	"github.com/bnema/go-wayland-bindings/server/wayland"
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// lockConfigure is an issued configure, or the last acknowledged configure.
13// It is copied into each commit: a later ACK cannot change a queued response.
14type lockConfigure struct {
15	serial        uint32
16	width, height int
17}
18
19type lockSurface struct {
20	resource       *extsessionlock.ExtSessionLockSurfaceV1
21	server         *Server
22	surface        *surface
23	output         *output
24	id             ports.WindowID
25	serials        []lockConfigure
26	ack            lockConfigure
27	mapped, closed bool
28	// onUnmap is a display-owner lifecycle notification, like Resource.OnDestroy.
29	// The lock owner must remain protected; this never requests an unlock.
30	onUnmap func(*lockSurface)
31}
32
33// newLockSurface only assigns the surface role. The caller validates ownership,
34// admission, output lifetime and duplicate outputs before calling it. No global
35// is registered and no locked/finished event is sent here.
36func (s *Server) newLockSurface(r *extsessionlock.ExtSessionLockV1, id uint32, surf *surface, out *output, onUnmap func(*lockSurface)) (*lockSurface, error) {
37	if surf.kind != roleNone {
38		r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorRole), "surface already has a role")
39		return nil, nil
40	}
41	if surf.current != nil || surf.next.buffer != nil || surf.bufferCommitted {
42		r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorAlreadyConstructed), "surface has an attached or previously committed buffer")
43		return nil, nil
44	}
45	l := &lockSurface{server: s, surface: surf, output: out, id: s.nextWindow, onUnmap: onUnmap}
46	res, err := extsessionlock.NewExtSessionLockSurfaceV1(r.Client(), r.Version(), id, l)
47	if err != nil {
48		return nil, err
49	}
50	l.resource = res
51	s.nextWindow++
52	surf.kind, surf.lock, surf.role = roleSessionLock, l, l.commit
53	if s.lockSurfaces == nil {
54		s.lockSurfaces = make(map[ports.WindowID]*lockSurface)
55	}
56	s.lockSurfaces[l.id] = l
57	s.lockSurfaceChanged()
58	res.OnDestroy = l.unmap
59	l.sendConfigure()
60	return l, nil
61}
62
63// sendConfigure uses exact logical dimensions, not the physical output mode.
64// Output inventory changes are routed here by the lock owner.
65func (l *lockSurface) sendConfigure() {
66	if l.closed || l.output == nil || !l.resource.Alive() {
67		return
68	}
69	l.server.serial++
70	// Serial zero is reserved internally as "no ACK".
71	if l.server.serial == 0 {
72		l.server.serial++
73	}
74	c := lockConfigure{serial: l.server.serial, width: l.output.place.Width, height: l.output.place.Height}
75	l.serials = append(l.serials, c)
76	l.resource.SendConfigure(c.serial, uint32(c.width), uint32(c.height))
77}
78
79func (l *lockSurface) AckConfigure(r *extsessionlock.ExtSessionLockSurfaceV1, serial uint32) {
80	for i, c := range l.serials {
81		if c.serial == serial {
82			l.ack = c
83			l.serials = l.serials[i+1:]
84			return
85		}
86	}
87	r.PostError(uint32(extsessionlock.ExtSessionLockSurfaceV1ErrorInvalidSerial), "unknown or consumed configure serial")
88}
89
90func (*lockSurface) Destroy(*extsessionlock.ExtSessionLockSurfaceV1) {}
91
92// lockSurfaceChanged publishes validated placements for the admitted owner.
93// Role commits never release protection or certify physical presentation.
94func (s *Server) lockSurfaceChanged() {
95	l := s.sessionLock
96	if l == nil {
97		return
98	}
99	placements := make([]ports.LockSurfacePlacement, 0, len(l.surfaces))
100	// Before locked, only compositor black is displayed. Protocol configure
101	// and buffer commits may still prepare the UI while proofs are pending.
102	if l.locked {
103		for _, surface := range l.surfaces {
104			if surface.mapped && !surface.closed && surface.output != nil && surface.resource.Alive() {
105				p := surface.output.place
106				c := surface.surface.content
107				if c.LogicalW == p.Width && c.LogicalH == p.Height {
108					placements = append(placements, ports.LockSurfacePlacement{ID: surface.id, Output: surface.output.name(), Width: p.Width, Height: p.Height})
109				}
110			}
111		}
112	}
113	slices.SortFunc(placements, func(a, b ports.LockSurfacePlacement) int { return cmp.Compare(a.ID, b.ID) })
114	s.emit(ports.SessionLockChanged{State: l.state, Surfaces: placements})
115}
116
117// lockBuffer is the buffer after all earlier captured commits, not merely the
118// currently applied buffer. A queued attach is sticky for subsequent commits.
119func (s *surface) lockBuffer() *wayland.Buffer {
120	if s.next.attached {
121		return s.next.buffer
122	}
123	for i := len(s.queue) - 1; i >= 0; i-- {
124		if s.queue[i].attached {
125			return s.queue[i].buffer
126		}
127	}
128	return s.current
129}
130
131// checkCommit runs before capture or fence acquisition. Waiting behind FIFO or
132// an acquire fence must not let a later ACK legalize this commit.
133func (l *lockSurface) checkCommit() bool {
134	s := l.surface
135	g := &surface{server: s.server, wl: s.wl, viewport: s.viewport, bufferScale: max(s.next.scale, 1), transform: s.next.transform}
136	if v := s.viewport; v != nil {
137		g.committedViewport = viewportState{destW: v.pendingW, destH: v.pendingH, dest: v.pendingSet, src: v.pendingSrc, crop: v.pendingCrop}
138	}
139	return l.checkGeometry(l.ack, s.lockBuffer(), g)
140}
141
142func (l *lockSurface) checkGeometry(ack lockConfigure, b *wayland.Buffer, g *surface) bool {
143	if ack.serial == 0 {
144		l.resource.PostError(uint32(extsessionlock.ExtSessionLockSurfaceV1ErrorCommitBeforeFirstAck), "commit before first configure ACK")
145		return false
146	}
147	if b == nil {
148		l.resource.PostError(uint32(extsessionlock.ExtSessionLockSurfaceV1ErrorNullBuffer), "null lock buffer")
149		return false
150	}
151	state := l.server.buffers[b.Resource]
152	var bw, bh int
153	if state != nil {
154		bw, bh = state.size()
155	} else if sameBuffer(b, l.surface.current) && l.surface.has {
156		// Destroying an applied wl_buffer does not detach its content. The
157		// retained physical size remains authoritative for no-attach commits.
158		bw, bh = l.surface.content.Width, l.surface.content.Height
159	} else {
160		l.resource.PostError(uint32(extsessionlock.ExtSessionLockSurfaceV1ErrorDimensionsMismatch), "unknown lock buffer dimensions")
161		return false
162	}
163	if !g.validateViewport(bw, bh) {
164		return false
165	}
166	w, h := g.logicalSize(bw, bh)
167	if w != ack.width || h != ack.height {
168		l.resource.PostError(uint32(extsessionlock.ExtSessionLockSurfaceV1ErrorDimensionsMismatch), "lock buffer does not match acknowledged logical dimensions")
169		return false
170	}
171	return true
172}
173
174// checkCaptured validates only captured ACK and geometry, never live requests.
175func (l *lockSurface) checkCaptured(u *update) bool {
176	s := l.surface
177	b := s.current
178	if u.attached {
179		b = u.buffer
180	}
181	g := &surface{server: s.server, wl: s.wl, viewport: u.vp, bufferScale: max(u.scale, 1), transform: u.transform}
182	if u.vp != nil {
183		g.committedViewport = viewportState{destW: u.vpW, destH: u.vpH, dest: u.vpSet, src: u.vpSrc, crop: u.vpCrop}
184	}
185	return l.checkGeometry(u.lockAck, b, g)
186}
187
188func (l *lockSurface) commit(buffer bool) {
189	if l.surface.destroyed || !buffer {
190		l.unmap()
191		return
192	}
193	if !l.closed {
194		l.mapped = true
195		l.server.lockSurfaceChanged()
196	}
197}
198
199// dropLockDeps retires the synchronized child prefixes captured by this root.
200// Incoming graph links keep retired nodes readable until the root releases its
201// refs. Later, uncaptured child commits remain owned by their own queues.
202func (s *surface) dropLockDeps() {
203	prefixes := make(map[*surface]int)
204	var visit func(*update)
205	seen := make(map[*update]bool)
206	visit = func(u *update) {
207		if u == nil || u.retired || seen[u] {
208			return
209		}
210		seen[u] = true
211		visit(u.prev)
212		for _, dep := range u.deps {
213			visit(dep)
214			if dep.retired || dep.owner == s {
215				continue
216			}
217			for i, queued := range dep.owner.queue {
218				if queued == dep {
219					prefixes[dep.owner] = max(prefixes[dep.owner], i+1)
220					break
221				}
222			}
223		}
224	}
225	for _, u := range s.queue {
226		visit(u)
227	}
228	for child, n := range prefixes {
229		child.dropQueuePrefix(n)
230	}
231}
232
233// unmap also handles output loss/owner teardown. Role identity is permanent.
234// Drop queued graphs through their normal retirement path and release current
235// content while the mapped identity is still available to the hold machinery.
236func (l *lockSurface) unmap() {
237	if l.closed {
238		return
239	}
240	s := l.surface
241	// Send leaves while outgoing role identity remains resolvable. Destroying
242	// the lock role may leave its wl_surface and seat resources alive.
243	if l.server.seat.focused == l.id {
244		l.server.changeFocus(0)
245	}
246	if l.server.seat.pointerFocus == l.id {
247		l.server.changePointerFocus(0, 0, 0)
248	}
249	s.dropQueue()
250	s.dropSync(s.next.sync)
251	s.next.sync = nil
252	if s.current != nil {
253		l.server.releaseBuffer(s, s.current, s.hold)
254	}
255	s.current, s.next.buffer, s.hold = nil, nil, syncHold{}
256	s.next.attached = false
257	s.content, s.has = ports.SurfaceContent{}, false
258	l.mapped, l.closed = false, true
259	l.serials = nil
260	delete(l.server.lockSurfaces, l.id)
261	s.lock, s.role = nil, nil
262	l.server.emitContent(ports.SurfaceContent{ID: l.id}, damage{full: true})
263	l.server.lockSurfaceChanged()
264	if l.onUnmap != nil {
265		l.onUnmap(l)
266	}
267}

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