Stream the panel as masked png deltas and seqlock the scanout reads

This commit is contained in:
flamingcow
2026-08-30 16:45:19 -07:00
parent dddff4d7e8
commit 45a531066c
3 changed files with 260 additions and 28 deletions
+41 -26
View File
@@ -1,7 +1,6 @@
package main
import (
"bytes"
"fmt"
"image"
"image/color"
@@ -73,20 +72,18 @@ func activeCrtc(fd int) (uint32, int, error) {
func (g *grabber) close() { unix.Close(g.fd) }
// Reading a buffer takes about as long as a frame, so where the read starts in
// the flip cycle decides whether it stays ahead of the writer. Woken at a
// blank, the buffer GETCRTC then names has just gone on screen.
func (g *grabber) waitVblank() error {
// A delta of 0 returns the current sequence without blocking.
func (g *grabber) vblank(delta uint32) (uint32, error) {
v := drm.WaitVblank{
Type: drm.VblankRelative | uint32(g.index<<drm.VblankHighCrtcShift)&drm.VblankHighCrtcMask,
Sequence: 1,
Sequence: delta,
}
for {
err := drm.Ioctl(g.fd, drm.WaitVblankIO, unsafe.Pointer(&v))
if err == unix.EINTR {
continue
}
return err
return v.Sequence, err
}
}
@@ -124,29 +121,47 @@ func (g *grabber) mapFront() (mem []byte, pitch, pw, ph int, err error) {
return mem, pitch, pw, ph, nil
}
// Scanout memory is uncached, so a read costs a good fraction of a frame and
// could be overtaken by the next redraw. Read twice and accept only if the two
// agree: the writer leaves a buffer alone for several frames after displaying
// it, which is long enough for both. Disagreement means that reasoning is
// wrong, which is worth hearing about rather than retrying past.
func (g *grabber) frame() (*image.NRGBA, error) {
if err := g.waitVblank(); err != nil {
return nil, fmt.Errorf("wait for vblank: %w", err)
const overrunLimit = 10
// The writer cycles its buffers in order, so the one on screen is not written
// again until three flips after it arrived, and GETCRTC can name the previous
// front for an instant around a flip. A copy whose start and end vblank
// sequences differ by at most one therefore provably finished before the
// rewrite could begin. Overrunning that is a timing miss, not corruption:
// sample again, and treat only systematic misses as an error.
func (g *grabber) rawFrame(dst []byte) ([]byte, int, int, int, error) {
for tries := 0; tries < overrunLimit; tries++ {
s0, err := g.vblank(1)
if err != nil {
return nil, 0, 0, 0, fmt.Errorf("wait for vblank: %w", err)
}
mem, pitch, pw, ph, err := g.mapFront()
if err != nil {
return nil, 0, 0, 0, err
}
if len(dst) < len(mem) {
dst = make([]byte, len(mem))
}
dst = dst[:len(mem)]
copy(dst, mem)
unix.Munmap(mem)
s1, err := g.vblank(0)
if err != nil {
return nil, 0, 0, 0, fmt.Errorf("query vblank: %w", err)
}
if s1-s0 <= 1 {
return dst, pitch, pw, ph, nil
}
}
mem, pitch, pw, ph, err := g.mapFront()
return nil, 0, 0, 0, fmt.Errorf("scanout read overran the writer %d times in a row", overrunLimit)
}
func (g *grabber) frame() (*image.NRGBA, error) {
raw, pitch, pw, ph, err := g.rawFrame(nil)
if err != nil {
return nil, err
}
a := make([]byte, len(mem))
copy(a, mem)
b := make([]byte, len(mem))
copy(b, mem)
unix.Munmap(mem)
if !bytes.Equal(a, b) {
return nil, fmt.Errorf("scanout buffer was redrawn while being read")
}
return decode(a, pitch, pw, ph), nil
return decode(raw, pitch, pw, ph), nil
}
// Undoes the quarter turn drm.Offset applies, to get back what a person