package main import ( "fmt" "os" "unsafe" "golang.org/x/sys/unix" ) const ( fbioGetVScreenInfo = 0x4600 fbioBlank = 0x4611 fbBlankUnblank = 0 ) // 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 ) type framebuffer struct { file *os.File mem []byte back []byte w int h int stride int rShift uint gShift uint bShift uint } 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, w: int(vi[viXres]), h: int(vi[viYres]), 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.h, 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.h) return fb, nil } func (fb *framebuffer) 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.h; y++ { copy(fb.back[y*fb.stride:], row) } } func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) { v := fb.pixel(c) for y := y0; y < y0+h; y++ { if y < 0 || y >= fb.h { continue } base := y * fb.stride for x := x0; x < x0+w; x++ { if x < 0 || x >= fb.w { continue } o := base + x*4 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) } } } // 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 := y*fb.stride + x*4 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) }