package main import ( "encoding/binary" "fmt" "math" "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 ( drmIoctlBase = 0x64 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 ) 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 { fd int back []byte w int h int pw int ph int stride int 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 { return x*fb.stride + (fb.pw-1-y)*4 } func (fb *framebuffer) fromPanel(x, y int) (int, int) { return y, fb.pw - 1 - x } 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 res.countCRTCs == 0 || res.countConns == 0 { return nil, nil, fmt.Errorf("card has no crtcs or connectors") } 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 } // 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< n { return } if typ == drmEventFlipComplete { select { case fb.flips <- struct{}{}: default: } } off += length } } } func (fb *framebuffer) 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)<> 8) row[x+2] = byte(v >> 16) row[x+3] = byte(v >> 24) } for y := 0; y < fb.ph; y++ { copy(fb.back[y*fb.stride:], row) } } // One logical column is contiguous after the turn, so it fills a span at a time. func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) { x1, y1 := min(x0+w, fb.w), min(y0+h, fb.h) x0, y0 = max(x0, 0), max(y0, 0) if x0 >= x1 || y0 >= y1 { return } v := fb.pixel(c) span := make([]byte, (y1-y0)*4) for i := 0; i+4 <= len(span); i += 4 { span[i+0] = byte(v) span[i+1] = byte(v >> 8) span[i+2] = byte(v >> 16) span[i+3] = byte(v >> 24) } for x := x0; x < x1; x++ { copy(fb.back[fb.offset(x, y1-1):], span) } } // Distance to the rectangle the corner radius sweeps around, which is zero // across the whole flat middle and grows only near a corner. Taking coverage // from that rather than from a plain inside test keeps the curves smooth // instead of stepped. func (fb *framebuffer) roundRect(x0, y0, w, h, r int, c rgb) { // A radius past half the shorter side has no meaning and would put the // swept rectangle inside out, which matters while something is growing from // nothing. r = min(r, min(w, h)/2) ix0, iy0 := float64(x0+r), float64(y0+r) ix1, iy1 := float64(x0+w-1-r), float64(y0+h-1-r) for y := y0; y < y0+h; y++ { for x := x0; x < x0+w; x++ { fx, fy := float64(x), float64(y) dx := math.Max(math.Max(ix0-fx, fx-ix1), 0) dy := math.Max(math.Max(iy0-fy, fy-iy1), 0) cov := float64(r) - math.Sqrt(dx*dx+dy*dy) + 0.5 if cov <= 0 { continue } fb.blend(x, y, c, uint8(math.Min(cov, 1)*255)) } } } // Blends src over the existing pixel, with cov as 0-255 coverage. func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) { if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 { return } o := fb.offset(x, y) if cov == 255 { v := fb.pixel(c) fb.back[o+0] = byte(v) fb.back[o+1] = byte(v >> 8) fb.back[o+2] = byte(v >> 16) fb.back[o+3] = byte(v >> 24) return } 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 >> 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), b: uint8((uint32(c.b)*a + uint32(ob)*(255-a)) / 255), } v := fb.pixel(mix) fb.back[o+0] = byte(v) fb.back[o+1] = byte(v >> 8) fb.back[o+2] = byte(v >> 16) fb.back[o+3] = byte(v >> 24) } // 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 }