v0.3.0 cf370a0

protection.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"time"
8
9	"github.com/bnema/neferwl/internal/adapters/syncfile"
10	"github.com/bnema/neferwl/internal/ports"
11	"golang.org/x/sys/unix"
12)
13
14// errProtectionPending asks the output owner to retry after the outstanding
15// commit ends. In particular, never queue a blocking disable behind a client
16// acquire fence: that fence need not signal in bounded time.
17var errProtectionPending = errors.New("output protection: commit pending")
18
19// detachProtectedPlanes detaches every plane owned by this output, and stray
20// planes currently on its CRTC (not planes belonging to another output). A
21// failed read is not evidence that a stray plane is detached.
22func (o *Output) detachProtectedPlanes(req *atomicReq) error {
23	var err error
24	owned := make(map[uint32]bool)
25	detach := func(p *plane) {
26		if p == nil {
27			return
28		}
29		// atomicReq.set silently skips zero property IDs. Partial cached
30		// metadata therefore cannot prove detachment of a previous desktop.
31		if p.id == 0 || p.prop("FB_ID") == 0 || p.prop("CRTC_ID") == 0 {
32			err = errors.Join(err, fmt.Errorf("protected plane %d: incomplete detachment properties", p.id))
33			return
34		}
35		req.set(p.id, p.prop("FB_ID"), 0)
36		req.set(p.id, p.prop("CRTC_ID"), 0)
37	}
38	detachOwned := func(p *plane) {
39		if p != nil {
40			owned[p.id] = true
41			detach(p)
42		}
43	}
44	detachOwned(o.primary)
45	if o.cursor != nil {
46		detachOwned(o.cursor.plane)
47	}
48	detachOwned(o.overlay)
49	for _, p := range o.stray {
50		// pickOverlay leaves the owned overlay in stray. Do not turn a
51		// redundant query failure into refusal of known-owned detachment.
52		if owned[p.id] {
53			continue
54		}
55		props, readErr := o.k.objProps(p.id, objPlane)
56		if readErr != nil {
57			err = errors.Join(err, fmt.Errorf("read protected plane %d: %w", p.id, readErr))
58			continue
59		}
60		crtc, ok := props["CRTC_ID"]
61		if !ok {
62			err = errors.Join(err, fmt.Errorf("protected plane %d: missing CRTC_ID", p.id))
63			continue
64		}
65		if crtc[1] == uint64(o.crtc) {
66			detach(p)
67		}
68	}
69	return err
70}
71
72// disableProtected is the atomic disable of a protected output. It is a full
73// disable: ACTIVE=0, MODE_ID=0, connector CRTC_ID=0, every plane detached, VRR
74// and HDR connector state off. amdgpu (amdgpu_dm, via
75// drm_atomic_helper_check_crtc_primary_plane) rejects with EINVAL a CRTC that
76// stays enabled (MODE_ID set) without its primary plane, even with ACTIVE=0,
77// so "inactive but every plane detached" is not a valid request. The other
78// valid shape, ACTIVE=0 with the primary still attached, would keep a desktop
79// framebuffer referenced by the CRTC until a later activation replaced it. A
80// full disable leaves nothing of the desktop attached and needs no blob; the
81// activation that follows is a full modeset either way. It is the same
82// request as Close's disable, which this hardware accepts.
83func (o *Output) disableProtected() error {
84	req := &atomicReq{}
85	req.set(o.crtc, o.crtcProps["ACTIVE"], 0)
86	req.set(o.crtc, o.crtcProps["MODE_ID"], 0)
87	req.set(o.crtc, o.vrrProp, 0)
88	req.set(o.conn.id, o.connCrtc, 0)
89	o.hdrConnectorProps(req, false)
90	o.forceBypass(req)
91	if err := o.detachProtectedPlanes(req); err != nil {
92		return protectedCommitError{err}
93	}
94	if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
95		return protectedCommitError{fmt.Errorf("disable protected output: %w", err)}
96	}
97	o.colorBypassed()
98	if o.modeBlob != 0 {
99		_ = o.k.destroyBlob(o.modeBlob) // MODE_ID is 0 now: nothing references it
100		o.modeBlob = 0
101	}
102	o.off, o.vrrGame, o.overlayOn = true, false, 0
103	o.setVRR(false)
104	o.sendFormats()
105	o.shown, o.queued = 0, 0
106	if o.cursor != nil {
107		o.cursor.applied = cursorState{}
108		o.cursor.screen, o.cursor.flying = 0, false
109	}
110	return nil
111}
112
113// protectedCommitError marks a recoverable failure of the protected disable or
114// activation: a refused KMS commit, unverifiable plane state, a mode blob or a
115// clear fence that failed. The output owner keeps the output, stays protected
116// and retries with bounded backoff instead of stopping (see commitFailed). A
117// renderer error is not wrapped: it stays fatal.
118type protectedCommitError struct{ error }
119
120func (e protectedCommitError) Unwrap() error { return e.error }
121
122// retryableProtected reports a refused protected KMS commit worth retrying
123// later. Busy and lost-master errors keep their own handling.
124func retryableProtected(err error) bool {
125	var pe protectedCommitError
126	return errors.As(err, &pe) && !lostMaster(err) && !errors.Is(err, unix.EBUSY)
127}
128
129const (
130	protectRetryMin = 50 * time.Millisecond
131	protectRetryMax = 2 * time.Second
132)
133
134// deferProtection schedules the next protected attempt after a refusal.
135func (o *Output) deferProtection() {
136	o.protectBackoff = nextProtectBackoff(o.protectBackoff)
137	o.protectNotBefore = time.Now().Add(o.protectBackoff)
138}
139
140// protectBackoffActive reports a pending retry delay.
141func (o *Output) protectBackoffActive() bool {
142	return !o.protectNotBefore.IsZero() && time.Now().Before(o.protectNotBefore)
143}
144
145// protectSucceeded clears the retry state.
146func (o *Output) protectSucceeded() { o.protectBackoff, o.protectNotBefore = 0, time.Time{} }
147
148// nextProtectBackoff doubles the delay, bounded by protectRetryMax.
149func nextProtectBackoff(d time.Duration) time.Duration {
150	if d < protectRetryMin {
151		return protectRetryMin
152	}
153	return min(2*d, protectRetryMax)
154}
155
156// protectedModeset is an internal output-owner primitive, not session-lock
157// readiness or a protocol entry point. It latches protection even on failure.
158// First stop scanout with a blocking full disable, then clear an unscanned
159// target and wait for the clear to complete before activating that exact FB.
160func (o *Output) protectedModeset(ctx context.Context, r ports.Renderer) error {
161	o.protected = true
162	if o.frame.pendingCommit() {
163		return errProtectionPending
164	}
165	if err := ctx.Err(); err != nil {
166		return err
167	}
168	if err := o.disableProtected(); err != nil {
169		return err
170	}
171	// Do not dropRead here: an earlier render whose KMS commit failed may
172	// still read client buffers. KMS detachment is not a GPU completion fence.
173	if err := ctx.Err(); err != nil {
174		return err
175	}
176	r.UseTarget(o.back)
177	fence, err := r.Render(ports.Scene{Seq: 0, Background: "#000000"}, nil)
178	if err != nil {
179		if fence != nil {
180			fence.Close()
181		}
182		return fmt.Errorf("clear protected output: %w", err)
183	}
184	if fence != nil {
185		err := syncfile.Wait(ctx, fence)
186		fence.Close()
187		if err != nil {
188			err = fmt.Errorf("wait protected clear: %w", err)
189			if ctx.Err() != nil {
190				return err
191			}
192			return protectedCommitError{err}
193		}
194	}
195	if err := ctx.Err(); err != nil {
196		return err
197	}
198	if o.wantOff {
199		o.protectSucceeded()
200		o.signalReady()
201		return nil // the blocking disable is the proof; do not activate or flip
202	}
203	if err := o.modesetImage(o.fbs[o.back], true); err != nil {
204		return err
205	}
206	o.back = 1 - o.back
207	o.protectSucceeded()
208	return nil
209}

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