detached f81774e

merge: occlusion culling for hidden shm surfaces

commit
f81774eaa3fb885f4c71e61b23aae8163343a8c4
author
brice · · 2026-09-26 15:52 UTC
merge of
f176c04 + a0f00e0 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +379 -230
M internal/adapters/vulkan/compose_test.go +63 -0
@@ -138,6 +138,69 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) {
138138 }
139139 }
140140
141+// A surface fully hidden by an opaque subsurface above it is not copied:
142+// only the subsurface's bytes are, and the frame shows it.
143+func TestRendererSkipsOccludedSurface(t *testing.T) {
144+ r, err := New(32, 32)
145+ if err != nil {
146+ t.Skipf("Vulkan unavailable: %v", err)
147+ }
148+ defer r.Close()
149+ green := color.RGBA{0, 255, 0, 255}
150+ root := solidContent(t, 16, 16, color.RGBA{255, 0, 0, 255})
151+ root.ID, root.Seq = 1, 1
152+ game := solidContent(t, 16, 16, green)
153+ game.Opaque = true
154+ root.Children = []ports.Subsurface{{SurfaceContent: game}}
155+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
156+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
157+ t.Fatal(err)
158+ }
159+ if r.copied != 16*16*4 {
160+ t.Fatalf("copied %d bytes, want only the subsurface's %d", r.copied, 16*16*4)
161+ }
162+ if got := r.Pixels().At(8, 8); got != green {
163+ t.Fatalf("At(8,8)=%v want %v", got, green)
164+ }
165+ // A smaller opaque child leaves part of the root visible: it is copied.
166+ small := solidContent(t, 8, 8, green)
167+ small.Opaque = true
168+ root.Seq = 2
169+ root.Children = []ports.Subsurface{{SurfaceContent: small}}
170+ r.copied = 0
171+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
172+ t.Fatal(err)
173+ }
174+ if r.copied != 16*16*4+8*8*4 {
175+ t.Fatalf("partly hidden root: copied %d", r.copied)
176+ }
177+}
178+
179+// A covering child whose buffer will not draw does not hide the root:
180+// an unimportable dmabuf still shows the surface below it, not a hole.
181+func TestRendererKeepsSurfaceBelowFailedCover(t *testing.T) {
182+ r, err := New(32, 32)
183+ if err != nil {
184+ t.Skipf("Vulkan unavailable: %v", err)
185+ }
186+ defer r.Close()
187+ red := color.RGBA{255, 0, 0, 255}
188+ root := solidContent(t, 16, 16, red)
189+ root.ID, root.Seq = 1, 1
190+ bad := &ports.DMABuf{ID: 9, Width: 16, Height: 16, Format: 0xdeadbeef}
191+ root.Children = []ports.Subsurface{{SurfaceContent: ports.SurfaceContent{Width: 16, Height: 16, Opaque: true, DMABuf: bad}}}
192+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
193+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
194+ t.Fatal(err)
195+ }
196+ if r.copied != 16*16*4 {
197+ t.Fatalf("copied %d bytes, want the root's %d", r.copied, 16*16*4)
198+ }
199+ if got := r.Pixels().At(8, 8); got != red {
200+ t.Fatalf("At(8,8)=%v want %v", got, red)
201+ }
202+}
203+
141204 // render draws a frame and closes its fence.
142205 func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
143206 done, err := r.Render(s, contents)
@@ -138,6 +138,69 @@ func TestRendererSHMCopiedOncePerContent(t *testing.T) {
138 }138 }
139 }139 }
140 140
141+// A surface fully hidden by an opaque subsurface above it is not copied:
142+// only the subsurface's bytes are, and the frame shows it.
143+func TestRendererSkipsOccludedSurface(t *testing.T) {
144+ r, err := New(32, 32)
145+ if err != nil {
146+ t.Skipf("Vulkan unavailable: %v", err)
147+ }
148+ defer r.Close()
149+ green := color.RGBA{0, 255, 0, 255}
150+ root := solidContent(t, 16, 16, color.RGBA{255, 0, 0, 255})
151+ root.ID, root.Seq = 1, 1
152+ game := solidContent(t, 16, 16, green)
153+ game.Opaque = true
154+ root.Children = []ports.Subsurface{{SurfaceContent: game}}
155+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
156+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
157+ t.Fatal(err)
158+ }
159+ if r.copied != 16*16*4 {
160+ t.Fatalf("copied %d bytes, want only the subsurface's %d", r.copied, 16*16*4)
161+ }
162+ if got := r.Pixels().At(8, 8); got != green {
163+ t.Fatalf("At(8,8)=%v want %v", got, green)
164+ }
165+ // A smaller opaque child leaves part of the root visible: it is copied.
166+ small := solidContent(t, 8, 8, green)
167+ small.Opaque = true
168+ root.Seq = 2
169+ root.Children = []ports.Subsurface{{SurfaceContent: small}}
170+ r.copied = 0
171+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
172+ t.Fatal(err)
173+ }
174+ if r.copied != 16*16*4+8*8*4 {
175+ t.Fatalf("partly hidden root: copied %d", r.copied)
176+ }
177+}
178+
179+// A covering child whose buffer will not draw does not hide the root:
180+// an unimportable dmabuf still shows the surface below it, not a hole.
181+func TestRendererKeepsSurfaceBelowFailedCover(t *testing.T) {
182+ r, err := New(32, 32)
183+ if err != nil {
184+ t.Skipf("Vulkan unavailable: %v", err)
185+ }
186+ defer r.Close()
187+ red := color.RGBA{255, 0, 0, 255}
188+ root := solidContent(t, 16, 16, red)
189+ root.ID, root.Seq = 1, 1
190+ bad := &ports.DMABuf{ID: 9, Width: 16, Height: 16, Format: 0xdeadbeef}
191+ root.Children = []ports.Subsurface{{SurfaceContent: ports.SurfaceContent{Width: 16, Height: 16, Opaque: true, DMABuf: bad}}}
192+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
193+ if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
194+ t.Fatal(err)
195+ }
196+ if r.copied != 16*16*4 {
197+ t.Fatalf("copied %d bytes, want the root's %d", r.copied, 16*16*4)
198+ }
199+ if got := r.Pixels().At(8, 8); got != red {
200+ t.Fatalf("At(8,8)=%v want %v", got, red)
201+ }
202+}
203+
141 // render draws a frame and closes its fence.204 // render draws a frame and closes its fence.
142 func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {205 func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
143 done, err := r.Render(s, contents)206 done, err := r.Render(s, contents)
M internal/adapters/vulkan/renderer.go +0 -230
@@ -261,236 +261,6 @@ func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (ui
261261 return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)
262262 }
263263
264-func parseColor(s string) [3]uint8 {
265- var c [3]uint8
266- if len(s) != 7 || s[0] != '#' {
267- return c
268- }
269- for i := range c {
270- v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8)
271- if e != nil {
272- return [3]uint8{}
273- }
274- c[i] = uint8(v)
275- }
276- return c
277-}
278-
279-func windowColor(id ports.WindowID) [3]uint8 {
280- h := math.Mod(float64(id)*0.618034, 1) * 6
281- sector := int(h)
282- f := h - float64(sector)
283- p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f))
284- var a, b, c float64
285- switch sector {
286- case 0:
287- a, b, c = 0.8, t, p
288- case 1:
289- a, b, c = q, 0.8, p
290- case 2:
291- a, b, c = p, 0.8, t
292- case 3:
293- a, b, c = p, q, 0.8
294- case 4:
295- a, b, c = t, p, 0.8
296- default:
297- a, b, c = 0.8, p, q
298- }
299- return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
300-}
301-
302-// draws walks the scene into quads in paint order, copying new wl_shm
303-// content into GPU buffers on the way.
304-func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw {
305- // Scene rects are logical; everything below works in physical pixels.
306- scale := s.Scale
307- if scale <= 0 {
308- scale = 1
309- }
310- phys := func(v int) int { return int(math.Round(float64(v) * scale)) }
311- physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }
312- var draws []draw
313- bounds := image.Rect(0, 0, r.width, r.height)
314- add := func(rect image.Rectangle, c [3]uint8) {
315- rect = rect.Intersect(bounds)
316- if rect.Empty() {
317- return
318- }
319- draws = append(draws, r.fillDraw(rect, c))
320- }
321- // addContent draws the part of a buffer mapped onto full (physical
322- // pixels) that dst shows. key names the surface, seq its content.
323- addContent := func(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
324- rect := dst.Intersect(bounds)
325- if rect.Empty() {
326- return
327- }
328- if content.DMABuf != nil {
329- im, err := r.importDMABuf(content.DMABuf)
330- if err != nil {
331- // The window shows its background until a buffer imports.
332- return
333- }
334- dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
335- dr.set, dr.im = im.set, im
336- dr.acquire = content.Acquire
337- draws = append(draws, dr)
338- return
339- }
340- b := content.SHM
341- pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
342- if err != nil {
343- return
344- }
345- st := shmState{w: content.Width, h: content.Height, seq: seq}
346- // Damage history is the root surface's: children copy in full.
347- var damage func(uint64) ([]ports.Rect, bool)
348- if key.index == 0 {
349- damage = content.DamageSince
350- }
351- c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
352- if err != nil {
353- return
354- }
355- dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
356- dr.set = c.set
357- dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
358- draws = append(draws, dr)
359- }
360- // drawSurface draws one surface buffer with its origin at (x, y)
361- // logical, clipped to clip (logical).
362- drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64) {
363- lw, lh := content.LogicalW, content.LogicalH
364- if lw <= 0 || lh <= 0 {
365- lw, lh = content.Width, content.Height
366- }
367- if content.SHM == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
368- return
369- }
370- if content.DMABuf == nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
371- return
372- }
373- full := physRect(x, y, lw, lh)
374- // A buffer drawn at the physical size (fractional-scale clients round
375- // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
376- near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
377- if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {
378- full.Max = full.Min.Add(image.Pt(content.Width, content.Height))
379- }
380- dst := full.Intersect(physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
381- if dst.Empty() {
382- return
383- }
384- if key.index == 0 {
385- dmg.content(key.win, content, full, dst)
386- }
387- addContent(dst, full, content, key, seq)
388- }
389- // place draws a surface tree with its window geometry at (x, y)
390- // logical, clipped to w×h logical: client shadows fall outside.
391- // Every surface of the tree is keyed by its window and place in it;
392- // the tree's Seq changes with any of them.
393- place := func(id ports.WindowID, content *ports.SurfaceContent, x, y, w, h int) {
394- clip := image.Rect(x, y, x+w, y+h)
395- ox, oy := x-content.Geometry.X, y-content.Geometry.Y
396- for i := range content.Children {
397- if ch := &content.Children[i]; ch.Below {
398- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
399- }
400- }
401- drawSurface(content, ox, oy, clip, shmKey{id, 0}, content.Seq)
402- for i := range content.Children {
403- if ch := &content.Children[i]; !ch.Below {
404- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
405- }
406- }
407- }
408- fullscreen := false
409- for _, w := range s.Windows {
410- fullscreen = fullscreen || (!w.Hidden && w.Fullscreen)
411- }
412- addLayers := func(afterWindows bool) {
413- for _, layer := range s.Layers {
414- if (layer.Layer >= ports.LayerTop) != afterWindows ||
415- (fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) ||
416- layer.Rect.W <= 0 || layer.Rect.H <= 0 {
417- continue
418- }
419- content := contents[layer.ID]
420- dmg.window(layer.ID, content, physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H))
421- place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
422- }
423- }
424- addLayers(false)
425- // Separators are drawn in order, active lines over inactive ones:
426- // tile lines (Window 0) over the tiles, a float's border with it.
427- addSeparators := func(id ports.WindowID) {
428- for _, sep := range s.Separators {
429- if sep.Window != id {
430- continue
431- }
432- col := s.Border.Inactive
433- if sep.Active {
434- col = s.Border.Active
435- }
436- if col != "" {
437- add(physRect(sep.Rect.X, sep.Rect.Y, sep.Rect.W, sep.Rect.H), parseColor(col))
438- }
439- }
440- }
441- tileLines := false
442- for _, w := range s.Windows {
443- if w.Floating && !tileLines {
444- addSeparators(0)
445- tileLines = true
446- }
447- if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
448- continue
449- }
450- x, y := w.Rect.X, w.Rect.Y
451- if w.Popup {
452- continue
453- }
454- // Content sits inside the border; core sized the client to match.
455- b, inset := 0, ports.Sides(0)
456- if !w.Fullscreen {
457- b, inset = s.Border.Width, w.Inset
458- }
459- c := w.Rect.Inset(inset, b)
460- cx, cy, cw, ch := c.X, c.Y, c.W, c.H
461- body := physRect(cx, cy, cw, ch)
462- content := contents[w.ID]
463- dmg.window(w.ID, content, physRect(x, y, w.Rect.W, w.Rect.H))
464- if content.Empty() {
465- add(body, windowColor(w.ID))
466- } else {
467- add(body, parseColor(s.Background))
468- place(w.ID, &content, cx, cy, cw, ch)
469- }
470- if w.Floating {
471- addSeparators(w.ID)
472- }
473- }
474- if !tileLines {
475- addSeparators(0)
476- }
477- // Popups draw only what the client drew, shadows clipped. Window popups
478- // stay with the windows; a layer's go over every layer.
479- addPopups := func(overLayers bool) {
480- for _, w := range s.Windows {
481- if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {
482- content := contents[w.ID]
483- dmg.window(w.ID, content, physRect(w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H))
484- place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
485- }
486- }
487- }
488- addPopups(false)
489- addLayers(true)
490- addPopups(true)
491- return draws
492-}
493-
494264 func (r *Renderer) Pixels() *image.RGBA {
495265 out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
496266 if r.mapped == nil || r.readback() != nil {
@@ -261,236 +261,6 @@ func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (ui
261 return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)261 return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)
262 }262 }
263 263
264-func parseColor(s string) [3]uint8 {
265- var c [3]uint8
266- if len(s) != 7 || s[0] != '#' {
267- return c
268- }
269- for i := range c {
270- v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8)
271- if e != nil {
272- return [3]uint8{}
273- }
274- c[i] = uint8(v)
275- }
276- return c
277-}
278-
279-func windowColor(id ports.WindowID) [3]uint8 {
280- h := math.Mod(float64(id)*0.618034, 1) * 6
281- sector := int(h)
282- f := h - float64(sector)
283- p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f))
284- var a, b, c float64
285- switch sector {
286- case 0:
287- a, b, c = 0.8, t, p
288- case 1:
289- a, b, c = q, 0.8, p
290- case 2:
291- a, b, c = p, 0.8, t
292- case 3:
293- a, b, c = p, q, 0.8
294- case 4:
295- a, b, c = t, p, 0.8
296- default:
297- a, b, c = 0.8, p, q
298- }
299- return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
300-}
301-
302-// draws walks the scene into quads in paint order, copying new wl_shm
303-// content into GPU buffers on the way.
304-func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw {
305- // Scene rects are logical; everything below works in physical pixels.
306- scale := s.Scale
307- if scale <= 0 {
308- scale = 1
309- }
310- phys := func(v int) int { return int(math.Round(float64(v) * scale)) }
311- physRect := func(x, y, w, h int) image.Rectangle { return image.Rect(phys(x), phys(y), phys(x+w), phys(y+h)) }
312- var draws []draw
313- bounds := image.Rect(0, 0, r.width, r.height)
314- add := func(rect image.Rectangle, c [3]uint8) {
315- rect = rect.Intersect(bounds)
316- if rect.Empty() {
317- return
318- }
319- draws = append(draws, r.fillDraw(rect, c))
320- }
321- // addContent draws the part of a buffer mapped onto full (physical
322- // pixels) that dst shows. key names the surface, seq its content.
323- addContent := func(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
324- rect := dst.Intersect(bounds)
325- if rect.Empty() {
326- return
327- }
328- if content.DMABuf != nil {
329- im, err := r.importDMABuf(content.DMABuf)
330- if err != nil {
331- // The window shows its background until a buffer imports.
332- return
333- }
334- dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
335- dr.set, dr.im = im.set, im
336- dr.acquire = content.Acquire
337- draws = append(draws, dr)
338- return
339- }
340- b := content.SHM
341- pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
342- if err != nil {
343- return
344- }
345- st := shmState{w: content.Width, h: content.Height, seq: seq}
346- // Damage history is the root surface's: children copy in full.
347- var damage func(uint64) ([]ports.Rect, bool)
348- if key.index == 0 {
349- damage = content.DamageSince
350- }
351- c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
352- if err != nil {
353- return
354- }
355- dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
356- dr.set = c.set
357- dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
358- draws = append(draws, dr)
359- }
360- // drawSurface draws one surface buffer with its origin at (x, y)
361- // logical, clipped to clip (logical).
362- drawSurface := func(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64) {
363- lw, lh := content.LogicalW, content.LogicalH
364- if lw <= 0 || lh <= 0 {
365- lw, lh = content.Width, content.Height
366- }
367- if content.SHM == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
368- return
369- }
370- if content.DMABuf == nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
371- return
372- }
373- full := physRect(x, y, lw, lh)
374- // A buffer drawn at the physical size (fractional-scale clients round
375- // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
376- near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
377- if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {
378- full.Max = full.Min.Add(image.Pt(content.Width, content.Height))
379- }
380- dst := full.Intersect(physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
381- if dst.Empty() {
382- return
383- }
384- if key.index == 0 {
385- dmg.content(key.win, content, full, dst)
386- }
387- addContent(dst, full, content, key, seq)
388- }
389- // place draws a surface tree with its window geometry at (x, y)
390- // logical, clipped to w×h logical: client shadows fall outside.
391- // Every surface of the tree is keyed by its window and place in it;
392- // the tree's Seq changes with any of them.
393- place := func(id ports.WindowID, content *ports.SurfaceContent, x, y, w, h int) {
394- clip := image.Rect(x, y, x+w, y+h)
395- ox, oy := x-content.Geometry.X, y-content.Geometry.Y
396- for i := range content.Children {
397- if ch := &content.Children[i]; ch.Below {
398- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
399- }
400- }
401- drawSurface(content, ox, oy, clip, shmKey{id, 0}, content.Seq)
402- for i := range content.Children {
403- if ch := &content.Children[i]; !ch.Below {
404- drawSurface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq)
405- }
406- }
407- }
408- fullscreen := false
409- for _, w := range s.Windows {
410- fullscreen = fullscreen || (!w.Hidden && w.Fullscreen)
411- }
412- addLayers := func(afterWindows bool) {
413- for _, layer := range s.Layers {
414- if (layer.Layer >= ports.LayerTop) != afterWindows ||
415- (fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) ||
416- layer.Rect.W <= 0 || layer.Rect.H <= 0 {
417- continue
418- }
419- content := contents[layer.ID]
420- dmg.window(layer.ID, content, physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H))
421- place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
422- }
423- }
424- addLayers(false)
425- // Separators are drawn in order, active lines over inactive ones:
426- // tile lines (Window 0) over the tiles, a float's border with it.
427- addSeparators := func(id ports.WindowID) {
428- for _, sep := range s.Separators {
429- if sep.Window != id {
430- continue
431- }
432- col := s.Border.Inactive
433- if sep.Active {
434- col = s.Border.Active
435- }
436- if col != "" {
437- add(physRect(sep.Rect.X, sep.Rect.Y, sep.Rect.W, sep.Rect.H), parseColor(col))
438- }
439- }
440- }
441- tileLines := false
442- for _, w := range s.Windows {
443- if w.Floating && !tileLines {
444- addSeparators(0)
445- tileLines = true
446- }
447- if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
448- continue
449- }
450- x, y := w.Rect.X, w.Rect.Y
451- if w.Popup {
452- continue
453- }
454- // Content sits inside the border; core sized the client to match.
455- b, inset := 0, ports.Sides(0)
456- if !w.Fullscreen {
457- b, inset = s.Border.Width, w.Inset
458- }
459- c := w.Rect.Inset(inset, b)
460- cx, cy, cw, ch := c.X, c.Y, c.W, c.H
461- body := physRect(cx, cy, cw, ch)
462- content := contents[w.ID]
463- dmg.window(w.ID, content, physRect(x, y, w.Rect.W, w.Rect.H))
464- if content.Empty() {
465- add(body, windowColor(w.ID))
466- } else {
467- add(body, parseColor(s.Background))
468- place(w.ID, &content, cx, cy, cw, ch)
469- }
470- if w.Floating {
471- addSeparators(w.ID)
472- }
473- }
474- if !tileLines {
475- addSeparators(0)
476- }
477- // Popups draw only what the client drew, shadows clipped. Window popups
478- // stay with the windows; a layer's go over every layer.
479- addPopups := func(overLayers bool) {
480- for _, w := range s.Windows {
481- if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {
482- content := contents[w.ID]
483- dmg.window(w.ID, content, physRect(w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H))
484- place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
485- }
486- }
487- }
488- addPopups(false)
489- addLayers(true)
490- addPopups(true)
491- return draws
492-}
493-
494 func (r *Renderer) Pixels() *image.RGBA {264 func (r *Renderer) Pixels() *image.RGBA {
495 out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))265 out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
496 if r.mapped == nil || r.readback() != nil {266 if r.mapped == nil || r.readback() != nil {
A internal/adapters/vulkan/scene.go +316 -0
@@ -0,0 +1,316 @@
1+package vulkan
2+
3+import (
4+ "image"
5+ "math"
6+ "strconv"
7+
8+ "github.com/bnema/nefertty/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.
13+type 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+ bounds image.Rectangle
21+ fullscreen bool
22+ draws []draw
23+}
24+
25+// draws walks the scene into quads in paint order.
26+func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw {
27+ w := &sceneWalk{r: r, s: s, contents: contents, dmg: dmg, scale: s.Scale, bounds: image.Rect(0, 0, r.width, r.height)}
28+ if w.scale <= 0 {
29+ w.scale = 1
30+ }
31+ for _, win := range s.Windows {
32+ w.fullscreen = w.fullscreen || (!win.Hidden && win.Fullscreen)
33+ }
34+ w.layers(false)
35+ w.windows()
36+ w.popups(false)
37+ w.layers(true)
38+ w.popups(true)
39+ return w.draws
40+}
41+
42+func (w *sceneWalk) phys(v int) int { return int(math.Round(float64(v) * w.scale)) }
43+
44+func (w *sceneWalk) physRect(x, y, width, height int) image.Rectangle {
45+ return image.Rect(w.phys(x), w.phys(y), w.phys(x+width), w.phys(y+height))
46+}
47+
48+// fill draws a solid rect (physical pixels).
49+func (w *sceneWalk) fill(rect image.Rectangle, c [3]uint8) {
50+ rect = rect.Intersect(w.bounds)
51+ if rect.Empty() {
52+ return
53+ }
54+ w.draws = append(w.draws, w.r.fillDraw(rect, c))
55+}
56+
57+// layers draws the layer surfaces below the windows, or above them.
58+// A fullscreen window hides the bottom and top layers.
59+func (w *sceneWalk) layers(afterWindows bool) {
60+ for _, layer := range w.s.Layers {
61+ if (layer.Layer >= ports.LayerTop) != afterWindows ||
62+ (w.fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) ||
63+ layer.Rect.W <= 0 || layer.Rect.H <= 0 {
64+ continue
65+ }
66+ content := w.contents[layer.ID]
67+ w.dmg.window(layer.ID, content, w.physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H))
68+ w.place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
69+ }
70+}
71+
72+// windows draws the tiled then floating windows with their borders.
73+// Tile lines (Window 0) go over the tiles, a float's border with it.
74+func (w *sceneWalk) windows() {
75+ tileLines := false
76+ for _, win := range w.s.Windows {
77+ if win.Floating && !tileLines {
78+ w.separators(0)
79+ tileLines = true
80+ }
81+ if win.Hidden || win.Popup || win.Rect.W <= 0 || win.Rect.H <= 0 {
82+ continue
83+ }
84+ // Content sits inside the border; core sized the client to match.
85+ b, inset := 0, ports.Sides(0)
86+ if !win.Fullscreen {
87+ b, inset = w.s.Border.Width, win.Inset
88+ }
89+ c := win.Rect.Inset(inset, b)
90+ body := w.physRect(c.X, c.Y, c.W, c.H)
91+ content := w.contents[win.ID]
92+ w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))
93+ if content.Empty() {
94+ w.fill(body, windowColor(win.ID))
95+ } else {
96+ w.fill(body, parseColor(w.s.Background))
97+ w.place(win.ID, &content, c.X, c.Y, c.W, c.H)
98+ }
99+ if win.Floating {
100+ w.separators(win.ID)
101+ }
102+ }
103+ if !tileLines {
104+ w.separators(0)
105+ }
106+}
107+
108+// separators draws a window's border lines in order, active lines over
109+// inactive ones.
110+func (w *sceneWalk) separators(id ports.WindowID) {
111+ for _, sep := range w.s.Separators {
112+ if sep.Window != id {
113+ continue
114+ }
115+ col := w.s.Border.Inactive
116+ if sep.Active {
117+ col = w.s.Border.Active
118+ }
119+ if col != "" {
120+ w.fill(w.physRect(sep.Rect.X, sep.Rect.Y, sep.Rect.W, sep.Rect.H), parseColor(col))
121+ }
122+ }
123+}
124+
125+// popups draws only what the client drew, shadows clipped. Window popups
126+// stay with the windows; a layer's go over every layer.
127+func (w *sceneWalk) popups(overLayers bool) {
128+ for _, win := range w.s.Windows {
129+ if win.Popup && win.OverLayers == overLayers && !win.Hidden && win.Rect.W > 0 && win.Rect.H > 0 {
130+ content := w.contents[win.ID]
131+ w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))
132+ w.place(win.ID, &content, win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H)
133+ }
134+ }
135+}
136+
137+// place draws a surface tree with its window geometry at (x, y) logical,
138+// clipped to width×height logical: client shadows fall outside. Every
139+// surface of the tree is keyed by its window and place in it; the tree's
140+// Seq changes with any of them.
141+func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y, width, height int) {
142+ clip := image.Rect(x, y, x+width, y+height)
143+ ox, oy := x-content.Geometry.X, y-content.Geometry.Y
144+ covers := w.opaqueChildren(content, ox, oy, clip)
145+ for i := range content.Children {
146+ if ch := &content.Children[i]; ch.Below {
147+ w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq, covers)
148+ }
149+ }
150+ w.surface(content, ox, oy, clip, shmKey{id, 0}, content.Seq, covers)
151+ for i := range content.Children {
152+ if ch := &content.Children[i]; !ch.Below {
153+ w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq, nil)
154+ }
155+ }
156+}
157+
158+// opaqueChildren gives the physical rects shown by the opaque subsurfaces
159+// above the root (e.g. a game's GPU surface over its window's shm buffer):
160+// they hide the surfaces below them. A child whose buffer will not draw is
161+// left out, so the surface below it still shows: a dmabuf that fails to
162+// import (opaqueChildren pre-imports it) or an unreadable shm buffer.
163+func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, clip image.Rectangle) []image.Rectangle {
164+ var covers []image.Rectangle
165+ for i := range content.Children {
166+ ch := &content.Children[i]
167+ if ch.Below || !ch.Opaque {
168+ continue
169+ }
170+ _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
171+ if !ok || !w.drawable(&ch.SurfaceContent) {
172+ continue
173+ }
174+ covers = append(covers, dst)
175+ }
176+ return covers
177+}
178+
179+// drawable reports whether the child's buffer will draw. GPU buffer
180+// imports are cached, so pre-importing here costs nothing.
181+func (w *sceneWalk) drawable(c *ports.SurfaceContent) bool {
182+ if c.DMABuf != nil {
183+ _, err := w.r.importDMABuf(c.DMABuf)
184+ return err == nil
185+ }
186+ b := c.SHM
187+ if b == nil {
188+ return false
189+ }
190+ _, err := w.r.shmPixels(b, b.Offset+(c.Height-1)*b.Stride+c.Width*4)
191+ return err == nil
192+}
193+
194+// surfaceRects gives the physical rect of a surface buffer with its origin
195+// at (x, y) logical, and the part of it clip (logical) shows. ok is false
196+// when there is nothing to draw.
197+func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y int, clip image.Rectangle) (full, dst image.Rectangle, ok bool) {
198+ lw, lh := content.LogicalW, content.LogicalH
199+ if lw <= 0 || lh <= 0 {
200+ lw, lh = content.Width, content.Height
201+ }
202+ if content.SHM == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
203+ return full, dst, false
204+ }
205+ if content.DMABuf == nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
206+ return full, dst, false
207+ }
208+ full = w.physRect(x, y, lw, lh)
209+ // A buffer drawn at the physical size (fractional-scale clients round
210+ // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
211+ near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
212+ if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {
213+ full.Max = full.Min.Add(image.Pt(content.Width, content.Height))
214+ }
215+ dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
216+ return full, dst, !dst.Empty()
217+}
218+
219+// surface draws one surface buffer with its origin at (x, y) logical,
220+// clipped to clip (logical), unless an opaque surface above it (covers,
221+// physical) hides all of it: its buffer is then neither copied nor drawn.
222+func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle) {
223+ full, dst, ok := w.surfaceRects(content, x, y, clip)
224+ if !ok {
225+ return
226+ }
227+ for _, c := range covers {
228+ if dst.In(c) {
229+ return
230+ }
231+ }
232+ if key.index == 0 {
233+ w.dmg.content(key.win, content, full, dst)
234+ }
235+ w.content(dst, full, content, key, seq)
236+}
237+
238+// content draws the part of a buffer mapped onto full (physical pixels)
239+// that dst shows. key names the surface, seq its content.
240+func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
241+ r := w.r
242+ rect := dst.Intersect(w.bounds)
243+ if rect.Empty() {
244+ return
245+ }
246+ if content.DMABuf != nil {
247+ im, err := r.importDMABuf(content.DMABuf)
248+ if err != nil {
249+ // The window shows its background until a buffer imports.
250+ return
251+ }
252+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
253+ dr.set, dr.im = im.set, im
254+ dr.acquire = content.Acquire
255+ w.draws = append(w.draws, dr)
256+ return
257+ }
258+ b := content.SHM
259+ pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
260+ if err != nil {
261+ return
262+ }
263+ st := shmState{w: content.Width, h: content.Height, seq: seq}
264+ // Damage history is the root surface's: children copy in full.
265+ var damage func(uint64) ([]ports.Rect, bool)
266+ if key.index == 0 {
267+ damage = content.DamageSince
268+ }
269+ c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
270+ if err != nil {
271+ return
272+ }
273+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
274+ dr.set = c.set
275+ dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
276+ w.draws = append(w.draws, dr)
277+}
278+
279+func parseColor(s string) [3]uint8 {
280+ var c [3]uint8
281+ if len(s) != 7 || s[0] != '#' {
282+ return c
283+ }
284+ for i := range c {
285+ v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8)
286+ if e != nil {
287+ return [3]uint8{}
288+ }
289+ c[i] = uint8(v)
290+ }
291+ return c
292+}
293+
294+// windowColor is the placeholder color of a window with no content yet.
295+func windowColor(id ports.WindowID) [3]uint8 {
296+ h := math.Mod(float64(id)*0.618034, 1) * 6
297+ sector := int(h)
298+ f := h - float64(sector)
299+ p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f))
300+ var a, b, c float64
301+ switch sector {
302+ case 0:
303+ a, b, c = 0.8, t, p
304+ case 1:
305+ a, b, c = q, 0.8, p
306+ case 2:
307+ a, b, c = p, 0.8, t
308+ case 3:
309+ a, b, c = p, q, 0.8
310+ case 4:
311+ a, b, c = t, p, 0.8
312+ default:
313+ a, b, c = 0.8, p, q
314+ }
315+ return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
316+}
@@ -0,0 +1,316 @@
1+package vulkan
2+
3+import (
4+ "image"
5+ "math"
6+ "strconv"
7+
8+ "github.com/bnema/nefertty/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.
13+type 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+ bounds image.Rectangle
21+ fullscreen bool
22+ draws []draw
23+}
24+
25+// draws walks the scene into quads in paint order.
26+func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw {
27+ w := &sceneWalk{r: r, s: s, contents: contents, dmg: dmg, scale: s.Scale, bounds: image.Rect(0, 0, r.width, r.height)}
28+ if w.scale <= 0 {
29+ w.scale = 1
30+ }
31+ for _, win := range s.Windows {
32+ w.fullscreen = w.fullscreen || (!win.Hidden && win.Fullscreen)
33+ }
34+ w.layers(false)
35+ w.windows()
36+ w.popups(false)
37+ w.layers(true)
38+ w.popups(true)
39+ return w.draws
40+}
41+
42+func (w *sceneWalk) phys(v int) int { return int(math.Round(float64(v) * w.scale)) }
43+
44+func (w *sceneWalk) physRect(x, y, width, height int) image.Rectangle {
45+ return image.Rect(w.phys(x), w.phys(y), w.phys(x+width), w.phys(y+height))
46+}
47+
48+// fill draws a solid rect (physical pixels).
49+func (w *sceneWalk) fill(rect image.Rectangle, c [3]uint8) {
50+ rect = rect.Intersect(w.bounds)
51+ if rect.Empty() {
52+ return
53+ }
54+ w.draws = append(w.draws, w.r.fillDraw(rect, c))
55+}
56+
57+// layers draws the layer surfaces below the windows, or above them.
58+// A fullscreen window hides the bottom and top layers.
59+func (w *sceneWalk) layers(afterWindows bool) {
60+ for _, layer := range w.s.Layers {
61+ if (layer.Layer >= ports.LayerTop) != afterWindows ||
62+ (w.fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) ||
63+ layer.Rect.W <= 0 || layer.Rect.H <= 0 {
64+ continue
65+ }
66+ content := w.contents[layer.ID]
67+ w.dmg.window(layer.ID, content, w.physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H))
68+ w.place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
69+ }
70+}
71+
72+// windows draws the tiled then floating windows with their borders.
73+// Tile lines (Window 0) go over the tiles, a float's border with it.
74+func (w *sceneWalk) windows() {
75+ tileLines := false
76+ for _, win := range w.s.Windows {
77+ if win.Floating && !tileLines {
78+ w.separators(0)
79+ tileLines = true
80+ }
81+ if win.Hidden || win.Popup || win.Rect.W <= 0 || win.Rect.H <= 0 {
82+ continue
83+ }
84+ // Content sits inside the border; core sized the client to match.
85+ b, inset := 0, ports.Sides(0)
86+ if !win.Fullscreen {
87+ b, inset = w.s.Border.Width, win.Inset
88+ }
89+ c := win.Rect.Inset(inset, b)
90+ body := w.physRect(c.X, c.Y, c.W, c.H)
91+ content := w.contents[win.ID]
92+ w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))
93+ if content.Empty() {
94+ w.fill(body, windowColor(win.ID))
95+ } else {
96+ w.fill(body, parseColor(w.s.Background))
97+ w.place(win.ID, &content, c.X, c.Y, c.W, c.H)
98+ }
99+ if win.Floating {
100+ w.separators(win.ID)
101+ }
102+ }
103+ if !tileLines {
104+ w.separators(0)
105+ }
106+}
107+
108+// separators draws a window's border lines in order, active lines over
109+// inactive ones.
110+func (w *sceneWalk) separators(id ports.WindowID) {
111+ for _, sep := range w.s.Separators {
112+ if sep.Window != id {
113+ continue
114+ }
115+ col := w.s.Border.Inactive
116+ if sep.Active {
117+ col = w.s.Border.Active
118+ }
119+ if col != "" {
120+ w.fill(w.physRect(sep.Rect.X, sep.Rect.Y, sep.Rect.W, sep.Rect.H), parseColor(col))
121+ }
122+ }
123+}
124+
125+// popups draws only what the client drew, shadows clipped. Window popups
126+// stay with the windows; a layer's go over every layer.
127+func (w *sceneWalk) popups(overLayers bool) {
128+ for _, win := range w.s.Windows {
129+ if win.Popup && win.OverLayers == overLayers && !win.Hidden && win.Rect.W > 0 && win.Rect.H > 0 {
130+ content := w.contents[win.ID]
131+ w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))
132+ w.place(win.ID, &content, win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H)
133+ }
134+ }
135+}
136+
137+// place draws a surface tree with its window geometry at (x, y) logical,
138+// clipped to width×height logical: client shadows fall outside. Every
139+// surface of the tree is keyed by its window and place in it; the tree's
140+// Seq changes with any of them.
141+func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y, width, height int) {
142+ clip := image.Rect(x, y, x+width, y+height)
143+ ox, oy := x-content.Geometry.X, y-content.Geometry.Y
144+ covers := w.opaqueChildren(content, ox, oy, clip)
145+ for i := range content.Children {
146+ if ch := &content.Children[i]; ch.Below {
147+ w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq, covers)
148+ }
149+ }
150+ w.surface(content, ox, oy, clip, shmKey{id, 0}, content.Seq, covers)
151+ for i := range content.Children {
152+ if ch := &content.Children[i]; !ch.Below {
153+ w.surface(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip, shmKey{id, i + 1}, content.Seq, nil)
154+ }
155+ }
156+}
157+
158+// opaqueChildren gives the physical rects shown by the opaque subsurfaces
159+// above the root (e.g. a game's GPU surface over its window's shm buffer):
160+// they hide the surfaces below them. A child whose buffer will not draw is
161+// left out, so the surface below it still shows: a dmabuf that fails to
162+// import (opaqueChildren pre-imports it) or an unreadable shm buffer.
163+func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, clip image.Rectangle) []image.Rectangle {
164+ var covers []image.Rectangle
165+ for i := range content.Children {
166+ ch := &content.Children[i]
167+ if ch.Below || !ch.Opaque {
168+ continue
169+ }
170+ _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip)
171+ if !ok || !w.drawable(&ch.SurfaceContent) {
172+ continue
173+ }
174+ covers = append(covers, dst)
175+ }
176+ return covers
177+}
178+
179+// drawable reports whether the child's buffer will draw. GPU buffer
180+// imports are cached, so pre-importing here costs nothing.
181+func (w *sceneWalk) drawable(c *ports.SurfaceContent) bool {
182+ if c.DMABuf != nil {
183+ _, err := w.r.importDMABuf(c.DMABuf)
184+ return err == nil
185+ }
186+ b := c.SHM
187+ if b == nil {
188+ return false
189+ }
190+ _, err := w.r.shmPixels(b, b.Offset+(c.Height-1)*b.Stride+c.Width*4)
191+ return err == nil
192+}
193+
194+// surfaceRects gives the physical rect of a surface buffer with its origin
195+// at (x, y) logical, and the part of it clip (logical) shows. ok is false
196+// when there is nothing to draw.
197+func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y int, clip image.Rectangle) (full, dst image.Rectangle, ok bool) {
198+ lw, lh := content.LogicalW, content.LogicalH
199+ if lw <= 0 || lh <= 0 {
200+ lw, lh = content.Width, content.Height
201+ }
202+ if content.SHM == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
203+ return full, dst, false
204+ }
205+ if content.DMABuf == nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
206+ return full, dst, false
207+ }
208+ full = w.physRect(x, y, lw, lh)
209+ // A buffer drawn at the physical size (fractional-scale clients round
210+ // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
211+ near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
212+ if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {
213+ full.Max = full.Min.Add(image.Pt(content.Width, content.Height))
214+ }
215+ dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
216+ return full, dst, !dst.Empty()
217+}
218+
219+// surface draws one surface buffer with its origin at (x, y) logical,
220+// clipped to clip (logical), unless an opaque surface above it (covers,
221+// physical) hides all of it: its buffer is then neither copied nor drawn.
222+func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle) {
223+ full, dst, ok := w.surfaceRects(content, x, y, clip)
224+ if !ok {
225+ return
226+ }
227+ for _, c := range covers {
228+ if dst.In(c) {
229+ return
230+ }
231+ }
232+ if key.index == 0 {
233+ w.dmg.content(key.win, content, full, dst)
234+ }
235+ w.content(dst, full, content, key, seq)
236+}
237+
238+// content draws the part of a buffer mapped onto full (physical pixels)
239+// that dst shows. key names the surface, seq its content.
240+func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
241+ r := w.r
242+ rect := dst.Intersect(w.bounds)
243+ if rect.Empty() {
244+ return
245+ }
246+ if content.DMABuf != nil {
247+ im, err := r.importDMABuf(content.DMABuf)
248+ if err != nil {
249+ // The window shows its background until a buffer imports.
250+ return
251+ }
252+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
253+ dr.set, dr.im = im.set, im
254+ dr.acquire = content.Acquire
255+ w.draws = append(w.draws, dr)
256+ return
257+ }
258+ b := content.SHM
259+ pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
260+ if err != nil {
261+ return
262+ }
263+ st := shmState{w: content.Width, h: content.Height, seq: seq}
264+ // Damage history is the root surface's: children copy in full.
265+ var damage func(uint64) ([]ports.Rect, bool)
266+ if key.index == 0 {
267+ damage = content.DamageSince
268+ }
269+ c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
270+ if err != nil {
271+ return
272+ }
273+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
274+ dr.set = c.set
275+ dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
276+ w.draws = append(w.draws, dr)
277+}
278+
279+func parseColor(s string) [3]uint8 {
280+ var c [3]uint8
281+ if len(s) != 7 || s[0] != '#' {
282+ return c
283+ }
284+ for i := range c {
285+ v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8)
286+ if e != nil {
287+ return [3]uint8{}
288+ }
289+ c[i] = uint8(v)
290+ }
291+ return c
292+}
293+
294+// windowColor is the placeholder color of a window with no content yet.
295+func windowColor(id ports.WindowID) [3]uint8 {
296+ h := math.Mod(float64(id)*0.618034, 1) * 6
297+ sector := int(h)
298+ f := h - float64(sector)
299+ p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f))
300+ var a, b, c float64
301+ switch sector {
302+ case 0:
303+ a, b, c = 0.8, t, p
304+ case 1:
305+ a, b, c = q, 0.8, p
306+ case 2:
307+ a, b, c = p, 0.8, t
308+ case 3:
309+ a, b, c = p, q, 0.8
310+ case 4:
311+ a, b, c = t, p, 0.8
312+ default:
313+ a, b, c = 0.8, p, q
314+ }
315+ return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
316+}

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