VGA Fonts

来自osdev
跳到导航 跳到搜索

这篇文章写得像个教程。请 编辑它以添加更多信息和文档,而不仅仅是示例代码和分步说明。

你已知道如何在文本模式下显示字符,现在你想要在图形模式下执行此操作。(译者注:内核中文本模式的显示方式是使用BIOS软中断INT指令,调用BIOS功能) 这并不复杂,但绝对比在内存中的特定偏移量下编写ASCII代码更复杂。 你必须逐像素地绘制。

但是你怎么知道该画什么呢? 它存储在称为位图字体(bitmap fonts)的数据矩阵中。

位图字体的解码

一个字符是如何存储在内存中的? 很简单,0编码代表背景,1编码代表前景色。 VGA字体总是8位宽,因此每个字节只包含一行。 对于典型8x16字体的字母‘A’,它将是(二进制,共16字节):

00000000b  byte  0
00000000b  byte  1
00000000b  byte  2
00010000b  byte  3
00111000b  byte  4
01101100b  byte  5
11000110b  byte  6
11000110b  byte  7
11111110b  byte  8
11000110b  byte  9
11000110b  byte 10
11000110b  byte 11
11000110b  byte 12
00000000b  byte 13
00000000b  byte 14
00000000b  byte 15

完整的位图包含每个ASCII码字符的位图,因此它是256*16字节,4096字节长。 如果你想得到一个特定字符的位图,你必须将ASCII码乘以16(字符中的行数),再加上位图的偏移量,你就可以开始了。

存储这些内容的一个非常简单的文件格式是Linux控制台使用的PC Screen Font。 它以上面描述的方式存储字体,并带有一个小标头。 另一个解决方案是可缩放屏幕字体格式,它附带一个非常小的免费呈现ANSI C库。

如何获取字体?

有几种方法。 你可以将其保存在文件系统中的文件中。 你可以在一个数组中对其进行硬编码。 但有时4k占用内存太多了,读取文件不是一个可行的选项(尤其在引导加载程序中),在这种情况下,你必须从VGA RAM中读取设备卡使用的字符(原用于显示文本模式)。

将其存储在数组中

最简单的方法,但会增加你的代码4k大小。 有几个源代码以二进制或源文件格式提供整个字体,因此您无需手动将其写出。

将其存储在文件中

最模块化的方式。 如果你愿意,你可以使用不同的字体。 缺点你需要一个有效的文件系统实现。 至于文件格式,我建议使用前面提到的PC屏幕字体(.psfu)或可缩放屏幕字体(.sfn)。

获取存储在VGA BIOS中的副本

这是一个标准的BIOS调用 (不需要检查它的持久性)。 如果你仍然处于实模式,它很容易使用。

		;输入: es:di=4k缓冲区
		;输出: 用字体填充的缓冲区
		push			ds
		push			es
		;要求BIOS返回VGA位图字体
		mov			ax, 1130h
		mov			bh, 6
		int			10h
		;复制字符映射
		push			es
		pop			ds
		pop			es
		mov			si, bp
		mov			cx, 256*16/4
		rep			movsd
		pop			ds

直接从VGA RAM获取

也许你已经处于保护模式,因此无法访问BIOS功能。 在这种情况下,你仍然可以通过编程VGA寄存器获得位图。 请注意,VGA始终以8x32字体保留空间,因此你需要在复制过程中修剪每个字符的底部16个字节:

		; 输入: edi = 4k缓冲区
		; 输出:用字体填充的缓冲区
		;清除偶数/奇数模式
		mov			dx, 03ceh
		mov			ax, 5
		out			dx, ax
		; 将VGA内存映射到0A0000h
		mov			ax, 0406h
		out			dx, ax
		; 设置bitplane 2
		mov			dx, 03c4h
		mov			ax, 0402h
		out			dx, ax
		; 清除偶数/奇数模式(另一种方式,不要问为什么)
		mov			ax, 0604h
		out			dx, ax
		; 复制charmap
		mov			esi, 0A0000h
		mov			ecx, 256
		; 将16个字节复制到位图
@@:		movsd
		movsd
		movsd
		movsd
		;再跳过16个字节
		add			esi, 16
		loop			@b
		; 将VGA状态恢复为正常运行
		mov			ax, 0302h
		out			dx, ax
		mov			ax, 0204h
		out			dx, ax
		mov			dx, 03ceh
		mov			ax, 1005h
		out			dx, ax
		mov			ax, 0E06h
		out			dx, ax

值得一提的是,切换到VBE图形模式之前必须先完成,因为VGA寄存器之后通常无法访问。 这意味着你将无法将VGA卡的字体内存映射到屏幕内存,并且只能读取到垃圾文件。

设置VGA字体

如果你仍处于文本模式,并且希望VGA卡绘制不同的字形,还可以设置VGA字体。 在图形模式下它毫无价值(因为字符是通过代码显示的,而不是卡显示的),我写这一部分只是为了描述完整。 如果你仔细阅读了到目前为止所写的内容,那么修改VGA RAM中的字体位图并不困难。 我会把它作为作业留给你。(译者注:注意以下内容仍属于这个大纲内,作者还是做了一些提示。)

通过BIOS设置字体

