225 lines
5.4 KiB
Go
225 lines
5.4 KiB
Go
// Package display provides framebuffer display functionality
|
|
package display
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"log/slog"
|
|
"os"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
// Display interface for showing images
|
|
type Display interface {
|
|
Show(img *image.RGBA) error
|
|
Clear() error
|
|
Close() error
|
|
}
|
|
|
|
// FramebufferDisplay implements Display for Linux framebuffer
|
|
type FramebufferDisplay struct {
|
|
device string
|
|
logger *slog.Logger
|
|
// Cached screen info
|
|
width uint32
|
|
height uint32
|
|
bpp uint32
|
|
}
|
|
|
|
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
|
|
_ [4]byte
|
|
}
|
|
|
|
type fbBitfield struct {
|
|
Offset uint32
|
|
Length uint32
|
|
Right uint32
|
|
}
|
|
|
|
const (
|
|
fbiogetVscreeninfo = 0x4600
|
|
bitsPerByte = 8
|
|
bpp32 = 32
|
|
bpp24 = 24
|
|
colorShift = 8
|
|
)
|
|
|
|
// NewFramebufferDisplay creates a new framebuffer display
|
|
func NewFramebufferDisplay(device string, logger *slog.Logger) (*FramebufferDisplay, error) {
|
|
// Open framebuffer device temporarily to get screen info
|
|
file, err := os.OpenFile(device, os.O_RDWR, 0) // #nosec G304 - device path is controlled
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening framebuffer: %w", err)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
var info fbVarScreenInfo
|
|
// #nosec G103 - required for framebuffer ioctl
|
|
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), fbiogetVscreeninfo,
|
|
uintptr(unsafe.Pointer(&info)))
|
|
if errno != 0 {
|
|
return nil, fmt.Errorf("getting screen info: %v", errno)
|
|
}
|
|
|
|
logger.Info("framebuffer initialized",
|
|
"device", device,
|
|
"width", info.XRes,
|
|
"height", info.YRes,
|
|
"bpp", info.BitsPerPixel)
|
|
|
|
return &FramebufferDisplay{
|
|
device: device,
|
|
logger: logger,
|
|
width: info.XRes,
|
|
height: info.YRes,
|
|
bpp: info.BitsPerPixel,
|
|
}, nil
|
|
}
|
|
|
|
// Show displays an image on the framebuffer
|
|
func (d *FramebufferDisplay) Show(img *image.RGBA) error {
|
|
if d == nil || d.device == "" {
|
|
return fmt.Errorf("invalid display")
|
|
}
|
|
|
|
// Open framebuffer device
|
|
file, err := os.OpenFile(d.device, os.O_RDWR, 0) // #nosec G304 - device path is controlled
|
|
if err != nil {
|
|
return fmt.Errorf("opening framebuffer: %w", err)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
// Get screen information (in case it changed)
|
|
var info fbVarScreenInfo
|
|
// #nosec G103 - required for framebuffer ioctl
|
|
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), fbiogetVscreeninfo,
|
|
uintptr(unsafe.Pointer(&info)))
|
|
if errno != 0 {
|
|
return fmt.Errorf("getting screen info: %v", errno)
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
width := bounds.Dx()
|
|
height := bounds.Dy()
|
|
|
|
if width > int(info.XRes) {
|
|
width = int(info.XRes)
|
|
}
|
|
if height > int(info.YRes) {
|
|
height = int(info.YRes)
|
|
}
|
|
|
|
// Create buffer for one line at a time
|
|
lineSize := int(info.XRes * info.BitsPerPixel / bitsPerByte)
|
|
line := make([]byte, lineSize)
|
|
|
|
// Write image data line by line
|
|
for y := 0; y < height; y++ {
|
|
// Clear line buffer
|
|
for i := range line {
|
|
line[i] = 0
|
|
}
|
|
|
|
// Fill line buffer with pixel data
|
|
for x := 0; x < width; x++ {
|
|
r, g, b, a := img.At(x, y).RGBA()
|
|
r, g, b = r>>colorShift, g>>colorShift, b>>colorShift
|
|
|
|
offset := x * int(info.BitsPerPixel/bitsPerByte)
|
|
if offset+3 < len(line) {
|
|
switch info.BitsPerPixel {
|
|
case bpp32:
|
|
line[offset] = byte(b)
|
|
line[offset+1] = byte(g)
|
|
line[offset+2] = byte(r)
|
|
line[offset+3] = byte(a >> colorShift)
|
|
case bpp24:
|
|
line[offset] = byte(b)
|
|
line[offset+1] = byte(g)
|
|
line[offset+2] = byte(r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Seek to correct position and write line
|
|
seekPos := int64(y) * int64(lineSize)
|
|
if _, err := file.Seek(seekPos, 0); err != nil {
|
|
return fmt.Errorf("seeking in framebuffer: %w", err)
|
|
}
|
|
if _, err := file.Write(line); err != nil {
|
|
return fmt.Errorf("writing to framebuffer: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Clear clears the framebuffer
|
|
func (d *FramebufferDisplay) Clear() error {
|
|
if d == nil || d.device == "" {
|
|
return fmt.Errorf("invalid display")
|
|
}
|
|
|
|
// Open framebuffer device
|
|
file, err := os.OpenFile(d.device, os.O_RDWR, 0) // #nosec G304 - device path is controlled
|
|
if err != nil {
|
|
return fmt.Errorf("opening framebuffer: %w", err)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
// Get screen information
|
|
var info fbVarScreenInfo
|
|
// #nosec G103 - required for framebuffer ioctl
|
|
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), fbiogetVscreeninfo,
|
|
uintptr(unsafe.Pointer(&info)))
|
|
if errno != 0 {
|
|
return fmt.Errorf("getting screen info: %v", errno)
|
|
}
|
|
|
|
// Create empty buffer
|
|
lineSize := int(info.XRes * info.BitsPerPixel / bitsPerByte)
|
|
emptyLine := make([]byte, lineSize)
|
|
|
|
// Write empty lines
|
|
for y := 0; y < int(info.YRes); y++ {
|
|
seekPos := int64(y) * int64(lineSize)
|
|
if _, err := file.Seek(seekPos, 0); err != nil {
|
|
return fmt.Errorf("seeking in framebuffer: %w", err)
|
|
}
|
|
if _, err := file.Write(emptyLine); err != nil {
|
|
return fmt.Errorf("writing to framebuffer: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close closes the framebuffer
|
|
func (d *FramebufferDisplay) Close() error {
|
|
// Nothing to close since we open/close on each operation
|
|
return nil
|
|
}
|
|
|
|
// GetWidth returns the framebuffer width
|
|
func (d *FramebufferDisplay) GetWidth() uint32 {
|
|
return d.width
|
|
}
|
|
|
|
// GetHeight returns the framebuffer height
|
|
func (d *FramebufferDisplay) GetHeight() uint32 {
|
|
return d.height
|
|
}
|