Linux Framebuffer 渲染:绕过 Wayland/X11 直接输出到显示屏

各位开发者、系统工程师以及对Linux底层图形机制感兴趣的朋友们,大家好。

今天,我们将深入探讨一个既经典又具有实践意义的主题:Linux Framebuffer 渲染,以及如何绕过上层图形环境(如 Wayland 或 X11),直接将像素输出到显示屏。这不仅仅是技术考古,更是在特定场景下(例如嵌入式系统、定制启动画面、性能敏感型应用、或者仅仅是为了深入理解图形栈)不可或缺的核心技能。

1. 绕过图形栈:为何以及何为 Linux Framebuffer

在现代Linux桌面环境中,我们习惯了由X Window System或Wayland这样的显示服务器来管理图形输出。它们提供了复杂的窗口管理、事件处理、硬件加速接口(如OpenGL/Vulkan),并抽象了底层显示硬件的细节。然而,在某些情况下,我们可能需要或必须绕过这些复杂的抽象层,直接与显示硬件对话。

为何要绕过 Wayland/X11?

  1. 嵌入式系统与资源受限环境: 许多嵌入式设备没有足够的资源运行完整的X或Wayland服务器。直接使用Framebuffer可以提供一个轻量级的图形界面。
  2. 定制启动画面 (Boot Splash Screens): 在系统启动过程中,X或Wayland尚未初始化。Framebuffer是显示启动Logo、进度条的唯一方式。
  3. 性能敏感型应用与Kiosk系统: 对于需要极致性能且没有多窗口需求的单应用系统,消除X/Wayland的开销可以减少延迟,提高响应速度。
  4. 调试与诊断: 在图形驱动或显示服务器出现问题时,Framebuffer提供了一个基础的显示输出,便于诊断问题。
  5. 理解底层机制: 深入学习Linux图形栈的基石,了解像素如何在屏幕上呈现,有助于更好地理解更高级的图形API。
  6. 特殊效果或专有硬件: 对于一些需要直接控制显示时序或进行非标准显示操作的专有硬件,Framebuffer提供了必要的低级接口。

何为 Linux Framebuffer?

Linux Framebuffer (通常缩写为 fbdev) 是Linux内核提供的一种抽象,用于管理图形显示硬件。它将显示内存(显存)映射到系统内存空间,使得用户空间的程序可以直接读写这块内存区域,从而控制屏幕上每个像素的颜色。它是一个字符设备,通常位于 /dev/fb0 (对于第一个显示器) 或 /dev/fb1 等。

你可以将其想象成一个巨大的二维数组,其中的每个元素代表屏幕上的一个像素。通过修改这个数组中的值,你就可以改变屏幕上对应像素的颜色。Framebuffer设备驱动程序负责将这些内存中的像素数据最终传输到显示器上。

这种直接操作内存的方式,是所有现代图形系统(包括X和Wayland)的基石。它们最终也需要一个底层机制来将渲染好的图像数据呈现在屏幕上。

接下来,我们将深入探讨 Framebuffer 的结构、编程接口以及如何利用它进行实际的图形渲染。

2. Linux Framebuffer 的核心概念与结构

要操作 Framebuffer,我们首先需要理解它的几个核心数据结构,它们通过 ioctl 系统调用暴露给用户空间。

2.1 设备文件与权限

Framebuffer 设备通常是 /dev/fb0。在大多数Linux发行版中,访问这个设备需要root权限,或者用户需要属于 video 用户组。

ls -l /dev/fb0

输出可能类似:
crw-rw---- 1 root video 29, 0 Jan 1 00:00 /dev/fb0

这表示 root 用户拥有读写权限,video 组的成员也拥有读写权限。如果你不是 root,你需要将你的用户添加到 video 组:

sudo usermod -a -G video $(whoami)

然后重新登录以使更改生效。

2.2 fb_fix_screeninfo:固定屏幕信息

这个结构体包含了 Framebuffer 设备的一些固定、不可更改的属性,例如设备名称、显存起始地址和长度等。

#include <linux/fb.h> // 通常在用户空间包含这个头文件

struct fb_fix_screeninfo {
    char id[16];        /* identification string eg "TT Builtin" */
    unsigned long smem_start;   /* Start of frame buffer mem */
                            /* (physical address) */
    __u32 smem_len;     /* Length of frame buffer mem */
    __u32 type;         /* see FB_TYPE_*        */
    __u32 type_aux;     /* Interleave for FB_TYPE_VGA_PLANES*/
    __u32 visual;       /* see FB_VISUAL_*      */
    __u16 xpanstep;     /* zero if no hardware panning  */
    __u16 ypanstep;     /* zero if no hardware panning  */
    __u16 ywrapstep;    /* zero if no hardware wrapping */
    __u32 line_length;  /* length of a line in bytes    */
    unsigned long mmio_start;   /* Start of Memory Mapped I/O */
                            /* (physical address) */
    __u32 mmio_len;     /* Length of Memory Mapped I/O */
    __u32 accel;        /* Type of acceleration available (FB_ACCEL_*) */
    __u16 reserved[3];  /* Reserved for future compatibility */
};

关键字段解释:

  • id: Framebuffer设备的名称,如 "vc4drmfb" (树莓派) 或 "efi vga" (UEFI引导)。
  • smem_start: 显存的物理起始地址。对于用户空间程序,我们通常通过 mmap 获取其虚拟地址。
  • smem_len: 显存的总长度,单位是字节。这是 mmap 时需要映射的区域大小。
  • type: Framebuffer的类型。常见的有 FB_TYPE_PACKED_PIXELS (像素紧密排列)。
  • visual: 视觉类型。常见的有 FB_VISUAL_TRUECOLOR (真彩色,每个像素的RGB值独立存储)。
  • line_length: 非常重要! 一行像素的字节长度。这通常是 xres_virtual * (bits_per_pixel / 8),但可能因为内存对齐或其他硬件限制而更大。它决定了你在内存中从一行跳到下一行需要跳过多少字节。

2.3 fb_var_screeninfo:可变屏幕信息

这个结构体包含了 Framebuffer 设备的一些可变属性,例如分辨率、颜色深度、屏幕偏移量等。这些属性可以通过 ioctl 进行修改。

#include <linux/fb.h>

struct fb_var_screeninfo {
    __u32 xres;         /* visible resolution       */
    __u32 yres;         /* visible resolution       */
    __u32 xres_virtual; /* virtual resolution       */
    __u32 yres_virtual; /* virtual resolution       */
    __u32 xoffset;      /* offset from virtual to visible */
    __u32 yoffset;      /* offset from virtual to visible */

    __u32 bits_per_pixel;   /* bits per pixel, 8, 16, 24, 32 */
    __u32 grayscale;    /* != 0 for grayscale */
    /* bitfield in fb mem if true color,
     * else only length is significant */
    struct fb_bitfield red;
    struct fb_bitfield green;
    struct fb_bitfield blue;
    struct fb_bitfield transp;  /* transparency        */

    __u32 nonstd;       /* != 0 Non standard pixel format */

    __u32 activate;     /* see FB_ACTIVATE_*        */

    __u32 height;       /* height of picture in mm    */
    __u32 width;        /* width of picture in mm     */

    __u32 accel_flags;  /* (OBSOLETE) see FB_ACCEL_X*   */

