detached 8b2b354

perf(drm): read frames from host-cached memory and copy BGRX rows directly

commit
8b2b354a69ac9bce79742c6ee6edb7e50788241f
author
brice · · 2026-09-24 08:35 UTC
parent
25625c3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +25 -35
M internal/adapters/drm/kms.go +0 -13
@@ -5,7 +5,6 @@ import (
55 "bytes"
66 "errors"
77 "fmt"
8- "image"
98 "unsafe"
109
1110 "golang.org/x/sys/unix"
@@ -247,18 +246,6 @@ func (b *dumbBuffer) destroy(fd int) {
247246 _ = ioctl(fd, ioctlDestroyDumb, unsafe.Pointer(&h))
248247 }
249248
250-// copyXRGB writes RGBA pixels into an XRGB8888 (little-endian BGRX) buffer.
251-func copyXRGB(dst []byte, pitch int, img *image.RGBA) {
252- w, h := img.Rect.Dx(), img.Rect.Dy()
253- for y := 0; y < h; y++ {
254- src := img.Pix[y*img.Stride : y*img.Stride+w*4]
255- row := dst[y*pitch : y*pitch+w*4]
256- for x := 0; x < w*4; x += 4 {
257- row[x], row[x+1], row[x+2], row[x+3] = src[x+2], src[x+1], src[x], 0xff
258- }
259- }
260-}
261-
262249 func setCrtc(fd int, crtc, conn, fb uint32, mode *modeInfo) error {
263250 ids := []uint32{conn}
264251 c := modeCrtc{crtcID: crtc, fbID: fb, countConnectors: 1, setConnectors: unsafe.Pointer(&ids[0])}
@@ -5,7 +5,6 @@ import (
5 "bytes"5 "bytes"
6 "errors"6 "errors"
7 "fmt"7 "fmt"
8- "image"
9 "unsafe"8 "unsafe"
10 9
11 "golang.org/x/sys/unix"10 "golang.org/x/sys/unix"
@@ -247,18 +246,6 @@ func (b *dumbBuffer) destroy(fd int) {
247 _ = ioctl(fd, ioctlDestroyDumb, unsafe.Pointer(&h))246 _ = ioctl(fd, ioctlDestroyDumb, unsafe.Pointer(&h))
248 }247 }
249 248
250-// copyXRGB writes RGBA pixels into an XRGB8888 (little-endian BGRX) buffer.
251-func copyXRGB(dst []byte, pitch int, img *image.RGBA) {
252- w, h := img.Rect.Dx(), img.Rect.Dy()
253- for y := 0; y < h; y++ {
254- src := img.Pix[y*img.Stride : y*img.Stride+w*4]
255- row := dst[y*pitch : y*pitch+w*4]
256- for x := 0; x < w*4; x += 4 {
257- row[x], row[x+1], row[x+2], row[x+3] = src[x+2], src[x+1], src[x], 0xff
258- }
259- }
260-}
261-
262 func setCrtc(fd int, crtc, conn, fb uint32, mode *modeInfo) error {249 func setCrtc(fd int, crtc, conn, fb uint32, mode *modeInfo) error {
263 ids := []uint32{conn}250 ids := []uint32{conn}
264 c := modeCrtc{crtcID: crtc, fbID: fb, countConnectors: 1, setConnectors: unsafe.Pointer(&ids[0])}251 c := modeCrtc{crtcID: crtc, fbID: fb, countConnectors: 1, setConnectors: unsafe.Pointer(&ids[0])}
M internal/adapters/drm/kms_test.go +0 -15
@@ -2,7 +2,6 @@ package drm
22
33 import (
44 "encoding/binary"
5- "image"
65 "testing"
76 "unsafe"
87 )
@@ -53,20 +52,6 @@ func TestPickConnector(t *testing.T) {
5352 }
5453 }
5554
56-func TestCopyXRGB(t *testing.T) {
57- img := image.NewRGBA(image.Rect(0, 0, 2, 2))
58- copy(img.Pix, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})
59- pitch := 12
60- dst := make([]byte, pitch*2)
61- copyXRGB(dst, pitch, img)
62- want := []byte{3, 2, 1, 255, 7, 6, 5, 255, 0, 0, 0, 0, 11, 10, 9, 255, 15, 14, 13, 255, 0, 0, 0, 0}
63- for i := range want {
64- if dst[i] != want[i] {
65- t.Fatalf("byte %d = %d, want %d (%v)", i, dst[i], want[i], dst)
66- }
67- }
68-}
69-
7055 func TestCountFlipEvents(t *testing.T) {
7156 ev := func(typ uint32, n int) []byte {
7257 b := make([]byte, n)
@@ -2,7 +2,6 @@ package drm
2 2
3 import (3 import (
4 "encoding/binary"4 "encoding/binary"
5- "image"
6 "testing"5 "testing"
7 "unsafe"6 "unsafe"
8 )7 )
@@ -53,20 +52,6 @@ func TestPickConnector(t *testing.T) {
53 }52 }
54 }53 }
55 54
56-func TestCopyXRGB(t *testing.T) {
57- img := image.NewRGBA(image.Rect(0, 0, 2, 2))
58- copy(img.Pix, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})
59- pitch := 12
60- dst := make([]byte, pitch*2)
61- copyXRGB(dst, pitch, img)
62- want := []byte{3, 2, 1, 255, 7, 6, 5, 255, 0, 0, 0, 0, 11, 10, 9, 255, 15, 14, 13, 255, 0, 0, 0, 0}
63- for i := range want {
64- if dst[i] != want[i] {
65- t.Fatalf("byte %d = %d, want %d (%v)", i, dst[i], want[i], dst)
66- }
67- }
68-}
69-
70 func TestCountFlipEvents(t *testing.T) {55 func TestCountFlipEvents(t *testing.T) {
71 ev := func(typ uint32, n int) []byte {56 ev := func(typ uint32, n int) []byte {
72 b := make([]byte, n)57 b := make([]byte, n)
M internal/adapters/drm/output.go +7 -6
@@ -4,7 +4,6 @@ import (
44 "context"
55 "errors"
66 "fmt"
7- "image"
87 "sync"
98 "time"
109
@@ -16,7 +15,8 @@ import (
1615 // Renderer draws a scene into an RGBA image of the output size.
1716 type Renderer interface {
1817 Render(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error
19- Pixels() *image.RGBA
18+ // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch.
19+ CopyBGRX(dst []byte, pitch int)
2020 Close()
2121 }
2222
@@ -88,9 +88,9 @@ func (o *Output) modeset() error {
8888 }
8989
9090 // present copies img into the back buffer and queues a page flip.
91-func (o *Output) present(img *image.RGBA) error {
91+func (o *Output) present(r Renderer) error {
9292 b := o.bufs[o.back]
93- copyXRGB(b.mem, int(b.pitch), img)
93+ r.CopyBGRX(b.mem, int(b.pitch))
9494 if err := flip(o.fd, o.crtc, b.fbID); err != nil {
9595 return err
9696 }
@@ -183,7 +183,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer,
183183 if err := r.Render(scene, surfaces); err != nil {
184184 return fmt.Errorf("render frame: %w", err)
185185 }
186- if err := o.present(r.Pixels()); err != nil {
186+ copyStart := time.Now()
187+ if err := o.present(r); err != nil {
187188 if errors.Is(err, unix.EBUSY) {
188189 // A flip is still in flight; retry after its event.
189190 o.pending, o.flipStart = true, time.Now()
@@ -200,7 +201,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer,
200201 }
201202 dirty = false
202203 frame++
203- o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("render", time.Since(start)).Msg("frame")
204+ o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("render", time.Since(start)).Dur("copy", time.Since(copyStart)).Msg("frame")
204205 }
205206 }
206207
@@ -4,7 +4,6 @@ import (
4 "context"4 "context"
5 "errors"5 "errors"
6 "fmt"6 "fmt"
7- "image"
8 "sync"7 "sync"
9 "time"8 "time"
10 9
@@ -16,7 +15,8 @@ import (
16 // Renderer draws a scene into an RGBA image of the output size.15 // Renderer draws a scene into an RGBA image of the output size.
17 type Renderer interface {16 type Renderer interface {
18 Render(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error17 Render(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error
19- Pixels() *image.RGBA18+ // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch.
19+ CopyBGRX(dst []byte, pitch int)
20 Close()20 Close()
21 }21 }
22 22
@@ -88,9 +88,9 @@ func (o *Output) modeset() error {
88 }88 }
89 89
90 // present copies img into the back buffer and queues a page flip.90 // present copies img into the back buffer and queues a page flip.
91-func (o *Output) present(img *image.RGBA) error {91+func (o *Output) present(r Renderer) error {
92 b := o.bufs[o.back]92 b := o.bufs[o.back]
93- copyXRGB(b.mem, int(b.pitch), img)93+ r.CopyBGRX(b.mem, int(b.pitch))
94 if err := flip(o.fd, o.crtc, b.fbID); err != nil {94 if err := flip(o.fd, o.crtc, b.fbID); err != nil {
95 return err95 return err
96 }96 }
@@ -183,7 +183,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer,
183 if err := r.Render(scene, surfaces); err != nil {183 if err := r.Render(scene, surfaces); err != nil {
184 return fmt.Errorf("render frame: %w", err)184 return fmt.Errorf("render frame: %w", err)
185 }185 }
186- if err := o.present(r.Pixels()); err != nil {186+ copyStart := time.Now()
187+ if err := o.present(r); err != nil {
187 if errors.Is(err, unix.EBUSY) {188 if errors.Is(err, unix.EBUSY) {
188 // A flip is still in flight; retry after its event.189 // A flip is still in flight; retry after its event.
189 o.pending, o.flipStart = true, time.Now()190 o.pending, o.flipStart = true, time.Now()
@@ -200,7 +201,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer,
200 }201 }
201 dirty = false202 dirty = false
202 frame++203 frame++
203- o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("render", time.Since(start)).Msg("frame")204+ o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("render", time.Since(start)).Dur("copy", time.Since(copyStart)).Msg("frame")
204 }205 }
205 }206 }
206 207
M internal/adapters/vulkan/renderer.go +18 -1
@@ -161,7 +161,11 @@ func New(width, height int) (r *Renderer, err error) {
161161 return
162162 }
163163 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)
164- kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
164+ // The CPU reads this buffer every frame: uncached memory makes that ~100x slower.
165+ kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)
166+ if err != nil {
167+ kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
168+ }
165169 if err != nil {
166170 return
167171 }
@@ -471,6 +475,19 @@ func (r *Renderer) Pixels() *image.RGBA {
471475 return out
472476 }
473477
478+// CopyBGRX writes the last frame into an XRGB8888 buffer with the given pitch.
479+// The image is B8G8R8A8, which is already XRGB8888 in memory.
480+func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
481+ if r.mapped == nil {
482+ return
483+ }
484+ row := r.width * 4
485+ src := unsafe.Slice((*byte)(r.mapped), row*r.height)
486+ for y := 0; y < r.height; y++ {
487+ copy(dst[y*pitch:y*pitch+row], src[y*row:(y+1)*row])
488+ }
489+}
490+
474491 func (r *Renderer) Close() {
475492 if r == nil {
476493 return
@@ -161,7 +161,11 @@ func New(width, height int) (r *Renderer, err error) {
161 return161 return
162 }162 }
163 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)163 r.dd.GetBufferMemoryRequirements(r.device, r.buffer, &req)
164- kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)164+ // The CPU reads this buffer every frame: uncached memory makes that ~100x slower.
165+ kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)
166+ if err != nil {
167+ kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
168+ }
165 if err != nil {169 if err != nil {
166 return170 return
167 }171 }
@@ -471,6 +475,19 @@ func (r *Renderer) Pixels() *image.RGBA {
471 return out475 return out
472 }476 }
473 477
478+// CopyBGRX writes the last frame into an XRGB8888 buffer with the given pitch.
479+// The image is B8G8R8A8, which is already XRGB8888 in memory.
480+func (r *Renderer) CopyBGRX(dst []byte, pitch int) {
481+ if r.mapped == nil {
482+ return
483+ }
484+ row := r.width * 4
485+ src := unsafe.Slice((*byte)(r.mapped), row*r.height)
486+ for y := 0; y < r.height; y++ {
487+ copy(dst[y*pitch:y*pitch+row], src[y*row:(y+1)*row])
488+ }
489+}
490+
474 func (r *Renderer) Close() {491 func (r *Renderer) Close() {
475 if r == nil {492 if r == nil {
476 return493 return

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