feat/data-dnd e05cb15

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.
17type imageKind int
18
19const (
20	imagesDriver      imageKind = iota // exported, modifier chosen by the driver (DCC planes allowed)
21	imagesSinglePlane                  // exported, driver modifier with one memory plane
22	imagesLinear                       // exported, linear
23)
24
25// maxFBPlanes is the number of planes a KMS framebuffer holds (fbCmd2).
26const maxFBPlanes = 4
27
28// showImages sets up images from kind on and modesets them. KMS may refuse
29// an image only in a modeset: a TEST_ONLY modeset checks it first, and the
30// next kind is tried on refusal. cause is why the
31// previous kind failed, for the log.
32func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
33	if o.hdr.wanted() {
34		o.hdr.on = true
35		// Reuse the live blob across VT resume. A changed EDID creates a
36		// replacement, but the old one remains alive until KMS accepts it.
37		sw, err := o.hdr.prepareBlob(o.k, hdrMetadata(o.monitor))
38		if err == nil {
39			r.SetHDR(float64(o.hdr.settings.SDRBrightness))
40			err = o.showImageKind(r, imagesDriver, nil)
41		}
42		var pe protectedCommitError
43		if errors.As(err, &pe) {
44			// A protected KMS failure is not an HDR failure: images exist and
45			// the owner retries the protected modeset. Never fall back to SDR
46			// or latch hdr.failed for it.
47			o.hdr.rollbackBlob(o.k, sw)
48			return err
49		}
50		if err == nil {
51			o.hdr.commitBlob(o.k, sw)
52			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")
53			return nil
54		}
55		if sw.old != 0 {
56			// Failed replacement: the existing modeset may still use it.
57			o.hdr.rollbackBlob(o.k, sw)
58		}
59		o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("HDR modeset unavailable; falling back to SDR")
60		o.hdr.failed = true
61		o.hdr.on = false
62		o.freeImages()
63		r.SetHDR(0)
64		_, _ = r.ExportTargets(0, nil, false)
65		// A failed test commit can leave the old HDR mode on screen;
66		// retain its blob until the SDR modeset succeeds or Close restores it.
67	} else {
68		r.SetHDR(0)
69	}
70	err := o.showImageKind(r, kind, cause)
71	if err == nil && o.hdr.failed && o.hdr.blob != 0 {
72		// The SDR commit has completed; the old HDR metadata is no longer in use.
73		o.hdr.releaseBlob(o.k)
74	}
75	return err
76}
77
78func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) error {
79	for {
80		got, err := o.setupImages(r, kind, cause)
81		if err != nil {
82			return err
83		}
84		o.kind = got
85		if err = o.testModeset(); err == nil {
86			err = o.modeset()
87			if err != nil && o.protected && !errors.Is(err, errSecurityScene) && !errors.Is(err, errProtectionPending) && !errors.As(err, new(protectedCommitError)) {
88				err = protectedCommitError{err}
89			}
90			// A protected failure is not an image refusal: never fall back to
91			// another image kind or SDR for it. The caller hands it to the
92			// bounded retry (commitFailed).
93			return err
94		}
95		if o.protected && !refused(err) {
96			// EBUSY, lost master or a blob failure of the test: the images
97			// are fine; retry the protected modeset later.
98			return protectedCommitError{err}
99		}
100		if !refused(err) || got == imagesLinear {
101			o.freeImages()
102			return err
103		}
104		if o.imageMod == 0 {
105			// The refused images were already linear: the later kinds
106			// would export the same ones again.
107			o.freeImages()
108			return err
109		}
110		o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
111		o.freeImages()
112		kind, cause = got+1, err
113		if kind == imagesSinglePlane && o.planes == 1 {
114			// The refused images already had one plane: single-plane
115			// images would be the same ones.
116			kind = imagesLinear
117		}
118	}
119}
120
121// setupImages gives the output the two renderer images it flips between,
122// exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that
123// cannot export a scanout-capable image cannot drive the output.
124func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
125	err := cause
126	for ; kind <= imagesLinear; kind++ {
127		if o.runContext != nil && o.runContext.Err() != nil {
128			return kind, o.runContext.Err()
129		}
130		// The renderer is asked for the modifiers the primary plane lists
131		// for the image format: DRM core refuses (ADDFB2: EINVAL) a
132		// framebuffer whose format and modifier no plane advertises, and
133		// the renderer's own preference may be such a one (RADV's
134		// pipe-aligned DCC, which amdgpu does not list for scanout). A
135		// plane without IN_FORMATS lists every format with modifier 0
136		// (readPlanes), so it gets linear images straight away rather than
137		// two refused driver attempts. AMD DCC retile modifiers are never
138		// exported, whatever the plane lists: they cost an extra
139		// full-surface GPU pass per frame (vulkan retilesDCC). On a Radeon
140		// 680M the plane-accepted pick is therefore single-plane tiled
141		// (0x200000010401b03). Only a plane listing no modifier at all for
142		// the format (SDR) leaves the choice to the renderer.
143		mods := o.primaryModifiers(o.imageFormat())
144		if o.hdr.on {
145			if len(mods) == 0 {
146				return kind, fmt.Errorf("primary plane has no XRGB2101010 modifiers")
147			}
148			if kind == imagesLinear {
149				if !slices.Contains(mods, uint64(0)) {
150					continue
151				}
152				mods = []uint64{0}
153			}
154		} else if kind == imagesLinear {
155			mods = []uint64{0}
156		}
157		if kind == imagesSinglePlane && len(mods) == 1 && mods[0] == 0 {
158			// The display only lists linear, already one plane: the driver
159			// attempt was the single-plane one.
160			continue
161		}
162		if err = o.exportImages(r, mods, kind == imagesSinglePlane); err == nil {
163			return kind, nil
164		}
165		o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
166	}
167	return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
168}
169
170// imageFormat is the framebuffer format of the output images.
171func (o *Output) imageFormat() uint32 {
172	if o.hdr.on {
173		return fourccXR30
174	}
175	return fourccXRGB
176}
177
178// primaryModifiers lists the modifiers the primary plane accepts for
179// format, in IN_FORMATS order.
180func (o *Output) primaryModifiers(format uint32) []uint64 {
181	var mods []uint64
182	for _, f := range o.primary.formats {
183		if f.Format == format {
184			mods = append(mods, f.Modifier)
185		}
186	}
187	return mods
188}
189
190// exportImages makes the renderer's exported targets the output images.
191func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) error {
192	bufs, err := r.ExportTargets(len(o.fbs), mods, singlePlane)
193	if err != nil {
194		return err
195	}
196	o.planes, o.imageMod = 0, 0
197	for i := range bufs {
198		if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {
199			err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))
200		}
201		if err == nil {
202			o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat())
203		}
204		// Every plane owns its file; the framebuffer keeps its own
205		// reference to the buffer.
206		for _, p := range bufs[i].Planes {
207			p.File.Close()
208		}
209	}
210	// GPU memory starts undefined (old VRAM contents): clear both images
211	// before the modeset shows one.
212	for i := range o.fbs {
213		if err != nil {
214			break
215		}
216		r.UseTarget(i)
217		var done *os.File
218		if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
219			// The modeset is a blocking commit: wait for the clear.
220			waitCtx := o.runContext
221			if waitCtx == nil {
222				waitCtx = context.Background()
223			}
224			err = errors.Join(err, syncfile.Wait(waitCtx, done))
225			done.Close()
226		}
227	}
228	if err != nil {
229		o.freeImages()
230		_, _ = r.ExportTargets(0, nil, false)
231		return err
232	}
233	o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
234	o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output")
235	return nil
236}
237
238// freeImages removes the output images' framebuffers.
239func (o *Output) freeImages() {
240	o.front.forget()
241	o.forgetOverlayProbes()
242	for i, fb := range o.fbs {
243		if fb != 0 {
244			_ = o.k.rmFB(fb)
245		}
246		o.fbs[i] = 0
247	}
248}

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