hdmistat/internal/display/display.go
2025-07-24 14:32:50 +02:00

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
}