detached 693b8c9

perf(vulkan): skip copying surfaces hidden by an opaque subsurface and split scene walk into scene.go

commit
693b8c96c59016c723651847bae1ff4b45a1bdbe
author
brice · · 2026-09-26 15:31 UTC
parent
f176c04
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +335 -230
M internal/adapters/vulkan/compose_test.go +38 -0
@@ -138,6 +138,44 @@ 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+
141179 // render draws a frame and closes its fence.
142180 func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
143181 done, err := r.Render(s, contents)
@@ -138,6 +138,44 @@ 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+
141 // render draws a frame and closes its fence.179 // render draws a frame and closes its fence.
142 func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {180 func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
143 done, err := r.Render(s, contents)181 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 +297 -0
@@ -0,0 +1,297 @@
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.
161+func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, clip image.Rectangle) []image.Rectangle {
162+ var covers []image.Rectangle
163+ for i := range content.Children {
164+ ch := &content.Children[i]
165+ if ch.Below || !ch.Opaque {
166+ continue
167+ }
168+ if _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip); ok {
169+ covers = append(covers, dst)
170+ }
171+ }
172+ return covers
173+}
174+
175+// surfaceRects gives the physical rect of a surface buffer with its origin
176+// at (x, y) logical, and the part of it clip (logical) shows. ok is false
177+// when there is nothing to draw.
178+func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y int, clip image.Rectangle) (full, dst image.Rectangle, ok bool) {
179+ lw, lh := content.LogicalW, content.LogicalH
180+ if lw <= 0 || lh <= 0 {
181+ lw, lh = content.Width, content.Height
182+ }
183+ if content.SHM == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
184+ return full, dst, false
185+ }
186+ if content.DMABuf == nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
187+ return full, dst, false
188+ }
189+ full = w.physRect(x, y, lw, lh)
190+ // A buffer drawn at the physical size (fractional-scale clients round
191+ // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
192+ near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
193+ if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {
194+ full.Max = full.Min.Add(image.Pt(content.Width, content.Height))
195+ }
196+ dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
197+ return full, dst, !dst.Empty()
198+}
199+
200+// surface draws one surface buffer with its origin at (x, y) logical,
201+// clipped to clip (logical), unless an opaque surface above it (covers,
202+// physical) hides all of it: its buffer is then neither copied nor drawn.
203+func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle) {
204+ full, dst, ok := w.surfaceRects(content, x, y, clip)
205+ if !ok {
206+ return
207+ }
208+ for _, c := range covers {
209+ if dst.In(c) {
210+ return
211+ }
212+ }
213+ if key.index == 0 {
214+ w.dmg.content(key.win, content, full, dst)
215+ }
216+ w.content(dst, full, content, key, seq)
217+}
218+
219+// content draws the part of a buffer mapped onto full (physical pixels)
220+// that dst shows. key names the surface, seq its content.
221+func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
222+ r := w.r
223+ rect := dst.Intersect(w.bounds)
224+ if rect.Empty() {
225+ return
226+ }
227+ if content.DMABuf != nil {
228+ im, err := r.importDMABuf(content.DMABuf)
229+ if err != nil {
230+ // The window shows its background until a buffer imports.
231+ return
232+ }
233+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
234+ dr.set, dr.im = im.set, im
235+ dr.acquire = content.Acquire
236+ w.draws = append(w.draws, dr)
237+ return
238+ }
239+ b := content.SHM
240+ pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
241+ if err != nil {
242+ return
243+ }
244+ st := shmState{w: content.Width, h: content.Height, seq: seq}
245+ // Damage history is the root surface's: children copy in full.
246+ var damage func(uint64) ([]ports.Rect, bool)
247+ if key.index == 0 {
248+ damage = content.DamageSince
249+ }
250+ c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
251+ if err != nil {
252+ return
253+ }
254+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
255+ dr.set = c.set
256+ dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
257+ w.draws = append(w.draws, dr)
258+}
259+
260+func parseColor(s string) [3]uint8 {
261+ var c [3]uint8
262+ if len(s) != 7 || s[0] != '#' {
263+ return c
264+ }
265+ for i := range c {
266+ v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8)
267+ if e != nil {
268+ return [3]uint8{}
269+ }
270+ c[i] = uint8(v)
271+ }
272+ return c
273+}
274+
275+// windowColor is the placeholder color of a window with no content yet.
276+func windowColor(id ports.WindowID) [3]uint8 {
277+ h := math.Mod(float64(id)*0.618034, 1) * 6
278+ sector := int(h)
279+ f := h - float64(sector)
280+ p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f))
281+ var a, b, c float64
282+ switch sector {
283+ case 0:
284+ a, b, c = 0.8, t, p
285+ case 1:
286+ a, b, c = q, 0.8, p
287+ case 2:
288+ a, b, c = p, 0.8, t
289+ case 3:
290+ a, b, c = p, q, 0.8
291+ case 4:
292+ a, b, c = t, p, 0.8
293+ default:
294+ a, b, c = 0.8, p, q
295+ }
296+ return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
297+}
@@ -0,0 +1,297 @@
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.
161+func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy int, clip image.Rectangle) []image.Rectangle {
162+ var covers []image.Rectangle
163+ for i := range content.Children {
164+ ch := &content.Children[i]
165+ if ch.Below || !ch.Opaque {
166+ continue
167+ }
168+ if _, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+ch.X, oy+ch.Y, clip); ok {
169+ covers = append(covers, dst)
170+ }
171+ }
172+ return covers
173+}
174+
175+// surfaceRects gives the physical rect of a surface buffer with its origin
176+// at (x, y) logical, and the part of it clip (logical) shows. ok is false
177+// when there is nothing to draw.
178+func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y int, clip image.Rectangle) (full, dst image.Rectangle, ok bool) {
179+ lw, lh := content.LogicalW, content.LogicalH
180+ if lw <= 0 || lh <= 0 {
181+ lw, lh = content.Width, content.Height
182+ }
183+ if content.SHM == nil && content.DMABuf == nil || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
184+ return full, dst, false
185+ }
186+ if content.DMABuf == nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
187+ return full, dst, false
188+ }
189+ full = w.physRect(x, y, lw, lh)
190+ // A buffer drawn at the physical size (fractional-scale clients round
191+ // w*scale, we round per edge) is copied 1:1, never resampled for 1px.
192+ near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
193+ if near(full.Dx(), content.Width) && near(full.Dy(), content.Height) {
194+ full.Max = full.Min.Add(image.Pt(content.Width, content.Height))
195+ }
196+ dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy()))
197+ return full, dst, !dst.Empty()
198+}
199+
200+// surface draws one surface buffer with its origin at (x, y) logical,
201+// clipped to clip (logical), unless an opaque surface above it (covers,
202+// physical) hides all of it: its buffer is then neither copied nor drawn.
203+func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y int, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle) {
204+ full, dst, ok := w.surfaceRects(content, x, y, clip)
205+ if !ok {
206+ return
207+ }
208+ for _, c := range covers {
209+ if dst.In(c) {
210+ return
211+ }
212+ }
213+ if key.index == 0 {
214+ w.dmg.content(key.win, content, full, dst)
215+ }
216+ w.content(dst, full, content, key, seq)
217+}
218+
219+// content draws the part of a buffer mapped onto full (physical pixels)
220+// that dst shows. key names the surface, seq its content.
221+func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64) {
222+ r := w.r
223+ rect := dst.Intersect(w.bounds)
224+ if rect.Empty() {
225+ return
226+ }
227+ if content.DMABuf != nil {
228+ im, err := r.importDMABuf(content.DMABuf)
229+ if err != nil {
230+ // The window shows its background until a buffer imports.
231+ return
232+ }
233+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
234+ dr.set, dr.im = im.set, im
235+ dr.acquire = content.Acquire
236+ w.draws = append(w.draws, dr)
237+ return
238+ }
239+ b := content.SHM
240+ pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
241+ if err != nil {
242+ return
243+ }
244+ st := shmState{w: content.Width, h: content.Height, seq: seq}
245+ // Damage history is the root surface's: children copy in full.
246+ var damage func(uint64) ([]ports.Rect, bool)
247+ if key.index == 0 {
248+ damage = content.DamageSince
249+ }
250+ c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage)
251+ if err != nil {
252+ return
253+ }
254+ dr := r.contentDraw(rect, full, content.Width, content.Height, modeBuffer, content.Opaque)
255+ dr.set = c.set
256+ dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
257+ w.draws = append(w.draws, dr)
258+}
259+
260+func parseColor(s string) [3]uint8 {
261+ var c [3]uint8
262+ if len(s) != 7 || s[0] != '#' {
263+ return c
264+ }
265+ for i := range c {
266+ v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8)
267+ if e != nil {
268+ return [3]uint8{}
269+ }
270+ c[i] = uint8(v)
271+ }
272+ return c
273+}
274+
275+// windowColor is the placeholder color of a window with no content yet.
276+func windowColor(id ports.WindowID) [3]uint8 {
277+ h := math.Mod(float64(id)*0.618034, 1) * 6
278+ sector := int(h)
279+ f := h - float64(sector)
280+ p, q, t := 0.4, 0.8*(1-0.5*f), 0.8*(1-0.5*(1-f))
281+ var a, b, c float64
282+ switch sector {
283+ case 0:
284+ a, b, c = 0.8, t, p
285+ case 1:
286+ a, b, c = q, 0.8, p
287+ case 2:
288+ a, b, c = p, 0.8, t
289+ case 3:
290+ a, b, c = p, q, 0.8
291+ case 4:
292+ a, b, c = t, p, 0.8
293+ default:
294+ a, b, c = 0.8, p, q
295+ }
296+ return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
297+}

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 23

Tags 10

↑↓ moveenter openesc close