Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f512391ee4 | ||
|
|
1c5bda37bf | ||
|
|
57d7c91df1 | ||
|
|
5d2dbffb4a |
@@ -1 +1,2 @@
|
|||||||
/cabletest
|
/cabletest
|
||||||
|
/shots/
|
||||||
|
|||||||
@@ -1,53 +1,149 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"path/filepath"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Drawing lands in a plain memory buffer and is blitted to a scanout buffer the
|
||||||
|
// display is not reading, which is then swapped in whole at a vertical blank.
|
||||||
|
// Writing into the live scanout buffer instead, as the fbdev interface invites,
|
||||||
|
// races the beam: the blit takes a few hundred microseconds, and whatever the
|
||||||
|
// display reads during it is part of one frame and part of the next.
|
||||||
const (
|
const (
|
||||||
fbioGetVScreenInfo = 0x4600
|
drmIoctlBase = 0x64
|
||||||
fbioBlank = 0x4611
|
|
||||||
fbBlankUnblank = 0
|
|
||||||
|
|
||||||
kdSetMode = 0x4b3a
|
drmModeConnected = 1
|
||||||
kdText = 0x00
|
drmModeTypePreferred = 1 << 3
|
||||||
kdGraphics = 0x01
|
drmModePageFlipEvent = 0x01
|
||||||
consolePath = "/dev/tty0"
|
drmEventFlipComplete = 0x02
|
||||||
fbPath = "/dev/fb0"
|
|
||||||
|
// What ADDFB means by 32 bits per pixel and 24 bits of colour.
|
||||||
|
xrgbRedShift = 16
|
||||||
|
xrgbGreenShift = 8
|
||||||
|
xrgbBlueShift = 0
|
||||||
|
|
||||||
|
// Two would be enough to stop the panel tearing, since one buffer being
|
||||||
|
// displayed while the other is drawn into is all double buffering means.
|
||||||
|
// More than two is for anything reading a frame back out: they are cycled
|
||||||
|
// in order, so a buffer is left alone for the three frames between going on
|
||||||
|
// screen and coming round again, and reading one out of uncached scanout
|
||||||
|
// memory takes a good fraction of a frame.
|
||||||
|
scanoutBuffers = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
// The variable screen info is a flat run of 40 u32s, so it is read as an array
|
func drmIO(nr uintptr) uintptr { return drmIoctlBase<<8 | nr }
|
||||||
// rather than a struct to sidestep any question of padding.
|
func drmIOWR(nr, size uintptr) uintptr { return 3<<30 | size<<16 | drmIoctlBase<<8 | nr }
|
||||||
const (
|
|
||||||
viXres = 0
|
type drmModeInfo struct {
|
||||||
viYres = 1
|
clock uint32
|
||||||
viBitsPerPixel = 6
|
hdisplay, hsyncStart, hsyncEnd, htotal, hskew uint16
|
||||||
viRedOffset = 8
|
vdisplay, vsyncStart, vsyncEnd, vtotal, vscan uint16
|
||||||
viGreenOffset = 11
|
vrefresh, flags, typ uint32
|
||||||
viBlueOffset = 14
|
name [32]byte
|
||||||
viScreenInfoLen = 40
|
}
|
||||||
|
|
||||||
|
type drmModeCardRes struct {
|
||||||
|
fbIDPtr, crtcIDPtr, connIDPtr, encIDPtr uint64
|
||||||
|
countFBs, countCRTCs, countConns, countEncs uint32
|
||||||
|
minWidth, maxWidth, minHeight, maxHeight uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type drmModeGetConnector struct {
|
||||||
|
encodersPtr, modesPtr, propsPtr, propValuesPtr uint64
|
||||||
|
countModes, countProps, countEncoders uint32
|
||||||
|
encoderID, connectorID, connectorType uint32
|
||||||
|
connectorTypeID, connection uint32
|
||||||
|
mmWidth, mmHeight, subpixel, pad uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type drmModeGetEncoder struct {
|
||||||
|
encoderID, encoderType uint32
|
||||||
|
crtcID uint32
|
||||||
|
possibleCRTCs, possibleClones 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 drmModeCreateDumb struct {
|
||||||
|
height, width, bpp, flags uint32
|
||||||
|
handle, pitch uint32
|
||||||
|
size uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type drmModeMapDumb struct {
|
||||||
|
handle, pad uint32
|
||||||
|
offset uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type drmModeCrtcPageFlip struct {
|
||||||
|
crtcID, fbID, flags, reserved uint32
|
||||||
|
userData uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
drmSetMaster = drmIO(0x1e)
|
||||||
|
drmDropMaster = drmIO(0x1f)
|
||||||
|
drmGetResources = drmIOWR(0xa0, unsafe.Sizeof(drmModeCardRes{}))
|
||||||
|
drmSetCrtc = drmIOWR(0xa2, unsafe.Sizeof(drmModeCrtc{}))
|
||||||
|
drmGetEncoder = drmIOWR(0xa6, unsafe.Sizeof(drmModeGetEncoder{}))
|
||||||
|
drmGetConnector = drmIOWR(0xa7, unsafe.Sizeof(drmModeGetConnector{}))
|
||||||
|
drmAddFB = drmIOWR(0xae, unsafe.Sizeof(drmModeFBCmd{}))
|
||||||
|
drmPageFlip = drmIOWR(0xb0, unsafe.Sizeof(drmModeCrtcPageFlip{}))
|
||||||
|
drmCreateDumb = drmIOWR(0xb2, unsafe.Sizeof(drmModeCreateDumb{}))
|
||||||
|
drmMapDumb = drmIOWR(0xb3, unsafe.Sizeof(drmModeMapDumb{}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 scanout struct {
|
||||||
|
fbID uint32
|
||||||
|
mem []byte
|
||||||
|
}
|
||||||
|
|
||||||
// w and h are the logical canvas, which is portrait; pw and ph are the panel,
|
// w and h are the logical canvas, which is portrait; pw and ph are the panel,
|
||||||
// which is landscape. Every draw is turned a quarter turn on its way to memory,
|
// which is landscape. Every draw is turned a quarter turn on its way to memory,
|
||||||
// so logical top lands on the panel's right edge.
|
// so logical top lands on the panel's right edge.
|
||||||
type framebuffer struct {
|
type framebuffer struct {
|
||||||
file *os.File
|
fd int
|
||||||
tty *os.File
|
|
||||||
mem []byte
|
|
||||||
back []byte
|
back []byte
|
||||||
w int
|
w int
|
||||||
h int
|
h int
|
||||||
pw int
|
pw int
|
||||||
ph int
|
ph int
|
||||||
stride int
|
stride int
|
||||||
rShift uint
|
|
||||||
gShift uint
|
bufs [scanoutBuffers]scanout
|
||||||
bShift uint
|
front int
|
||||||
|
crtcID uint32
|
||||||
|
connID uint32
|
||||||
|
|
||||||
|
// One token per completed flip. The render loop waits on this rather than on
|
||||||
|
// a timer, so drawing is paced by the panel instead of by a guess at its rate.
|
||||||
|
flips chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *framebuffer) offset(x, y int) int {
|
func (fb *framebuffer) offset(x, y int) int {
|
||||||
@@ -58,88 +154,261 @@ func (fb *framebuffer) fromPanel(x, y int) (int, int) {
|
|||||||
return y, fb.pw - 1 - x
|
return y, fb.pw - 1 - x
|
||||||
}
|
}
|
||||||
|
|
||||||
// The kernel console draws into the same framebuffer, including a blinking
|
func cardResources(fd int) (crtcs, conns []uint32, err error) {
|
||||||
// cursor and any keyboard echo, which fights every frame we write. Putting the
|
var res drmModeCardRes
|
||||||
// active console into graphics mode stops it touching the framebuffer at all.
|
if err := drmIoctl(fd, drmGetResources, unsafe.Pointer(&res)); err != nil {
|
||||||
func takeConsole() (*os.File, error) {
|
return nil, nil, fmt.Errorf("get resources: %w", err)
|
||||||
tty, err := os.OpenFile(consolePath, os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, tty.Fd(),
|
if res.countCRTCs == 0 || res.countConns == 0 {
|
||||||
kdSetMode, kdGraphics); errno != 0 {
|
return nil, nil, fmt.Errorf("card has no crtcs or connectors")
|
||||||
tty.Close()
|
|
||||||
return nil, fmt.Errorf("console to graphics mode: %w", errno)
|
|
||||||
}
|
}
|
||||||
return tty, nil
|
crtcs = make([]uint32, res.countCRTCs)
|
||||||
|
conns = make([]uint32, res.countConns)
|
||||||
|
res.countFBs, res.countEncs = 0, 0
|
||||||
|
res.fbIDPtr, res.encIDPtr = 0, 0
|
||||||
|
res.crtcIDPtr = uint64(uintptr(unsafe.Pointer(&crtcs[0])))
|
||||||
|
res.connIDPtr = uint64(uintptr(unsafe.Pointer(&conns[0])))
|
||||||
|
if err := drmIoctl(fd, drmGetResources, unsafe.Pointer(&res)); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("get resources: %w", err)
|
||||||
|
}
|
||||||
|
return crtcs, conns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func openFramebuffer(path string) (*framebuffer, error) {
|
// The preferred mode is the panel's native one; anything else would be the
|
||||||
f, err := os.OpenFile(path, os.O_RDWR, 0)
|
// driver scaling a wrong-sized image onto it.
|
||||||
|
func preferredMode(fd int, connID uint32) (drmModeInfo, error) {
|
||||||
|
c := drmModeGetConnector{connectorID: connID}
|
||||||
|
if err := drmIoctl(fd, drmGetConnector, unsafe.Pointer(&c)); err != nil {
|
||||||
|
return drmModeInfo{}, err
|
||||||
|
}
|
||||||
|
if c.countModes == 0 {
|
||||||
|
return drmModeInfo{}, fmt.Errorf("connector %d reported no modes", connID)
|
||||||
|
}
|
||||||
|
modes := make([]drmModeInfo, c.countModes)
|
||||||
|
q := drmModeGetConnector{
|
||||||
|
connectorID: connID,
|
||||||
|
countModes: c.countModes,
|
||||||
|
modesPtr: uint64(uintptr(unsafe.Pointer(&modes[0]))),
|
||||||
|
}
|
||||||
|
if err := drmIoctl(fd, drmGetConnector, unsafe.Pointer(&q)); err != nil {
|
||||||
|
return drmModeInfo{}, err
|
||||||
|
}
|
||||||
|
if q.countModes == 0 {
|
||||||
|
return drmModeInfo{}, fmt.Errorf("connector %d reported no modes", connID)
|
||||||
|
}
|
||||||
|
for _, m := range modes[:q.countModes] {
|
||||||
|
if m.typ&drmModeTypePreferred != 0 {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modes[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func crtcFor(fd int, c drmModeGetConnector, crtcs []uint32) (uint32, error) {
|
||||||
|
encoders := []uint32{c.encoderID}
|
||||||
|
if c.countEncoders > 0 {
|
||||||
|
list := make([]uint32, c.countEncoders)
|
||||||
|
q := drmModeGetConnector{
|
||||||
|
connectorID: c.connectorID,
|
||||||
|
countEncoders: c.countEncoders,
|
||||||
|
encodersPtr: uint64(uintptr(unsafe.Pointer(&list[0]))),
|
||||||
|
}
|
||||||
|
if err := drmIoctl(fd, drmGetConnector, unsafe.Pointer(&q)); err == nil {
|
||||||
|
encoders = append(encoders, list[:q.countEncoders]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, id := range encoders {
|
||||||
|
if id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e := drmModeGetEncoder{encoderID: id}
|
||||||
|
if err := drmIoctl(fd, drmGetEncoder, unsafe.Pointer(&e)); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Already driving this connector, otherwise anything it can be wired to.
|
||||||
|
if e.crtcID != 0 {
|
||||||
|
return e.crtcID, nil
|
||||||
|
}
|
||||||
|
for i, crtc := range crtcs {
|
||||||
|
if e.possibleCRTCs&(1<<uint(i)) != 0 {
|
||||||
|
return crtc, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("connector %d has no usable crtc", c.connectorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The connector to drive and a crtc that can drive it.
|
||||||
|
func findDisplay(fd int) (connID, crtcID uint32, err error) {
|
||||||
|
crtcs, conns, err := cardResources(fd)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
for _, id := range conns {
|
||||||
|
c := drmModeGetConnector{connectorID: id}
|
||||||
|
if err := drmIoctl(fd, drmGetConnector, unsafe.Pointer(&c)); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.connection != drmModeConnected || c.countModes == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
crtc, err := crtcFor(fd, c, crtcs)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return id, crtc, nil
|
||||||
|
}
|
||||||
|
return 0, 0, fmt.Errorf("no connected connector with a mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The card number is not stable across machines, so the card driving a
|
||||||
|
// connected display is the one we want.
|
||||||
|
func openCard() (int, error) {
|
||||||
|
paths, err := filepath.Glob("/dev/dri/card*")
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
fd, err := unix.Open(p, unix.O_RDWR|unix.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, _, err := findDisplay(fd); err == nil {
|
||||||
|
return fd, nil
|
||||||
|
}
|
||||||
|
unix.Close(fd)
|
||||||
|
}
|
||||||
|
return -1, fmt.Errorf("no drm device with a connected display")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *framebuffer) addScanout(i int) error {
|
||||||
|
create := drmModeCreateDumb{width: uint32(fb.pw), height: uint32(fb.ph), bpp: 32}
|
||||||
|
if err := drmIoctl(fb.fd, drmCreateDumb, unsafe.Pointer(&create)); err != nil {
|
||||||
|
return fmt.Errorf("create dumb buffer: %w", err)
|
||||||
|
}
|
||||||
|
fb.stride = int(create.pitch)
|
||||||
|
|
||||||
|
add := drmModeFBCmd{
|
||||||
|
width: uint32(fb.pw),
|
||||||
|
height: uint32(fb.ph),
|
||||||
|
pitch: create.pitch,
|
||||||
|
bpp: 32,
|
||||||
|
depth: 24,
|
||||||
|
handle: create.handle,
|
||||||
|
}
|
||||||
|
if err := drmIoctl(fb.fd, drmAddFB, unsafe.Pointer(&add)); err != nil {
|
||||||
|
return fmt.Errorf("add fb: %w", err)
|
||||||
|
}
|
||||||
|
fb.bufs[i].fbID = add.fbID
|
||||||
|
|
||||||
|
m := drmModeMapDumb{handle: create.handle}
|
||||||
|
if err := drmIoctl(fb.fd, drmMapDumb, unsafe.Pointer(&m)); err != nil {
|
||||||
|
return fmt.Errorf("map dumb buffer: %w", err)
|
||||||
|
}
|
||||||
|
mem, err := unix.Mmap(fb.fd, int64(m.offset), int(create.size),
|
||||||
|
unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mmap scanout: %w", err)
|
||||||
|
}
|
||||||
|
fb.bufs[i].mem = mem
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openFramebuffer() (*framebuffer, error) {
|
||||||
|
fd, err := openCard()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
fb := &framebuffer{fd: fd, flips: make(chan struct{}, 1)}
|
||||||
|
|
||||||
var vi [viScreenInfoLen]uint32
|
// Without master the modeset below is refused, and taking it is also what
|
||||||
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
|
// stops the kernel console drawing into the display behind us.
|
||||||
fbioGetVScreenInfo, uintptr(unsafe.Pointer(&vi[0]))); errno != 0 {
|
if err := drmIoctl(fd, drmSetMaster, nil); err != nil {
|
||||||
f.Close()
|
unix.Close(fd)
|
||||||
return nil, fmt.Errorf("get screen info: %w", errno)
|
return nil, fmt.Errorf("take drm master: %w", err)
|
||||||
}
|
|
||||||
if vi[viBitsPerPixel] != 32 {
|
|
||||||
f.Close()
|
|
||||||
return nil, fmt.Errorf("only 32bpp supported, got %d", vi[viBitsPerPixel])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wake the panel; the console blanks it after a timeout.
|
connID, crtcID, err := findDisplay(fd)
|
||||||
unix.Syscall(unix.SYS_IOCTL, f.Fd(), fbioBlank, fbBlankUnblank)
|
|
||||||
|
|
||||||
stride, ok := readUint("/sys/class/graphics/fb0/stride")
|
|
||||||
if !ok {
|
|
||||||
f.Close()
|
|
||||||
return nil, fmt.Errorf("cannot read framebuffer stride")
|
|
||||||
}
|
|
||||||
|
|
||||||
fb := &framebuffer{
|
|
||||||
file: f,
|
|
||||||
pw: int(vi[viXres]),
|
|
||||||
ph: int(vi[viYres]),
|
|
||||||
w: int(vi[viYres]),
|
|
||||||
h: int(vi[viXres]),
|
|
||||||
stride: int(stride),
|
|
||||||
rShift: uint(vi[viRedOffset]),
|
|
||||||
gShift: uint(vi[viGreenOffset]),
|
|
||||||
bShift: uint(vi[viBlueOffset]),
|
|
||||||
}
|
|
||||||
|
|
||||||
fb.mem, err = unix.Mmap(int(f.Fd()), 0, fb.stride*fb.ph,
|
|
||||||
unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.Close()
|
fb.close()
|
||||||
return nil, fmt.Errorf("mmap: %w", err)
|
return nil, err
|
||||||
|
}
|
||||||
|
mode, err := preferredMode(fd, connID)
|
||||||
|
if err != nil {
|
||||||
|
fb.close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fb.connID, fb.crtcID = connID, crtcID
|
||||||
|
fb.pw, fb.ph = int(mode.hdisplay), int(mode.vdisplay)
|
||||||
|
fb.w, fb.h = fb.ph, fb.pw
|
||||||
|
|
||||||
|
for i := range fb.bufs {
|
||||||
|
if err := fb.addScanout(i); err != nil {
|
||||||
|
fb.close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fb.back = make([]byte, fb.stride*fb.ph)
|
fb.back = make([]byte, fb.stride*fb.ph)
|
||||||
|
|
||||||
fb.tty, err = takeConsole()
|
set := drmModeCrtc{
|
||||||
if err != nil {
|
setConnectorsPtr: uint64(uintptr(unsafe.Pointer(&fb.connID))),
|
||||||
unix.Munmap(fb.mem)
|
countConnectors: 1,
|
||||||
f.Close()
|
crtcID: crtcID,
|
||||||
return nil, err
|
fbID: fb.bufs[0].fbID,
|
||||||
|
modeValid: 1,
|
||||||
|
mode: mode,
|
||||||
}
|
}
|
||||||
|
if err := drmIoctl(fd, drmSetCrtc, unsafe.Pointer(&set)); err != nil {
|
||||||
|
fb.close()
|
||||||
|
return nil, fmt.Errorf("set crtc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go fb.readEvents()
|
||||||
|
// Nothing has been flipped yet, so the first frame is owed its turn.
|
||||||
|
fb.flips <- struct{}{}
|
||||||
return fb, nil
|
return fb, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flip completions arrive on the drm fd as a stream of length-prefixed events.
|
||||||
|
func (fb *framebuffer) readEvents() {
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
n, err := unix.Read(fb.fd, buf)
|
||||||
|
if n <= 0 || err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for off := 0; off+8 <= n; {
|
||||||
|
typ := binary.LittleEndian.Uint32(buf[off:])
|
||||||
|
length := int(binary.LittleEndian.Uint32(buf[off+4:]))
|
||||||
|
if length < 8 || off+length > n {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if typ == drmEventFlipComplete {
|
||||||
|
select {
|
||||||
|
case fb.flips <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
off += length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (fb *framebuffer) close() {
|
func (fb *framebuffer) close() {
|
||||||
fb.fill(rgb{})
|
for i := range fb.bufs {
|
||||||
fb.flush()
|
if fb.bufs[i].mem != nil {
|
||||||
unix.Syscall(unix.SYS_IOCTL, fb.tty.Fd(), kdSetMode, kdText)
|
unix.Munmap(fb.bufs[i].mem)
|
||||||
fb.tty.Close()
|
}
|
||||||
unix.Munmap(fb.mem)
|
}
|
||||||
fb.file.Close()
|
// Dropping master hands the display back to the kernel console, which
|
||||||
|
// restores its own mode. The framebuffers and dumb buffers are reclaimed
|
||||||
|
// when the last reference to the fd goes.
|
||||||
|
drmIoctl(fb.fd, drmDropMaster, nil)
|
||||||
|
unix.Close(fb.fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *framebuffer) pixel(c rgb) uint32 {
|
func (fb *framebuffer) pixel(c rgb) uint32 {
|
||||||
return uint32(c.r)<<fb.rShift | uint32(c.g)<<fb.gShift | uint32(c.b)<<fb.bShift
|
return uint32(c.r)<<xrgbRedShift | uint32(c.g)<<xrgbGreenShift | uint32(c.b)<<xrgbBlueShift
|
||||||
}
|
}
|
||||||
|
|
||||||
type rgb struct {
|
type rgb struct {
|
||||||
@@ -197,9 +466,9 @@ func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) {
|
|||||||
a := uint32(cov)
|
a := uint32(cov)
|
||||||
old := uint32(fb.back[o+0]) | uint32(fb.back[o+1])<<8 |
|
old := uint32(fb.back[o+0]) | uint32(fb.back[o+1])<<8 |
|
||||||
uint32(fb.back[o+2])<<16 | uint32(fb.back[o+3])<<24
|
uint32(fb.back[o+2])<<16 | uint32(fb.back[o+3])<<24
|
||||||
orr := uint8(old >> fb.rShift)
|
orr := uint8(old >> xrgbRedShift)
|
||||||
og := uint8(old >> fb.gShift)
|
og := uint8(old >> xrgbGreenShift)
|
||||||
ob := uint8(old >> fb.bShift)
|
ob := uint8(old >> xrgbBlueShift)
|
||||||
mix := rgb{
|
mix := rgb{
|
||||||
r: uint8((uint32(c.r)*a + uint32(orr)*(255-a)) / 255),
|
r: uint8((uint32(c.r)*a + uint32(orr)*(255-a)) / 255),
|
||||||
g: uint8((uint32(c.g)*a + uint32(og)*(255-a)) / 255),
|
g: uint8((uint32(c.g)*a + uint32(og)*(255-a)) / 255),
|
||||||
@@ -212,6 +481,21 @@ func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) {
|
|||||||
fb.back[o+3] = byte(v >> 24)
|
fb.back[o+3] = byte(v >> 24)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *framebuffer) flush() {
|
// Copies into the scanout buffer furthest from being displayed and asks for it
|
||||||
copy(fb.mem, fb.back)
|
// at the next blank. The copy cannot tear because nothing is displaying that
|
||||||
|
// buffer, and the swap cannot tear because the hardware does it between frames.
|
||||||
|
func (fb *framebuffer) flush() error {
|
||||||
|
next := (fb.front + 1) % scanoutBuffers
|
||||||
|
copy(fb.bufs[next].mem, fb.back)
|
||||||
|
|
||||||
|
flip := drmModeCrtcPageFlip{
|
||||||
|
crtcID: fb.crtcID,
|
||||||
|
fbID: fb.bufs[next].fbID,
|
||||||
|
flags: drmModePageFlipEvent,
|
||||||
|
}
|
||||||
|
if err := drmIoctl(fb.fd, drmPageFlip, unsafe.Pointer(&flip)); err != nil {
|
||||||
|
return fmt.Errorf("page flip: %w", err)
|
||||||
|
}
|
||||||
|
fb.front = next
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+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
|
||||||
|
}
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
// Runs cabletest for a fixed time and dumps the panel to png, since the machine
|
||||||
|
// 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
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
shotsDir = "shots"
|
||||||
|
grace = 3 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
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")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if err := run(*runFor, *at, flag.Args()); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseShots(s string, runFor time.Duration) ([]time.Duration, error) {
|
||||||
|
if s == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var out []time.Duration
|
||||||
|
for _, f := range strings.Split(s, ",") {
|
||||||
|
d, err := time.ParseDuration(strings.TrimSpace(f))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bad offset %q: %w", f, err)
|
||||||
|
}
|
||||||
|
if d < 0 || d >= runFor {
|
||||||
|
return nil, fmt.Errorf("offset %s falls outside the %s run", d, runFor)
|
||||||
|
}
|
||||||
|
out = append(out, d)
|
||||||
|
}
|
||||||
|
slices.Sort(out)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(runFor time.Duration, at string, args []string) error {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
return fmt.Errorf("needs root for the raw sockets and the framebuffer: sudo go run ./harness")
|
||||||
|
}
|
||||||
|
shots, err := parseShots(at, runFor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(shots) > 0 {
|
||||||
|
if err := os.MkdirAll(shotsDir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("go", append([]string{"run", "."}, args...)...)
|
||||||
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||||
|
// Its own process group: go run's compiled child is reparented rather than
|
||||||
|
// killed when go run dies, and an orphan holding the wire poisons every
|
||||||
|
// measurement after it.
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pgid := cmd.Process.Pid
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- cmd.Wait() }()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
for _, d := range shots {
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
return exited(start, err)
|
||||||
|
case <-time.After(time.Until(start.Add(d))):
|
||||||
|
}
|
||||||
|
name := filepath.Join(shotsDir, d.String()+".png")
|
||||||
|
if err := shoot(name); err != nil {
|
||||||
|
stop(pgid, done)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("captured %s\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
return exited(start, err)
|
||||||
|
case <-time.After(time.Until(start.Add(runFor))):
|
||||||
|
}
|
||||||
|
return stop(pgid, done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exited(start time.Time, err error) error {
|
||||||
|
ran := time.Since(start).Round(time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cabletest exited after %s: %w", ran, err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cabletest exited after %s", ran)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop(pgid int, done chan error) error {
|
||||||
|
syscall.Kill(-pgid, syscall.SIGINT)
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(grace):
|
||||||
|
syscall.Kill(-pgid, syscall.SIGKILL)
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
// Reaping go run says nothing about what it left behind, so the group itself
|
||||||
|
// is what gets checked.
|
||||||
|
if groupGone(pgid) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
syscall.Kill(-pgid, syscall.SIGKILL)
|
||||||
|
if groupGone(pgid) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("process group %d survived SIGKILL; check with pgrep -a cabletest", pgid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupGone(pgid int) bool {
|
||||||
|
deadline := time.Now().Add(grace)
|
||||||
|
for {
|
||||||
|
if syscall.Kill(-pgid, 0) == syscall.ESRCH {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shoot(name string) error {
|
||||||
|
g, err := openGrabber()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer g.close()
|
||||||
|
|
||||||
|
img, err := g.frame()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := png.Encode(out, img); err != nil {
|
||||||
|
out.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return out.Close()
|
||||||
|
}
|
||||||
@@ -50,12 +50,17 @@ type direction struct {
|
|||||||
probeRxFD int
|
probeRxFD int
|
||||||
cable *cableStats
|
cable *cableStats
|
||||||
|
|
||||||
|
// Guards everything the sampler touches. The counters are read on their own
|
||||||
|
// clock and drawn on another, and the two must not read them at once:
|
||||||
|
// sampleDrops consumes what it reads, so a second caller would see a gap.
|
||||||
|
mu sync.Mutex
|
||||||
prevConsole counterSet
|
prevConsole counterSet
|
||||||
win *rateWindow
|
win *rateWindow
|
||||||
heldFrames heldValue
|
|
||||||
heldSent heldValue
|
|
||||||
drops uint64
|
drops uint64
|
||||||
base counterSet
|
base counterSet
|
||||||
|
|
||||||
|
heldFrames heldValue
|
||||||
|
heldSent heldValue
|
||||||
nic atomic.Uint64
|
nic atomic.Uint64
|
||||||
poller *nicPoller
|
poller *nicPoller
|
||||||
}
|
}
|
||||||
@@ -266,7 +271,10 @@ func (d *direction) snapshot() sample {
|
|||||||
// everything is measured from. Rates are deliberately left running, since they
|
// everything is measured from. Rates are deliberately left running, since they
|
||||||
// are instantaneous and would only blink to zero and back.
|
// are instantaneous and would only blink to zero and back.
|
||||||
func (d *direction) reset() {
|
func (d *direction) reset() {
|
||||||
|
d.mu.Lock()
|
||||||
d.base = d.capture(time.Now())
|
d.base = d.capture(time.Now())
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
d.heldFrames = heldValue{}
|
d.heldFrames = heldValue{}
|
||||||
d.heldSent = heldValue{}
|
d.heldSent = heldValue{}
|
||||||
d.cable.reset()
|
d.cable.reset()
|
||||||
@@ -364,6 +372,9 @@ func totalView(views []view) view {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *direction) view(t time.Time) view {
|
func (d *direction) view(t time.Time) view {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
now := d.capture(t)
|
now := d.capture(t)
|
||||||
p := d.prevConsole
|
p := d.prevConsole
|
||||||
d.prevConsole = now
|
d.prevConsole = now
|
||||||
@@ -380,21 +391,36 @@ func (d *direction) view(t time.Time) view {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *direction) displayView(t time.Time) view {
|
// The one place the counters are read for the ring. Runs on its own ticker, so
|
||||||
now := d.capture(t)
|
// what the buckets measure does not move when the drawing does.
|
||||||
d.win.push(now)
|
func (d *direction) sample(t time.Time) {
|
||||||
|
d.mu.Lock()
|
||||||
|
d.win.push(d.capture(t))
|
||||||
|
d.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
v := d.counters(now)
|
// Draws whatever the sampler last put in the ring rather than reading the
|
||||||
v.rxFrames = d.heldFrames.get(t, v.rxFrames)
|
// counters again, so the display is a consumer of the measurement and never a
|
||||||
v.rxGot = d.heldSent.get(t, v.rxGot)
|
// participant in it.
|
||||||
if n := d.win.count(); n >= 2 {
|
func (d *direction) displayView(t time.Time) view {
|
||||||
|
d.mu.Lock()
|
||||||
|
n := d.win.count()
|
||||||
|
if n == 0 {
|
||||||
|
d.mu.Unlock()
|
||||||
|
return view{cable: d.cable.view()}
|
||||||
|
}
|
||||||
|
v := d.counters(d.win.at(n - 1))
|
||||||
|
if n >= 2 {
|
||||||
v.window = errsBetween(d.win.at(0), d.win.at(n-1))
|
v.window = errsBetween(d.win.at(0), d.win.at(n-1))
|
||||||
}
|
}
|
||||||
|
|
||||||
v.txPPS = d.win.median(txRatePPS)
|
v.txPPS = d.win.median(txRatePPS)
|
||||||
v.rxPPS = d.win.median(rxRatePPS)
|
v.rxPPS = d.win.median(rxRatePPS)
|
||||||
v.txGbps = d.win.median(txRateGbps)
|
v.txGbps = d.win.median(txRateGbps)
|
||||||
v.rxGbps = d.win.median(rxRateGbps)
|
v.rxGbps = d.win.median(rxRateGbps)
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
v.rxFrames = d.heldFrames.get(t, v.rxFrames)
|
||||||
|
v.rxGot = d.heldSent.get(t, v.rxGot)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,6 +463,7 @@ func buildDirection(label string, tx, rx endpoint, sizes []int, cfg config) (*di
|
|||||||
cable: newCableStats(),
|
cable: newCableStats(),
|
||||||
}
|
}
|
||||||
d.poller = &nicPoller{txName: tx.name, rxName: rx.name, total: &d.nic}
|
d.poller = &nicPoller{txName: tx.name, rxName: rx.name, total: &d.nic}
|
||||||
|
d.win = newRateWindow(int(rateWindowSpan/sampleInterval) + 1)
|
||||||
|
|
||||||
for i := 0; i < cfg.streams; i++ {
|
for i := 0; i < cfg.streams; i++ {
|
||||||
et := uint16(etherBase + i)
|
et := uint16(etherBase + i)
|
||||||
@@ -575,15 +602,39 @@ func main() {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
reportInterval = time.Second
|
reportInterval = time.Second
|
||||||
// Redraw fast so the panel feels live, but measure rates over a much longer
|
// What a bucket covers. Nothing to do with how often the panel is redrawn:
|
||||||
// window than a frame, since a frame's worth of a bursty sender is noise.
|
// how often the counters are read is a property of the measurement, and
|
||||||
displayInterval = 16 * time.Millisecond
|
// letting the refresh set it would let a slow or blocked draw stretch the
|
||||||
|
// window it reports. Short enough that a step lands promptly, long enough
|
||||||
|
// that a bucket holds tens of thousands of frames at line rate and is not
|
||||||
|
// itself noise.
|
||||||
|
sampleInterval = 16 * time.Millisecond
|
||||||
// Both how far back the shown errors reach and how many buckets the median
|
// Both how far back the shown errors reach and how many buckets the median
|
||||||
// runs over, so a step in the rate lands half this late.
|
// runs over, so a step in the rate lands half this late.
|
||||||
rateWindowSpan = time.Second
|
rateWindowSpan = time.Second
|
||||||
totalsHold = 50 * time.Millisecond
|
totalsHold = 50 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// One sampler for both directions, so their buckets share an instant and the
|
||||||
|
// cable length, which needs a figure from each, is never mixing two moments.
|
||||||
|
type sampler struct {
|
||||||
|
dirs []*direction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sampler) run(done *atomic.Bool, startTx <-chan struct{}) {
|
||||||
|
<-startTx
|
||||||
|
|
||||||
|
tick := time.NewTicker(sampleInterval)
|
||||||
|
defer tick.Stop()
|
||||||
|
|
||||||
|
for !done.Load() {
|
||||||
|
now := <-tick.C
|
||||||
|
for _, d := range s.dirs {
|
||||||
|
d.sample(now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func run(aName, bName, sizesArg string,
|
func run(aName, bName, sizesArg string,
|
||||||
nStreams, batch int, nsPerM float64) error {
|
nStreams, batch int, nsPerM float64) error {
|
||||||
|
|
||||||
@@ -692,6 +743,12 @@ func run(aName, bName, sizesArg string,
|
|||||||
for _, d := range dirs {
|
for _, d := range dirs {
|
||||||
d.start(&wg, &doneTx, &doneRx, cfg, &rxReady, startTx)
|
d.start(&wg, &doneTx, &doneRx, cfg, &rxReady, startTx)
|
||||||
}
|
}
|
||||||
|
samp := &sampler{dirs: dirs}
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
samp.run(&doneRx, startTx)
|
||||||
|
}()
|
||||||
rxReady.Wait()
|
rxReady.Wait()
|
||||||
|
|
||||||
sig := make(chan os.Signal, 1)
|
sig := make(chan os.Signal, 1)
|
||||||
@@ -719,14 +776,9 @@ func run(aName, bName, sizesArg string,
|
|||||||
close(startTx)
|
close(startTx)
|
||||||
tick := time.NewTicker(reportInterval)
|
tick := time.NewTicker(reportInterval)
|
||||||
defer tick.Stop()
|
defer tick.Stop()
|
||||||
frame := time.NewTicker(displayInterval)
|
|
||||||
defer frame.Stop()
|
|
||||||
|
|
||||||
views := make([]view, len(dirs))
|
views := make([]view, len(dirs))
|
||||||
rows := make([]view, len(dirs))
|
rows := make([]view, len(dirs))
|
||||||
for _, d := range dirs {
|
|
||||||
d.win = newRateWindow(int(rateWindowSpan/displayInterval) + 1)
|
|
||||||
}
|
|
||||||
stats := &streamTable{cols: intervalCols, headerEvery: 20}
|
stats := &streamTable{cols: intervalCols, headerEvery: 20}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -737,7 +789,8 @@ func run(aName, bName, sizesArg string,
|
|||||||
return nil
|
return nil
|
||||||
case <-space:
|
case <-space:
|
||||||
start = resetAll(dirs, stats)
|
start = resetAll(dirs, stats)
|
||||||
case now := <-frame.C:
|
case <-disp.fb.flips:
|
||||||
|
now := time.Now()
|
||||||
px, py, down := touch.get()
|
px, py, down := touch.get()
|
||||||
x, y := disp.fb.fromPanel(px, py)
|
x, y := disp.fb.fromPanel(px, py)
|
||||||
if disp.holdReset(x, y, down, now) {
|
if disp.holdReset(x, y, down, now) {
|
||||||
@@ -750,8 +803,10 @@ func run(aName, bName, sizesArg string,
|
|||||||
if m, ok := cfg.cableMetres(views); ok {
|
if m, ok := cfg.cableMetres(views); ok {
|
||||||
cable = fmt.Sprintf("%.1f m", m)
|
cable = fmt.Sprintf("%.1f m", m)
|
||||||
}
|
}
|
||||||
disp.render(totalView(views), now.Sub(start),
|
if err := disp.render(totalView(views), now.Sub(start),
|
||||||
target*float64(len(dirs)), cable)
|
target*float64(len(dirs)), cable); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case now := <-tick.C:
|
case now := <-tick.C:
|
||||||
elapsed := now.Sub(start)
|
elapsed := now.Sub(start)
|
||||||
// Length needs both directions, so every row is sampled before any of
|
// Length needs both directions, so every row is sampled before any of
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ type display struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newDisplay() (*display, error) {
|
func newDisplay() (*display, error) {
|
||||||
fb, err := openFramebuffer(fbPath)
|
fb, err := openFramebuffer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@ func rateColor(gb, target float64) rgb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *display) render(v view, elapsed time.Duration, target float64, cable string) {
|
func (d *display) render(v view, elapsed time.Duration, target float64, cable string) error {
|
||||||
fb := d.fb
|
fb := d.fb
|
||||||
fb.fill(uiBg)
|
fb.fill(uiBg)
|
||||||
|
|
||||||
@@ -267,5 +267,5 @@ func (d *display) render(v view, elapsed time.Duration, target float64, cable st
|
|||||||
d.errBlock(x, w, y+blockGap, v.since)
|
d.errBlock(x, w, y+blockGap, v.since)
|
||||||
|
|
||||||
d.drawResetButton()
|
d.drawResetButton()
|
||||||
fb.flush()
|
return fb.flush()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user