各位开发者、系统工程师以及对Linux底层图形机制感兴趣的朋友们,大家好。
今天,我们将深入探讨一个既经典又具有实践意义的主题:Linux Framebuffer 渲染,以及如何绕过上层图形环境(如 Wayland 或 X11),直接将像素输出到显示屏。这不仅仅是技术考古,更是在特定场景下(例如嵌入式系统、定制启动画面、性能敏感型应用、或者仅仅是为了深入理解图形栈)不可或缺的核心技能。
1. 绕过图形栈:为何以及何为 Linux Framebuffer
在现代Linux桌面环境中,我们习惯了由X Window System或Wayland这样的显示服务器来管理图形输出。它们提供了复杂的窗口管理、事件处理、硬件加速接口(如OpenGL/Vulkan),并抽象了底层显示硬件的细节。然而,在某些情况下,我们可能需要或必须绕过这些复杂的抽象层,直接与显示硬件对话。
为何要绕过 Wayland/X11?
- 嵌入式系统与资源受限环境: 许多嵌入式设备没有足够的资源运行完整的X或Wayland服务器。直接使用Framebuffer可以提供一个轻量级的图形界面。
- 定制启动画面 (Boot Splash Screens): 在系统启动过程中,X或Wayland尚未初始化。Framebuffer是显示启动Logo、进度条的唯一方式。
- 性能敏感型应用与Kiosk系统: 对于需要极致性能且没有多窗口需求的单应用系统,消除X/Wayland的开销可以减少延迟,提高响应速度。
- 调试与诊断: 在图形驱动或显示服务器出现问题时,Framebuffer提供了一个基础的显示输出,便于诊断问题。
- 理解底层机制: 深入学习Linux图形栈的基石,了解像素如何在屏幕上呈现,有助于更好地理解更高级的图形API。
- 特殊效果或专有硬件: 对于一些需要直接控制显示时序或进行非标准显示操作的专有硬件,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_virtual是yres的两倍,你就可以拥有两个完整的屏幕缓冲区。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 = 8green.offset = 8,green.length = 8blue.offset = 0,blue.length = 8transp.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 = 5green.offset = 5,green.length = 6blue.offset = 0,blue.length = 5transp.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");
}
代码解释:
open("/dev/fb0", O_RDWR): 打开 Framebuffer 设备文件,允许读写操作。ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo): 使用ioctl系统调用获取 Framebuffer 的固定信息。FBIOGET_FSCREENINFO是一个宏,表示获取固定屏幕信息的操作码。ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo): 获取 Framebuffer 的可变信息。FBIOGET_VSCREENINFO是获取可变屏幕信息的操作码。screensize = finfo.smem_len: 获取 Framebuffer 内存的总大小。这是mmap函数所需的长度。mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0): 这是核心步骤。它将 Framebuffer 设备的物理内存区域映射到当前进程的虚拟地址空间。0: 让内核选择一个合适的虚拟地址。screensize: 映射的字节数。PROT_READ | PROT_WRITE: 映射区域可读可写。MAP_SHARED: 映射区域的更改会写入到文件中(即实际的显存),并且其他进程也能看到这些更改。fbfd: 文件描述符。0: 映射的偏移量,从文件开头开始。- 返回的
fbp是一个char *指针,指向映射内存的起始地址。我们可以通过操作这个指针来修改显存内容。
munmap(fbp, screensize): 在程序结束时,调用munmap解除内存映射。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 = 3,r右移 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(完全不透明)。
- 通常,对于 24 或 32 位深度的 Framebuffer,每个颜色分量都是 8 位。所以直接将
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;
}
}
}
}
优化清屏:
上述清屏方法是逐像素写入,效率较低。对于整个区域填充,可以使用 memset 或 memcpy 进行块操作。
// 优化后的清屏函数,仅适用于单色填充且像素字节数是 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) 技术。
原理:
- 在显存中分配两个(或更多)完整的屏幕缓冲区。这通过设置
vinfo.yres_virtual大于vinfo.yres来实现。例如,如果yres_virtual = 2 * yres,则有两个缓冲区。 - 程序在其中一个不可见的缓冲区(后台缓冲区)上进行所有绘图操作。
- 当一帧图像绘制完成后,通过修改
vinfo.yoffset来将后台缓冲区设置为可见,同时将之前的可见缓冲区切换到后台。这个操作称为页面翻转 (Page Flipping)。 - 页面翻转通常是硬件原子操作,因此切换是瞬间完成的,避免了画面撕裂。
实现:
在 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。 xoffset和yoffset在绘制函数内部不再用于计算像素位置,因为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/目录并根据设备的name或capabilities来识别它们(例如,使用ioctl(fd, EVIOCGNAME(...)))。 - 非阻塞读取:
O_NONBLOCK标志让read()函数在没有数据时立即返回,而不是阻塞。这对于主循环中同时处理渲染和输入非常重要。 - 轮询 (Polling): 在主循环中,你可以定期调用
handle_input_events来检查新事件。对于更复杂的应用,可以使用poll()或select()系统调用来等待多个文件描述符上的事件,避免忙等待。
6. 性能考量与优化
直接 Framebuffer 渲染通常比通过 X11/Wayland 间接渲染更快,因为它减少了层层抽象和 IPC 开销。然而,仍然有优化空间。
- 内存访问模式:
- 缓存友好: 连续访问内存比跳跃式访问更能利用 CPU 缓存。例如,逐行绘制比逐列绘制更高效。
- 块操作: 使用
memcpy()或memset()来操作大块内存区域,而不是逐像素循环。CPU 针对这些函数有高度优化的实现。例如,填充一个矩形可以分解为多行memcpy。
- 颜色转换:
rgb_to_pixel函数可能会被频繁调用。如果颜色深度是固定的,可以预计算位移和掩码,或者针对特定深度编写特化版本。
- 双缓冲:
- 如前所述,双缓冲是消除画面闪烁和撕裂的关键,对于动画和流畅的用户体验至关重要。
- 硬件加速 (局限性):
- 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 不依赖的场景。
volatile关键字:- FrameBuffer 内存映射区域的读写是与硬件交互。编译器有时会优化掉对内存的重复读写。为了防止这种优化,可以将
fbp声明为volatile char *fbp;。但这通常不是必需的,因为mmap的MAP_SHARED属性已经暗示了内存可能被外部修改。
- FrameBuffer 内存映射区域的读写是与硬件交互。编译器有时会优化掉对内存的重复读写。为了防止这种优化,可以将
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.