package main import ( "encoding/binary" "fmt" "math" "path/filepath" "unsafe" "golang.org/x/sys/unix" "g.fc.run/theater/cabletest/internal/drm" ) // Drawing lands in memory and is blitted to a buffer the display is not // reading, then swapped in whole at a vertical blank. Writing into the live // scanout buffer instead, as fbdev invites, races the beam: the blit takes a // few hundred microseconds and the display reads half of each frame. // // Two buffers would be enough to stop the panel tearing. 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. const scanoutBuffers = 4 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. type framebuffer struct { fd int back []byte scratch []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 drm.Offset(x, y, fb.stride, fb.pw) } 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 drm.ModeCardRes if err := drm.Ioctl(fd, drm.GetResources, 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 := drm.Ioctl(fd, drm.GetResources, 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) (drm.ModeInfo, error) { c := drm.ModeGetConnector{ConnectorID: connID} if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&c)); err != nil { return drm.ModeInfo{}, err } if c.CountModes == 0 { return drm.ModeInfo{}, fmt.Errorf("connector %d reported no modes", connID) } modes := make([]drm.ModeInfo, c.CountModes) q := drm.ModeGetConnector{ ConnectorID: connID, CountModes: c.CountModes, ModesPtr: uint64(uintptr(unsafe.Pointer(&modes[0]))), } if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&q)); err != nil { return drm.ModeInfo{}, err } if q.CountModes == 0 { return drm.ModeInfo{}, fmt.Errorf("connector %d reported no modes", connID) } for _, m := range modes[:q.CountModes] { if m.Type&drm.TypePreferred != 0 { return m, nil } } return modes[0], nil } func crtcFor(fd int, c drm.ModeGetConnector, crtcs []uint32) (uint32, error) { encoders := []uint32{c.EncoderID} if c.CountEncoders > 0 { list := make([]uint32, c.CountEncoders) q := drm.ModeGetConnector{ ConnectorID: c.ConnectorID, CountEncoders: c.CountEncoders, EncodersPtr: uint64(uintptr(unsafe.Pointer(&list[0]))), } if err := drm.Ioctl(fd, drm.GetConnector, unsafe.Pointer(&q)); err == nil { encoders = append(encoders, list[:q.CountEncoders]...) } } for _, id := range encoders { if id == 0 { continue } e := drm.ModeGetEncoder{EncoderID: id} if err := drm.Ioctl(fd, drm.GetEncoder, 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 == drm.EventFlipComplete { 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. drm.Ioctl(fb.fd, drm.DropMaster, nil) unix.Close(fb.fd) } type rgb struct { r, g, b uint8 } func pixel(c rgb) uint32 { return drm.Pack(c.r, c.g, c.b) } // A run of one repeated pixel, reused between calls: a whole panel row is the // longest anything here needs, and every draw is on the one render goroutine. func (fb *framebuffer) pixelRun(n int, c rgb) []byte { v := pixel(c) s := fb.scratch[:n] for i := 0; i+4 <= n; i += 4 { s[i+0] = byte(v) s[i+1] = byte(v >> 8) s[i+2] = byte(v >> 16) s[i+3] = byte(v >> 24) } return s } func (fb *framebuffer) fill(c rgb) { row := fb.pixelRun(fb.stride, c) 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 } span := fb.pixelRun((y1-y0)*4, c) for x := x0; x < x1; x++ { copy(fb.back[fb.offset(x, y1-1):], span) } } // The rectangle the corner radius sweeps around. Distance to it 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. type sweep struct { ix0, iy0, ix1, iy1 float64 r int } func (s sweep) pixel(fb *framebuffer, x, y int, c rgb) { fx, fy := float64(x), float64(y) dx := math.Max(math.Max(s.ix0-fx, fx-s.ix1), 0) dy := math.Max(math.Max(s.iy0-fy, fy-s.iy1), 0) cov := float64(s.r) - math.Sqrt(dx*dx+dy*dy) + 0.5 if cov <= 0 { return } fb.blend(x, y, c, uint8(math.Min(cov, 1)*255)) } // Partial coverage reaches no further than the corner blocks and the one line // of pixels the curve runs tangent to along each flat edge. The sweep contains // everything else outright, which fills as spans instead of a pixel at a time. 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) s := sweep{float64(x0 + r), float64(y0 + r), float64(x0 + w - 1 - r), float64(y0 + h - 1 - r), r} // With no radius the sweep never reaches a whole pixel, so the sliver it // leaves is partly covered throughout rather than solid anywhere. if r == 0 { for y := y0; y < y0+h; y++ { for x := x0; x < x0+w; x++ { s.pixel(fb, x, y, c) } } return } for j := 0; j < r; j++ { for i := 0; i < r; i++ { s.pixel(fb, x0+i, y0+j, c) s.pixel(fb, x0+w-1-i, y0+j, c) s.pixel(fb, x0+i, y0+h-1-j, c) s.pixel(fb, x0+w-1-i, y0+h-1-j, c) } } for x := x0 + r; x < x0+w-r; x++ { s.pixel(fb, x, y0, c) s.pixel(fb, x, y0+h-1, c) } for y := y0 + r; y < y0+h-r; y++ { s.pixel(fb, x0, y, c) s.pixel(fb, x0+w-1, y, c) } fb.rect(x0+r, y0+1, w-2*r, h-2, c) fb.rect(x0+1, y0+r, r-1, h-2*r, c) fb.rect(x0+w-r, y0+r, r-1, h-2*r, c) } // 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 := 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, og, ob := drm.Unpack(old) 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 := 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) } // 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 := drm.ModeCrtcPageFlip{ CrtcID: fb.crtcID, FBID: fb.bufs[next].fbID, Flags: drm.PageFlipEvent, } if err := drm.Ioctl(fb.fd, drm.PageFlip, unsafe.Pointer(&flip)); err != nil { return fmt.Errorf("page flip: %w", err) } fb.front = next return nil }