又一个使用汇编进行的光追渲染实例。
这一次并非编译期间渲染,而是运行期间渲染。也就是在DOS的8086实模式下, 设置显示模式为图形模式 ,然后在图形模式下通过 写显存 的方式实现光追渲染。
由于使用的图形模式是 0x13
模式,也就是320x200分辨率、256色的 VGA模式 ,在有限的颜色数量上通过计算索引颜色的索引实现对任意颜色的绘制是一件具有挑战性的事。不过任何索引颜色难题都难不倒我。我通过使用 抖动算法 实现了颜色降级。
觉得不错的话请 在github上给我的源码repo 打一个星星。
思路来源
我编写这玩意儿的初衷仅仅是为了玩一玩纯汇编语言的DOS编程。
这个工程从源码上,可以展示如何使用 NASM 语法编写16位程序指令(只使用286指令集)进行光追的渲染计算,以及抖动算法的实现。
除此以外,就是展示如何在VGA图形模式下写显存进行绘图。
因为之前实现了 使用NASM汇编器的宏进行编译期间的光追渲染(直接编译出图) ,有点儿玩腻了,所以这一次打算用指令来实现运行时的光追渲染,也就是编译成可执行程序,再运行这个程序来渲染光追并显示。
实现难点
主要的难点有这么几个:
程序的整体设计
16 位 DOS 程序只有很有限的内存空间可用。最原始“640 KB内存”的时代,16位程序的内存地址空间总共 16 个大页,每页64 kb,并且高 6 页都被映射到了硬件上,包括显存,而低 10 页里面最低那一页不仅包含中断向量表,还包含 BIOS 的自检信息,以及 DOS 的系统实现。不过貌似 DOSBOX 自己并没有把 DOS 的实现放到这块内存里,它看起来是自己钦定了各种 DOS 功能(包括鼠标API接口中断 0x33)。这其实很不清真,因为我尝试自己使用 8042 端口操作来初始化 PS/2 接口的鼠标从而获得对鼠标滚轮的支持的时候,我发现 DOSBOX 并不能提供相关的调试机会。它钦定了鼠标中断功能默认被安装而且无法被Hook。
抛开这些东西,话题回到纯粹的 DOS 的应用层编程方面。通常情况下,如果是用C语言编写程序,它会在函数的入口通过 减少SP寄存器的值 (即栈顶)来从栈上给局部变量和子函数的参数分配临时内存。在调用子函数的时候,将函数的参数通过 mov
指令填写到栈上对应的位置, 即可完成传参 ,并调用函数。在函数的出口,C编译器会把SP寄存器的值恢复到之前,从而平衡栈并正确让函数返回。使用栈进行临时内存的分配和释放是很方便的,尤其是在 386 指令集下可以直接使用 [esp + xxx]
从栈上进行相对于栈顶的位置 来寻址数据。但是在 8086 下,并不能直接使用 [SP + xx]
从栈上寻址,只能是 [BP + xx]
的方式,也就是必须 建立栈帧 。
单说一个COM程序在一个大页里,这一页的末尾是栈顶,开头是 COM 程序被 DOS 系统建立的 PSP 头。在这样的内存布局下,如果大量使用栈,会导致 栈顶位置低于程序长度导致对程序尾部的代码和数据的复写 ,从而造成 无法由操作系统拦截的程序崩溃 (实模式编程就是这样,程序崩了后的行为是不会受管控的)。所以, 不敢大量使用栈 。
此外,我也不想成为人肉编译器+优化器,所以我并不想写那么多宏,也不想在建立栈帧上浪费 CPU 周期。 我直接用全局变量来给函数传参并接收函数的返回值 ,不建立栈帧,也不从栈上传参。不想走C编译器那套调用约定,毕竟也并不需要调用任何外部库的功能。
颜色降级的实现
初进入 0x13 图形模式的时候,它是有一个 默认的调色板 的。我不喜欢这个调色板,因为如果要使用它作为我颜色降级用的调色板,我就需要对每个像素都遍历一下整个 256 色调色板,用勾股定理判断颜色距离(虽说可以省略掉开方),然后找出 颜色差异最小 的那个索引,作为目标颜色。
此外,直接使用最接近颜色值,不做抖动处理的话,画面上会出现很多“龟纹”。为了能使用抖动算法,并且简化最接近颜色的匹配方式,我把颜色索引值进行了编码,让它成为 R:G:B = 2:3:3 这样的位格式。再通过对应的编码, 生成一份自己的调色板 ,也就是通过修改 VGA 控制器的 DAC 寄存器来实现。这样一来,每个这样的索引可以从调色板里取出对应的颜色。
在渲染出像素的RGB颜色值后,让其各组分的值都加上对应像素坐标的抖动矩阵的值乘上一个权重值,做 颜色深浅度的扰动 ,然后再取最接近颜色,即可实现矩阵抖动。其中, 权重值的取值 应该相当于编码颜色格式里,对应颜色通道的 位数减少量 的情况。位数减少越多,权重值越大,抖动算法越能跨越不同的颜色来实现 仿色 ,所以对应上述红色只有 2 bit 的情况——只有4个值的情况下,权重值应为 0.25 ,而绿色和蓝色因为各有 3 bit ,也就是 8 个值,权重值应为 0.125 。
除此以外的此处不再赘述,请参考 维基百科的矩阵抖动算法介绍文章 。
调试相关
在 DOS 里调试 DOS 程序是 最麻烦的 。因为需要用到 DEBUG.EXE 这个非常老的 DOS 调试软件去 调试一个图形模式的程序 ,它并不能在图形界面上正确显示它的内容,包括其输出内容和你对其输入的命令。你看不到光标闪烁,按键它也不一定理你,打印寄存器内容的时候也 不能显示完整 。所以我放弃了在 DOS 中直接调试它,而是通过修改每一个程序分支的行为来看屏幕上显示内容的变化。