package main import ( "fmt" "os" "unsafe" "golang.org/x/sys/unix" ) const ( fbioGetVScreenInfo = 0x4600 fbioBlank = 0x4611 fbBlankUnblank = 0 kdSetMode = 0x4b3a kdText = 0x00 kdGraphics = 0x01 consolePath = "/dev/tty0" fbPath = "/dev/fb0" ) // 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 ) // 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 back []byte w int h int pw int ph int stride int rShift uint gShift uint bShift uint } 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 } // 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 } 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) } return tty, nil } func openFramebuffer(path string) (*framebuffer, error) { f, err := os.OpenFile(path, os.O_RDWR, 0) if err != nil { return nil, err } 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]) } // 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) if err != nil { f.Close() return nil, fmt.Errorf("mmap: %w", 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 } return fb, nil } 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() } 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) } } // 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 >> fb.rShift) og := uint8(old >> fb.gShift) ob := uint8(old >> fb.bShift) 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) } func (fb *framebuffer) flush() { copy(fb.mem, fb.back) }