提示:检查Ralph Brown中断列表Int 10/AX = 1110h。

直接设置字体

提示:使用与上面相同的代码,但将源代码和目标代码交换为“movsd”。

显示字符

最后,我们到了可以显示字符的地步。 我想你已经准备好了。 我们必须绘制8x16像素,位图中的每一位都需要一个像素。

// 这是您加载的位图字体
unsigned char *font;

void drawchar(unsigned char c, int x, int y, int fgcolor, int bgcolor)
{
	int cx,cy;
	int mask[8]={1,2,4,8,16,32,64,128};
	unsigned char *gylph=font+(int)c*16;

	for(cy=0;cy<16;cy++){
		for(cx=0;cx<8;cx++){
			putpixel(glyph[cy]&mask[cx]?fgcolor:bgcolor,x+cx,y+cy-12);
		}
	}
}

参数很简单。 你可能想知道为什么要从y中减去12。 它用于对齐基线(baseline):认为你指定的y坐标作为字符的底部,未将向下的字形中的“猪尾(piggy tail)”计算在内 (例如在 “p”,“g”,“q” 等中)。 换言之,就是字母“A”的最底有已设置1的bit位的那行。

虽然直接擦除字形下的屏幕像素也够用,但在某些情况下可能不好(例如:在闪亮的渐变按钮上写字)。 这是一个稍微修改的版本,考虑了透明背景。

// 这是您加载的位图字体
unsigned char *font;

void drawchar_transparent(unsigned char c, int x, int y, int fgcolor)
{
	int cx,cy;
	int mask[8]={1,2,4,8,16,32,64,128};
	unsigned char *gylph=font+(int)c*16;

	for(cy=0;cy<16;cy++){
		for(cx=0;cx<8;cx++){
			if(glyph[cy]&mask[cx]) putpixel(fgcolor,x+cx,y+cy-12);
		}
	}
}

正如你所见,我们这次只有前景色,putpixel调用有一个条件:仅当位图中的相应位被设置时调用。

当然,上面的代码会非常慢(主要是因为一次只处理一个像素,并且反复重新计算“putPixel()”函数中每个像素的地址)。 为了获得更好的性能,上面的代码可以优化为使用布尔运算和 “掩码查找表”。 例如(对于8-bpp模式):

// 这是您加载的位图字体
unsigned char *font;

void drawchar_8BPP(unsigned char c, int x, int y, int fgcolor, int bgcolor)
{
	void *dest;
	uint32_t *dest32;
	unsigned char *src;
	int row;
	uint32_t fgcolor32;
	uint32_t bgcolor32;

	fgcolor32 = fgcolor | (fgcolor << 8) | (fgcolor << 16) | (fgcolor << 24);
	bgcolor32 = bgcolor | (bgcolor << 8) | (bgcolor << 16) | (bgcolor << 24);
	src = font + c * 16;
	dest = videoBuffer + y * bytes_per_line + x;
	for(row = 0; row < 16; row++) {
		if(*src != 0) {
			mask_low = mask_table[*src][0];
			mask_high = mask_table[*src][1];
			dest32 = dest;
			dest32[0] = (bgcolor32 & ~mask_low) | (fgcolor32 & mask_low);
			dest32[1] = (bgcolor32 & ~mask_high) | (fgcolor32 & mask_high);
		}
		src++;
		dest += bytes_per_line;
	}
}


void drawchar_transparent_8BPP(unsigned char c, int x, int y, int fgcolor)
{
	void *dest;
	uint32_t *dest32;
	unsigned char *src;
	int row;
	uint32_t fgcolor32;

	fgcolor32 = fgcolor | (fgcolor << 8) | (fgcolor << 16) | (fgcolor << 24);
	src = font + c * 16;
	dest = videoBuffer + y * bytes_per_line + x;
	for(row = 0; row < 16; row++) {
		if(*src != 0) {
			mask_low = mask_table[*src][0];
			mask_high = mask_table[*src][1];
			dest32 = dest;
			dest32[0] = (dest[0] & ~mask_low) | (fgcolor32 & mask_low);
			dest32[1] = (dest[1] & ~mask_high) | (fgcolor32 & mask_high);
		}
		src++;
		dest += bytes_per_line;
	}
}

在这种情况下,显示内存(display memory)中的地址只计算一次(而不是最多128次),并且并行计算8个像素(这完全消除了内部循环)。

这种方法的主要缺点是,你需要为每个 “每像素位” 使用不同的功能,除了15-bpp和16-bpp可以使用相同的代码。 在最坏的情况下(32-bpp),查找表的成本为8kib。 用于32-bpp的查找表可以重新用于24-bpp,而对于4-BPP根本不需要查找表。 为了支持VBE能够实现的所有标准位深度; 给出了每个 “绘制字符” 功能的总共5个版本 (4-bpp,8-bpp,15-bpp和16-bpp,24-bpp,32-bpp) 和3个查找表 (8-bpp,15-bpp和16-bpp,24-bpp和32-bpp),如果使用静态表,则总共花费14 KiB的数据 (不在需要时动态生成所需的查找表的话)。

另见

外部链接

  • UNI-VGA - 免费的Unicode VGA字体(.bdf)
  • bdf2c - .bdf字体到C源代码转换器。