Stream the panel as masked png deltas and seqlock the scanout reads
This commit is contained in:
+41
-26
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user