1package drm
2
3import (
4 "errors"
5 "os"
6
7 "golang.org/x/sys/unix"
8)
9
10// The output side of the plane colour pipelines: per-plane state, commit
11// properties, refusal caching. The pure part (reading and matching
12// pipelines, the maths, planeColor) is in colorop_match.go.
13
14// hasColor reports whether the plane has a COLOR_PIPELINE property.
15func (p *plane) hasColor() bool { return p != nil && p.props[propColorPipeline] != 0 }
16
17// colorStale reports whether mode must be written: the plane applied
18// another one, or nobody knows. A plane without a matched pipeline never
19// leaves Bypass (modesets and Close write it), so it is never stale.
20func (p *plane) colorStale(mode colorMode) bool {
21 return p.hasColor() && p.pipeline != nil && (!p.colorKnown || p.colorApplied != mode)
22}
23
24// colorProps puts mode on p: Bypass selects the plane's Bypass pipeline;
25// colorSDRToPQ selects the matched one with its four operations enabled and
26// every other one bypassed. mult is the S31.32 multiplier and ctm the matrix
27// blob.
28func (p *plane) colorProps(req *atomicReq, mode colorMode, mult uint64, ctm uint32) {
29 if !p.hasColor() {
30 return
31 }
32 prop := p.props[propColorPipeline]
33 cp := p.pipeline
34 if mode != colorSDRToPQ || cp == nil {
35 req.set(p.id, prop, 0)
36 return
37 }
38 req.set(p.id, prop, cp.id)
39 for _, s := range cp.others {
40 req.set(s.op, s.bypass, 1)
41 }
42 for _, s := range [...]struct {
43 stage colorStage
44 val uint64
45 }{{cp.srgb, cp.srgb.curve}, {cp.mult, mult}, {cp.ctm, uint64(ctm)}, {cp.pq, cp.pq.curve}} {
46 req.set(s.stage.op, s.stage.bypass, 0)
47 req.set(s.stage.op, s.stage.prop, s.val)
48 }
49}
50
51// colorVerdict is KMS's answer to a TEST_ONLY with the pipeline on a plane,
52// kept per buffer format and cursor state.
53type colorVerdict struct {
54 format uint32
55 cursor bool
56 ok bool
57}
58
59// verdict returns a cached answer.
60func (p *plane) verdict(format uint32, cursor bool) (ok, known bool) {
61 for _, v := range p.verdicts {
62 if v.format == format && v.cursor == cursor {
63 return v.ok, true
64 }
65 }
66 return false, false
67}
68
69// setVerdict caches KMS's answer for the pipeline on p.
70func (p *plane) setVerdict(format uint32, cursor, ok bool) {
71 for i, v := range p.verdicts {
72 if v.format == format && v.cursor == cursor {
73 p.verdicts[i].ok = ok
74 return
75 }
76 }
77 p.verdicts = append(p.verdicts, colorVerdict{format: format, cursor: cursor, ok: ok})
78}
79
80// cursorShown reports whether a frame committed now shows the hardware cursor.
81func (o *Output) cursorShown() bool { return o.cursor != nil && !o.protected && o.cursor.desired().on }
82
83// primaryColorProps puts mode on the primary plane if it applies another one.
84func (o *Output) primaryColorProps(req *atomicReq, mode colorMode) {
85 if o.primary.colorStale(mode) {
86 o.primary.colorProps(req, mode, o.colorMult, o.ctmBlob)
87 }
88}
89
90// forceBypass sets Bypass on the primary and overlay planes whether or not
91// the applied state is known to differ: for requests that must leave a
92// known state (modeset, close).
93// The CRTC's DEGAMMA_LUT, CTM and GAMMA_LUT are cleared with them: this
94// compositor never sets them, and a ramp left by another DRM master would
95// apply on top of the PQ output.
96func (o *Output) forceBypass(req *atomicReq) {
97 o.primary.colorProps(req, colorBypass, 0, 0)
98 o.overlay.colorProps(req, colorBypass, 0, 0)
99 for _, name := range [...]string{"DEGAMMA_LUT", "CTM", "GAMMA_LUT"} {
100 req.set(o.crtc, o.crtcProps[name], 0)
101 }
102}
103
104// overlayColorProps puts mode on the overlay plane if it applies another one.
105func (o *Output) overlayColorProps(req *atomicReq, mode colorMode) {
106 if o.overlay.colorStale(mode) {
107 o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)
108 }
109}
110
111// colorBypassed records that a commit left both planes on Bypass.
112func (o *Output) colorBypassed() {
113 for _, p := range [...]*plane{o.primary, o.overlay} {
114 if p != nil {
115 p.colorApplied, p.colorKnown = colorBypass, true
116 }
117 }
118}
119
120// forgetColor drops what is known about the planes' colour state: the next
121// frame commit writes it. Every modeset (VT resume and recovery included)
122// calls it, since the planes' state is then not ours to assume. The async
123// probe follows a modeset and needs no call of its own.
124func (o *Output) forgetColor() {
125 for _, p := range [...]*plane{o.primary, o.overlay} {
126 if p != nil {
127 p.colorKnown = false
128 }
129 }
130}
131
132// forgetColorVerdicts drops the cached refusals: after a modeset the driver
133// is asked again.
134func (o *Output) forgetColorVerdicts() {
135 for _, p := range [...]*plane{o.primary, o.overlay} {
136 if p != nil {
137 p.verdicts = p.verdicts[:0]
138 }
139 }
140}
141
142// colorStaleFor reports whether a frame commit with primary colour pc and
143// overlay ov writes colour state: such a commit cannot be async.
144func (o *Output) colorStaleFor(pc colorUse, ov overlayWin) bool {
145 return o.primary.colorStale(pc.mode) || o.overlay.colorStale(ov.color.mode)
146}
147
148// colorCommitted records the colour state a successful synchronous frame
149// commit left.
150func (o *Output) colorCommitted(pc colorUse, ov overlayWin) {
151 if o.primary.hasColor() {
152 o.primary.colorApplied, o.primary.colorKnown, o.primary.colorFormat = pc.mode, true, pc.format
153 }
154 if o.overlay.hasColor() {
155 o.overlay.colorApplied, o.overlay.colorKnown, o.overlay.colorFormat = ov.color.mode, true, ov.color.format
156 }
157}
158
159// errColorRefused: KMS refused a frame with a plane colour pipeline that it
160// takes with Bypass. The caller composes the frame.
161var errColorRefused = errors.New("plane colour pipeline refused")
162
163// colorRefused handles a frame commit that failed with EINVAL while a plane
164// showed the pipeline: when the same frame passes TEST_ONLY with Bypass, and
165// differs from the commit in colour alone, the pipeline is the cause. The refusal is cached for each plane that showed it,
166// its buffer format and the cursor state, and the frame is not retried.
167func (o *Output) colorRefused(err error, fb uint32, fence *os.File, vrr bool, cur cursorState, ov overlayWin, rect planeRect, pc colorUse) bool {
168 if !o.colorUsed(pc, ov) {
169 return false
170 }
171 bypassed := ov
172 bypassed.color = colorUse{}
173 // The same frame as the failed commit, content hint included, with
174 // Bypass as the only difference: if the hint was the cause, this fails
175 // too and the pipeline is not blamed.
176 if !o.frameTest(fb, fence, vrr, cur, bypassed, rect, colorUse{}, true) {
177 return false
178 }
179 if pc.mode != colorBypass {
180 o.primary.setVerdict(pc.format, cur.on, false)
181 }
182 if ov.buf != 0 && ov.color.mode != colorBypass {
183 o.overlay.setVerdict(ov.color.format, cur.on, false)
184 }
185 o.renderLog.Info().Err(err).Str("connector", o.conn.name).Bool("cursor", cur.on).Msg("plane colour pipeline refused")
186 return true
187}
188
189// colorConflict handles a commit without a frame, which turned the cursor
190// on or moved it, refused while a plane applies the pipeline: KMS may refuse
191// that combination although it took each alone. The pipeline is refused for
192// that buffer format with the cursor shown, so the next frame is composed
193// (the cursor then commits with it). It reports whether it did.
194func (o *Output) colorConflict(err error, cursorOn bool) bool {
195 if !cursorOn || !errors.Is(err, unix.EINVAL) {
196 return false
197 }
198 hit := false
199 for _, p := range [...]*plane{o.primary, o.overlay} {
200 if p.hasColor() && p.colorKnown && p.colorApplied == colorSDRToPQ {
201 p.setVerdict(p.colorFormat, true, false)
202 hit = true
203 }
204 }
205 if hit {
206 o.renderLog.Info().Err(err).Str("connector", o.conn.name).Msg("plane colour pipeline refused with the cursor")
207 }
208 return hit
209}
210
211// colorAllowed tests, once per plane, buffer format and cursor state, that
212// KMS takes the SDR to PQ pipeline on p for the request test builds (the frame
213// as it would commit). A refusal is cached: the buffer is composed from then
214// on, never tested each frame.
215func (o *Output) colorAllowed(p *plane, format uint32, test func(req *atomicReq)) bool {
216 cursor := o.cursorShown()
217 if ok, known := p.verdict(format, cursor); known {
218 return ok
219 }
220 req := &o.probeReq
221 req.reset()
222 test(req)
223 p.colorProps(req, colorSDRToPQ, o.colorMult, o.ctmBlob)
224 if cursor {
225 o.cursor.props(req, o.crtc, o.cursor.desired())
226 }
227 err := o.k.commit(req, atomicTestOnly, 0)
228 if err != nil && !refused(err) {
229 // Not an answer about the pipeline (e.g. master lost): ask again.
230 return false
231 }
232 p.setVerdict(format, cursor, err == nil)
233 if err != nil {
234 o.renderLog.Info().Err(err).Uint32("format", format).Uint32("plane", p.id).Bool("cursor", cursor).Str("connector", o.conn.name).Msg("plane colour pipeline refused")
235 }
236 return err == nil
237}
238
239// setupColor makes the output's colour objects: the CTM blob and the
240// multiplier, when HDR can be on and a plane has a matched pipeline. A
241// failure drops the pipelines: SDR content is composed as before.
242func (o *Output) setupColor() {
243 if !o.hdr.wanted() {
244 // The pipelines serve HDR outputs only: SDR stays on Bypass.
245 o.dropPipelines()
246 return
247 }
248 if o.primary.pipeline == nil && (o.overlay == nil || o.overlay.pipeline == nil) {
249 return
250 }
251 blob, err := o.k.createBlob(ctmBytes(bt709ToBT2020))
252 if err != nil {
253 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("colour matrix blob; plane colour pipelines off")
254 o.dropPipelines()
255 return
256 }
257 o.ctmBlob = blob
258 o.colorMult = s3132(float64(o.hdr.settings.SDRBrightness) / 80)
259}
260
261// dropPipelines leaves the output's planes without a pipeline: SDR content
262// is composed.
263func (o *Output) dropPipelines() {
264 for _, p := range [...]*plane{o.primary, o.overlay} {
265 if p != nil {
266 p.pipeline = nil
267 }
268 }
269}
270
271// readColor finds the planes' SDR to PQ pipelines, once at output creation.
272func (o *Output) readColor() {
273 for _, p := range [...]*plane{o.primary, o.overlay} {
274 if p == nil || !p.hasColor() {
275 continue
276 }
277 cp, why := readColorPipeline(o.k, p)
278 p.pipeline = cp
279 o.log.Info().Str("connector", o.conn.name).Uint32("plane", p.id).Bool("matched", cp != nil).Str("reason", why).Msg("plane colour pipeline")
280 }
281}
282
283// colorUsed reports whether a frame with primary colour pc and overlay ov
284// has a plane show the pipeline.
285func (o *Output) colorUsed(pc colorUse, ov overlayWin) bool {
286 return pc.mode != colorBypass || ov.buf != 0 && ov.color.mode != colorBypass
287}