310 lines
6.4 KiB
Go
310 lines
6.4 KiB
Go
package fbdraw
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
// FBDisplay renders to a Linux framebuffer device
|
|
type FBDisplay struct {
|
|
device string
|
|
file *os.File
|
|
info fbVarScreeninfo
|
|
fixInfo fbFixScreeninfo
|
|
data []byte
|
|
charWidth int
|
|
charHeight int
|
|
}
|
|
|
|
// fbFixScreeninfo from linux/fb.h
|
|
type fbFixScreeninfo struct {
|
|
ID [16]byte
|
|
SMEMStart uint64
|
|
SMEMLen uint32
|
|
Type uint32
|
|
TypeAux uint32
|
|
Visual uint32
|
|
XPanStep uint16
|
|
YPanStep uint16
|
|
YWrapStep uint16
|
|
_ uint16
|
|
LineLength uint32
|
|
MMIOStart uint64
|
|
MMIOLen uint32
|
|
Accel uint32
|
|
Capabilities uint16
|
|
Reserved [2]uint16
|
|
}
|
|
|
|
// fbVarScreeninfo from linux/fb.h
|
|
type fbVarScreeninfo struct {
|
|
XRes uint32
|
|
YRes uint32
|
|
XResVirtual uint32
|
|
YResVirtual uint32
|
|
XOffset uint32
|
|
YOffset uint32
|
|
BitsPerPixel uint32
|
|
Grayscale uint32
|
|
Red fbBitfield
|
|
Green fbBitfield
|
|
Blue fbBitfield
|
|
Transp fbBitfield
|
|
NonStd uint32
|
|
Activate uint32
|
|
Height uint32
|
|
Width uint32
|
|
AccelFlags uint32
|
|
PixClock uint32
|
|
LeftMargin uint32
|
|
RightMargin uint32
|
|
UpperMargin uint32
|
|
LowerMargin uint32
|
|
HSyncLen uint32
|
|
VSyncLen uint32
|
|
Sync uint32
|
|
VMode uint32
|
|
Rotate uint32
|
|
Colorspace uint32
|
|
Reserved [4]uint32
|
|
}
|
|
|
|
type fbBitfield struct {
|
|
Offset uint32
|
|
Length uint32
|
|
Right uint32
|
|
}
|
|
|
|
const (
|
|
fbiogetVscreeninfo = 0x4600
|
|
fbiogetFscreeninfo = 0x4602
|
|
)
|
|
|
|
// NewFBDisplay creates a display for a specific framebuffer device
|
|
func NewFBDisplay(device string) (*FBDisplay, error) {
|
|
file, err := os.OpenFile(device, os.O_RDWR, 0) //nolint:gosec
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening framebuffer %s: %w", device, err)
|
|
}
|
|
|
|
display := &FBDisplay{
|
|
device: device,
|
|
file: file,
|
|
}
|
|
|
|
// Get variable screen info
|
|
if err := display.getScreenInfo(); err != nil {
|
|
_ = file.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// Get fixed screen info
|
|
if err := display.getFixedInfo(); err != nil {
|
|
_ = file.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// Memory map the framebuffer
|
|
size := int(display.fixInfo.SMEMLen)
|
|
display.data, err = syscall.Mmap(
|
|
int(file.Fd()),
|
|
0,
|
|
size,
|
|
syscall.PROT_READ|syscall.PROT_WRITE,
|
|
syscall.MAP_SHARED,
|
|
)
|
|
if err != nil {
|
|
_ = file.Close()
|
|
return nil, fmt.Errorf("mmap framebuffer: %w", err)
|
|
}
|
|
|
|
// Calculate character dimensions (rough approximation)
|
|
display.charWidth = 8
|
|
display.charHeight = 16
|
|
|
|
return display, nil
|
|
}
|
|
|
|
// NewFBDisplayAuto auto-detects and opens the framebuffer
|
|
func NewFBDisplayAuto() (*FBDisplay, error) {
|
|
// Try common framebuffer devices
|
|
devices := []string{"/dev/fb0", "/dev/fb1", "/dev/graphics/fb0"}
|
|
|
|
for _, device := range devices {
|
|
if _, err := os.Stat(device); err == nil {
|
|
display, err := NewFBDisplay(device)
|
|
if err == nil {
|
|
return display, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("no framebuffer device found")
|
|
}
|
|
|
|
func (d *FBDisplay) getScreenInfo() error {
|
|
_, _, errno := syscall.Syscall(
|
|
syscall.SYS_IOCTL,
|
|
d.file.Fd(),
|
|
fbiogetVscreeninfo,
|
|
uintptr(unsafe.Pointer(&d.info)), //nolint:gosec
|
|
)
|
|
if errno != 0 {
|
|
return fmt.Errorf("ioctl FBIOGET_VSCREENINFO: %w", errno)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *FBDisplay) getFixedInfo() error {
|
|
_, _, errno := syscall.Syscall(
|
|
syscall.SYS_IOCTL,
|
|
d.file.Fd(),
|
|
fbiogetFscreeninfo,
|
|
uintptr(unsafe.Pointer(&d.fixInfo)), //nolint:gosec
|
|
)
|
|
if errno != 0 {
|
|
return fmt.Errorf("ioctl FBIOGET_FSCREENINFO: %w", errno)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Write renders a grid to the framebuffer
|
|
func (d *FBDisplay) Write(grid *CharGrid) error {
|
|
// Render grid to image
|
|
img, err := grid.Render()
|
|
if err != nil {
|
|
return fmt.Errorf("rendering grid: %w", err)
|
|
}
|
|
|
|
// Clear framebuffer
|
|
for i := range d.data {
|
|
d.data[i] = 0
|
|
}
|
|
|
|
// Copy image to framebuffer
|
|
bounds := img.Bounds()
|
|
bytesPerPixel := int(d.info.BitsPerPixel / 8)
|
|
lineLength := int(d.fixInfo.LineLength)
|
|
|
|
for y := bounds.Min.Y; y < bounds.Max.Y && y < int(d.info.YRes); y++ {
|
|
for x := bounds.Min.X; x < bounds.Max.X && x < int(d.info.XRes); x++ {
|
|
r, g, b, _ := img.At(x, y).RGBA()
|
|
|
|
offset := y*lineLength + x*bytesPerPixel
|
|
if offset+bytesPerPixel <= len(d.data) {
|
|
// Assuming 32-bit BGRA format (most common)
|
|
if bytesPerPixel == 4 {
|
|
d.data[offset+0] = byte(b >> 8)
|
|
d.data[offset+1] = byte(g >> 8)
|
|
d.data[offset+2] = byte(r >> 8)
|
|
d.data[offset+3] = 0xFF
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Size returns the display size in characters
|
|
func (d *FBDisplay) Size() (width, height int) {
|
|
width = int(d.info.XRes) / d.charWidth
|
|
height = int(d.info.YRes) / d.charHeight
|
|
return
|
|
}
|
|
|
|
// Close closes the framebuffer
|
|
func (d *FBDisplay) Close() error {
|
|
if d.data != nil {
|
|
if err := syscall.Munmap(d.data); err != nil {
|
|
log.Printf("munmap error: %v", err)
|
|
}
|
|
d.data = nil
|
|
}
|
|
|
|
if d.file != nil {
|
|
if err := d.file.Close(); err != nil {
|
|
return err
|
|
}
|
|
d.file = nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TerminalDisplay renders to the terminal using ANSI escape codes
|
|
type TerminalDisplay struct {
|
|
width int
|
|
height int
|
|
}
|
|
|
|
// NewTerminalDisplay creates a terminal display
|
|
func NewTerminalDisplay(width, height int) *TerminalDisplay {
|
|
return &TerminalDisplay{
|
|
width: width,
|
|
height: height,
|
|
}
|
|
}
|
|
|
|
// Write renders a grid to the terminal
|
|
func (d *TerminalDisplay) Write(grid *CharGrid) error {
|
|
// Clear screen
|
|
fmt.Print("\033[2J\033[H")
|
|
|
|
// Print ANSI representation
|
|
fmt.Print(grid.ToANSI())
|
|
|
|
return nil
|
|
}
|
|
|
|
// Size returns the terminal size in characters
|
|
func (d *TerminalDisplay) Size() (width, height int) {
|
|
return d.width, d.height
|
|
}
|
|
|
|
// Close is a no-op for terminal display
|
|
func (d *TerminalDisplay) Close() error {
|
|
// Clear screen one last time
|
|
fmt.Print("\033[2J\033[H")
|
|
return nil
|
|
}
|
|
|
|
// LogDisplay renders to a logger for debugging
|
|
type LogDisplay struct {
|
|
width int
|
|
height int
|
|
logger *log.Logger
|
|
}
|
|
|
|
// NewLogDisplay creates a log display
|
|
func NewLogDisplay(width, height int, logger *log.Logger) *LogDisplay {
|
|
if logger == nil {
|
|
logger = log.New(os.Stderr, "[fbdraw] ", log.LstdFlags)
|
|
}
|
|
|
|
return &LogDisplay{
|
|
width: width,
|
|
height: height,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Write logs the grid as text
|
|
func (d *LogDisplay) Write(grid *CharGrid) error {
|
|
d.logger.Printf("=== Frame ===\n%s\n", grid.ToText())
|
|
return nil
|
|
}
|
|
|
|
// Size returns the display size
|
|
func (d *LogDisplay) Size() (width, height int) {
|
|
return d.width, d.height
|
|
}
|
|
|
|
// Close is a no-op for log display
|
|
func (d *LogDisplay) Close() error {
|
|
return nil
|
|
}
|