// 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 }