C64演示效果详解:Rodents In The Attic

Lobsters Hottest 新闻

摘要

一篇关于Commodore 64演示的技术解析,该演示利用边框开启技巧、VIC芯片幽灵字节故障以及精确的光栅时间编码,将彩色精灵放置在边框区域。

<p><a href="https://lobste.rs/s/zsq34k/c64_demo_effects_explained_rodents_attic">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/31 04:45

TL;DR:一个名为 *Rodents in the Attic* 的 Commodore 64 演示程序,利用经典的边框开启技巧、一个称为“幽灵字节”的 VIC 芯片故障、巧妙的精灵定位以及紧凑的光栅时间编程,将彩色高分辨率的旅鼠放在了边框区域。 ## 演示程序及其不可能的效果 我为 Commodore 64 制作了一个名为 *Rodents in the Attic* 的演示程序。在这个视频中,我解释了它的工作原理。我在视频描述中放了演示程序本身的链接,强烈建议先看视频,因为它有一个可爱的故事,而解释中会有剧透。 演示程序中还有一个小小的魔术:一个看似不可能的效果,大量彩色高分辨率物体在边框区域移动。我会解释为什么这不应该能实现,以及我又是如何设法做到的。但最难实现的部分其实在另一边,我也会解释它是如何工作的。 ## 快速回顾:C64 视频输出如何工作 C64 有一个 320×200 像素的主区域,可以在其中显示图片或文本。你最多可以放置八个所谓的精灵,既可以位于所有前景像素之前,也可以位于其后——在本例中,是位于文本之后。 一个精灵是 24×21 像素,但你可以在 X 方向、Y 方向或两个方向上把像素放大一倍。你会在演示中看到的精灵处于高分辨率模式,每个像素要么是透明的,要么是某一种特定颜色。还有一种多色模式,每像素两位,允许三种颜色加透明,但水平分辨率减半。 在所有东西前面是边框。你可以选择四种不同尺寸配置的边框,并改变其颜色,但不能完全关闭它。除非……实际上可以关掉,我稍后会回到这一点。边框的存在是为了帮助游戏开发者:例如,敌方精灵可以从显示器的边缘出现。 ## PETSCII 图形 在演示中,背景图形——房间内部以及角色——填满了整个主区域。这就是所谓的 PETSCII 图形,意味着它使用文本模式和来自 C64 ROM 的内置字体。该字体包含一系列图形字符,用这套有限的字符集来创作图片已经是一种公认的艺术形式。 屏幕可以容纳 40×25 个字符,每个字符有独立的前景色,但所有字符必须共享同一种背景色。对于这幅图,我使用了中灰色作为全局颜色,但最常见的选择是黑色,这也是我这次最终采用的方案。 使用 PETSCII 是一种风格上的选择,但一个技术上的好处是,动画化这些图形几乎不消耗 CPU 算力,因为你只需要更新字符引用,而不是逐像素更新。 你不需要数字符就能看出这幅图使用了完整的主图形区域,因为基本的开机主屏幕就是这样,而演示开始时其边界保持不变。但这意味着旅鼠在边框区域内走来走去,这通常是不可能的——除非边框被关闭,或者用 C64 的行话来说,被“打开”了。 ## 打开顶部和底部边框 随着视频信号逐行生成,从左到右、从上到下,C64 内部的视频芯片——VIC 芯片——会跟踪两个内部内存位,两个标志。 - 一个标志表示当前是否正在绘制边框。我们称之为 B,代表边框(border)。 - 另一个表示当前是否处于边框占满整个光栅线的屏幕区域。我们称之为 V 标志,V 代表垂直(vertical)。 每当 B 标志为低电平时,VIC 芯片显示由芯片其余部分生成的任意像素,可能来自精灵或主图形区域。但当标志为高电平时,则输出当前的边框颜色。 以下逻辑是硬连线到芯片中的: - 当我们到达可见区域的顶部边缘时,拉低 V 标志。 - 当我们到达底部边缘时,拉高 V 标志。 这些事件每视频帧发生一次,因此在欧洲是每秒 50 次。 B 标志则更活跃: - 每次我们到达可见区域的左边缘时,如果 V 标志为低,则拉低 B 标志。 - 当到达右边缘时,总是拉高 B 标志,开启边框。 因为我们可以选择四种不同的边框尺寸,并且可以在视频信号生成过程中更改此配置,所以我们可以操纵这些标志。 要打开顶部和底部边框,我们希望 V 标志始终保持低电平。因此我们要阻止 VIC 芯片拉高它,而它原本会在可见区域的底部边缘拉高。我们从一个高边框配置开始,允许 VIC 芯片生成视频直到屏幕上一个窄带中的某一点,然后切换到另一种边框尺寸,使得 VIC 芯片永远不会恰好处于可见区域的底部边缘,也就永远不会拉高 V 标志。 一旦我们安全通过了关键窄带,我们再次选择高边框,以便可以重复此过程。我们必须每个视频帧都执行这些操作,但这并不太复杂,我们可以从光栅中断处理程序中完成。 ## 打开侧边框 打开侧边框更加棘手,但基本原理相同。在这里,我们希望避免拉高 B 标志,而 VIC 芯片每次到达可见区域右边缘时都会拉高它。 所以我们从一个宽边框开始,当 VIC 芯片到达这里时,我们切换到窄边框配置。VIC 芯片的边框逻辑永远不会处于应该拉高标志的位置,因此它永远不会拉高。然后再恢复宽配置。 打开侧边框之所以棘手,有两个原因。 第一,我们必须对希望打开的每一条光栅线都执行此操作。不是每秒 50 次,而是必须以每秒超过 15,000 次的频率执行此过程。这有很大的性能影响,留给 CPU 做其他事情的时间更少。 第二,时序极其关键。这种边框尺寸重新配置必须发生在某一个特定的时钟周期:每条光栅线的第 56 个周期。因此我们必须将 CPU 与视频芯片的操作精确同步,并计算代码的时钟周期。 这些技术在 1980 年代就已经被发现。首先是如何打开顶部和底部边框,后来是如何也打开侧边框。 ## 旅鼠:技巧的核心 即使边框完全关闭,主图形区域仍停留在屏幕中央。因此,你只能在边框区域显示精灵图形。而精灵受到限制:任何一条光栅线上最多八个精灵,每个精灵宽度不超过 24 像素,可能进行 X 方向扩展,但这会使单个像素变宽。多色模式则使像素更宽。 所以顶部边框中的旅鼠不可能是单独的精灵,因为: 1. 它们太多了。 2. 它们每一个都有不止一种颜色,并且是高分辨率。 这看起来是不可能的。 Commodore 64 的 VIC 芯片还有一个在此起作用的特性:一个称为“幽灵字节”的硬件故障。 当你打开顶部和底部边框时,这些区域中看到的不仅仅是一种纯背景色。相反,你看到的是主区域图形生成的一个伪影,通常被边框隐藏。具体来说,VIC 芯片能够寻址的最后一个内存字节会以八个黑色或透明像素的重复图案显示。 通常,你会把这个所谓的幽灵字节设为全零以显示背景色,或全一来获得纯黑色区域。但你可以将其设置为任何图案,甚至可以在显示生成过程中更改图案。你无法在每个时钟周期都更改幽灵字节——CPU 太慢了——但你当然可以每条光栅线更改一次。 回想故事中的内容,行走的旅鼠动画是八像素宽,与幽灵字节的八个重复像素完美匹配。这一切都是华丽的高分辨率,但到目前为止只是单色图形。 你可以将精灵放在前景文本之后,同样它们也可以放到幽灵字节之后。要覆盖主图形区域的完整宽度,你需要七个 X 方向扩展的精灵。但旅鼠有三种颜色——蓝色、绿色和灰色——所以我们需要将精灵置于多色模式。现在问题来了,因为旅鼠充满了精细的高分辨率细节。但由于我们使用扩展的多色精灵,我们必须使用笨重的四像素宽块。 这就引出了整个技巧的核心,是使整个演示成为可能的关键洞察。 当我研究 Gary Timmons 的原始旅鼠行走循环时(就像人们常做的那样),我发现动画的每一帧中,几乎每一行像素除了黑色之外只包含两种颜色,并且这两种颜色之间只有一条分界线。 例如,某一行包含灰色部分和蓝色部分,分界线在这里。另一行是绿色和灰色,分界线在这里。 我们可以从一条光栅线到另一条光栅线调整精灵的水平位置。所以如果我们那些块状扩展精灵看起来像这样,但我们在每一行调整精灵位置,最终会得到这样。然后我们应用黑色的幽灵字节图案来隐藏旅鼠外部的部分。 位置调整对于每个动画帧都不同。这是没有幽灵字节遮罩的样子,而现在是有遮罩的样子。 回到原始素材,几乎所有行都有两种颜色,由一条分界线分隔,但有一些例外。例如这一行:这里我不得不偏离原始像素数据,把脚趾尖做成蓝色而不是灰色。 Commodore 64 产生模拟视频信号,其中颜色信息受到带宽限制。因此,可以说单个高分辨率像素并没有真正的颜色,只有亮度。但另一种理解方式是:我在作弊。 ## 隐藏单个旅鼠 在演示中,你并不会一直看到所有旅鼠。队列中有间隙。全局背景色是黑色,因此多色精灵中的透明像素与黑色幽灵字节像素无法区分,这就是我能隐藏单个旅鼠的方式。 事实上,相当大比例的可用 CPU 算力都花在了将透明像素遮罩滚动穿过多色精灵图形上。 ## 第一只旅鼠:特殊情况 演示中的第一只旅鼠径直走出边缘,从屏幕侧面掉落。这需要打开侧边框,而侧边框要求每条光栅线都有 CPU 参与,通常会影响性能。 但顶部边框的光栅代码在性能上已经满负荷了。每一条光栅线,它都需要更新幽灵字节以及每一个多色精灵的 X 位置。因此时序非常紧张,没有空间再同时打开侧边框。 所以那第一只旅鼠是个例外,以完全不同的方式显示:将一个灰色、一个绿色和一个蓝色高分辨率精灵叠放在一起,以便为 CPU 留下足够的时钟周期来打开侧边框。 然后挖了一个洞来分流旅鼠队列,这样我们就不需要在忙于更新精灵和幽灵字节的同时,还在顶部边框区域打开侧边框。 此外,挖洞的时间足够早,使得我们不需要用精灵覆盖主区域图形的整个宽度。在顶部边框放置六个 X 方向扩展的多色精灵就足够了,这样就剩下两个精灵可用于实现掉落的旅鼠。它们用一个黑色高分辨率精灵放在一个未扩展的多色精灵前面来渲染。你会注意到一种节奏,它们交替地落入队伍中和走出队伍。 ## 真正的挑战:下部区域、侧边框和坏行 这虽然繁琐,但真正的挑战在这个区域,我们必须重复顶部边框的技巧——尽管只有很短的一段——同时打开侧边框,并且没有幽灵字节的帮助。 记住,幽灵字节是图形生成过程中的一个故障,发生主图形区域之外。但这里一切正常,没有幽灵字节。相反,我们在这里放一个多色精灵,然后使用前景色为黑色的常规字符图形来模拟幽灵字节图案。这意味着我们不再需要每条光栅线更新幽灵字节,但仍然需要每条光栅线调整精灵的 X 位置并打开侧边框。 当旅鼠走出主区域时,我们不能再使用字符图形作为黑色遮罩,也不能用幽灵字节。所以这里我们需要第二个高分辨率模式的精灵来提供黑色遮罩。然后我们还需要两个精灵来实现掉落的旅鼠,同样在这里交替落入队伍中,并在侧边框处走出队伍。 让事情更复杂的是这个小东西,叫做坏行。每当新一行字符开始时——例如,就在我们时间紧迫的旅鼠绘制例程中间——VIC 芯片会暂停 CPU 40 个时钟周期,以便从内存中获取新的字符数据。这使得时序比原本更加受限。 长话短说,我们只能在坏行上同时显示最多四个精灵并同时打开侧边框。我们有四个,所以没问题,但这意味着我们已经到了极限。 此外,在坏行上没有时间来更新精灵的 X 坐标。但幸运的是:原始动画数据中的第三和第四行像素在所有帧中都可以使用相同的 X 位置,只有一帧例外,而那里我们最终只有一个错误的像素。 ## PETSCII 字体修改与精灵复用 还有另一个复杂因素。这些 PETSCII 图形是使用默认 ROM 字体在文本模式下绘制的。我实际上在 RAM 中复制了一份字体,以便修改它。虽然图片是由图形字符组成的,而文本气泡包含反转的字母字符,但我从不使用非反转的字母字符。所以我可以在隧道将要经过的这个屏幕区域重新利用这些字符代码。 由于全局背景色是黑色,当第二只旅鼠挖洞和撞击穿过时,我只需清除字体定义中相关的像素即可。 但一旦大批旅鼠到达这个区域,我就必须使用这些位置的字符来模拟幽灵字节。这意味着它们需要有黑色前景色。但全局背景色也是黑色,所以这整个区域最终都会变成黑色。 那我如何显示背景图片的其余部分——那些没有被撞掉的橙色和蓝色像素呢? 我不能在这些字符单元中间改变前景色,因为那会引入另一个坏行,从而破坏时序。所以剩下的选择是使用精灵,但我不能在这个区域再增加精灵了,还记得吗? 相反,我复用了多色扩展精灵和模拟幽灵字节的黑色精灵来显示橙色和蓝色部分,然后在旅鼠队列到来之前及时重新配置它们。 ## 动态侧边框代码 打开侧边框在时钟周期方面代价高昂,所以我不想打开超过必要数量的光栅线。但侧边框中的旅鼠在掉落,所以我用于打开侧边框的代码必须能够在任何垂直位置运行。 同时,坏行是固定的,它们会影响代码的时序。因此,用于打开屏幕下部侧边框的代码会动态检测坏行何时到来,并采取不同计时的代码路径。有时它甚至需要在这里运行,重叠……

相似文章

模拟器调试:Area 5150 的 Lake Effect

Lobsters Hottest

本文详细介绍了在MartyPC模拟器上调试Area5150演示中“Lake”效应的过程,解释了需要特定标题hack的原因,以及通过总线嗅探和动态时钟实现周期精确CGA模拟的后续修复方法。

详解:16字节x86代码,让矩阵雨变成声音

Hacker News Top

一篇详细的解析,关于一个16字节的x86实模式DOS演示程序,它在视频内存中生成无限的谢尔宾斯基分形,同时产生音频输出,展现了demoscene传统中极致的算法密度。

Five monitors on a Commodore 128 [video]

Hacker News Top

A retrocomputing experiment driving five independent monitors from a Commodore 128 by splitting RGBI signals with a custom circuit board, along with similar tests on IBM CGA/EGA systems.

C64 Dead Test 字体

Hacker News Top

对 C64 Dead Test 诊断卡中使用的独特字体的详细探索,包括其字符集、一个彩蛋以及可下载的字符 ROM。