1package vulkan
2
3import (
4 "image"
5 "math"
6 "strconv"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// sceneWalk turns one scene into quads in paint order, copying new wl_shm
12// content into GPU buffers on the way. It lives for one frame.
13type sceneWalk struct {
14 r *Renderer
15 s ports.Scene
16 contents map[ports.WindowID]ports.SurfaceContent
17 dmg *damageRegion
18 // scale maps the scene's logical pixels to the target's physical ones.
19 scale float64
20 // zoom shrinks the surfaces of the window being placed: an overview
21 // preview (ports.SceneWindow.Preview); 1 otherwise.
22 zoom float64
23 // pulse is the focus effect of the window being placed
24 // (ports.SceneWindow.FocusEffect); 0 otherwise.
25 pulse float32
26 bounds image.Rectangle
27 draws []draw
28}
29
30// draws walks the scene into quads in paint order.
31func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw {
32 clear(r.scratchDraws) // release references from longer earlier scenes
33 w := &sceneWalk{r: r, s: s, contents: contents, dmg: dmg, scale: s.Scale, zoom: 1, bounds: image.Rect(0, 0, r.width, r.height), draws: r.scratchDraws[:0]}
34 r.scratchCovers = r.scratchCovers[:0]
35 if w.scale <= 0 {
36 w.scale = 1
37 }
38 w.layers(false)
39 outputBounds := w.bounds
40 if s.WorkspaceClip != (ports.Rect{}) {
41 c := s.WorkspaceClip
42 w.bounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H))
43 }
44 w.windows()
45 w.popups(false)
46 w.dropHints()
47 w.bounds = outputBounds
48 w.layers(true)
49 w.popups(true)
50 w.captureIndicators()
51 r.scratchDraws = w.draws
52 return w.draws
53}
54
55// captureIndicators draws the compositor's capture indicator over everything
56// (ports.CaptureIndicator). The border is four fills on the inside edge of the
57// target, so it stays visible on a monitor edge; the pill is a small rounded
58// square. Sizes are logical like the rest of the scene and scale with it. A
59// scene without indicators draws nothing: captures are rendered from scenes
60// that never carry them.
61func (w *sceneWalk) captureIndicators() {
62 if len(w.s.CaptureIndicators) == 0 {
63 return
64 }
65 col := parseColor(ports.CaptureBorderColor)
66 for _, m := range w.s.CaptureIndicators {
67 t := m.Rect
68 if t.W <= 0 || t.H <= 0 {
69 continue
70 }
71 if m.Pill {
72 w.pill(w.physRect(t.X, t.Y, t.W, t.H), col)
73 continue
74 }
75 b := ports.CaptureBorderOf(t.W, t.H)
76 if b <= 0 {
77 // Too thin for a border (core inflates such marks, so this is a
78 // 1-pixel output): a listed mark always draws something.
79 w.fill(w.physRect(t.X, t.Y, t.W, t.H), col)
80 continue
81 }
82 w.fill(w.physRect(t.X, t.Y, t.W, b), col)
83 w.fill(w.physRect(t.X, t.Y+t.H-b, t.W, b), col)
84 w.fill(w.physRect(t.X, t.Y+b, b, t.H-2*b), col)
85 w.fill(w.physRect(t.X+t.W-b, t.Y+b, b, t.H-2*b), col)
86 }
87}
88
89// dropHints fills where a dragged tile would land (ports.Scene.DropHints),
90// in the active border color, over the workspace's windows.
91func (w *sceneWalk) dropHints() {
92 if len(w.s.DropHints) == 0 {
93 return
94 }
95 col := parseColor(w.s.Border.Active)
96 for _, r := range w.s.DropHints {
97 if r.W > 0 && r.H > 0 {
98 w.fill(w.physRect(r.X, r.Y, r.W, r.H), col)
99 }
100 }
101}
102
103// pill fills a rounded square (physical pixels) with three rects: a full
104// middle and two bands narrowed by the corner radius.
105func (w *sceneWalk) pill(r image.Rectangle, c [3]uint8) {
106 rad := min(w.phys(ports.CapturePillRadius), r.Dx()/2, r.Dy()/2)
107 if rad <= 0 {
108 w.fill(r, c)
109 return
110 }
111 cut := (rad + 1) / 2
112 w.fill(image.Rect(r.Min.X+cut, r.Min.Y, r.Max.X-cut, r.Min.Y+rad), c)
113 w.fill(image.Rect(r.Min.X, r.Min.Y+rad, r.Max.X, r.Max.Y-rad), c)
114 w.fill(image.Rect(r.Min.X+cut, r.Max.Y-rad, r.Max.X-cut, r.Max.Y), c)
115}
116
117func (w *sceneWalk) phys(v int) int { return int(math.Round(float64(v) * w.scale)) }
118
119func (w *sceneWalk) physRect(x, y, width, height int) image.Rectangle {
120 return image.Rect(w.phys(x), w.phys(y), w.phys(x+width), w.phys(y+height))
121}
122
123// physRectF is physRect for fractional logical coordinates (zoomed surfaces).
124func (w *sceneWalk) physRectF(x, y, width, height float64) image.Rectangle {
125 p := func(v float64) int { return int(math.Round(v * w.scale)) }
126 return image.Rect(p(x), p(y), p(x+width), p(y+height))
127}
128
129// fill draws a solid rect (physical pixels).
130func (w *sceneWalk) fill(rect image.Rectangle, c [3]uint8) {
131 rect = rect.Intersect(w.bounds)
132 if rect.Empty() {
133 return
134 }
135 w.draws = append(w.draws, w.r.fillDraw(rect, c))
136}
137
138// dim paints a translucent black quad over rect (physical), clipped to the
139// output. Damage clipping later limits it to the region being repainted,
140// just like other fills.
141func (w *sceneWalk) dim(rect image.Rectangle, alpha float64) {
142 if rect = rect.Intersect(w.bounds); !rect.Empty() {
143 w.draws = append(w.draws, w.r.dimDraw(rect, alpha))
144 }
145}
146
147// layers draws the layer surfaces below the windows, or above them.
148// The scene holds only the shown ones: core hides them under a fullscreen
149// window.
150func (w *sceneWalk) layers(afterWindows bool) {
151 for _, layer := range w.s.Layers {
152 if (layer.Layer >= ports.LayerTop) != afterWindows || layer.Rect.W <= 0 || layer.Rect.H <= 0 {
153 continue
154 }
155 content := w.contents[layer.ID]
156 w.dmg.window(layer.ID, content, w.physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H))
157 w.place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
158 }
159}
160
161// windows draws ordered placements with borders. Tile lines go after
162// the column group, before the first float above it; an overview preview
163// of a float is not one.
164func (w *sceneWalk) windows() {
165 tileLines := false
166 for _, win := range w.s.Windows {
167 if win.Hidden || win.Popup || win.Rect.W <= 0 || win.Rect.H <= 0 {
168 continue
169 }
170 if !win.Below && win.Floating && win.Preview == 0 && !tileLines {
171 w.separators(0)
172 tileLines = true
173 if w.s.Dim > 0 {
174 w.dim(w.bounds, w.s.Dim)
175 }
176 }
177 // Content sits inside the border; core sized the client to match.
178 b, inset := 0, ports.Sides(0)
179 if !win.Fullscreen {
180 b, inset = w.s.Border.Width, win.Inset
181 }
182 c := win.Rect.Inset(inset, b)
183 body := w.physRect(c.X, c.Y, c.W, c.H)
184 content := w.contents[win.ID]
185 w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))
186 // Until its first buffer, a window shows the background: no flash.
187 w.fill(body, parseColor(w.s.Background))
188 if !content.Empty() {
189 if win.Preview > 0 {
190 w.zoom = win.Preview
191 }
192 w.pulse = float32(max(0, min(win.FocusEffect, 1)))
193 w.place(win.ID, &content, c.X, c.Y, c.W, c.H)
194 w.zoom, w.pulse = 1, 0
195 }
196 if win.Floating {
197 w.separators(win.ID)
198 }
199 if win.Dim > 0 {
200 // A stashed window peeking in: dimmed with its border.
201 w.dim(w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H), win.Dim)
202 }
203 }
204 if !tileLines {
205 w.separators(0)
206 }
207}
208
209// separators draws a window's border lines in order, active lines over
210// inactive ones.
211func (w *sceneWalk) separators(id ports.WindowID) {
212 for _, sep := range w.s.Separators {
213 if sep.Window != id {
214 continue
215 }
216 col := w.s.Border.Inactive
217 if sep.Active {
218 col = w.s.Border.Active
219 }
220 if col != "" {
221 w.fill(w.physRect(sep.Rect.X, sep.Rect.Y, sep.Rect.W, sep.Rect.H), parseColor(col))
222 }
223 }
224}
225
226// popups draws only what the client drew, shadows clipped. Window popups
227// stay with the windows; a layer's go over every layer.
228func (w *sceneWalk) popups(overLayers bool) {
229 for _, win := range w.s.Windows {
230 if win.Popup && win.OverLayers == overLayers && !win.Hidden && win.Rect.W > 0 && win.Rect.H > 0 {
231 content := w.contents[win.ID]
232 w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))
233 w.place(win.ID, &content, win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H)
234 }
235 }
236}
237
238// place draws a surface tree with its window geometry at (x, y) logical,
239// clipped to width×height logical: client shadows fall outside. Every
240// surface of the tree is keyed by its stable surface identity.
241func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y, width, height int) {
242 clip := image.Rect(x, y, x+width, y+height)
243 // Offsets inside the tree shrink with a zoomed (preview) window.
244 z := w.zoom
245 ox, oy := float64(x)-float64(content.Geometry.X)*z, float64(y)-float64(content.Geometry.Y)*z
246 at := func(ch *ports.Subsurface) (float64, float64) { return ox + float64(ch.X)*z, oy + float64(ch.Y)*z }
247 covers := w.opaqueChildren(content, ox, oy, clip)
248 for i := range content.Children {
249 if ch := &content.Children[i]; ch.Below {
250 cx, cy := at(ch)
251 w.surface(&ch.SurfaceContent, cx, cy, clip, shmKey{id, ch.Surface}, ch.Version, covers, false)
252 }
253 }
254 w.surface(content, ox, oy, clip, shmKey{id, content.Surface}, content.Version, covers, true)
255 for i := range content.Children {
256 if ch := &content.Children[i]; !ch.Below {
257 cx, cy := at(ch)
258 w.surface(&ch.SurfaceContent, cx, cy, clip, shmKey{id, ch.Surface}, ch.Version, nil, false)
259 }
260 }
261}
262
263// opaqueChildren gives the physical rects shown by the opaque subsurfaces
264// above the root (e.g. a game's GPU surface over its window's shm buffer):
265// they hide the surfaces below them. A child whose buffer will not draw is
266// left out, so the surface below it still shows: a dmabuf that fails to
267// import (opaqueChildren pre-imports it) or an unreadable shm buffer.
268func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy float64, clip image.Rectangle) []image.Rectangle {
269 covers := w.r.scratchCovers[:0]
270 for i := range content.Children {
271 ch := &content.Children[i]
272 if ch.Below || !ch.Opaque {
273 continue
274 }
275 _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+float64(ch.X)*w.zoom, oy+float64(ch.Y)*w.zoom, clip)
276 if !ok || !w.drawable(&ch.SurfaceContent) {
277 continue
278 }
279 covers = append(covers, dst)
280 }
281 w.r.scratchCovers = covers
282 return covers
283}
284
285// drawable reports whether the child's buffer will draw. GPU buffer
286// imports are cached, so pre-importing here costs nothing.
287func (w *sceneWalk) drawable(c *ports.SurfaceContent) bool {
288 if c.DMABuf != nil {
289 _, err := w.r.importDMABuf(c.DMABuf)
290 return err == nil
291 }
292 b := c.SHM
293 if b == nil {
294 return false
295 }
296 _, err := w.r.shmPixels(b, b.Offset+(c.Height-1)*b.Stride+c.Width*4)
297 return err == nil
298}
299
300// surfaceRects gives the physical rect of a surface buffer with its origin
301// at (x, y) logical, shrunk by the walk's zoom, and the part of it clip
302// (logical) shows. ok is false when there is nothing to draw.
303func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y float64, clip image.Rectangle) (full, dst image.Rectangle, ok bool) {
304 lw, lh := content.LogicalW, content.LogicalH
305 if lw <= 0 || lh <= 0 {
306 lw, lh = content.Width, content.Height
307 }
308 if content.SHM == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
309 return full, dst, false
310 }
311 if content.DMABuf == nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
312 return full, dst, false
313 }
314 full = w.physRectF(x, y, float64(lw)*w.zoom, float64(lh)*w.zoom)
315 // A buffer drawn at the physical size (fractional-scale clients round
316 // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
317 near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
318 sourceW, sourceH := content.Width, content.Height
319 if content.Source[2] > 0 {
320 sourceW, sourceH = int(content.Source[2]), int(content.Source[3])
321 }
322 if content.Transform.Rotated() {
323 sourceW, sourceH = sourceH, sourceW
324 }
325 if near(full.Dx(), sourceW) && near(full.Dy(), sourceH) {
326 full.Max = full.Min.Add(image.Pt(sourceW, sourceH))
327 }
328 dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy())).Intersect(w.bounds)
329 return full, dst, !dst.Empty()
330}
331
332// surface draws one surface buffer with its origin at (x, y) logical,
333// clipped to clip (logical), unless an opaque surface above it (covers,
334// physical) hides all of it: its buffer is then neither copied nor drawn.
335func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle, root bool) {
336 full, dst, ok := w.surfaceRects(content, x, y, clip)
337 if !ok {
338 return
339 }
340 for _, c := range covers {
341 if dst.In(c) {
342 return
343 }
344 }
345 if root {
346 w.dmg.content(key.win, content, full, dst)
347 }
348 w.content(dst, full, content, key, seq, root)
349}
350
351// content draws the part of a buffer mapped onto full (physical pixels)
352// that dst shows. key names the surface, seq its content.
353func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64, root bool) {
354 r := w.r
355 rect := dst
356 if rect.Empty() {
357 return
358 }
359 if content.DMABuf != nil {
360 im, err := r.importDMABuf(content.DMABuf)
361 if err != nil {
362 // The window shows its background until a buffer imports.
363 return
364 }
365 dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeImage, content.Opaque)
366 r.setContentColor(&dr, content)
367 dr.pc.mapy[2] = w.pulse
368 if im.yuv {
369 dr.pc.misc[1] |= flagYUV
370 if content.DMABuf.Format == fourcc('P', '0', '1', '0') {
371 dr.pc.misc[1] |= flagP010
372 }
373 coeff, ran := content.Color.Coefficients, content.Color.Range
374 if coeff == 0 {
375 coeff = 2 /* BT.709 */
376 }
377 if ran == 0 {
378 ran = 2 /* limited */
379 }
380 dr.pc.buf[3] = uint32(coeff) | uint32(ran)<<8 | uint32(content.Color.Chroma)<<16
381 }
382 dr.set, dr.im = im.set, im
383 dr.acquire = content.Acquire
384 w.draws = append(w.draws, dr)
385 return
386 }
387 b := content.SHM
388 pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
389 if err != nil {
390 return
391 }
392 st := shmState{w: content.Width, h: content.Height, seq: seq, windowSeq: content.Seq}
393 // Damage history is the root surface's: children copy in full.
394 var damage func(uint64) ([]ports.Rect, bool)
395 if root {
396 damage = content.DamageSince
397 }
398 c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
399 if err != nil {
400 return
401 }
402 dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque)
403 r.setContentColor(&dr, content)
404 dr.pc.mapy[2] = w.pulse
405 dr.set = c.set
406 dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
407 w.draws = append(w.draws, dr)
408}
409
410// setContentColor selects only color encodings the protocol accepts, and
411// the surface's fade.
412func (r *Renderer) setContentColor(dr *draw, content *ports.SurfaceContent) {
413 dr.pc.color[3] = 1 - max(0, min(1, content.Fade))
414 c := content.Color
415 if c.IsPQ2020() {
416 dr.pc.misc[1] |= flagPQ
417 } else if c.IsExtendedLinear() {
418 dr.pc.misc[1] |= flagExtendedLinear
419 } else {
420 return
421 }
422 dr.pc.color[0] = float32(r.hdrNits)
423 if dr.pc.color[0] <= 0 {
424 dr.pc.color[0] = 203
425 }
426}
427
428func parseColor(s string) [3]uint8 {
429 var c [3]uint8
430 if len(s) != 7 || s[0] != '#' {
431 return c
432 }
433 for i := range c {
434 v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8)
435 if e != nil {
436 return [3]uint8{}
437 }
438 c[i] = uint8(v)
439 }
440 return c
441}