182 lines
5.4 KiB
Go
182 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"path/filepath"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
"g.fc.run/theater/cabletest/internal/drm"
|
|
)
|
|
|
|
// cabletest drives the panel through drm, so /dev/fb0 holds the kernel console
|
|
// and is not worth reading. The displayed buffer is reachable from out here
|
|
// instead: GETCRTC names it, and root is allowed a handle to it without being
|
|
// drm master, so it can be read without the owning process cooperating.
|
|
const drmCardGlob = "/dev/dri/card*"
|
|
|
|
type grabber struct {
|
|
fd int
|
|
crtc uint32
|
|
index int
|
|
}
|
|
|
|
func openGrabber() (*grabber, error) {
|
|
paths, err := filepath.Glob(drmCardGlob)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, p := range paths {
|
|
fd, err := unix.Open(p, unix.O_RDWR|unix.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
crtc, index, err := activeCrtc(fd)
|
|
if err == nil {
|
|
// Opening the node while no master exists hands this fd implicit
|
|
// drm master; give it back so cabletest can take it.
|
|
drm.Ioctl(fd, drm.DropMaster, nil)
|
|
return &grabber{fd: fd, crtc: crtc, index: index}, nil
|
|
}
|
|
unix.Close(fd)
|
|
}
|
|
return nil, fmt.Errorf("no drm crtc is scanning out a framebuffer")
|
|
}
|
|
|
|
func activeCrtc(fd int) (uint32, int, error) {
|
|
var res drm.ModeCardRes
|
|
if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if res.CountCRTCs == 0 {
|
|
return 0, 0, fmt.Errorf("card has no crtcs")
|
|
}
|
|
crtcs := make([]uint32, res.CountCRTCs)
|
|
res.CountFBs, res.CountConns, res.CountEncs = 0, 0, 0
|
|
res.FBIDPtr, res.ConnIDPtr, res.EncIDPtr = 0, 0, 0
|
|
res.CrtcIDPtr = uint64(uintptr(unsafe.Pointer(&crtcs[0])))
|
|
if err := drm.Ioctl(fd, drm.GetResources, unsafe.Pointer(&res)); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
for i, id := range crtcs {
|
|
c := drm.ModeCrtc{CrtcID: id}
|
|
if err := drm.Ioctl(fd, drm.GetCrtc, unsafe.Pointer(&c)); err != nil {
|
|
continue
|
|
}
|
|
if c.FBID != 0 && c.ModeValid != 0 {
|
|
return id, i, nil
|
|
}
|
|
}
|
|
return 0, 0, fmt.Errorf("no crtc is scanning out a framebuffer")
|
|
}
|
|
|
|
func (g *grabber) close() { unix.Close(g.fd) }
|
|
|
|
// 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: delta,
|
|
}
|
|
for {
|
|
err := drm.Ioctl(g.fd, drm.WaitVblankIO, unsafe.Pointer(&v))
|
|
if err == unix.EINTR {
|
|
continue
|
|
}
|
|
return v.Sequence, err
|
|
}
|
|
}
|
|
|
|
func (g *grabber) mapFront() (mem []byte, pitch, pw, ph int, err error) {
|
|
c := drm.ModeCrtc{CrtcID: g.crtc}
|
|
if err := drm.Ioctl(g.fd, drm.GetCrtc, unsafe.Pointer(&c)); err != nil {
|
|
return nil, 0, 0, 0, fmt.Errorf("get crtc: %w", err)
|
|
}
|
|
if c.FBID == 0 {
|
|
return nil, 0, 0, 0, fmt.Errorf("crtc %d is not scanning out anything", g.crtc)
|
|
}
|
|
|
|
// Only a drm master or root is given a handle to someone else's framebuffer.
|
|
fb := drm.ModeFBCmd{FBID: c.FBID}
|
|
if err := drm.Ioctl(g.fd, drm.GetFB, unsafe.Pointer(&fb)); err != nil {
|
|
return nil, 0, 0, 0, fmt.Errorf("get fb %d: %w", c.FBID, err)
|
|
}
|
|
if fb.Handle == 0 {
|
|
return nil, 0, 0, 0, fmt.Errorf("kernel gave no handle for fb %d", c.FBID)
|
|
}
|
|
if fb.Bpp != 32 {
|
|
return nil, 0, 0, 0, fmt.Errorf("expected 32bpp, got %d", fb.Bpp)
|
|
}
|
|
|
|
m := drm.ModeMapDumb{Handle: fb.Handle}
|
|
if err := drm.Ioctl(g.fd, drm.MapDumb, unsafe.Pointer(&m)); err != nil {
|
|
return nil, 0, 0, 0, fmt.Errorf("map fb %d: %w", c.FBID, err)
|
|
}
|
|
pitch, pw, ph = int(fb.Pitch), int(fb.Width), int(fb.Height)
|
|
mem, err = unix.Mmap(g.fd, int64(m.Offset), pitch*ph,
|
|
unix.PROT_READ, unix.MAP_SHARED)
|
|
if err != nil {
|
|
return nil, 0, 0, 0, fmt.Errorf("mmap fb %d: %w", c.FBID, err)
|
|
}
|
|
return mem, pitch, pw, ph, nil
|
|
}
|
|
|
|
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() ([]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
|
|
}
|
|
dst := make([]byte, 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
|
|
}
|
|
}
|
|
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()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return decode(raw, pitch, pw, ph), nil
|
|
}
|
|
|
|
// Undoes the quarter turn drm.Offset applies, to get back what a person
|
|
// standing in front of the panel sees.
|
|
func decode(buf []byte, pitch, pw, ph int) *image.NRGBA {
|
|
img := image.NewNRGBA(image.Rect(0, 0, ph, pw))
|
|
for x := 0; x < ph; x++ {
|
|
for y := 0; y < pw; y++ {
|
|
o := drm.Offset(x, y, pitch, ph)
|
|
v := uint32(buf[o]) | uint32(buf[o+1])<<8 |
|
|
uint32(buf[o+2])<<16 | uint32(buf[o+3])<<24
|
|
r, g, b := drm.Unpack(v)
|
|
img.SetNRGBA(x, y, color.NRGBA{R: r, G: g, B: b, A: 255})
|
|
}
|
|
}
|
|
return img
|
|
}
|