    /* Timing: All values in pixclocks, except pixclock (ps) */
    __u32 pixclock;     /* pixel clock in ps (pico seconds) */
    __u32 left_margin;  /* time from sync to picture    */
    __u32 right_margin; /* time from picture to sync    */
    __u32 upper_margin; /* time from sync to picture    */
    __u32 lower_margin; /* time from picture to sync    */
    __u32 hsync_len;    /* length of horizontal sync    */
    __u32 vsync_len;    /* length of vertical sync    */
    __u32 sync;         /* see FB_SYNC_*        */
    __u32 vmode;        /* see FB_VMODE_*       */
    __u32 rotate;       /* angle we rotate counter clockwise */
    __u32 colorspace;   /* colorspace for P_TYPE_YUV_... */
    __u32 reserved[4];  /* Reserved for future compatibility */
};

struct fb_bitfield {
    __u32 offset;       /* bits offset from LSB */
    __u32 length;       /* length of bitfield   */
    __u32 msb_right;    /* 0 = LSB first, 1 = MSB first */
};

关键字段解释:

  • xres, yres: 当前可见屏幕的分辨率。例如 1920×1080。
  • xres_virtual, yres_virtual: 虚拟屏幕的分辨率。这可以大于可见分辨率,用于实现双缓冲、屏幕平移或滚动。例如,如果 yres_virtualyres 的两倍,你就可以拥有两个完整的屏幕缓冲区。
  • xoffset, yoffset: 可见屏幕在虚拟屏幕中的偏移量。通过修改 yoffset,可以实现页面翻转 (page flipping) 来进行双缓冲。
  • bits_per_pixel: 每个像素的位数 (颜色深度)。常见的有 8 (256色), 16 (RGB565), 24 (真彩色), 32 (ARGB或XRGB)。
  • red, green, blue, transp: fb_bitfield 结构体定义了每个颜色分量在像素数据中的位偏移和长度。这对于正确地构造像素颜色值至关重要。
    • offset: 该颜色分量从像素数据最低有效位 (LSB) 开始的偏移量。
    • length: 该颜色分量所占的位数。
    • msb_right: 通常为 0,表示 LSB 优先。

示例:32位真彩色 (XRGB8888)

假设 bits_per_pixel 是 32。

  • red.offset = 16, red.length = 8
  • green.offset = 8, green.length = 8
  • blue.offset = 0, blue.length = 8
  • transp.offset = 24, transp.length = 8 (或 transp.length = 0 如果没有透明度)

这意味着一个32位的像素 0xAARRGGBB

  • A (Alpha/Transparency) 占据第 24-31 位
  • R (Red) 占据第 16-23 位
  • G (Green) 占据第 8-15 位
  • B (Blue) 占据第 0-7 位

示例:16位真彩色 (RGB565)

假设 bits_per_pixel 是 16。

  • red.offset = 11, red.length = 5
  • green.offset = 5, green.length = 6
  • blue.offset = 0, blue.length = 5
  • transp.length = 0

这意味着一个16位的像素:

  • R (Red) 占据第 11-15 位 (5位)
  • G (Green) 占据第 5-10 位 (6位)
  • B (Blue) 占据第 0-4 位 (5位)

理解这些位域是正确构造像素颜色值的关键。

3. 与 Framebuffer 交互:编程接口

现在我们知道了 Framebuffer 的基本结构,接下来是如何通过 C 语言代码与之交互。

3.1 打开设备与获取信息

我们使用标准的文件操作函数 open 来打开 Framebuffer 设备,然后使用 ioctl 来获取其固定和可变信息。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <linux/fb.h>

// 全局变量,方便访问
static int fbfd = 0;
static struct fb_var_screeninfo vinfo;
static struct fb_fix_screeninfo finfo;
static char *fbp = 0; // Framebuffer 内存指针
static long screensize = 0;

// 颜色转换函数,稍后实现
static unsigned int rgb_to_pixel(unsigned char r, unsigned char g, unsigned char b);

int fb_init(const char *dev_path) {
    // 1. 打开 Framebuffer 设备
    fbfd = open(dev_path, O_RDWR);
    if (fbfd == -1) {
        perror("Error: cannot open framebuffer device");
        return -1;
    }
    printf("The framebuffer device was opened successfully.n");

    // 2. 获取固定屏幕信息
    if (ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo) == -1) {
        perror("Error reading fixed information");
        return -1;
    }
    printf("Fixed screen info:n");
    printf("  id: %sn", finfo.id);
    printf("  smem_len: %d bytesn", finfo.smem_len);
    printf("  line_length: %d bytesn", finfo.line_length);

    // 3. 获取可变屏幕信息
    if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo) == -1) {
        perror("Error reading variable information");
        return -1;
    }
    printf("Variable screen info:n");
    printf("  Resolution: %dx%dn", vinfo.xres, vinfo.yres);
    printf("  Virtual Resolution: %dx%dn", vinfo.xres_virtual, vinfo.yres_virtual);
    printf("  Bits per pixel: %dn", vinfo.bits_per_pixel);
    printf("  Red: offset=%d, length=%dn", vinfo.red.offset, vinfo.red.length);
    printf("  Green: offset=%d, length=%dn", vinfo.green.offset, vinfo.green.length);
    printf("  Blue: offset=%d, length=%dn", vinfo.blue.offset, vinfo.blue.length);
    printf("  Transparency: offset=%d, length=%dn", vinfo.transp.offset, vinfo.transp.length);

    // 4. 计算屏幕总大小 (字节)
    screensize = finfo.smem_len; // 或者 vinfo.xres_virtual * vinfo.yres_virtual * vinfo.bits_per_pixel / 8;
                               // 使用 finfo.smem_len 更安全,因为它反映了实际分配的显存大小

    // 5. 将 Framebuffer 内存映射到用户空间
    fbp = (char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);
    if ((intptr_t)fbp == -1) { // 检查mmap是否失败,-1转换为char*可能不是NULL
        perror("Error: failed to mmap framebuffer device to memory");
        close(fbfd);
        return -1;
    }
    printf("The framebuffer device was mapped to memory successfully.n");

    return 0;
}

void fb_exit() {
    if (fbp != (char *)-1 && fbp != 0) {
        munmap(fbp, screensize);
        fbp = 0;
    }
    if (fbfd != 0) {
        close(fbfd);
        fbfd = 0;
    }
    printf("Framebuffer device closed and memory unmapped.n");
}

代码解释:

  1. open("/dev/fb0", O_RDWR): 打开 Framebuffer 设备文件,允许读写操作。
  2. ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo): 使用 ioctl 系统调用获取 Framebuffer 的固定信息。FBIOGET_FSCREENINFO 是一个宏,表示获取固定屏幕信息的操作码。
  3. ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo): 获取 Framebuffer 的可变信息。FBIOGET_VSCREENINFO 是获取可变屏幕信息的操作码。
  4. screensize = finfo.smem_len: 获取 Framebuffer 内存的总大小。这是 mmap 函数所需的长度。
  5. mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0): 这是核心步骤。它将 Framebuffer 设备的物理内存区域映射到当前进程的虚拟地址空间。
    • 0: 让内核选择一个合适的虚拟地址。
    • screensize: 映射的字节数。
    • PROT_READ | PROT_WRITE: 映射区域可读可写。
    • MAP_SHARED: 映射区域的更改会写入到文件中(即实际的显存),并且其他进程也能看到这些更改。
    • fbfd: 文件描述符。
    • 0: 映射的偏移量,从文件开头开始。
    • 返回的 fbp 是一个 char * 指针,指向映射内存的起始地址。我们可以通过操作这个指针来修改显存内容。
  6. munmap(fbp, screensize): 在程序结束时,调用 munmap 解除内存映射。
  7. close(fbfd): 关闭文件描述符。

