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}