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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +25 -35
M internal/adapters/drm/kms.go +0 -13
| @@ -5,7 +5,6 @@ import ( | |||
| 5 | 5 | "bytes" | |
| 6 | 6 | "errors" | |
| 7 | 7 | "fmt" | |
| 8 | - | "image" | |
| 9 | 8 | "unsafe" | |
| 10 | 9 | ||
| 11 | 10 | "golang.org/x/sys/unix" | |
| @@ -247,18 +246,6 @@ func (b *dumbBuffer) destroy(fd int) { | |||
| 247 | 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 | 249 | func setCrtc(fd int, crtc, conn, fb uint32, mode *modeInfo) error { | |
| 263 | 250 | ids := []uint32{conn} | |
| 264 | 251 | 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/binary" | |
| 5 | - | "image" | |
| 6 | 5 | "testing" | |
| 7 | 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 | 55 | func TestCountFlipEvents(t *testing.T) { | |
| 71 | 56 | ev := func(typ uint32, n int) []byte { | |
| 72 | 57 | 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 ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | - | "image" | |
| 8 | 7 | "sync" | |
| 9 | 8 | "time" | |
| 10 | 9 | ||
| @@ -16,7 +15,8 @@ import ( | |||
| 16 | 15 | // Renderer draws a scene into an RGBA image of the output size. | |
| 17 | 16 | type Renderer interface { | |
| 18 | 17 | 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) | |
| 20 | 20 | Close() | |
| 21 | 21 | } | |
| 22 | 22 | ||
| @@ -88,9 +88,9 @@ func (o *Output) modeset() error { | |||
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | 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 | 92 | b := o.bufs[o.back] | |
| 93 | - | copyXRGB(b.mem, int(b.pitch), img) | |
| 93 | + | r.CopyBGRX(b.mem, int(b.pitch)) | |
| 94 | 94 | if err := flip(o.fd, o.crtc, b.fbID); err != nil { | |
| 95 | 95 | return err | |
| 96 | 96 | } | |
| @@ -183,7 +183,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||
| 183 | 183 | if err := r.Render(scene, surfaces); err != nil { | |
| 184 | 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 | 188 | if errors.Is(err, unix.EBUSY) { | |
| 188 | 189 | // A flip is still in flight; retry after its event. | |
| 189 | 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 | 202 | dirty = false | |
| 202 | 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 | ||
| @@ -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) error | 17 | 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) | |||
| 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 err | 95 | 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 = false | 202 | 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) { | |||
| 161 | 161 | return | |
| 162 | 162 | } | |
| 163 | 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 | 169 | if err != nil { | |
| 166 | 170 | return | |
| 167 | 171 | } | |
| @@ -471,6 +475,19 @@ func (r *Renderer) Pixels() *image.RGBA { | |||
| 471 | 475 | 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 | 491 | func (r *Renderer) Close() { | |
| 475 | 492 | if r == nil { | |
| 476 | 493 | return | |
| @@ -161,7 +161,11 @@ func New(width, height int) (r *Renderer, err error) { | |||||
| 161 | return | 161 | 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 | return | 170 | return | ||
| 167 | } | 171 | } | ||
| @@ -471,6 +475,19 @@ func (r *Renderer) Pixels() *image.RGBA { | |||||
| 471 | return out | 475 | 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 | return | 493 | return | ||