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 { file *os.File info *fbVarScreenInfo memory []byte logger *slog.Logger } 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 ) // NewFramebufferDisplay creates a new framebuffer display func NewFramebufferDisplay(device string, logger *slog.Logger) (*FramebufferDisplay, error) { file, err := os.OpenFile(device, os.O_RDWR, 0) if err != nil { return nil, fmt.Errorf("opening framebuffer: %w", err) } var info fbVarScreenInfo _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), fbiogetVscreeninfo, uintptr(unsafe.Pointer(&info))) if errno != 0 { file.Close() return nil, fmt.Errorf("getting screen info: %v", errno) } size := int(info.XRes * info.YRes * info.BitsPerPixel / 8) memory, 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("mapping framebuffer: %w", err) } logger.Info("framebuffer initialized", "device", device, "width", info.XRes, "height", info.YRes, "bpp", info.BitsPerPixel) return &FramebufferDisplay{ file: file, info: &info, memory: memory, logger: logger, }, nil } // Show displays an image on the framebuffer func (d *FramebufferDisplay) Show(img *image.RGBA) error { bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() if width > int(d.info.XRes) { width = int(d.info.XRes) } if height > int(d.info.YRes) { height = int(d.info.YRes) } for y := 0; y < height; y++ { for x := 0; x < width; x++ { r, g, b, a := img.At(x, y).RGBA() r, g, b = r>>8, g>>8, b>>8 offset := (y*int(d.info.XRes) + x) * int(d.info.BitsPerPixel/8) if offset+3 < len(d.memory) { if d.info.BitsPerPixel == 32 { d.memory[offset] = byte(b) d.memory[offset+1] = byte(g) d.memory[offset+2] = byte(r) d.memory[offset+3] = byte(a >> 8) } else if d.info.BitsPerPixel == 24 { d.memory[offset] = byte(b) d.memory[offset+1] = byte(g) d.memory[offset+2] = byte(r) } } } } return nil } // Clear clears the framebuffer func (d *FramebufferDisplay) Clear() error { for i := range d.memory { d.memory[i] = 0 } return nil } // Close closes the framebuffer func (d *FramebufferDisplay) Close() error { if err := syscall.Munmap(d.memory); err != nil { d.logger.Error("unmapping framebuffer", "error", err) } return d.file.Close() }