detached 145db8b

perf(vulkan): stage solid fills as one strip copied down by the GPU

Window backgrounds, borders and empty-window colors were written pixel by
pixel into staging every frame: ~44 MB per 5K window, 30% CPU while a
video played. A fill now stages up to 64 rows once and one copy region per
band repeats it. Headless 5K with mpv: 30% -> 2% CPU.
commit
145db8b244ec05e3ae4ab1bd41892638ab7b44bb
author
brice · · 2026-09-25 08:14 UTC
parent
b3d7f77
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +77 -25
M internal/adapters/vulkan/renderer.go +46 -24
@@ -42,8 +42,14 @@ type Renderer struct {
4242 dmabuf ports.DMABufSupport
4343 imports map[uint64]*imported
4444 frame uint64
45+ // fillRegions is reused by Render for the bands of solid fills.
46+ fillRegions []vk.BufferImageCopy
4547 }
4648
49+// fillRows is the height of the strip staged for a solid fill: the CPU
50+// writes it once, the GPU copies it down the rect.
51+const fillRows = 64
52+
4753 func checked(name string, result vk.Result) error {
4854 if err := vk.Check(result); err != nil {
4955 return fmt.Errorf("%s: %w", name, err)
@@ -360,6 +366,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
360366 opaque bool
361367 // dma is a client GPU buffer, blitted instead of copied from staging.
362368 dma *imported
369+ // fill is a solid color: staging holds one strip of rows.
370+ fill bool
363371 }
364372 var uploads []upload
365373 used := 0
@@ -369,8 +377,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
369377 if rect.Empty() {
370378 return
371379 }
372- uploads = append(uploads, upload{rect: rect, offset: used, color: c})
373- used += rect.Dx() * rect.Dy() * 4
380+ // A fill stages a strip of up to fillRows rows; the GPU copies the
381+ // strip down the rect.
382+ uploads = append(uploads, upload{rect: rect, offset: used, color: c, fill: true})
383+ used += rect.Dx() * min(rect.Dy(), fillRows) * 4
374384 }
375385 // addContent maps src (buffer pixels) onto dst (physical pixels).
376386 addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) {
@@ -498,31 +508,32 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
498508 if u.dma != nil {
499509 continue
500510 }
511+ if u.fill {
512+ line := data[u.offset : u.offset+u.rect.Dx()*min(u.rect.Dy(), fillRows)*4]
513+ for i := 0; i < len(line); i += 4 {
514+ line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255
515+ }
516+ continue
517+ }
501518 dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
502519 for row := 0; row < u.rect.Dy(); row++ {
503520 line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
504- if u.pixels != nil {
505- y := u.rect.Min.Y + row
506- if u.dst.Size() == u.src.Size() {
507- start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
508- copy(line, u.pixels[start:start+len(line)])
509- } else {
510- // Nearest-neighbour resample, sampling pixel centres.
511- sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
512- src := u.pixels[sy*u.stride:]
513- for i := 0; i < u.rect.Dx(); i++ {
514- sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
515- copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
516- }
517- }
518- if u.opaque {
519- for i := 3; i < len(line); i += 4 {
520- line[i] = 255
521- }
522- }
521+ y := u.rect.Min.Y + row
522+ if u.dst.Size() == u.src.Size() {
523+ start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
524+ copy(line, u.pixels[start:start+len(line)])
523525 } else {
524- for i := 0; i < len(line); i += 4 {
525- line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255
526+ // Nearest-neighbour resample, sampling pixel centres.
527+ sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
528+ src := u.pixels[sy*u.stride:]
529+ for i := 0; i < u.rect.Dx(); i++ {
530+ sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
531+ copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
532+ }
533+ }
534+ if u.opaque {
535+ for i := 3; i < len(line); i += 4 {
536+ line[i] = 255
526537 }
527538 }
528539 }
@@ -599,7 +610,18 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
599610 d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, r.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
600611 continue
601612 }
602- region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
613+ layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
614+ if f.fill {
615+ // Every band reads the same staged strip.
616+ r.fillRegions = r.fillRegions[:0]
617+ for y := rect.Min.Y; y < rect.Max.Y; y += fillRows {
618+ h := min(fillRows, rect.Max.Y-y)
619+ r.fillRegions = append(r.fillRegions, vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(h), Depth: 1}})
620+ }
621+ d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0])
622+ continue
623+ }
624+ region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
603625 d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
604626 }
605627 ownership(false)
@@ -42,8 +42,14 @@ type Renderer struct {
42 dmabuf ports.DMABufSupport42 dmabuf ports.DMABufSupport
43 imports map[uint64]*imported43 imports map[uint64]*imported
44 frame uint6444 frame uint64
45+ // fillRegions is reused by Render for the bands of solid fills.
46+ fillRegions []vk.BufferImageCopy
45 }47 }
46 48
49+// fillRows is the height of the strip staged for a solid fill: the CPU
50+// writes it once, the GPU copies it down the rect.
51+const fillRows = 64
52+
47 func checked(name string, result vk.Result) error {53 func checked(name string, result vk.Result) error {
48 if err := vk.Check(result); err != nil {54 if err := vk.Check(result); err != nil {
49 return fmt.Errorf("%s: %w", name, err)55 return fmt.Errorf("%s: %w", name, err)
@@ -360,6 +366,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
360 opaque bool366 opaque bool
361 // dma is a client GPU buffer, blitted instead of copied from staging.367 // dma is a client GPU buffer, blitted instead of copied from staging.
362 dma *imported368 dma *imported
369+ // fill is a solid color: staging holds one strip of rows.
370+ fill bool
363 }371 }
364 var uploads []upload372 var uploads []upload
365 used := 0373 used := 0
@@ -369,8 +377,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
369 if rect.Empty() {377 if rect.Empty() {
370 return378 return
371 }379 }
372- uploads = append(uploads, upload{rect: rect, offset: used, color: c})380+ // A fill stages a strip of up to fillRows rows; the GPU copies the
373- used += rect.Dx() * rect.Dy() * 4381+ // strip down the rect.
382+ uploads = append(uploads, upload{rect: rect, offset: used, color: c, fill: true})
383+ used += rect.Dx() * min(rect.Dy(), fillRows) * 4
374 }384 }
375 // addContent maps src (buffer pixels) onto dst (physical pixels).385 // addContent maps src (buffer pixels) onto dst (physical pixels).
376 addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) {386 addContent := func(dst, src image.Rectangle, content *ports.SurfaceContent) {
@@ -498,31 +508,32 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
498 if u.dma != nil {508 if u.dma != nil {
499 continue509 continue
500 }510 }
511+ if u.fill {
512+ line := data[u.offset : u.offset+u.rect.Dx()*min(u.rect.Dy(), fillRows)*4]
513+ for i := 0; i < len(line); i += 4 {
514+ line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255
515+ }
516+ continue
517+ }
501 dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]518 dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
502 for row := 0; row < u.rect.Dy(); row++ {519 for row := 0; row < u.rect.Dy(); row++ {
503 line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]520 line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
504- if u.pixels != nil {521+ y := u.rect.Min.Y + row
505- y := u.rect.Min.Y + row522+ if u.dst.Size() == u.src.Size() {
506- if u.dst.Size() == u.src.Size() {523+ start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4
507- start := (u.src.Min.Y+y-u.dst.Min.Y)*u.stride + (u.src.Min.X+u.rect.Min.X-u.dst.Min.X)*4524+ copy(line, u.pixels[start:start+len(line)])
508- copy(line, u.pixels[start:start+len(line)])
509- } else {
510- // Nearest-neighbour resample, sampling pixel centres.
511- sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
512- src := u.pixels[sy*u.stride:]
513- for i := 0; i < u.rect.Dx(); i++ {
514- sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
515- copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
516- }
517- }
518- if u.opaque {
519- for i := 3; i < len(line); i += 4 {
520- line[i] = 255
521- }
522- }
523 } else {525 } else {
524- for i := 0; i < len(line); i += 4 {526+ // Nearest-neighbour resample, sampling pixel centres.
525- line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255527+ sy := u.src.Min.Y + ((y-u.dst.Min.Y)*2+1)*u.src.Dy()/(u.dst.Dy()*2)
528+ src := u.pixels[sy*u.stride:]
529+ for i := 0; i < u.rect.Dx(); i++ {
530+ sx := u.src.Min.X + ((u.rect.Min.X+i-u.dst.Min.X)*2+1)*u.src.Dx()/(u.dst.Dx()*2)
531+ copy(line[i*4:i*4+4], src[sx*4:sx*4+4])
532+ }
533+ }
534+ if u.opaque {
535+ for i := 3; i < len(line); i += 4 {
536+ line[i] = 255
526 }537 }
527 }538 }
528 }539 }
@@ -599,7 +610,18 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
599 d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, r.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)610 d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, r.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
600 continue611 continue
601 }612 }
602- region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}613+ layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
614+ if f.fill {
615+ // Every band reads the same staged strip.
616+ r.fillRegions = r.fillRegions[:0]
617+ for y := rect.Min.Y; y < rect.Max.Y; y += fillRows {
618+ h := min(fillRows, rect.Max.Y-y)
619+ r.fillRegions = append(r.fillRegions, vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(h), Depth: 1}})
620+ }
621+ d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, uint32(len(r.fillRegions)), &r.fillRegions[0])
622+ continue
623+ }
624+ region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: layers, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
603 d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)625 d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
604 }626 }
605 ownership(false)627 ownership(false)
M internal/adapters/vulkan/renderer_test.go +31 -1
@@ -58,7 +58,7 @@ func TestRendererRender(t *testing.T) {
5858 c := windowColor(1)
5959 wc := color.RGBA{c[0], c[1], c[2], 255}
6060 s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}}
61- check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: {0x31, 0x32, 0x44, 255}, {10, 10}: wc, {16, 16}: wc})
61+ check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: {0x31, 0x32, 0x44, 255}, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 21}: {0x31, 0x32, 0x44, 255}, {21, 22}: {0x31, 0x32, 0x44, 255}, {23, 23}: {0x31, 0x32, 0x44, 255}, {24, 24}: bg})
6262 s.Windows[0].Focused = true
6363 check(s, map[image.Point]color.RGBA{{9, 9}: {0xb4, 0xbe, 0xfe, 255}, {10, 10}: wc, {16, 16}: wc})
6464 s.Windows[0].Fullscreen = true
@@ -303,3 +303,33 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
303303 t.Errorf("child only: %v", got)
304304 }
305305 }
306+
307+// A fill taller than one staged strip is copied down in bands: every row
308+// through the last one gets the color.
309+func TestRendererTallFill(t *testing.T) {
310+ h := fillRows*2 + 7
311+ r, err := New(8, h)
312+ if err != nil {
313+ t.Skipf("Vulkan unavailable: %v", err)
314+ }
315+ defer r.Close()
316+ scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
317+ if err := r.Render(scene, nil); err != nil {
318+ t.Fatal(err)
319+ }
320+ c := windowColor(1)
321+ wc := color.RGBA{c[0], c[1], c[2], 255}
322+ bg := color.RGBA{16, 32, 48, 255}
323+ px := r.Pixels()
324+ for y := 0; y < h; y++ {
325+ if got := px.At(1, y); got != wc {
326+ t.Fatalf("At(1,%d)=%v want %v", y, got, wc)
327+ }
328+ if got := px.At(6, y); got != wc {
329+ t.Fatalf("At(6,%d)=%v want %v", y, got, wc)
330+ }
331+ if got := px.At(7, y); got != bg {
332+ t.Fatalf("At(7,%d)=%v want background", y, got)
333+ }
334+ }
335+}
@@ -58,7 +58,7 @@ func TestRendererRender(t *testing.T) {
58 c := windowColor(1)58 c := windowColor(1)
59 wc := color.RGBA{c[0], c[1], c[2], 255}59 wc := color.RGBA{c[0], c[1], c[2], 255}
60 s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}}60 s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}}
61- check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: {0x31, 0x32, 0x44, 255}, {10, 10}: wc, {16, 16}: wc})61+ check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: {0x31, 0x32, 0x44, 255}, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 21}: {0x31, 0x32, 0x44, 255}, {21, 22}: {0x31, 0x32, 0x44, 255}, {23, 23}: {0x31, 0x32, 0x44, 255}, {24, 24}: bg})
62 s.Windows[0].Focused = true62 s.Windows[0].Focused = true
63 check(s, map[image.Point]color.RGBA{{9, 9}: {0xb4, 0xbe, 0xfe, 255}, {10, 10}: wc, {16, 16}: wc})63 check(s, map[image.Point]color.RGBA{{9, 9}: {0xb4, 0xbe, 0xfe, 255}, {10, 10}: wc, {16, 16}: wc})
64 s.Windows[0].Fullscreen = true64 s.Windows[0].Fullscreen = true
@@ -303,3 +303,33 @@ func TestRendererGeometryAndSubsurfaces(t *testing.T) {
303 t.Errorf("child only: %v", got)303 t.Errorf("child only: %v", got)
304 }304 }
305 }305 }
306+
307+// A fill taller than one staged strip is copied down in bands: every row
308+// through the last one gets the color.
309+func TestRendererTallFill(t *testing.T) {
310+ h := fillRows*2 + 7
311+ r, err := New(8, h)
312+ if err != nil {
313+ t.Skipf("Vulkan unavailable: %v", err)
314+ }
315+ defer r.Close()
316+ scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
317+ if err := r.Render(scene, nil); err != nil {
318+ t.Fatal(err)
319+ }
320+ c := windowColor(1)
321+ wc := color.RGBA{c[0], c[1], c[2], 255}
322+ bg := color.RGBA{16, 32, 48, 255}
323+ px := r.Pixels()
324+ for y := 0; y < h; y++ {
325+ if got := px.At(1, y); got != wc {
326+ t.Fatalf("At(1,%d)=%v want %v", y, got, wc)
327+ }
328+ if got := px.At(6, y); got != wc {
329+ t.Fatalf("At(6,%d)=%v want %v", y, got, wc)
330+ }
331+ if got := px.At(7, y); got != bg {
332+ t.Fatalf("At(7,%d)=%v want background", y, got)
333+ }
334+ }
335+}

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