3.2 颜色转换函数

由于不同的 Framebuffer 设备可能有不同的颜色深度和位域排列,我们需要一个通用的函数将标准的 RGB (0-255) 值转换为 Framebuffer 期望的像素格式。

static unsigned int rgb_to_pixel(unsigned char r, unsigned char g, unsigned char b) {
    unsigned int pixel_value = 0;

    // 确保 R, G, B 值在 0-255 范围内
    r = r > 255 ? 255 : r;
    g = g > 255 ? 255 : g;
    b = b > 255 ? 255 : b;

    // 根据 Framebuffer 的颜色位域信息进行转换
    // 注意:这里的转换是基于 RGB888 到 Framebuffer 的转换。
    // 如果 Framebuffer 是 RGB565,则需要进行位移和截断。

    switch (vinfo.bits_per_pixel) {
        case 16: // RGB565
            // 假设 Framebuffer 期望的是 B(5bit) G(6bit) R(5bit) 顺序,即低位是B,高位是R
            // 实际位域由 vinfo.red/green/blue.offset 和 .length 决定
            // 这里我们使用一个通用的方法来构造像素值
            pixel_value |= ((r >> (8 - vinfo.red.length)) & ((1 << vinfo.red.length) - 1)) << vinfo.red.offset;
            pixel_value |= ((g >> (8 - vinfo.green.length)) & ((1 << vinfo.green.length) - 1)) << vinfo.green.offset;
            pixel_value |= ((b >> (8 - vinfo.blue.length)) & ((1 << vinfo.blue.length) - 1)) << vinfo.blue.offset;
            break;
        case 24: // RGB888 或 BGR888
        case 32: // ARGB8888 或 XRGB8888
            // 对于 24/32 位,通常是直接将 8-bit R/G/B 放到对应的位置
            pixel_value |= (r << vinfo.red.offset);
            pixel_value |= (g << vinfo.green.offset);
            pixel_value |= (b << vinfo.blue.offset);
            // 如果有透明度通道,通常设置为不透明 (0xFF)
            if (vinfo.transp.length > 0) {
                pixel_value |= (0xFF << vinfo.transp.offset);
            }
            break;
        case 8: // 256色,需要一个调色板。这里暂时不处理,假设是灰度或其他简单映射
            // 简单的灰度映射
            pixel_value = (unsigned int)((r + g + b) / 3);
            break;
        default:
            fprintf(stderr, "Unsupported bits_per_pixel: %dn", vinfo.bits_per_pixel);
            return 0; // 默认返回黑色
    }
    return pixel_value;
}

rgb_to_pixel 函数详解:

这个函数是 Framebuffer 渲染中至关重要的一环,因为它将我们熟悉的 (R, G, B) 颜色值转换为 Framebuffer 显存中存储的实际像素格式。

  • 参数 r, g, b: 标准的 8 位颜色分量,范围从 0 到 255。
  • switch (vinfo.bits_per_pixel): 根据 Framebuffer 的颜色深度进行不同的处理。
    • case 16 (RGB565):
      • r >> (8 - vinfo.red.length): 将 8 位的 r 值右移,使其适应 Framebuffer 中红色分量的位数。例如,如果 vinfo.red.length 是 5,则 8 - 5 = 3r 右移 3 位,将 8 位的 r 映射到 5 位。
      • & ((1 << vinfo.red.length) - 1): 创建一个掩码,确保值不超过 Framebuffer 红色分量的最大值(例如 5 位是 0x1F)。
      • << vinfo.red.offset: 将处理后的红色值左移到其在像素值中的正确偏移位置。
      • 绿色和蓝色分量也进行类似处理,只是位数不同。
    • case 24 / case 32 (RGB888 / XRGB8888):
      • 通常,对于 24 或 32 位深度的 Framebuffer,每个颜色分量都是 8 位。所以直接将 r, g, b 左移到其 offset 即可。
      • 如果 transp.length > 0,表示有透明度通道,我们通常将其设置为 0xFF(完全不透明)。
    • case 8 (256色):
      • 这种情况比较特殊,通常需要一个颜色查找表 (Palette)。这里为了简化,只做了一个简单的灰度转换。在实际应用中,你需要加载或定义一个调色板,然后将 RGB 值映射到调色板中的索引。

3.3 绘制像素

有了 fbp 指针和 rgb_to_pixel 函数,我们就可以在屏幕上绘制单个像素了。

// 绘制单个像素的函数
void draw_pixel(int x, int y, unsigned char r, unsigned char g, unsigned char b) {
    if (!fbp || x < 0 || x >= vinfo.xres || y < 0 || y >= vinfo.yres) {
        return; // 越界或未初始化
    }

    unsigned int pixel_color = rgb_to_pixel(r, g, b);

    // 计算像素在显存中的偏移量
    // 偏移量 = (y * 每行字节数) + (x * 每个像素字节数)
    long location = (x + vinfo.xoffset) * (vinfo.bits_per_pixel / 8) +
                    (y + vinfo.yoffset) * finfo.line_length;

    // 将像素数据写入显存
    switch (vinfo.bits_per_pixel) {
        case 8:
            *((char*)(fbp + location)) = (char)pixel_color;
            break;
        case 16:
            *((unsigned short*)(fbp + location)) = (unsigned short)pixel_color;
            break;
        case 24: // 24位像素通常按3个字节存储
            // 注意:这里需要考虑字节序和颜色分量存储顺序
            // 假设是 RGB 或 BGR 顺序
            *((char*)(fbp + location + (vinfo.red.offset / 8))) = (char)(r);
            *((char*)(fbp + location + (vinfo.green.offset / 8))) = (char)(g);
            *((char*)(fbp + location + (vinfo.blue.offset / 8))) = (char)(b);
            break;
        case 32:
            *((unsigned int*)(fbp + location)) = pixel_color;
            break;
        default:
            // 不支持的颜色深度
            break;
    }
}

draw_pixel 函数详解:

  • location 计算: 这是最关键的一步,用于确定 (x, y) 坐标对应的像素在 fbp 内存中的精确字节偏移量。
    • (y + vinfo.yoffset) * finfo.line_length: 计算当前行 (考虑虚拟屏幕偏移 yoffset) 的起始字节偏移。finfo.line_length 是每行实际的字节数,它可能大于 xres * (bits_per_pixel / 8)
    • (x + vinfo.xoffset) * (vinfo.bits_per_pixel / 8): 计算当前列 (考虑虚拟屏幕偏移 xoffset) 的字节偏移。vinfo.bits_per_pixel / 8 是每个像素的字节数。
  • 写入显存: 根据 bits_per_pixel 的不同,我们使用不同大小的指针来写入数据。
    • char* 用于 8 位像素。
    • unsigned short* 用于 16 位像素。
    • unsigned int* 用于 32 位像素。
    • 对于 24 位像素,由于它不是一个标准的字长,通常需要按字节写入 R、G、B 分量。这里简化处理,直接用 r, g, b,实际应该用 pixel_color 分解后的字节。一个更严谨的 24 位写入方式是:
      // 假设 pixel_color 已经包含了正确的 24 位值 (例如,高8位为0)
      // 并且我们知道 R G B 的字节顺序
      // 这需要更精确地使用 vinfo.red/green/blue.offset 来确定每个字节的位置
      // 简单起见,这里假设是 BGR 顺序
      *((char*)(fbp + location + 0)) = (char)(pixel_color & 0xFF);         // Blue
      *((char*)(fbp + location + 1)) = (char)((pixel_color >> 8) & 0xFF);  // Green
      *((char*)(fbp + location + 2)) = (char)((pixel_color >> 16) & 0xFF); // Red

      但由于 rgb_to_pixel 已经将 R/G/B 放置在 pixel_color 的正确位域,对于 24/32 位,直接 *((unsigned int*)(fbp + location)) = pixel_color; 是最简洁且通常正确的方式(因为 pixel_color 的高位为0,不会影响 24 位情况下的低 3 字节)。如果 pixel_color 有透明度通道,并且 Framebuffer 是 24 位,透明度信息会被截断。

