scene.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close