images.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"os"
8	"slices"
9
10	"github.com/bnema/neferwl/internal/adapters/syncfile"
11	"github.com/bnema/neferwl/internal/ports"
12)
13
14// Output images: the renderer targets frames alternate between.
15
16// imageKind is how output images are made, best first. Each kind is tried
17// with GBM allocation first, then with images the renderer allocates and
18// exports.
19type imageKind int
20
21const (
22	imagesDriver      imageKind = iota // modifier chosen by the driver (DCC planes allowed)
23	imagesSinglePlane                  // driver modifier with one memory plane
24	imagesLinear                       // linear
25)
26
27// Who allocated the output images (the "alloc" log field).
28const (
29	allocGBM    = "gbm"
30	allocVulkan = "vulkan"
31)
32
33// maxFBPlanes is the number of planes a KMS framebuffer holds (fbCmd2).
34const maxFBPlanes = 4
35
36// showImages sets up images from kind on and modesets them. KMS may refuse
37// an image only in a modeset: a TEST_ONLY modeset checks it first, and the
38// next kind is tried on refusal. cause is why the
39// previous kind failed, for the log.
40func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
41	if o.hdr.wanted() {
42		o.hdr.on = true
43		// Reuse the live blob across VT resume. A changed EDID creates a
44		// replacement, but the old one remains alive until KMS accepts it.
45		sw, err := o.hdr.prepareBlob(o.k, hdrMetadata(o.monitor))
46		if err == nil {
47			r.SetHDR(float64(o.hdr.settings.SDRBrightness))
48			err = o.showImageKind(r, imagesDriver, nil)
49		}
50		var pe protectedCommitError
51		if errors.As(err, &pe) {
52			// A protected KMS failure is not an HDR failure: images exist and
53			// the owner retries the protected modeset. Never fall back to SDR
54			// or latch hdr.failed for it.
55			o.hdr.rollbackBlob(o.k, sw)
56			return err
57		}
58		if err == nil {
59			o.hdr.commitBlob(o.k, sw)
60			o.log.Info().Str("connector", o.conn.name).Int("sdr_brightness", o.hdr.settings.SDRBrightness).Float64("max_luminance", o.hdr.cap.MaxLuminance).Float64("max_frame_average", o.hdr.cap.MaxFrameAverage).Msg("HDR10 on")
61			return nil
62		}
63		if sw.old != 0 {
64			// Failed replacement: the existing modeset may still use it.
65			o.hdr.rollbackBlob(o.k, sw)
66		}
67		o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("HDR modeset unavailable; falling back to SDR")
68		o.hdr.failed = true
69		o.hdr.on = false
70		o.dropImages(r)
71		r.SetHDR(0)
72		_, _ = r.ExportTargets(0, nil, false)
73		// A failed test commit can leave the old HDR mode on screen;
74		// retain its blob until the SDR modeset succeeds or Close restores it.
75	} else {
76		r.SetHDR(0)
77	}
78	err := o.showImageKind(r, kind, cause)
79	if err == nil && o.hdr.failed && o.hdr.blob != 0 {
80		// The SDR commit has completed; the old HDR metadata is no longer in use.
81		o.hdr.releaseBlob(o.k)
82	}
83	return err
84}
85
86func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) error {
87	// skipGBM makes the next setup use renderer-allocated images for kind:
88	// GBM images of this kind were refused by the modeset test.
89	skipGBM := false
90	for {
91		got, err := o.setupImages(r, kind, cause, skipGBM)
92		if err != nil {
93			return err
94		}
95		o.kind = got
96		if err = o.testModeset(); err == nil {
97			err = o.modeset()
98			if err != nil && o.protected && !errors.Is(err, errSecurityScene) && !errors.Is(err, errProtectionPending) && !errors.As(err, new(protectedCommitError)) {
99				err = protectedCommitError{err}
100			}
101			// A protected failure is not an image refusal: never fall back to
102			// another image kind or SDR for it. The caller hands it to the
103			// bounded retry (commitFailed).
104			return err
105		}
106		if o.protected && !refused(err) {
107			// EBUSY, lost master or a blob failure of the test: the images
108			// are fine; retry the protected modeset later.
109			return protectedCommitError{err}
110		}
111		if !refused(err) {
112			o.dropImages(r)
113			return err
114		}
115		if len(o.images) > 0 {
116			// GBM images (the renderer imported them).
117			// The renderer's own images of this kind may still pass.
118			o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocGBM).Msg("modeset refused the output images")
119			o.dropImages(r)
120			kind, cause, skipGBM = got, err, true
121			continue
122		}
123		if got == imagesLinear {
124			o.dropImages(r)
125			return err
126		}
127		if o.imageMod == 0 {
128			// The refused images were already linear: the later kinds
129			// would export the same ones again.
130			o.dropImages(r)
131			return err
132		}
133		o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Str("alloc", allocVulkan).Msg("modeset refused the output images")
134		o.dropImages(r)
135		kind, cause, skipGBM = got+1, err, false
136		if kind == imagesSinglePlane && o.planes == 1 {
137			// The refused images already had one plane: single-plane
138			// images would be the same ones.
139			kind = imagesLinear
140		}
141	}
142}
143
144// setupImages gives the output the two renderer images it flips between,
145// as dmabufs: allocated with GBM and imported by the renderer, else
146// allocated and exported by the renderer, for each kind in turn. skipGBM
147// leaves GBM out for the first kind. There is no CPU fallback (ADR 014): a
148// GPU that cannot export a scanout-capable image cannot drive the output.
149func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error, skipGBM bool) (imageKind, error) {
150	err := cause
151	start := kind
152	for ; kind <= imagesLinear; kind++ {
153		noGBM := skipGBM && kind == start
154		if o.runContext != nil && o.runContext.Err() != nil {
155			return kind, o.runContext.Err()
156		}
157		// The renderer is asked for the modifiers the primary plane lists
158		// for the image format: DRM core refuses (ADDFB2: EINVAL) a
159		// framebuffer whose format and modifier no plane advertises, and
160		// the renderer's own preference may be such a one (RADV's
161		// pipe-aligned DCC, which amdgpu does not list for scanout). A
162		// plane without IN_FORMATS lists every format with modifier 0
163		// (readPlanes), so it gets linear images straight away rather than
164		// two refused driver attempts. AMD DCC retile modifiers are never
165		// exported, whatever the plane lists: they cost an extra
166		// full-surface GPU pass per frame (vulkan retilesDCC). On a Radeon
167		// 680M the plane-accepted pick is therefore single-plane tiled
168		// (0x200000010401b03). Only a plane listing no modifier at all for
169		// the format (SDR) leaves the choice to the renderer.
170		mods := o.primaryModifiers(o.imageFormat())
171		if o.hdr.on {
172			if len(mods) == 0 {
173				return kind, fmt.Errorf("primary plane has no XRGB2101010 modifiers")
174			}
175			if kind == imagesLinear {
176				if !slices.Contains(mods, uint64(0)) {
177					continue
178				}
179				mods = []uint64{0}
180			}
181		} else if kind == imagesLinear {
182			mods = []uint64{0}
183		}
184		if kind == imagesSinglePlane && len(mods) == 1 && mods[0] == 0 {
185			// The display only lists linear, already one plane: the driver
186			// attempt was the single-plane one.
187			continue
188		}
189		if !noGBM && o.openAlloc != nil {
190			gerr := o.gbmImages(r, mods, kind == imagesSinglePlane)
191			if gerr == nil {
192				return kind, nil
193			}
194			o.log.Info().Err(gerr).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocGBM).Msg("output image allocation")
195		}
196		// Deprecated fallback (neferwl#23): renderer-exported images, kept
197		// while GBM is validated on few setups. Remove once no report shows
198		// "alloc":"vulkan" for a few releases.
199		if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil {
200			return kind, nil
201		}
202		o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Str("alloc", allocVulkan).Msg("output image export")
203	}
204	return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
205}
206
207// imageFormat is the framebuffer format of the output images.
208func (o *Output) imageFormat() uint32 {
209	if o.hdr.on {
210		return fourccXR30
211	}
212	return fourccXRGB
213}
214
215// primaryModifiers lists the modifiers the primary plane accepts for
216// format, in IN_FORMATS order.
217func (o *Output) primaryModifiers(format uint32) []uint64 {
218	var mods []uint64
219	for _, f := range o.primary.formats {
220		if f.Format == format {
221			mods = append(mods, f.Modifier)
222		}
223	}
224	return mods
225}
226
227// closeBufs closes every plane file of bufs. The callers own them: the
228// renderer and the framebuffers keep their own references to the memory.
229func closeBufs(bufs []ports.DMABuf) {
230	for _, b := range bufs {
231		for _, p := range b.Planes {
232			_ = p.File.Close()
233		}
234	}
235}
236
237// exportImages makes the renderer's exported targets the output images.
238//
239// Deprecated fallback for gbmImages, to remove with neferwl#23. Exported
240// Vulkan images corrupt scanout on nvidia-drm.
241func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
242	o.dropImages(r)
243	bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
244	if err != nil {
245		return err
246	}
247	defer closeBufs(bufs)
248	if err = o.showBufs(r, bufs, allocVulkan); err != nil {
249		o.dropImages(r)
250		_, _ = r.ExportTargets(0, nil, false)
251		return err
252	}
253	return nil
254}
255
256// showBufs makes framebuffers of the output images bufs (already the
257// renderer's targets), clears them and records their layout. The caller
258// closes the plane files and drops the images on error.
259func (o *Output) showBufs(r ports.Renderer, bufs []ports.DMABuf, alloc string) error {
260	o.planes, o.imageMod = 0, 0
261	for i := range bufs {
262		if n := len(bufs[i].Planes); n == 0 || n > maxFBPlanes {
263			return fmt.Errorf("output image has %d planes", n)
264		}
265		fb, err := o.k.addFB(&bufs[i], o.imageFormat())
266		if err != nil {
267			return err
268		}
269		o.fbs[i] = fb
270	}
271	if err := o.clearImages(r); err != nil {
272		return err
273	}
274	o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
275	ev := o.log.Info()
276	if alloc == allocVulkan {
277		// The deprecated fallback is in use: make it visible in reports.
278		ev = o.log.Warn()
279	}
280	ev.Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", o.planes).Str("alloc", alloc).Msg("zero-copy output")
281	return nil
282}
283
284// clearImages draws black into both output images and waits for the GPU.
285// GPU memory starts undefined (old VRAM contents): both are cleared before
286// the modeset shows one.
287func (o *Output) clearImages(r ports.Renderer) error {
288	var err error
289	for i := range o.fbs {
290		r.UseTarget(i)
291		var done *os.File
292		if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
293			// The modeset is a blocking commit: wait for the clear.
294			waitCtx := o.runContext
295			if waitCtx == nil {
296				waitCtx = context.Background()
297			}
298			err = errors.Join(err, syncfile.Wait(waitCtx, done))
299			done.Close()
300		}
301		if err != nil {
302			return err
303		}
304	}
305	return nil
306}
307
308// gbmImages allocates the output images with GBM, makes them the
309// renderer's targets and framebuffers, and clears them. The modifiers are
310// those the plane lists (mods, as for exportImages) that the renderer can
311// render into and import. On failure nothing is left allocated.
312func (o *Output) gbmImages(r ports.Renderer, mods []uint64, singlePlane bool) (err error) {
313	o.dropImages(r)
314	if o.alloc == nil {
315		if o.allocErr != nil {
316			return o.allocErr
317		}
318		if o.alloc, o.allocErr = o.openAlloc(); o.allocErr != nil {
319			o.log.Info().Err(o.allocErr).Str("connector", o.conn.name).Str("alloc", allocGBM).Msg("GBM unavailable; the renderer allocates the output images")
320			return o.allocErr
321		}
322	}
323	usable := r.TargetModifiers(mods, singlePlane)
324	if len(usable) == 0 {
325		return errors.New("no modifier both the plane and the renderer accept for GBM images")
326	}
327	var bufs []ports.DMABuf
328	defer func() {
329		closeBufs(bufs)
330		if err != nil {
331			o.dropImages(r)
332		}
333	}()
334	for range o.fbs {
335		img, cerr := o.alloc.create(o.Width(), o.Height(), o.imageFormat(), usable)
336		if cerr != nil {
337			return cerr
338		}
339		o.images = append(o.images, img)
340		b, derr := img.dmabuf()
341		if derr != nil {
342			return derr
343		}
344		bufs = append(bufs, b)
345	}
346	if err = r.ImportTargets(bufs); err != nil {
347		return err
348	}
349	return o.showBufs(r, bufs, allocGBM)
350}
351
352// dropImages frees the output images: their framebuffers, the GBM buffers
353// and the renderer targets built over them (they would keep the memory
354// alive after the buffers are gone). It is a no-op when there are none.
355func (o *Output) dropImages(r ports.Renderer) {
356	if len(o.images) > 0 {
357		_ = r.ImportTargets(nil)
358	}
359	o.freeImages()
360}
361
362// freeImages removes the output images' framebuffers and frees the images
363// GBM allocated. Callers that still have a renderer use dropImages: this
364// alone leaves the renderer's targets over the freed images (the renderer
365// drops them when it closes, before Close frees the output's images).
366func (o *Output) freeImages() {
367	o.front.forget()
368	o.forgetOverlayProbes()
369	for i, fb := range o.fbs {
370		if fb != 0 {
371			_ = o.k.rmFB(fb)
372		}
373		o.fbs[i] = 0
374	}
375	for _, img := range o.images {
376		img.close()
377	}
378	o.images = nil
379}

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 23

Tags 10

↑↓ moveenter openesc close