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}