Capture the panel by reading the displayed drm buffer instead of fbdev
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"path/filepath"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cabletest drives the panel through drm and page flips between two scanout
|
||||||
|
// buffers of its own, so /dev/fb0 holds the kernel console and there is nothing
|
||||||
|
// in it worth reading. The buffer being displayed is reachable from out here
|
||||||
|
// though: GETCRTC names it, and root is allowed a handle to it without being
|
||||||
|
// drm master, so it can be mapped and read without the program that owns it
|
||||||
|
// having to cooperate or even notice.
|
||||||
|
const (
|
||||||
|
drmCardGlob = "/dev/dri/card*"
|
||||||
|
|
||||||
|
drmVblankRelative = 0x1
|
||||||
|
drmVblankHighCrtcMask = 0x3e
|
||||||
|
drmVblankHighCrtcShft = 1
|
||||||
|
|
||||||
|
xrgbRedShift = 16
|
||||||
|
xrgbGreenShift = 8
|
||||||
|
xrgbBlueShift = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func drmIOWR(nr, size uintptr) uintptr { return 3<<30 | size<<16 | 0x64<<8 | nr }
|
||||||
|
|
||||||
|
type drmModeInfo struct {
|
||||||
|
clock uint32
|
||||||
|
hdisplay, hsyncStart, hsyncEnd, htotal, hskew uint16
|
||||||
|
vdisplay, vsyncStart, vsyncEnd, vtotal, vscan uint16
|
||||||
|
vrefresh, flags, typ uint32
|
||||||
|
name [32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type drmModeCardRes struct {
|
||||||
|
fbIDPtr, crtcIDPtr, connIDPtr, encIDPtr uint64
|
||||||
|
countFBs, countCRTCs, countConns, countEncs uint32
|
||||||
|
minWidth, maxWidth, minHeight, maxHeight uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type drmModeCrtc struct {
|
||||||
|
setConnectorsPtr uint64
|
||||||
|
countConnectors uint32
|
||||||
|
crtcID uint32
|
||||||
|
fbID uint32
|
||||||
|
x, y uint32
|
||||||
|
gammaSize uint32
|
||||||
|
modeValid uint32
|
||||||
|
mode drmModeInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type drmModeFBCmd struct {
|
||||||
|
fbID, width, height, pitch, bpp, depth uint32
|
||||||
|
handle uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type drmModeMapDumb struct {
|
||||||
|
handle, pad uint32
|
||||||
|
offset uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type drmWaitVblank struct {
|
||||||
|
typ uint32
|
||||||
|
sequence uint32
|
||||||
|
signal uint64
|
||||||
|
tvSec int64
|
||||||
|
tvUsec int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
drmGetResources = drmIOWR(0xa0, unsafe.Sizeof(drmModeCardRes{}))
|
||||||
|
drmGetCrtc = drmIOWR(0xa1, unsafe.Sizeof(drmModeCrtc{}))
|
||||||
|
drmGetFB = drmIOWR(0xad, unsafe.Sizeof(drmModeFBCmd{}))
|
||||||
|
drmMapDumb = drmIOWR(0xb3, unsafe.Sizeof(drmModeMapDumb{}))
|
||||||
|
drmWaitVblankIO = drmIOWR(0x3a, 24)
|
||||||
|
)
|
||||||
|
|
||||||
|
func drmIoctl(fd int, req uintptr, arg unsafe.Pointer) error {
|
||||||
|
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg)); errno != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type grabber struct {
|
||||||
|
fd int
|
||||||
|
crtc uint32
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
// The card and crtc actually putting something on a display, which is whichever
|
||||||
|
// one cabletest chose when it set its mode.
|
||||||
|
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 {
|
||||||
|
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 drmModeCardRes
|
||||||
|
if err := drmIoctl(fd, drmGetResources, 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 := drmIoctl(fd, drmGetResources, unsafe.Pointer(&res)); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
for i, id := range crtcs {
|
||||||
|
c := drmModeCrtc{crtcID: id}
|
||||||
|
if err := drmIoctl(fd, drmGetCrtc, 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) }
|
||||||
|
|
||||||
|
// Reading the buffer takes about as long as a frame, so where the read starts
|
||||||
|
// in the flip cycle is what decides whether it stays ahead of the writer. Woken
|
||||||
|
// at a blank, the buffer named next has just gone on screen, which leaves a
|
||||||
|
// whole frame plus however long cabletest spends drawing before anything
|
||||||
|
// touches it again.
|
||||||
|
func (g *grabber) waitVblank() error {
|
||||||
|
v := drmWaitVblank{
|
||||||
|
typ: drmVblankRelative | uint32(g.index<<drmVblankHighCrtcShft)&drmVblankHighCrtcMask,
|
||||||
|
sequence: 1,
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
err := drmIoctl(g.fd, drmWaitVblankIO, unsafe.Pointer(&v))
|
||||||
|
if err == unix.EINTR {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps whichever buffer is on screen right now. The caller is expected to have
|
||||||
|
// just woken at a blank, so this is the one with the longest life ahead of it.
|
||||||
|
func (g *grabber) mapFront() (mem []byte, pitch, pw, ph int, err error) {
|
||||||
|
c := drmModeCrtc{crtcID: g.crtc}
|
||||||
|
if err := drmIoctl(g.fd, drmGetCrtc, 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 := drmModeFBCmd{fbID: c.fbID}
|
||||||
|
if err := drmIoctl(g.fd, drmGetFB, 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 := drmModeMapDumb{handle: fb.handle}
|
||||||
|
if err := drmIoctl(g.fd, drmMapDumb, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanout memory is uncached, so reading a frame out of it costs a good
|
||||||
|
// fraction of a frame's time and could in principle be overtaken by the next
|
||||||
|
// redraw. Rather than trust that it was not, the buffer is read twice and the
|
||||||
|
// pair only accepted if they agree: the writer cycles through its buffers in
|
||||||
|
// order and leaves this one alone for several frames after putting it on
|
||||||
|
// screen, which is comfortably long enough for both reads. Disagreement means
|
||||||
|
// that reasoning is wrong somewhere, which is worth hearing about rather than
|
||||||
|
// papering over with another go.
|
||||||
|
func (g *grabber) frame() (*image.NRGBA, error) {
|
||||||
|
if err := g.waitVblank(); err != nil {
|
||||||
|
return nil, fmt.Errorf("wait for vblank: %w", err)
|
||||||
|
}
|
||||||
|
mem, pitch, pw, ph, err := g.mapFront()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// The panel is landscape and every draw is turned a quarter turn on its way
|
||||||
|
// into it, so the turn is undone here to get back what a person standing in
|
||||||
|
// front of it 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 := x*pitch + (pw-1-y)*4
|
||||||
|
v := uint32(buf[o]) | uint32(buf[o+1])<<8 |
|
||||||
|
uint32(buf[o+2])<<16 | uint32(buf[o+3])<<24
|
||||||
|
img.SetNRGBA(x, y, color.NRGBA{
|
||||||
|
R: uint8(v >> xrgbRedShift),
|
||||||
|
G: uint8(v >> xrgbGreenShift),
|
||||||
|
B: uint8(v >> xrgbBlueShift),
|
||||||
|
A: 255,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
}
|
||||||
+4
-74
@@ -1,5 +1,5 @@
|
|||||||
// Runs cabletest for a fixed time and dumps the panel to png, since the machine
|
// Runs cabletest for a fixed time and dumps the panel to png, since the machine
|
||||||
// with the framebuffer on it is not the machine looking at it.
|
// with the display on it is not the machine looking at it.
|
||||||
//
|
//
|
||||||
// sudo go run ./harness -for 20s -at 3s,15s -- -a enp1s0f0np0 -b enp1s0f1np1
|
// sudo go run ./harness -for 20s -at 3s,15s -- -a enp1s0f0np0 -b enp1s0f1np1
|
||||||
package main
|
package main
|
||||||
@@ -7,41 +7,21 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/png"
|
"image/png"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
fbPath = "/dev/fb0"
|
|
||||||
shotsDir = "shots"
|
shotsDir = "shots"
|
||||||
grace = 3 * time.Second
|
grace = 3 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Matching fb.go, which reads the variable screen info as a flat run of u32s
|
|
||||||
// rather than a struct to sidestep any question of padding.
|
|
||||||
const (
|
|
||||||
fbioGetVScreenInfo = 0x4600
|
|
||||||
viXres = 0
|
|
||||||
viYres = 1
|
|
||||||
viBitsPerPixel = 6
|
|
||||||
viRedOffset = 8
|
|
||||||
viGreenOffset = 11
|
|
||||||
viBlueOffset = 14
|
|
||||||
viScreenInfoLen = 40
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
runFor := flag.Duration("for", 15*time.Second, "how long to let cabletest run")
|
runFor := flag.Duration("for", 15*time.Second, "how long to let cabletest run")
|
||||||
at := flag.String("at", "", "offsets to capture the panel at, comma separated, e.g. 3s,10s")
|
at := flag.String("at", "", "offsets to capture the panel at, comma separated, e.g. 3s,10s")
|
||||||
@@ -165,55 +145,17 @@ func groupGone(pgid int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shoot(name string) error {
|
func shoot(name string) error {
|
||||||
f, err := os.Open(fbPath)
|
g, err := openGrabber()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer g.close()
|
||||||
|
|
||||||
var vi [viScreenInfoLen]uint32
|
img, err := g.frame()
|
||||||
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
|
|
||||||
fbioGetVScreenInfo, uintptr(unsafe.Pointer(&vi[0]))); errno != 0 {
|
|
||||||
return fmt.Errorf("get screen info: %w", errno)
|
|
||||||
}
|
|
||||||
if vi[viBitsPerPixel] != 32 {
|
|
||||||
return fmt.Errorf("only 32bpp supported, got %d", vi[viBitsPerPixel])
|
|
||||||
}
|
|
||||||
stride, err := readUint("/sys/class/graphics/fb0/stride")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pw, ph := int(vi[viXres]), int(vi[viYres])
|
|
||||||
mem, err := unix.Mmap(int(f.Fd()), 0, int(stride)*ph, unix.PROT_READ, unix.MAP_SHARED)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("mmap: %w", err)
|
|
||||||
}
|
|
||||||
defer unix.Munmap(mem)
|
|
||||||
|
|
||||||
// cabletest flushes a whole frame into this mapping every 16ms, so the pixel
|
|
||||||
// loop below is far too slow to read it directly: consecutive scanlines come
|
|
||||||
// from different frames and every changing digit ends up drawn twice. One
|
|
||||||
// memmove out first narrows the window to about the length of its own copy.
|
|
||||||
buf := make([]byte, len(mem))
|
|
||||||
copy(buf, mem)
|
|
||||||
|
|
||||||
rs, gs, bs := vi[viRedOffset], vi[viGreenOffset], vi[viBlueOffset]
|
|
||||||
// The panel is landscape and every draw is turned a quarter turn on its way
|
|
||||||
// into it, so the turn is undone here to get back what a person standing in
|
|
||||||
// front of it sees.
|
|
||||||
img := image.NewNRGBA(image.Rect(0, 0, ph, pw))
|
|
||||||
for x := 0; x < ph; x++ {
|
|
||||||
for y := 0; y < pw; y++ {
|
|
||||||
o := x*int(stride) + (pw-1-y)*4
|
|
||||||
v := uint32(buf[o]) | uint32(buf[o+1])<<8 |
|
|
||||||
uint32(buf[o+2])<<16 | uint32(buf[o+3])<<24
|
|
||||||
img.SetNRGBA(x, y, color.NRGBA{
|
|
||||||
R: uint8(v >> rs), G: uint8(v >> gs), B: uint8(v >> bs), A: 255,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := os.Create(name)
|
out, err := os.Create(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -224,15 +166,3 @@ func shoot(name string) error {
|
|||||||
}
|
}
|
||||||
return out.Close()
|
return out.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func readUint(path string) (uint64, error) {
|
|
||||||
b, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
v, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("%s: %w", path, err)
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user