模拟器是一种程序,它运行从支持模拟器的主机平台为不同计算机体系结构构建的程序。方法不同,但大多数仿真器都以某种方式模拟原始硬件。模拟器至少会解释原始的CPU指令,并为输入和输出提供模拟的硬件级设备。例如,从主机平台获取键盘输入,并将其转换为原始硬件格式,从而使模拟程序“看到”相同的击键序列。相反,模拟器将把原始硬件屏幕格式转换为主机上的等价格式。
仿真器类似于实现Java虚拟机(JVM)的程序。这只是程度上的差别。JVM被设计成支持高效和可处理的实现,而模拟器的机器是由实际硬件定义的,通常对模拟器施加了不希望的约束。最重要的是,原始硬件可能只能根据现有软件如何使用它来完整地描述。JVM倾向于前瞻性,期望在JVM下编写和运行新代码,以增加其可移植性。模拟器往往是向后看的,只希望使旧代码更可移植。
在任何一种情况下,仿真器和JVM都为在它们下面运行的程序提供了一套与主机系统交互所需的服务。JVM将这些服务表示为一系列API调用;仿真器将它们表示为模拟硬件。尽管如此,模拟硬件不是大多数程序员所期望的api形式。
作为一个硬件API示例,考虑TRS-80视频系统,它在经过改装的电视机上显示文本和图形。它有16行字符,每行64列。它支持普通的ASCII字符集,具有额外的64个图形字符,允许2像素* 3像素子块的各种可能组合。明智地使用图形字符提供了128像素* 48像素的有效分辨率,尽管像素只有西瓜种子大小。TRS-80程序通过将字符值写入与所需位置相关联的内存位置来显示字符。实际上,API只有一个调用:
模拟如此简单的图形格式是微不足道的。它可以在512 × 192的图像上充分渲染,每个字符分配一个8 × 12的矩形。每个图形像素都是一个4乘4的矩形,而字符本身占据了上面的三分之二,或8乘8的区域。虽然您可以使用任何旧字体来显示字符,但再多做一些工作就会得到与原始字符完全相同的内容。(为了获得与原始图像相同的纵横比,图像的高度应该翻倍。为了简化说明,我们将保持数字不变。)
图1演示模拟器如何将硬件级字符值转换为实际图像表示。虽然该图是模拟器的蓝图,但它还显示了TRS-80程序为了显示某些内容必须做什么。遗漏了一个细节:对屏幕内存的更改不会立即显示。硬件每隔1/60重绘一次显示器th秒。这意味着模拟器必须按所述组装静态图像并每隔1/60显示一次th秒。
生成的仿真将非常类似于原始的仿真。大多数在模拟器下运行的程序看起来与在原始硬件上运行时完全相同,但如果您非常注意,您会发现一些不同。看着屏幕从全黑变成全白。在原版游戏和模拟游戏中,你都会遇到一些问题,因为填充屏幕需要花费足够长的时间,以至于它会跨越多个视频帧。在填充开始之前,框架将是黑色的。填充部分在下一帧完成,显示显示顶部为白色,底部为黑色。这只发生了一瞬间;下一帧填充完成,帧填充白色。
虽然这是在感知的边缘,但两者所表现出的眼泪是完全不同的。你将需要一个摄像机来看到它的原始,但模拟器可以转储帧以离线分析或被告知一步一个帧。图2显示原始硬件上的撕裂和在模拟器中看到的撕裂之间的差异。
虽然看起来很小,但这种差异令人费解。模拟器完全按照编写的方式实现了规范,在代码中没有发现任何错误。此外,该规范显然是正确和完整的。除非手头有证据,否则情况是不可能的。当然,问题在于规范,它只是看起来完整。假设某个字符存在或不存在是不正确的。硬件不是一次画一个字符;它每次只画一行字符。这个角色在抽奖开始后有机会改变。原始字符的撕破是由于最初的字符是空白的,随后被填充。 Put another way, the character is not atomic but made up of 12 pieces stacked on top of one another; each piece is 8-by-1. Incidentally, those 8-by-1 pieces are atomicthey are displayed entirely or not at all. The graphics hardware ends up reading each displayed character 12 times.
精炼仿真器以纠正这种差异是很简单的。而不是等待整个1/60th在绘制屏幕前的一秒钟,它将每次绘制一条线。192行模拟循环看起来像这样:
模拟器上的撕裂和硬件是一样的。由于输出保真度的大幅提高,您可能会倾向于宣布规范和模拟器已经完成。然而,作为一名认真的开发人员,你的反应必须完全相反。一个相当小的测试用例需要对程序进行相当大的更改。现在是时候进一步调查并查找其他错误了。该规范很可能需要进一步完善。至少,新功能需要一个更好的测试用例。稍作思考后,很明显,显示一行高的像素(1 × 4)将构成这样的测试用例。
这可以通过三个简单的步骤来完成。
所有在屏幕上可见的是像素的1 × 4部分,在你从4 × 4像素下拉出地毯之前绘制的部分。许多像素可以组合在一起,创造出在普通TRS-80上看似不可能的东西:高分辨率的对角线。
唯一缺少的是知道硬件在任何一点绘制哪条线的方法。幸运的是,图形硬件在绘制帧时生成一个中断。当中断发生时,你就知道图形硬件的确切位置了。构建仍然存在一些困难,但它们归结为一些琐碎的事情,比如在内存访问之间设置延迟,以确保您在绘制每条线时同步打开和关闭像素。
在这里,模拟器是一个福音。在真正的硬件上运行这样一个精心安排时间的过程是非常困难的。任何时间上的错误都会导致没有显示,因为一个像素擦除的太快,或者一个像素擦除的太慢,导致一行阻塞。调试器不仅不关心时间,而且完全避免时间。单步遍历代码是没有用的。公平地说,调试器不能单步处理图形硬件。即使是这样,在你看到发生了什么之前,荧光粉也会从你的视线中消失。
仿真器可以单步处理处理器和图形。它可以准确地显示正在绘制的内容,并指出何时屏幕内存写操作发生在不正确的时间。很快就编写了一个演示程序,在简单的模拟器中显示块状线,在更精确的模拟器中显示对角线(参见图3).
大多数在模拟器下运行的程序看起来与在原始硬件上运行时完全相同,但如果您非常注意,您会发现一些不同。
该程序是令人印象深刻的,因为它必须读/写的显示微秒级的时间。真正令人兴奋的是在原来的机器上运行程序。毕竟,从理论上讲,PC模拟器的输出是引人注目的,但实际上它产生的图像与平台上的任何其他东西相比都相形见绌。在真正的机器上,它将产生前所未见的东西。
不幸的是,这个程序完全不能在真正的机器上工作。大多数时候显示屏是空白的。它偶尔会闪现一帧的普通块线,很少会出现一个小像素,就像偶然出现一样。
同样,精确的模拟并不那么精确。原始撕裂效应证明了基本方法的有效性。一定是时机本身出了问题。对于那些擅长软件的人来说,一些实验程序可以梳理出其中的差异。硬件类型将直接进入详细记录图形硬件的原理图。无论哪种方式,以下几个特征都将变得明显:
最值得注意的是,模拟器在模拟撕裂时表现得非常准确,而实际上是完全错误的。关于计算机系统的脆弱性的文章已经写得太多了,以至于人们很容易忘记它们有时是多么的灵活和宽容。这些数字为模拟器代码本身带来了一些宽慰。看起来是用于计时的浮点值,实际上只是系统时钟的倍数。很简单,CPU速度和图形硬件之间存在整数关系。
我们可以从CPU的角度重申计时:
每秒帧数仍然是一个浮点数,但是模拟器核心可以返回整数,正如您对数字系统所期望的那样。
有了新的计时后,模拟器会出现与实际硬件相同的问题。只需稍微(繁琐)调整一下时间,该程序几乎可以在真正的硬件上工作。
只剩下一个问题了。还记得在CPU和图形硬件之间提供同步点的中断吗?结果发现这种事每天都发生第二个框架。这个程序可以运行,但会在完美的对角线和粗粗的对角线之间闪烁。这里没有硬件设施来帮助解决这个问题,但有一个明显的(虽然令人反感)软件解决方案。
一旦画好了对角线,您就确切地知道什么时候必须再画一次:从第一次开始画它的时候算起,33,792个周期。如果需要T次循环才能画出这条线,那么只需编写一个运行33,792 T次循环的浪费周期的循环,然后回到画线例程。因为这一跳跃需要10个循环,所以你最好将其设置为33792 - t -10循环。这似乎是一个很棘手的问题,但即使是计数中的单个循环也会失去同步性。两秒钟内,同步就差了整整一行。失去同步的影响类似于困扰老式电视机的垂直滚动。
一个特别的解决方案就可以了。概念论证演示程序将完成。很明显,我们有可能创造出更令人印象深刻的图像。然而,手工计时是乏味、缓慢且容易出错的。您不得不在程序集中编写代码,但计时工作将您带回到手工组装代码的时代。通过将指令计时表从模拟器中取出并放入汇编程序,可以减轻大部分负担。汇编程序总是能够测量输出的大小,通常是填充缓冲区大小之类的。这是用来定义的工具长度作为消息中的字节数,如果消息被更改,它将会变化:
这是因为特殊的“*”变量会跟踪数据和代码组装到的内存位置。要自动计时,只需添加一个时间()
函数,它表示到那一点为止程序使用了多少循环。它不能解释循环和分支,但可以为直线代码提供准确的结果。在一个高水平的对角斜杠演示将是:
很简单,但是浪费周期的代码呢?可以扩展汇编程序以自动提供该代码。相反,根据最小设计原则,该任务可以留给一个普通的子程序。编写运行给定周期数的子例程与您习惯的要求不同,但这是可能的。(参见附带的边栏,了解这种浪费周期的子例程。)
作为程序员,我们可以看到对角线演示程序的潜力。尽管它每行只有一个像素,但有一条清晰的路径通向更复杂和更引人注目的图像,更不用说动画和其他效果了。前方还有最后的坎坷。每次CPU访问屏幕内存时,它都拒绝访问图形硬件。这将导致出现两个或三个字符宽的空行。每行改变的像素越多,被省略的部分就越多。你会再次发现,尽管这些图像在模拟器上看起来很好,但在真正的机器上,由于消去的副作用,它们会布满“洞”。
此外,当你试图在每行做更多的工作时,空白点的确切位置将非常重要。它们的精确位置将是模拟器精度的衡量标准,可以用于最大化每行显示的图形。在仿真器改进、测试程序开发、原始系统的测量导致进一步的仿真器改进等反馈循环过程中,将会有一些发现等着我们。一路上你会发现以下几点:
换句话说,我们已经离起点很远了。模拟器不再一次绘制整个屏幕或一次绘制一条线,而是只绘制1/12th并在CPU周期级别上交织CPU和图形硬件。图形模拟变得非常精确。不仅会看到像撕裂这样的副作用,而且它们将与在原始硬件上显示的完全相同。研究结果也不是纯学术的。测试程序演示仿真器的保真度,同时仍然在原始硬件上实现相同的输出。结果不仅仅是专家感兴趣的微小差异,而是精确模拟器和草率模拟器之间程序行为的极为明显的差异。
还能有别的吗?
在被这么多模拟器的缺点绊倒之后,答案是肯定的吗?事实上,有一种双宽模式,在32 × 16的显示器中,字符的大小是原来的两倍。根据我们到目前为止所看到的情况,它所带来的复杂性比预期的要多得多,这并不令人惊讶。即使不考虑这个问题,模拟器还有一个更明显的局限性。最初的显示器是CRT。它的每个像素看起来都与现代液晶平板完全不同。那里的像素是方形的,而CRT产生的是边缘柔和的椭圆形磷光。图4比较了字母A的两个特写。
硬边像素产生的图像在功能上与原始图像相同,但有完全不同的感觉。两者之间的区别是显而易见的。还要观察到,真实的像素既不是不同的,也不是独立的。相邻行的像素重叠。相邻列中的像素不仅重叠,而且在一行中有单个像素和多个像素时显示也不同。一排发光像素中的第一个像素较大。所有这些细微的差别结合在一起,形成了一幅截然不同的图景。
问题本身比功能问题简单得多,因为没有对其余实现的反馈。不需要更改CPU定时或CPU与图形系统的交互方式。这仅仅是将每个点绘制为alpha混合的补丁,而不是一两个像素的硬边关闭/打开设置。麻烦的是主机CPU完成这一任务所需的工作量增加了。所涉及的工作比以前多了许多倍。只有通过图形协处理器或适度优化的渲染代码的帮助,才能以这种方式实时绘制屏幕。很难相信绘制一台已有30年历史的电脑显示屏会占用现代系统这么多的空间。这就是为什么精确模拟需要很长时间才能完善的原因之一。我们可以决定做一个更好的显示,但今天的平台可能没有完成它的马力。
真实的模糊像素可以重叠,导致明显的视觉假象。两个像素在开和关之间交替,并排坐在一起将看起来是三个像素:两端各有两个闪烁像素,中间两个像素重叠的地方有一个总是开的像素。这件藏物会有什么有用的作用,就留给你们自己去想象吧。
系统的复杂性很容易被低估。即使是TRS-80的简单视频系统也比预期的有更大的深度。隐藏在表面之下的东西远比高层的描述要伟大得多。把它当作KISS原则的侧面强化。然而,不要绝望。你还必须考虑到工具的力量。每次模拟器的改进都带来了一些发现,一旦构建了必要的支持工具,就可以很好地利用这些发现。然而,最重要的是,要提防完美。没有一个系统是完美的,追求完美的成本比投入的时间和金钱要大得多。
相关文章
在queue.acm.org
没有源代码?没问题!
彼得·菲利普斯和乔治·菲利普斯
http://queue.acm.org/detail.cfm?id=945155
增强的跟踪调试
彼得·菲利普斯
http://queue.acm.org/detail.cfm?id=1753170
©2010 acm 0001-0782/10/0600 $10.00
允许为个人或课堂使用本作品的全部或部分制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,需要事先获得特定的许可和/或付费。
数字图书馆是由计算机协会出版的。版权所有©2010 ACM, Inc。
没有发现记录