3.4 清屏操作

清屏就是用单一颜色填充整个可见屏幕区域。

void clear_screen(unsigned char r, unsigned char g, unsigned char b) {
    if (!fbp) {
        return;
    }

    unsigned int pixel_color = rgb_to_pixel(r, g, b);
    long bytes_per_pixel = vinfo.bits_per_pixel / 8;

    // 对于简单填充,可以直接使用 memset 或循环写入
    // 考虑双缓冲,只清当前可见缓冲区
    for (int y = 0; y < vinfo.yres; y++) {
        for (int x = 0; x < vinfo.xres; x++) {
            long location = (x + vinfo.xoffset) * bytes_per_pixel +
                            (y + vinfo.yoffset) * finfo.line_length;
            switch (vinfo.bits_per_pixel) {
                case 8:
                    *((char*)(fbp + location)) = (char)pixel_color;
                    break;
                case 16:
                    *((unsigned short*)(fbp + location)) = (unsigned short)pixel_color;
                    break;
                case 24:
                    // 24位清屏需按字节填充
                    *((char*)(fbp + location + (vinfo.blue.offset / 8))) = b;
                    *((char*)(fbp + location + (vinfo.green.offset / 8))) = g;
                    *((char*)(fbp + location + (vinfo.red.offset / 8))) = r;
                    break;
                case 32:
                    *((unsigned int*)(fbp + location)) = pixel_color;
                    break;
            }
        }
    }
}

优化清屏:
上述清屏方法是逐像素写入,效率较低。对于整个区域填充,可以使用 memsetmemcpy 进行块操作。

// 优化后的清屏函数,仅适用于单色填充且像素字节数是 1, 2, 4 字节的情况
void clear_screen_optimized(unsigned char r, unsigned char g, unsigned char b) {
    if (!fbp) {
        return;
    }

    unsigned int pixel_color = rgb_to_pixel(r, g, b);
    long bytes_per_pixel = vinfo.bits_per_pixel / 8;

    // 只清当前可见屏幕区域
    for (int y = 0; y < vinfo.yres; y++) {
        long line_start_offset = (y + vinfo.yoffset) * finfo.line_length;
        char *line_ptr = fbp + line_start_offset + (vinfo.xoffset * bytes_per_pixel);

        if (bytes_per_pixel == 1) {
            memset(line_ptr, (char)pixel_color, vinfo.xres * bytes_per_pixel);
        } else if (bytes_per_pixel == 2) {
            unsigned short val = (unsigned short)pixel_color;
            for (int x = 0; x < vinfo.xres; x++) {
                *((unsigned short*)(line_ptr + x * bytes_per_pixel)) = val;
            }
        } else if (bytes_per_pixel == 4) {
            unsigned int val = pixel_color;
            for (int x = 0; x < vinfo.xres; x++) {
                *((unsigned int*)(line_ptr + x * bytes_per_pixel)) = val;
            }
        } else if (vinfo.bits_per_pixel == 24) {
            // 24位依然需要逐像素写入,或者实现一个更复杂的 memcpy 循环
            unsigned char b_val = b;
            unsigned char g_val = g;
            unsigned char r_val = r;
            for (int x = 0; x < vinfo.xres; x++) {
                char *pixel_ptr = line_ptr + x * 3; // 24 bits = 3 bytes
                *((char*)(pixel_ptr + (vinfo.blue.offset / 8))) = b_val;
                *((char*)(pixel_ptr + (vinfo.green.offset / 8))) = g_val;
                *((char*)(pixel_ptr + (vinfo.red.offset / 8))) = r_val;
            }
        }
    }
}

3.5 绘制图形:线段与矩形

基于 draw_pixel 函数,我们可以实现更复杂的图形绘制。

绘制线段 (Bresenham’s Line Algorithm):
Bresenham 算法是一种高效的线段绘制算法,它只使用整数运算,避免了浮点数计算,非常适合嵌入式系统。

void draw_line(int x0, int y0, int x1, int y1, unsigned char r, unsigned char g, unsigned char b) {
    int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
    int dy = abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
    int err = (dx > dy ? dx : -dy) / 2, e2;

    for (;;) {
        draw_pixel(x0, y0, r, g, b);
        if (x0 == x1 && y0 == y1) break;
        e2 = err;
        if (e2 > -dx) { err -= dy; x0 += sx; }
        if (e2 < dy) { err += dx; y0 += sy; }
    }
}

绘制矩形:

void draw_rect(int x, int y, int w, int h, unsigned char r, unsigned char g, unsigned char b, int filled) {
    if (x < 0) x = 0;
    if (y < 0) y = 0;
    if (x + w > vinfo.xres) w = vinfo.xres - x;
    if (y + h > vinfo.yres) h = vinfo.yres - y;

    if (w <= 0 || h <= 0) return;

    if (filled) {
        // 填充矩形
        unsigned int pixel_color = rgb_to_pixel(r, g, b);
        long bytes_per_pixel = vinfo.bits_per_pixel / 8;

        for (int current_y = y; current_y < y + h; current_y++) {
            long start_location = (x + vinfo.xoffset) * bytes_per_pixel +
                                  (current_y + vinfo.yoffset) * finfo.line_length;
            char *current_row_ptr = fbp + start_location;

            if (bytes_per_pixel == 1) {
                memset(current_row_ptr, (char)pixel_color, w * bytes_per_pixel);
            } else if (bytes_per_pixel == 2) {
                unsigned short val = (unsigned short)pixel_color;
                for (int current_x = 0; current_x < w; current_x++) {
                    *((unsigned short*)(current_row_ptr + current_x * bytes_per_pixel)) = val;
                }
            } else if (bytes_per_pixel == 4) {
                unsigned int val = pixel_color;
                for (int current_x = 0; current_x < w; current_x++) {
                    *((unsigned int*)(current_row_ptr + current_x * bytes_per_pixel)) = val;
                }
            } else if (vinfo.bits_per_pixel == 24) {
                unsigned char b_val = b;
                unsigned char g_val = g;
                unsigned char r_val = r;
                for (int current_x = 0; current_x < w; current_x++) {
                    char *pixel_ptr = current_row_ptr + current_x * 3; // 24 bits = 3 bytes
                    *((char*)(pixel_ptr + (vinfo.blue.offset / 8))) = b_val;
                    *((char*)(pixel_ptr + (vinfo.green.offset / 8))) = g_val;
                    *((char*)(pixel_ptr + (vinfo.red.offset / 8))) = r_val;
                }
            }
        }
    } else {
        // 绘制边框
        draw_line(x, y, x + w - 1, y, r, g, b); // Top
        draw_line(x, y + h - 1, x + w - 1, y + h - 1, r, g, b); // Bottom
        draw_line(x, y, x, y + h - 1, r, g, b); // Left
        draw_line(x + w - 1, y, x + w - 1, y + h - 1, r, g, b); // Right
    }
}

