202 lines
4.5 KiB
Go
202 lines
4.5 KiB
Go
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"
|
|
)
|
|
|
|
// 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
|
|
tty *os.File
|
|
mem []byte
|
|
back []byte
|
|
w int
|
|
h int
|
|
stride int
|
|
rShift uint
|
|
gShift uint
|
|
bShift uint
|
|
}
|
|
|
|
// 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,
|
|
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)
|
|
|
|
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)<<fb.rShift | uint32(c.g)<<fb.gShift | uint32(c.b)<<fb.bShift
|
|
}
|
|
|
|
type rgb struct {
|
|
r, g, b uint8
|
|
}
|
|
|
|
func (fb *framebuffer) fill(c rgb) {
|
|
v := fb.pixel(c)
|
|
row := make([]byte, fb.stride)
|
|
for x := 0; x+4 <= fb.stride; x += 4 {
|
|
row[x+0] = byte(v)
|
|
row[x+1] = byte(v >> 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)
|
|
}
|