Drawing In a Linear Framebuffer
现在,你已经知道如何使用硬件VGA支持轻松地将文本写入屏幕,你可能想知道如何能够显示漂亮的图像、窗口、菜单、图标、漂亮的光标和按钮等。 本页介绍如何在线性帧缓冲区中显示图形,线性帧缓冲区是在内存中映射的一个简单数组,代表屏幕。
图形模式
- 正文: Getting VBE Mode Info
- 正文: GOP
VGA和VBE模式可以使用BIOS中断0x10选择(在实模式下)。 INT 0x10, VESA Video Modes 和 VESA 是 VGA 和 VBE/VESA的参考资料。
VGA仅限于16色640x480,而VBE(BIOS系统)和GOP(UEFI机器,不包括一些非常早期的机器)可以达到显示器和视频卡支持的最大分辨率。
切换
设置视频模式最干净的方法是通过视频BIOS。 它可以通过常规的Int 0x10接口执行,也可以通过VBE3提供的(可选)包含模式接口执行。 正如你所猜测的,Int 0x10 需要一个16位的环境,所以你只能在实模式或虚拟8086模式中使用它
实际上,可用选项按难度排序是:
- 在进入保护模式之前,在早期阶段(在引导加载程序中)设置所需的模式。
- 在UEFI系统上开发,并在启动时获得GOP帧缓冲区。
- 让你的Bootloader帮你切换。
- 切换回真实模式或非实模式以设置正确的视频模式(Napalm 于rohitab.com有一个简洁的小功能供参考。)
- 编写一个 VGA驱动程序,可以在几乎所有硬件上执行低分辨率模式
- 如果存在,使用VBE3中的PMID
- 设置一个V8086的Monitor来执行模式切换代码
- 运行软件代码翻译工具,从bios rmode代码中生成pmode代码。(SANiK正在进行)
- 为特定的图形卡编写驱动程序
定位视频内存
对于标准VGA视频模式,对于EGA/VGA视频模式,视频内存将位于地址0xA0000,对于CGA和文本模式,视频内存将位于地址0xB8000。 要找出哪一个,请查看下表:
00 | text 40*25 16 color (mono) |
01 | text 40*25 16 color |
02 | text 80*25 16 color (mono) |
03 | text 80*25 16 color |
04 | CGA 320*200 4 color |
05 | CGA 320*200 4 color (m) |
06 | CGA 640*200 2 color |
07 | MDA monochrome text 80*25 |
08 | PCjr |
09 | PCjr |
0A | PCjr |
0B | reserved |
0C | reserved |
0D | EGA 320*200 16 color |
0E | EGA 640*200 16 color |
0F | EGA 640*350 mono |
10 | EGA 640*350 16 color |
11 | VGA 640*480 mono |
12 | VGA 640*480 16 color |
13 | VGA 320*200 256 color |
对于VESA模式,帧缓冲区地址存储在mode info block中。 这是线性帧缓冲区的物理地址(它不是16位远指针,而是32位线性指针): 如果你使用分页,你必须把它映射到某个地方才能使用它。 帧缓冲区的长度(字节)是pitch*height。
对于GOP,帧缓冲区地址位于EFI_GRAPHICS_PROTOCOL结构中,GOP->Mode->FrameBufferBase。 GOP不支持电传字符模式,只支持面向图形像素的模式。
绘制像素(Plotting Pixels)
定位
如果你想在屏幕中间绘制一个红色像素。 你首先要知道的是屏幕中间的位置。 在320x200x8(mode 13)中,这将是偏移100x320+160=32160。 一般来说,你的屏幕可以通过以下方式描述:
宽度 width | 水平线上有多少像素 |
高度 height | 有多少像素的水平线 |
间距 pitch | 为了降低一个像素,你应该跳过多少字节的VRAM |
深度 depth | 你有多少种颜色 |
“像素宽度pixelwidth” | 你应该跳过多少字节的VRAM才能向右移动一个像素。 |
“间距”和“宽度”乍一看可能显得多余,但事实并非如此。 当你使用更高的分辨率时,比如每行8K字节,而你的屏幕实际上是1500像素宽(每像素32位),这种情况并不罕见。 好消息是,它允许平滑的水平滚动(主要用于2D游戏:P)
间距和像素宽度通常由VESA模式信息发布。一旦你知道了它们,你就可以计算出你绘制像素的位置:
unsigned char *pixel = vram + y*pitch + x*pixelwidth;
颜色
要知道的第二件事是你应该为“红色”(或其它颜色)写什么值。 这同样取决于你的屏幕设置。 在EGA模式下,你有一个固定的调色板,具有暗红色(颜色4)和浅红色(颜色12)。 然而,EGA要求你在不同的像素平面上绘制每一位,因此,如果你 实在希望支持这种模式,请参阅EGA编程教程。 在传统的320x200x8 VGA模式下,你的4色和12色与EGA中的颜色相同,因此你可以使用如下方式
*pixel = 4;
然而,在VGA中,调色板是可重新编程的(正如你可以在FreeVGA文档中了解到的),因此几乎任何介于0..255之间的值都可能为“红色”。
所以VGA的完整“putpixel”功能如下
/* 320x200 VGA的示例 */
void putpixel(int pos_x, int pos_y, unsigned char VGA_COLOR)
{
unsigned char* location = (unsigned char*)0xA0000 + 320 * pos_y + pos_x;
*location = VGA_COLOR;
}
最后,在VESA和GOP模式下,通常使用truecolor或hicolor,在这两种模式下,必须为每个像素提供独立的红色、绿色和蓝色值。 modeinfo将(再次)指导你如何以像素位组织RGB组件。 例如,对于15位模式,你将使用0xRRRRRGGGBBBBB,这意味着#ff0000红色在那里0x7800,而#808080灰色在那里0x4210 (拿起铅笔,画出比特,自己看)
static void putpixel(unsigned char* screen, int x,int y, int color) {
unsigned where = x*pixelwidth + y*pitch;
screen[where] = color & 255; // BLUE
screen[where + 1] = (color >> 8) & 255; // GREEN
screen[where + 2] = (color >> 16) & 255; // RED
}
优化
从这里可以很容易地编写其他绘图函数,从调用putpixel... 不要这样做。绘制一个填充矩形意味着你可以访问连续的像素,然后按“pitch-rect_width”前进以填充下一行。 如果你执行"for(y=100;y<200;y++) for(x=100;x<200;x++) putpixel (screen,x,y,RED);"循环时,你将重新计算大约10000次“where”,并进行过多的函数调用。 即使编译器将y*pitch优化为加法和移位,而不是乘法,在可能的情况下浪费CPU时间也是愚蠢的
static void fillrect(unsigned char *vram, unsigned char r, unsigned char g, unsigned char b, unsigned char w, unsigned char h) {
unsigned char *where = vram;
int i, j;
for (i = 0; i < w; i++) {
for (j = 0; j < h; j++) {
//putpixel(vram, 64 + j, 64 + i, (r << 16) + (g << 8) + b);
where[j*pixelwidth] = r;
where[j*pixelwidth + 1] = g;
where[j*pixelwidth + 2] = b;
}
where+=pitch;
}
}
这应该足以让你开始编写(或在谷歌上搜索)一个像样的视频库。
绘制文本
- 正文: VGA Fonts
一旦进入图形模式,你就不再有BIOS或硬件来为你绘制字体。 基本思想是为每个字符提供字体数据,并使用它来绘制(或留白)像素。 存储这些字体的方法有很多,这取决于它们是否有多种颜色、alpha通道等。 然而,你基本上拥有的是:
// 保存你所需要的每一个char
font_char* font_data[CHARS];
// 在给定font_data的情况下渲染其中一个character
void draw_char(screen, where, font_char*);
void draw_string(screen, where, char* input) {
while(*input) {
draw_char(screen,where,font_data[input]);
where += char_width;
input++;
}
}
void draw_char(screen, where, font_char*) {
for (l = 0; l < 8; l++) {
for (i = 8; i > 0; i--) {
j++;
if ((font_char[l] & (1 << i))) {
c = c1;
put_pixel(j, h, c);
}
}
h++;
j = x;
}
}
绘制图标
- 正文: Loading Icons
对于GUI,你可能需要显示图标。就如下这么简单:
void draw_icon(x, y, w, h, pixels) {
for (l = j = 0; l < h; l++) {
for (i = 0; i < w; i++, j++) {
put_pixel(x + i, y + l, pixels[j]);
}
}
}
困难在于,图标不是以宽度、高度和像素阵列的形式存储在磁盘上。 首先,你必须解码图像文件才能获得这些信息。