4. 双缓冲与页面翻转

直接在屏幕上绘制可能会导致画面闪烁 (tearing),尤其是在动画或快速更新的场景中。为了解决这个问题,我们通常使用双缓冲 (Double Buffering) 技术。

原理:

  1. 在显存中分配两个(或更多)完整的屏幕缓冲区。这通过设置 vinfo.yres_virtual 大于 vinfo.yres 来实现。例如,如果 yres_virtual = 2 * yres,则有两个缓冲区。
  2. 程序在其中一个不可见的缓冲区(后台缓冲区)上进行所有绘图操作。
  3. 当一帧图像绘制完成后,通过修改 vinfo.yoffset 来将后台缓冲区设置为可见,同时将之前的可见缓冲区切换到后台。这个操作称为页面翻转 (Page Flipping)
  4. 页面翻转通常是硬件原子操作,因此切换是瞬间完成的,避免了画面撕裂。

实现:

fb_init 中,我们可以尝试将 yres_virtual 设置为 2 * yres 来启用双缓冲。

// 在 fb_init 函数中,获取 vinfo 后
    struct fb_var_screeninfo old_vinfo = vinfo; // 保存原始 vinfo
    vinfo.yres_virtual = vinfo.yres * 2; // 尝试启用双缓冲
    vinfo.yoffset = 0; // 默认显示第一个缓冲区

    if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo) == -1) {
        perror("Error setting variable information for double buffering, trying single buffer");
        vinfo = old_vinfo; // 失败则恢复原始设置 (单缓冲)
        if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo) == -1) {
             perror("Error restoring original variable information");
             // 致命错误,无法继续
             close(fbfd);
             return -1;
        }
    }
    // 重新获取实际设置的 vinfo,因为硬件可能不接受我们请求的 yres_virtual
    if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo) == -1) {
        perror("Error reading variable information after setting");
        close(fbfd);
        return -1;
    }
    printf("Actual Virtual Resolution: %dx%dn", vinfo.xres_virtual, vinfo.yres_virtual);
    if (vinfo.yres_virtual > vinfo.yres) {
        printf("Double buffering enabled.n");
    } else {
        printf("Double buffering not available or failed, using single buffer.n");
    }

    // 重新计算 screensize,如果 yres_virtual 改变
    screensize = finfo.line_length * vinfo.yres_virtual;
    // 重新 mmap,因为 screensize 可能改变
    if (fbp != (char *)-1 && fbp != 0) {
        munmap(fbp, screensize); // 先解除之前的映射
    }
    fbp = (char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);
    if ((intptr_t)fbp == -1) {
        perror("Error: failed to mmap framebuffer device to memory after setting virtual resolution");
        close(fbfd);
        return -1;
    }

页面翻转函数:

// 当前显示的缓冲区索引
static int current_buffer_idx = 0;
static int num_buffers = 1; // 默认单缓冲

void fb_flip() {
    if (vinfo.yres_virtual <= vinfo.yres) {
        // 没有双缓冲,直接返回
        return;
    }

    num_buffers = vinfo.yres_virtual / vinfo.yres; // 确定有多少个缓冲区

    // 切换到下一个缓冲区
    current_buffer_idx = (current_buffer_idx + 1) % num_buffers;
    vinfo.yoffset = current_buffer_idx * vinfo.yres;

    // 更新 Framebuffer 变量信息,触发页面翻转
    if (ioctl(fbfd, FBIOPAN_DISPLAY, &vinfo) == -1) { // FBIOPAN_DISPLAY 也可以用于页面翻转
        // 如果 FBIOPAN_DISPLAY 不支持,尝试 FBIOPUT_VSCREENINFO
        if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo) == -1) {
            perror("Error flipping display buffer");
        }
    }
}

// 获取当前绘图缓冲区的指针
char* get_current_draw_buffer() {
    if (vinfo.yres_virtual <= vinfo.yres) {
        // 单缓冲,直接返回主缓冲区
        return fbp;
    }
    // 返回非当前可见的缓冲区
    int draw_buffer_idx = (current_buffer_idx + 1) % num_buffers;
    return fbp + draw_buffer_idx * finfo.line_length * vinfo.yres;
}

// 修改绘制函数,使其在当前绘图缓冲区上操作
// 例如,draw_pixel 函数需要修改为:
void draw_pixel_buffered(int x, int y, unsigned char r, unsigned g, unsigned b) {
    if (!fbp || x < 0 || x >= vinfo.xres || y < 0 || y >= vinfo.yres) {
        return;
    }

    unsigned int pixel_color = rgb_to_pixel(r, g, b);
    char *draw_buffer_ptr = get_current_draw_buffer();

    // 偏移量 = (y * 每行字节数) + (x * 每个像素字节数)
    // 注意:这里的 yoffset 和 xoffset 应该始终为0,因为我们直接操作缓冲区起始点
    // 实际的 yoffset 是通过 fb_flip 切换的
    long location = x * (vinfo.bits_per_pixel / 8) + y * finfo.line_length;

    // 将像素数据写入显存 (draw_buffer_ptr + location)
    // ... (同 draw_pixel 内部的 switch 逻辑,但操作的是 draw_buffer_ptr)
    switch (vinfo.bits_per_pixel) {
        case 8: *((char*)(draw_buffer_ptr + location)) = (char)pixel_color; break;
        case 16: *((unsigned short*)(draw_buffer_ptr + location)) = (unsigned short)pixel_color; break;
        case 24: // 24位像素通常按3个字节存储
            *((char*)(draw_buffer_ptr + location + (vinfo.blue.offset / 8))) = b;
            *((char*)(draw_buffer_ptr + location + (vinfo.green.offset / 8))) = g;
            *((char*)(draw_buffer_ptr + location + (vinfo.red.offset / 8))) = r;
            break;
        case 32: *((unsigned int*)(draw_buffer_ptr + location)) = pixel_color; break;
    }
}

双缓冲注意事项:

  • 所有的绘制操作(draw_pixel, draw_line, draw_rect, clear_screen)都必须修改为在 get_current_draw_buffer() 返回的缓冲区上操作,而不是直接操作 fbp
  • xoffsetyoffset 在绘制函数内部不再用于计算像素位置,因为 get_current_draw_buffer() 已经返回了正确缓冲区的起始地址。这两个偏移量只在 fb_flip 时通过 ioctl 传递给内核,由内核控制哪个缓冲区可见。
  • 每次完成一帧的绘制后,调用 fb_flip() 来显示新帧。

5. 输入处理:键盘与鼠标

一个图形界面通常需要用户输入。在绕过 X11/Wayland 的情况下,我们需要直接从 Linux 输入子系统 (evdev) 获取键盘和鼠标事件。

Linux 的输入设备通常表现为 /dev/input/eventX 文件。我们可以打开这些文件并读取 input_event 结构体来获取事件。

#include <linux/input.h> // for struct input_event
#include <poll.h>        // for poll()

// 输入设备的文件描述符
static int keyboard_fd = -1;
static int mouse_fd = -1;

