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}