Drive the panel through drm and page flip between four scanout buffers
This commit is contained in:
@@ -1,53 +1,149 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"unsafe"
|
||||
|
||||
"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 (
|
||||
fbioGetVScreenInfo = 0x4600
|
||||
fbioBlank = 0x4611
|
||||
fbBlankUnblank = 0
|
||||
drmIoctlBase = 0x64
|
||||
|
||||
kdSetMode = 0x4b3a
|
||||
kdText = 0x00
|
||||
kdGraphics = 0x01
|
||||
consolePath = "/dev/tty0"
|
||||
fbPath = "/dev/fb0"
|
||||
drmModeConnected = 1
|
||||
drmModeTypePreferred = 1 << 3
|
||||
drmModePageFlipEvent = 0x01
|
||||
drmEventFlipComplete = 0x02
|
||||
|
||||
// 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
|
||||
// rather than a struct to sidestep any question of padding.
|
||||
const (
|
||||
viXres = 0
|
||||
viYres = 1
|
||||
viBitsPerPixel = 6
|
||||
viRedOffset = 8
|
||||
viGreenOffset = 11
|
||||
viBlueOffset = 14
|
||||
viScreenInfoLen = 40
|
||||
func drmIO(nr uintptr) uintptr { return drmIoctlBase<<8 | nr }
|
||||
func drmIOWR(nr, size uintptr) uintptr { return 3<<30 | size<<16 | drmIoctlBase<<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 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,
|
||||
// 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.
|
||||
type framebuffer struct {
|
||||
file *os.File
|
||||
tty *os.File
|
||||
mem []byte
|
||||
fd int
|
||||
back []byte
|
||||
w int
|
||||
h int
|
||||
pw int
|
||||
ph int
|
||||
stride int
|
||||
rShift uint
|
||||
gShift uint
|
||||
bShift uint
|
||||
|
||||
bufs [scanoutBuffers]scanout
|
||||
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 {
|
||||
@@ -58,88 +154,261 @@ func (fb *framebuffer) fromPanel(x, y int) (int, int) {
|
||||
return y, fb.pw - 1 - x
|
||||
}
|
||||
|
||||
// The kernel console draws into the same framebuffer, including a blinking
|
||||
// cursor and any keyboard echo, which fights every frame we write. Putting the
|
||||
// active console into graphics mode stops it touching the framebuffer at all.
|
||||
func takeConsole() (*os.File, error) {
|
||||
tty, err := os.OpenFile(consolePath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func cardResources(fd int) (crtcs, conns []uint32, err error) {
|
||||
var res drmModeCardRes
|
||||
if err := drmIoctl(fd, drmGetResources, unsafe.Pointer(&res)); err != nil {
|
||||
return nil, nil, fmt.Errorf("get resources: %w", err)
|
||||
}
|
||||
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, tty.Fd(),
|
||||
kdSetMode, kdGraphics); errno != 0 {
|
||||
tty.Close()
|
||||
return nil, fmt.Errorf("console to graphics mode: %w", errno)
|
||||
if res.countCRTCs == 0 || res.countConns == 0 {
|
||||
return nil, nil, fmt.Errorf("card has no crtcs or connectors")
|
||||
}
|
||||
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) {
|
||||
f, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
// The preferred mode is the panel's native one; anything else would be the
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
fb := &framebuffer{fd: fd, flips: make(chan struct{}, 1)}
|
||||
|
||||
var vi [viScreenInfoLen]uint32
|
||||
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
|
||||
fbioGetVScreenInfo, uintptr(unsafe.Pointer(&vi[0]))); errno != 0 {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("get screen info: %w", errno)
|
||||
}
|
||||
if vi[viBitsPerPixel] != 32 {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("only 32bpp supported, got %d", vi[viBitsPerPixel])
|
||||
// Without master the modeset below is refused, and taking it is also what
|
||||
// stops the kernel console drawing into the display behind us.
|
||||
if err := drmIoctl(fd, drmSetMaster, nil); err != nil {
|
||||
unix.Close(fd)
|
||||
return nil, fmt.Errorf("take drm master: %w", err)
|
||||
}
|
||||
|
||||
// Wake the panel; the console blanks it after a timeout.
|
||||
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)
|
||||
connID, crtcID, err := findDisplay(fd)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("mmap: %w", err)
|
||||
fb.close()
|
||||
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.tty, err = takeConsole()
|
||||
if err != nil {
|
||||
unix.Munmap(fb.mem)
|
||||
f.Close()
|
||||
return nil, err
|
||||
set := drmModeCrtc{
|
||||
setConnectorsPtr: uint64(uintptr(unsafe.Pointer(&fb.connID))),
|
||||
countConnectors: 1,
|
||||
crtcID: crtcID,
|
||||
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
|
||||
}
|
||||
|
||||
// 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() {
|
||||
fb.fill(rgb{})
|
||||
fb.flush()
|
||||
unix.Syscall(unix.SYS_IOCTL, fb.tty.Fd(), kdSetMode, kdText)
|
||||
fb.tty.Close()
|
||||
unix.Munmap(fb.mem)
|
||||
fb.file.Close()
|
||||
for i := range fb.bufs {
|
||||
if fb.bufs[i].mem != nil {
|
||||
unix.Munmap(fb.bufs[i].mem)
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
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 {
|
||||
@@ -197,9 +466,9 @@ func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) {
|
||||
a := uint32(cov)
|
||||
old := uint32(fb.back[o+0]) | uint32(fb.back[o+1])<<8 |
|
||||
uint32(fb.back[o+2])<<16 | uint32(fb.back[o+3])<<24
|
||||
orr := uint8(old >> fb.rShift)
|
||||
og := uint8(old >> fb.gShift)
|
||||
ob := uint8(old >> fb.bShift)
|
||||
orr := uint8(old >> xrgbRedShift)
|
||||
og := uint8(old >> xrgbGreenShift)
|
||||
ob := uint8(old >> xrgbBlueShift)
|
||||
mix := rgb{
|
||||
r: uint8((uint32(c.r)*a + uint32(orr)*(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)
|
||||
}
|
||||
|
||||
func (fb *framebuffer) flush() {
|
||||
copy(fb.mem, fb.back)
|
||||
// Copies into the scanout buffer furthest from being displayed and asks for it
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -602,14 +602,12 @@ func main() {
|
||||
|
||||
const (
|
||||
reportInterval = time.Second
|
||||
// Redraw fast so the panel feels live, but measure rates over a much longer
|
||||
// window than a frame, since a frame's worth of a bursty sender is noise.
|
||||
displayInterval = 16 * time.Millisecond
|
||||
// What a bucket covers. Deliberately not displayInterval: how often the
|
||||
// counters are read is a property of the measurement, and tying it to the
|
||||
// refresh 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.
|
||||
// What a bucket covers. Nothing to do with how often the panel is redrawn:
|
||||
// how often the counters are read is a property of the measurement, and
|
||||
// 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
|
||||
// runs over, so a step in the rate lands half this late.
|
||||
@@ -778,8 +776,6 @@ func run(aName, bName, sizesArg string,
|
||||
close(startTx)
|
||||
tick := time.NewTicker(reportInterval)
|
||||
defer tick.Stop()
|
||||
frame := time.NewTicker(displayInterval)
|
||||
defer frame.Stop()
|
||||
|
||||
views := make([]view, len(dirs))
|
||||
rows := make([]view, len(dirs))
|
||||
@@ -793,7 +789,8 @@ func run(aName, bName, sizesArg string,
|
||||
return nil
|
||||
case <-space:
|
||||
start = resetAll(dirs, stats)
|
||||
case now := <-frame.C:
|
||||
case <-disp.fb.flips:
|
||||
now := time.Now()
|
||||
px, py, down := touch.get()
|
||||
x, y := disp.fb.fromPanel(px, py)
|
||||
if disp.holdReset(x, y, down, now) {
|
||||
@@ -806,8 +803,10 @@ func run(aName, bName, sizesArg string,
|
||||
if m, ok := cfg.cableMetres(views); ok {
|
||||
cable = fmt.Sprintf("%.1f m", m)
|
||||
}
|
||||
disp.render(totalView(views), now.Sub(start),
|
||||
target*float64(len(dirs)), cable)
|
||||
if err := disp.render(totalView(views), now.Sub(start),
|
||||
target*float64(len(dirs)), cable); err != nil {
|
||||
return err
|
||||
}
|
||||
case now := <-tick.C:
|
||||
elapsed := now.Sub(start)
|
||||
// Length needs both directions, so every row is sampled before any of
|
||||
|
||||
@@ -58,7 +58,7 @@ type display struct {
|
||||
}
|
||||
|
||||
func newDisplay() (*display, error) {
|
||||
fb, err := openFramebuffer(fbPath)
|
||||
fb, err := openFramebuffer()
|
||||
if err != nil {
|
||||
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.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.drawResetButton()
|
||||
fb.flush()
|
||||
return fb.flush()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user