int input_init() {
    // 尝试打开键盘设备,通常是 event0, event1 等
    // 需要遍历 /dev/input/event* 来找到正确的设备
    // 这里简化为直接打开一个假定的设备
    keyboard_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK);
    if (keyboard_fd == -1) {
        perror("Error opening keyboard device /dev/input/event0, trying event1");
        keyboard_fd = open("/dev/input/event1", O_RDONLY | O_NONBLOCK);
        if (keyboard_fd == -1) {
            perror("Error opening keyboard device /dev/input/event1, trying event2");
            keyboard_fd = open("/dev/input/event2", O_RDONLY | O_NONBLOCK);
             if (keyboard_fd == -1) {
                fprintf(stderr, "Warning: Could not open any keyboard device.n");
             }
        }
    }
    if (keyboard_fd != -1) {
        printf("Keyboard device opened: %dn", keyboard_fd);
    }

    // 类似地,打开鼠标设备
    mouse_fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);
    if (mouse_fd == -1) {
        perror("Error opening mouse device /dev/input/event3, trying event4");
        mouse_fd = open("/dev/input/event4", O_RDONLY | O_NONBLOCK);
        if (mouse_fd == -1) {
            perror("Error opening mouse device /dev/input/event4, trying event5");
            mouse_fd = open("/dev/input/event5", O_RDONLY | O_NONBLOCK);
             if (mouse_fd == -1) {
                fprintf(stderr, "Warning: Could not open any mouse device.n");
             }
        }
    }
    if (mouse_fd != -1) {
        printf("Mouse device opened: %dn", mouse_fd);
    }
    return 0;
}

void input_exit() {
    if (keyboard_fd != -1) {
        close(keyboard_fd);
        keyboard_fd = -1;
    }
    if (mouse_fd != -1) {
        close(mouse_fd);
        mouse_fd = -1;
    }
    printf("Input devices closed.n");
}

// 处理输入事件的函数
void handle_input_events(int *running) {
    struct input_event ev;
    ssize_t bytes_read;

    // 检查键盘事件
    if (keyboard_fd != -1) {
        while ((bytes_read = read(keyboard_fd, &ev, sizeof(ev))) > 0) {
            if (ev.type == EV_KEY && ev.value == 1) { // Key press
                printf("Key pressed: %xn", ev.code);
                if (ev.code == KEY_ESC) { // ESC 键
                    *running = 0; // 退出程序
                }
            }
        }
    }

    // 检查鼠标事件
    if (mouse_fd != -1) {
        while ((bytes_read = read(mouse_fd, &ev, sizeof(ev))) > 0) {
            if (ev.type == EV_REL) { // Relative motion event (mouse move)
                if (ev.code == REL_X) {
                    // printf("Mouse X relative motion: %dn", ev.value);
                } else if (ev.code == REL_Y) {
                    // printf("Mouse Y relative motion: %dn", ev.value);
                }
            } else if (ev.type == EV_KEY && (ev.code == BTN_LEFT || ev.code == BTN_RIGHT) && ev.value == 1) {
                // Mouse button press
                printf("Mouse button %s pressedn", (ev.code == BTN_LEFT) ? "Left" : "Right");
            }
        }
    }
}

input_event 结构体:

struct input_event {
    struct timeval time;
    __u16 type;     // 事件类型,如 EV_KEY (键盘), EV_REL (相对位移,鼠标), EV_ABS (绝对位移,触摸屏)
    __u16 code;     // 事件代码,如 KEY_ESC (ESC键), REL_X (X轴位移), BTN_LEFT (鼠标左键)
    __s32 value;    // 事件值,如按键状态 (0: 松开, 1: 按下, 2: 重复), 鼠标位移量
};

输入处理注意事项:

  • 设备路径: /dev/input/eventX 中的 X 值取决于系统配置和连接的设备顺序。你可能需要遍历 /dev/input/ 目录并根据设备的 namecapabilities 来识别它们(例如,使用 ioctl(fd, EVIOCGNAME(...)))。
  • 非阻塞读取: O_NONBLOCK 标志让 read() 函数在没有数据时立即返回,而不是阻塞。这对于主循环中同时处理渲染和输入非常重要。
  • 轮询 (Polling): 在主循环中,你可以定期调用 handle_input_events 来检查新事件。对于更复杂的应用,可以使用 poll()select() 系统调用来等待多个文件描述符上的事件,避免忙等待。

6. 性能考量与优化

直接 Framebuffer 渲染通常比通过 X11/Wayland 间接渲染更快,因为它减少了层层抽象和 IPC 开销。然而,仍然有优化空间。

  1. 内存访问模式:
    • 缓存友好: 连续访问内存比跳跃式访问更能利用 CPU 缓存。例如,逐行绘制比逐列绘制更高效。
    • 块操作: 使用 memcpy()memset() 来操作大块内存区域,而不是逐像素循环。CPU 针对这些函数有高度优化的实现。例如,填充一个矩形可以分解为多行 memcpy
  2. 颜色转换:
    • rgb_to_pixel 函数可能会被频繁调用。如果颜色深度是固定的,可以预计算位移和掩码,或者针对特定深度编写特化版本。
  3. 双缓冲:
    • 如前所述,双缓冲是消除画面闪烁和撕裂的关键,对于动画和流畅的用户体验至关重要。
  4. 硬件加速 (局限性):
    • fbdev 本身不提供硬件加速。 所有的像素操作都是在 CPU 上完成的。这意味着复杂的图形渲染(如 3D 渲染、高级混合)会消耗大量 CPU 资源。
    • DRM/KMS (Direct Rendering Manager / Kernel Mode Setting): 现代 Linux 图形栈的趋势是使用 DRM/KMS API,它允许用户空间应用程序直接与 GPU 硬件进行交互,利用 GPU 的硬件加速功能。DRM 提供了 libdrm 库,可以用于创建帧缓冲、进行模式设置、管理显存等。虽然这超出了本讲座直接 Framebuffer 的范畴,但理解 fbdev 的局限性并知道 DRM/KMS 是其现代替代品非常重要。对于需要硬件加速的场景,DRM/KMS 是首选。
    • fbdev 仍然适用于简单图形、嵌入式系统、引导阶段或对 GPU 不依赖的场景。
  5. volatile 关键字:
    • FrameBuffer 内存映射区域的读写是与硬件交互。编译器有时会优化掉对内存的重复读写。为了防止这种优化,可以将 fbp 声明为 volatile char *fbp;。但这通常不是必需的,因为 mmapMAP_SHARED 属性已经暗示了内存可能被外部修改。

7. 实用示例:一个简单的图形演示程序

我们将把上面讨论的所有概念整合到一个完整的 C 程序中。


#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h> // For memset
#include <errno.h>  // For errno
#include <math.h>   // For abs

#include <linux/fb.h>
#include <linux/input.h>
#include <poll.h>

// --- 全局 Framebuffer 变量 ---
static int fbfd = -1;
static struct fb_var_screeninfo vinfo;
static struct fb_fix_screeninfo finfo;
static char *fbp = NULL;
static long screensize = 0;
static int current_buffer_idx = 0;
static int num_buffers = 1; // 默认单缓冲
static char *draw_buffer_ptr = NULL; // 当前绘图缓冲区的指针

// --- 全局输入变量 ---
static int keyboard_fd = -1;
static int mouse_fd = -1;
static int running = 1; // 控制主循环

