v0.2.0 13c6a90

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
21	imagesLinear                  // exported, linear
22)
23
24// showImages sets up images from kind on and modesets them. KMS may refuse
25// an image only in a modeset: a TEST_ONLY modeset checks it first, and the
26// next kind is tried on refusal. cause is why the
27// previous kind failed, for the log.
28func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
29	if o.hdr.wanted() {
30		o.hdr.on = true
31		// Reuse the live blob across VT resume. A changed EDID creates a
32		// replacement, but the old one remains alive until KMS accepts it.
33		sw, err := o.hdr.prepareBlob(o.k, hdrMetadata(o.monitor))
34		if err == nil {
35			r.SetHDR(float64(o.hdr.settings.SDRBrightness))
36			err = o.showImageKind(r, imagesDriver, nil)
37		}
38		var pe protectedCommitError
39		if errors.As(err, &pe) {
40			// A protected KMS failure is not an HDR failure: images exist and
41			// the owner retries the protected modeset. Never fall back to SDR
42			// or latch hdr.failed for it.
43			o.hdr.rollbackBlob(o.k, sw)
44			return err
45		}
46		if err == nil {
47			o.hdr.commitBlob(o.k, sw)
48			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")
49			return nil
50		}
51		if sw.old != 0 {
52			// Failed replacement: the existing modeset may still use it.
53			o.hdr.rollbackBlob(o.k, sw)
54		}
55		o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("HDR modeset unavailable; falling back to SDR")
56		o.hdr.failed = true
57		o.hdr.on = false
58		o.freeImages()
59		r.SetHDR(0)
60		_, _ = r.ExportTargets(0, nil)
61		// A failed test commit can leave the old HDR mode on screen;
62		// retain its blob until the SDR modeset succeeds or Close restores it.
63	} else {
64		r.SetHDR(0)
65	}
66	err := o.showImageKind(r, kind, cause)
67	if err == nil && o.hdr.failed && o.hdr.blob != 0 {
68		// The SDR commit has completed; the old HDR metadata is no longer in use.
69		o.hdr.releaseBlob(o.k)
70	}
71	return err
72}
73
74func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) error {
75	for {
76		got, err := o.setupImages(r, kind, cause)
77		if err != nil {
78			return err
79		}
80		o.kind = got
81		if err = o.testModeset(); err == nil {
82			err = o.modeset()
83			if err != nil && o.protected && !errors.Is(err, errSecurityScene) && !errors.Is(err, errProtectionPending) && !errors.As(err, new(protectedCommitError)) {
84				err = protectedCommitError{err}
85			}
86			// A protected failure is not an image refusal: never fall back to
87			// another image kind or SDR for it. The caller hands it to the
88			// bounded retry (commitFailed).
89			return err
90		}
91		if o.protected && !refused(err) {
92			// EBUSY, lost master or a blob failure of the test: the images
93			// are fine; retry the protected modeset later.
94			return protectedCommitError{err}
95		}
96		if !refused(err) || got == imagesLinear {
97			o.freeImages()
98			return err
99		}
100		o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
101		o.freeImages()
102		kind, cause = got+1, err
103	}
104}
105
106// setupImages gives the output the two renderer images it flips between,
107// exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that
108// cannot export a scanout-capable image cannot drive the output.
109func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
110	err := cause
111	for ; kind <= imagesLinear; kind++ {
112		if o.runContext != nil && o.runContext.Err() != nil {
113			return kind, o.runContext.Err()
114		}
115		mods := []uint64(nil)
116		if o.hdr.on {
117			for _, f := range o.primary.formats {
118				if f.Format == fourccXR30 {
119					mods = append(mods, f.Modifier)
120				}
121			}
122			if len(mods) == 0 {
123				return kind, fmt.Errorf("primary plane has no XRGB2101010 modifiers")
124			}
125			if kind == imagesLinear {
126				if !slices.Contains(mods, uint64(0)) {
127					continue
128				}
129				mods = []uint64{0}
130			}
131		} else if kind == imagesLinear {
132			mods = []uint64{0}
133		}
134		if err = o.exportImages(r, mods); err == nil {
135			return kind, nil
136		}
137		o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
138	}
139	return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
140}
141
142// exportImages makes the renderer's exported targets the output images.
143func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
144	bufs, err := r.ExportTargets(len(o.fbs), mods)
145	if err != nil {
146		return err
147	}
148	for i := range bufs {
149		if err == nil {
150			format := uint32(fourccXRGB)
151			if o.hdr.on {
152				format = fourccXR30
153			}
154			o.fbs[i], err = o.k.addFB(&bufs[i], format)
155		}
156		bufs[i].Planes[0].File.Close()
157	}
158	// GPU memory starts undefined (old VRAM contents): clear both images
159	// before the modeset shows one.
160	for i := range o.fbs {
161		if err != nil {
162			break
163		}
164		r.UseTarget(i)
165		var done *os.File
166		if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
167			// The modeset is a blocking commit: wait for the clear.
168			waitCtx := o.runContext
169			if waitCtx == nil {
170				waitCtx = context.Background()
171			}
172			err = errors.Join(err, syncfile.Wait(waitCtx, done))
173			done.Close()
174		}
175	}
176	if err != nil {
177		o.freeImages()
178		_, _ = r.ExportTargets(0, nil)
179		return err
180	}
181	o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")
182	return nil
183}
184
185// freeImages removes the output images' framebuffers.
186func (o *Output) freeImages() {
187	for i, fb := range o.fbs {
188		if fb != 0 {
189			_ = o.k.rmFB(fb)
190		}
191		o.fbs[i] = 0
192	}
193}

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