// --- 颜色转换函数 ---
static unsigned int rgb_to_pixel(unsigned char r, unsigned char g, unsigned char b) {
    unsigned int pixel_value = 0;
    r = r > 255 ? 255 : r;
    g = g > 255 ? 255 : g;
    b = b > 255 ? 255 : b;

    switch (vinfo.bits_per_pixel) {
        case 8: // 256色,简单灰度映射
            pixel_value = (unsigned int)((r + g + b) / 3);
            break;
        case 16: // RGB565
            pixel_value |= ((r >> (8 - vinfo.red.length)) & ((1 << vinfo.red.length) - 1)) << vinfo.red.offset;
            pixel_value |= ((g >> (8 - vinfo.green.length)) & ((1 << vinfo.green.length) - 1)) << vinfo.green.offset;
            pixel_value |= ((b >> (8 - vinfo.blue.length)) & ((1 << vinfo.blue.length) - 1)) << vinfo.blue.offset;
            break;
        case 24: // RGB888
        case 32: // ARGB8888 or XRGB8888
            pixel_value |= (r << vinfo.red.offset);
            pixel_value |= (g << vinfo.green.offset);
            pixel_value |= (b << vinfo.blue.offset);
            if (vinfo.transp.length > 0) {
                pixel_value |= (0xFF << vinfo.transp.offset); // Full alpha
            }
            break;
        default:
            fprintf(stderr, "Unsupported bits_per_pixel: %dn", vinfo.bits_per_pixel);
            return 0; // Black
    }
    return pixel_value;
}

// --- Framebuffer 初始化与清理 ---
int fb_init(const char *dev_path) {
    fbfd = open(dev_path, O_RDWR);
    if (fbfd == -1) {
        perror("Error: cannot open framebuffer device");
        return -1;
    }
    printf("Framebuffer device '%s' opened.n", dev_path);

    if (ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo) == -1) {
        perror("Error reading fixed information");
        close(fbfd); fbfd = -1; return -1;
    }
    printf("  ID: %s, Line Length: %d bytes, Total Mem: %d bytesn", finfo.id, finfo.line_length, finfo.smem_len);

    if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo) == -1) {
        perror("Error reading variable information");
        close(fbfd); fbfd = -1; return -1;
    }
    printf("  Resolution: %dx%d, BPP: %dn", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel);
    printf("  Red: off=%d len=%d, Green: off=%d len=%d, Blue: off=%d len=%dn",
           vinfo.red.offset, vinfo.red.length, vinfo.green.offset, vinfo.green.length, vinfo.blue.offset, vinfo.blue.length);

    // 尝试启用双缓冲
    struct fb_var_screeninfo original_vinfo = vinfo;
    vinfo.yres_virtual = vinfo.yres * 2; // Request 2 buffers
    vinfo.yoffset = 0; // Start at the first buffer

    if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo) == -1) {
        // If setting failed, try to restore original and proceed with single buffer
        perror("Warning: Failed to set virtual resolution for double buffering. Trying single buffer.");
        vinfo = original_vinfo; // Restore original vinfo
        if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo) == -1) {
            perror("Error: Failed to restore original variable information. Cannot proceed.");
            close(fbfd); fbfd = -1; return -1;
        }
    }
    // Read back the actual vinfo, as hardware might not grant the exact request
    if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo) == -1) {
        perror("Error reading variable information after setting resolution");
        close(fbfd); fbfd = -1; return -1;
    }

    if (vinfo.yres_virtual > vinfo.yres) {
        num_buffers = vinfo.yres_virtual / vinfo.yres;
        printf("  Double buffering enabled with %d buffers. Virtual Res: %dx%dn", num_buffers, vinfo.xres_virtual, vinfo.yres_virtual);
    } else {
        printf("  Double buffering not available. Using single buffer. Virtual Res: %dx%dn", vinfo.xres_virtual, vinfo.yres_virtual);
        num_buffers = 1;
    }

    screensize = finfo.line_length * vinfo.yres_virtual;
    fbp = (char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);
    if ((intptr_t)fbp == -1 || fbp == NULL) { // Check for mmap failure
        perror("Error: failed to mmap framebuffer device to memory");
        close(fbfd); fbfd = -1; return -1;
    }
    printf("Framebuffer mapped to memory, size: %ld bytes.n", screensize);

    // Initialize draw_buffer_ptr to the first (non-visible) buffer if double buffering is on
    draw_buffer_ptr = fbp; // Default to main buffer
    if (num_buffers > 1) {
         // If current_buffer_idx is 0 (visible), draw to buffer 1
         draw_buffer_ptr = fbp + (current_buffer_idx + 1) % num_buffers * finfo.line_length * vinfo.yres;
    }

    return 0;
}

void fb_exit() {
    if (fbp != NULL && fbp != (char *)-1) {
        munmap(fbp, screensize);
        fbp = NULL;
    }
    if (fbfd != -1) {
        close(fbfd);
        fbfd = -1;
    }
    printf("Framebuffer device closed and memory unmapped.n");
}

void fb_flip() {
    if (num_buffers <= 1) {
        return; // No double buffering
    }

    current_buffer_idx = (current_buffer_idx + 1) % num_buffers;
    vinfo.yoffset = current_buffer_idx * vinfo.yres;

    // Pan the display to the new buffer
    if (ioctl(fbfd, FBIOPAN_DISPLAY, &vinfo) == -1) {
        // Fallback to FBIOPUT_VSCREENINFO if FBIOPAN_DISPLAY fails
        if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo) == -1) {
            perror("Error flipping display buffer");
        }
    }
    // Update draw_buffer_ptr to the next *non-visible* buffer
    draw_buffer_ptr = fbp + (current_buffer_idx + 1) % num_buffers * finfo.line_length * vinfo.yres;
}

// --- 绘制函数 ---
void draw_pixel(int x, int y, unsigned char r, unsigned char g, unsigned char b) {
    if (!draw_buffer_ptr || x < 0 || x >= vinfo.xres || y < 0 || y >= vinfo.yres) {
        return;
    }

    unsigned int pixel_color = rgb_to_pixel(r, g, b);
    long location = x * (vinfo.bits_per_pixel / 8) + y * finfo.line_length;
    char *target_ptr = draw_buffer_ptr + location;

    switch (vinfo.bits_per_pixel) {
        case 8:
            *((char*)target_ptr) = (char)pixel_color;
            break;
        case 16:
            *((unsigned short*)target_ptr) = (unsigned short)pixel_color;
            break;
        case 24:
            *((char*)(target_ptr + (vinfo.blue.offset / 8))) = b;
            *((char*)(target_ptr + (vinfo.green.offset / 8))) = g;
            *((char*)(target_ptr + (vinfo.red.offset / 8))) = r;
            break;
        case 32:
            *((unsigned int*)target_ptr) = pixel_color;
            break;
        default:
            break;
    }
}

void clear_screen(unsigned char r, unsigned char g, unsigned char b) {
    if (!draw_buffer_ptr) {
        return;
    }

    unsigned int pixel_color = rgb_to_pixel(r, g, b);
    long bytes_per_pixel = vinfo.bits_per_pixel / 8;

    for (int y = 0; y < vinfo.yres; y++) {
        long line_start_offset = y * finfo.line_length;
        char *line_ptr = draw_buffer_ptr + line_start_offset;

        if (bytes_per_pixel == 1) {
            memset(line_ptr, (char)pixel_color, vinfo.xres * bytes_per_pixel);
        } else if (bytes_per_pixel == 2) {
            unsigned short val = (unsigned short)pixel_color;
            for (int x = 0; x < vinfo.xres; x++) {
                *((unsigned short*)(line_ptr + x * bytes_per_pixel)) = val;
            }
        } else if (bytes_per_pixel == 4) {
            unsigned int val = pixel_color;
            for (int x = 0; x < vinfo.xres; x++) {
                *((unsigned int*)(line_ptr + x * bytes_per_pixel)) = val;
            }
        } else if (vinfo.bits_per_pixel == 24) {
            unsigned char b_val = b;
            unsigned char g_val = g;
            unsigned char r_val = r;
            for (int x = 0; x < vinfo.xres; x++) {
                char *pixel_ptr = line_ptr + x * 3;
                *((char*)(pixel_ptr + (vinfo.blue.offset / 8))) = b_val;
                *((char*)(pixel_ptr + (vinfo.green.offset / 8))) = g_val;
                *((char*)(pixel_ptr + (vinfo.red.offset / 8))) = r_val;
            }
        }
    }
}

void draw_line(int x0, int y0, int x1, int y1, unsigned char r, unsigned char g, unsigned char b) {
    int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
    int dy = abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
    int err = (dx > dy ? dx : -dy) / 2, e2;

    for (;;) {
        draw_pixel(x0, y0, r, g, b);
        if (x0 == x1 && y0 == y1) break;
        e2 = err;
        if (e2 > -dx) { err -= dy; x0 += sx; }
        if (e2 < dy) { err += dx; y0 += sy; }
    }
}

void draw_rect(int x, int y, int w, int h, unsigned char r, unsigned char g, unsigned char b, int filled) {
    if (x < 0) x = 0; if (x >= vinfo.xres) return;
    if (y < 0) y = 0; if (y >= vinfo.yres) return;
    if (x + w > vinfo.xres) w = vinfo.xres - x;
    if (y + h > vinfo.yres) h = vinfo.yres - y;

    if (w <= 0 || h <= 0) return;

    if (filled) {
        unsigned int pixel_color = rgb_to_pixel(r, g, b);
        long bytes_per_pixel = vinfo.bits_per_pixel / 8;

        for (int current_y = y; current_y < y + h; current_y++) {
            long start_location = x * bytes_per_pixel + current_y * finfo.line_length;
            char *current_row_ptr = draw_buffer_ptr + start_location;

            if (bytes_per_pixel == 1) {
                memset(current_row_ptr, (char)pixel_color, w * bytes_per_pixel);
            } else if (bytes_per_pixel == 2) {
                unsigned short val = (unsigned short)pixel_color;
                for (int current_x = 0; current_x < w; current_x++) {
                    *((unsigned short*)(current_row_ptr + current_x * bytes_per_pixel)) = val;
                }
            } else if (bytes_per_pixel == 4) {
                unsigned int val = pixel_color;
                for (int current_x = 0; current_x < w; current_x++) {
                    *((unsigned int*)(current_row_ptr + current_x * bytes_per_pixel)) = val;
                }
            } else if (vinfo.bits_per_pixel == 24) {
                unsigned char b_val = b;
                unsigned char g_val = g;
                unsigned char r_val = r;
                for (int current_x = 0; current_x < w; current_x++) {
                    char *pixel_ptr = current_row_ptr + current_x * 3;
                    *((char*)(pixel_ptr + (vinfo.blue.offset / 8))) = b_val;
                    *((char*)(pixel_ptr + (vinfo.green.offset / 8))) = g_val;
                    *((char*)(pixel_ptr + (vinfo.red.offset / 8))) = r_val;
                }
            }
        }
    } else {
        draw_line(x, y, x + w - 1, y, r, g, b);
        draw_line(x, y + h - 1, x + w - 1, y + h - 1, r, g, b);
        draw_line(x, y, x, y + h - 1, r, g, b);
        draw_line(x + w - 1, y, x + w - 1, y + h - 1, r, g, b);
    }
}

// --- 输入初始化与清理 ---
int input_init() {
    // 尝试打开键盘设备,通常是 event0, event1 等
    // 实际应用中,应遍历 /dev/input/event* 并通过 EVIOCGNAME 或 EVIOCGBIT 来识别设备
    keyboard_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK);
    if (keyboard_fd == -1) {
        keyboard_fd = open("/dev/input/event1", O_RDONLY | O_NONBLOCK);
    }
    if (keyboard_fd == -1) {
        keyboard_fd = open("/dev/input/event2", O_RDONLY | O_NONBLOCK);
    }
    if (keyboard_fd != -1) {
        printf("Keyboard device opened: %dn", keyboard_fd);
    } else {
        fprintf(stderr, "Warning: Could not open any common keyboard device.n");
    }

    mouse_fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);
    if (mouse_fd == -1) {
        mouse_fd = open("/dev/input/event4", O_RDONLY | O_NONBLOCK);
    }
    if (mouse_fd == -1) {
        mouse_fd = open("/dev/input/event5", O_RDONLY | O_NONBLOCK);
    }
    if (mouse_fd != -1) {
        printf("Mouse device opened: %dn", mouse_fd);
    } else {
        fprintf(stderr, "Warning: Could not open any common mouse device.n");
    }
    return 0;
}

void input_exit() {
    if (keyboard_fd != -1) {
        close(keyboard_fd);
        keyboard_fd = -1;
    }
    if (mouse_fd != -1) {
        close(mouse_fd);
        mouse_fd = -1;
    }
    printf("Input devices closed.n");
}

void handle_input_events() {
    struct input_event ev;
    ssize_t bytes_read;

    struct pollfd fds[2];
    int nfds = 0;

    if (keyboard_fd != -1) {
        fds[nfds].fd = keyboard_fd;
        fds[nfds].events = POLLIN;
        nfds++;
    }
    if (mouse_fd != -1) {
        fds[nfds].fd = mouse_fd;
        fds[nfds].events = POLLIN;
        nfds++;
    }

    if (nfds == 0) return;

    // Poll with a short timeout to not block the rendering loop
    int ret = poll(fds, nfds, 0); // 0 timeout means non-blocking check
    if (ret < 0) {
        perror("poll");
        return;
    }

    if (ret > 0) { // Events are available
        for (int i = 0; i < nfds; i++) {
            if (fds[i].revents & POLLIN) {
                while ((bytes_read = read(fds[i].fd, &ev, sizeof(ev))) == sizeof(ev)) {
                    if (ev.type == EV_KEY && ev.value == 1) { // Key press
                        // printf("Key pressed: %xn", ev.code);
                        if (ev.code == KEY_ESC) {
                            running = 0;
                            printf("ESC pressed, exiting.n");
                        }
                    } else if (ev.type == EV_REL) { // Relative motion
                        // printf("Mouse move: code=%d, value=%dn", ev.code, ev.value);
                    } else if (ev.type == EV_KEY && (ev.code == BTN_LEFT || ev.code == BTN_RIGHT) && ev.value == 1) {
                        // printf("Mouse button pressed: %sn", (ev.code == BTN_LEFT) ? "Left" : "Right");
                    }
                }
                if (bytes_read == -1 && errno != EAGAIN) {
                    perror("read input event");
                }
            }
        }
    }
}

// --- 主程序 ---
int main(int argc, char *argv[]) {
    const char *fb_dev = "/dev/fb0";
    if (argc > 1) {
        fb_dev = argv[1];
    }

    if (fb_init(fb_dev) == -1) {
        fprintf(stderr, "Failed to initialize framebuffer. Exiting.n");
        return 1;
    }

    // Set console to graphics mode, preventing console text from appearing over our graphics.
    // This typically requires root privileges and is system-specific.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注