将沃尔芬斯坦型发动机移植到MEGA65

2020-07-11 23:32:05

这篇帖子是关于我脑海中想了很久的东西:几年前(或者可能更久),有人向我指出,理论上DMAic控制器可以用来绘制纹理,因为纹理绘制实际上只是将纹理复制到屏幕上的问题。嗯,然后适当地缩放它。处理屏幕内存布局,这样每个连续的写入都会转到下一个像素。幸运的是,使用一些相当简单的技巧就可以同时做到这两点。

首先,让我们先看看像素写入。首先,请记住MEGA65-IV的VIC-IV没有帧缓冲器。相反,就像C64-II的VIC-II一样,屏幕通常由8x8字符单元格组成。位图模式和文本模式的主要区别在于,您是否可以选择哪个单元格出现在哪里,或者它们是否以固定的排列出现。*这实际上是沃尔芬斯坦类型引擎的VIC系列视频控制器的强项,我们需要绘制垂直像素条纹。让我解释一下:

这是位图模式下的正常排列,升序字符在屏幕上排列。因此,要找到一列中下一个像素的地址,我们必须计算是否越过了字符边界,如果是,则添加足够的地址来跳过行中的其余字符。但是如果我们使用文本模式,并将像素放入字符数据中,我们可以不同地排列屏幕,向下条带化行,而不是跨列,如下所示:

现在,如果我们想要在屏幕上向下推进一个像素,我们总是添加相同的数量,即在一个字符中向下跳过一行的字节数。在VIC-II上,这总是一个字节,因为一个位定义每个像素。但是,为了一个好看的引擎,我们希望有更多的像素颜色。所以对于这个引擎,我使用的是VIC-IV的全彩色文本模式。您可以在“MEGA65程序员参考指南”中了解更多信息,但最重要的是,字符的每个像素现在都用一个字节而不是一个位来表示。这意味着由8个像素组成的一行字符数据现在占用8个字节,而不是1个字节(8位)。因此,要从一个像素转到下一个像素,我们需要告诉DMA控制器在每次生成要写入的地址时将地址加8。

这就是写作方面。因此,让我们考虑另一半:从纹理读取,并对其进行缩放。第一个观察结果是,缩放应该在读取端进行,因为我们总是希望只向每个目标像素写入一次。因此,解决方案是更改每个新像素的源地址的计算方式。

如果纹理是以1:1的比例绘制的,那么很简单:我们只需读取纹理数据的每个连续字节,并在写入屏幕时使用我们生成的x8地址将其写出。

如果纹理需要在屏幕上显示得比在内存中大,那么我们只需要偶尔增加源地址,这样相同的像素值就会重复。

相反,要在屏幕上绘制比内存中更小的纹理,则需要将源地址平均递增1以上,以便跳过一些像素。(理想情况下,我们应该计算像素值的某个平均值,但是这对于我们可怜的8位机器来说太多了,当然也要求DMA控制器为我们做太多工作)。

我们对此问题的解决方案是允许DMA控制器每次将源和/或目标地址递增1/256字节到255字节之间。这样,我们可以在写入端一次跳过8个字节,并在读取端相当平滑地缩放我们正在读取的纹理。“有时最后会有很小一部分像素误差,但这将是一致的误差,因此在屏幕上不会被注意到。

最棒的是,我们可以在任何高度的屏幕上以每绘制2个周期的固定时间成本,在任何高度的屏幕上缩放和绘制任何分辨率的纹理。对于320x200的屏幕来说,这意味着大约128,000个周期。鉴于MEGA65在NTSC中每帧至少有675,000个周期(或在PAL中为810,000个周期),这意味着理论上我们可以以全帧率绘制屏幕,还有一些余地。

与任何其他可能的方法相比,这是非常有利的,其他任何可能的方法可能必须至少涉及一个加载和存储操作(可能更多),导致每绘制一个像素的成本远远超过9个周期。“当我移植引擎时,我直接看到了不同之处,就像我做的那样';我最初没有实现DMA绘画,如果没有DMA的帮助,画一帧画面需要几秒钟,也就是大约200个PAL帧来画屏幕。但是实施DMA绘画之后,我能够达到每秒10帧的帧率,也就是每帧画大约5个PAL帧。这意味着我们的DMA方法比没有DMA方法要快40倍左右!这是在40倍的速度之上,这要归功于CPU的速度。

因此,在这个特殊的情况下,我们能够超过C64大约1600倍的性能,这就是定制芯片的力量。有趣的是,我们的速度也比我发现的最好的Amiga Wolfenstein类型的引擎快得多。“这是意料之中的,因为全色文本模式实际上为我们提供了一种厚实的视频模式--但还有一个额外的优势,我们可以按照我们所做的方式安排屏幕,这样地址之间的跨度是恒定的,因此比那个时代的PC都快。

但是我们在这里有点超前了--我们开始讨论如何做纹理绘画,现在讨论的是一个完整的引擎,而不是我向你们展示中间的步骤……。

因此,让我们来谈谈沃尔芬斯坦打字机及其工作原理。因为他们是第一个成功的第一人称引擎,可以让用户向任何方向看。他们做到这一点是通过铸造光线来实现的。在不同的方向,然后计算出他们击中的地方,然后画出那个东西。他们还必须计算出光线击中的东西有多远,这样他们才能计算出它有多大,来模拟距离的印象。在屏幕上,每列像素投射一条光线,这样就有了快速绘制那些列的兴趣。

他们为快速绘图而进行的算法和优化相当有趣,而且市面上有很多关于它们的信息,还有引擎示例。“我尝试了几个引擎,最后选定了一个专为快速定点计算(与浮点计算相比)而设计的引擎,因为MEGA65没有浮点数学单元。”然后我在GitHub上对其进行了分叉,并开始在MEGA65上进行开发。

回顾提交日志,只花了不到4个小时就将其修改为在MEGA65上绘制显示,包括使用上述DMA绘制方法。但又花了几天时间来修复一些零碎内容,使用光标键实现地图周围的移动,包括撞到墙壁等。

天空纹理增加了很大的现实感,并有助于根据玩家面对的方向调整他们的方向,结果证明非常容易做到:因为天空是垂直的;我们可以只画一个环形纹理,然后在屏幕的每一列中绘制它,一直画到纹理墙应该出现的地方。另一方面,地板是痛苦的,因为在这个视图中,地面是水平的,所以当你向前或向后走时,以与天空相同的方式绘制它看起来很奇怪。但是,当你静止不动的时候,它看起来相当不错。为了让它看起来更好,我旋转了它,但它仍然不完美,而且我在上面做的方式有一些错误。当你朝某些方向走时,它会朝相反的方向滚动。但这是一个开始。

正确的键盘输入很有趣,至于直观的移动,我想知道是否有特定的键被按住,而不是仅仅被按下。这意味着MEGA65;的硬件加速键盘扫描仪只有有限的用途,因为它没有。不指示按键何时释放。但是,它确实以一种方便的方式提供了对键盘矩阵的单独访问,类似于C64内核扫描键盘的方式,但是在单独的寄存器对中,这样您就可以同时扫描操纵杆和键盘。因为MEGA65键盘完全受二极管保护,所以可以单独测试每个键的状态。因此,我实现了WASD和光标键控制。此外,我还在端口2中实现了操纵杆。我还在端口1中实现了1351或Amiga鼠标。

由于我使用的引擎示例地图有点无聊,我决定用一个随机迷宫生成器来代替它。我在上面花了比预期更多的时间,直到我最终找到了一个好的、完整的伪代码描述,让我能够实现一个工作的迷宫生成器。*结果是在repo中的mazegen.c中。

由于我想支持各种大小的迷宫,我决定使用64K以上的内存板来保存每个迷宫单元的信息,并创建一个例程来查找使用MEGA65 libc中的lpeek()函数的值。然而,我发现引擎的绘制速度从大约10 FPS下降到了大约2或3。这是因为检查特定块是否为空就发生在光线投射算法的核心。因此,执行复杂的内存提取会减慢速度。

更糟糕的是,由于CC65将参数传递给函数的速度非常慢,提取的时间甚至更长。因此,我实现了一个小位图,其中只包含给定单元格是否为空,并将其设置为$C000,这样就可以使用简单的内存读取。我将其封装在C预处理器宏中,这样就不会有函数调用,帧速率提高了很多,可能高达每秒6或7帧。

在此过程中,我还更改了几个执行乘法的例程,以使用MEGA65的硬件乘法器。但核心例程仍然是用相当枯燥的C语言编写的。我猜想,通过一些手工调整,应该可以使它比现在快得多。

一旦核心引擎开始工作,我就开始考虑如何制作需要出现在游戏中的标题和其他文本。我可以将它们绘制到渲染的3D显示屏上,但这会带来很多问题。*目前,因为渲染速度太快,所以从左到右连续渲染看起来很好,不使用双缓冲。这很棒,因为我不容易有多余的64KB RAM来专用于双缓冲(我可以抛弃ROM来获得)。“这很棒,因为我不容易有多余的64KB RAM来专用于双缓冲(我可以丢弃ROM来获得。这是一个相当绝望的措施)。(但是如果我们想要在上面绘制文本就不太好了,因为我们必须进行第二层渲染,这会减慢速度,并且有可能导致显示出现故障。

取而代之的是,我使用了VIC-IV的一项功能,即可以多次在栅格上绘制。也就是说,我们基本上可以要求VIC-IV在硬件中进行绘制,而这在软件中会很麻烦。这之所以有效,是因为VIC-IV只有一个栅格线缓冲区,可以在其中渲染。如果有足够的栅格时间,可以告诉VIC-IV在栅格上重新绘制,并渲染更多内容。如果设置了适当的标志,VIC-IV中的任何背景像素都可以进行渲染。如果设置了适当的标志,VIC-IV中的任何背景像素都可以在栅格上进行渲染。如果设置了适当的标志,VIC-IV中的任何背景像素都可以在栅格上进行渲染。如果设置了适当的标志,有效地将新前景内容键控于来自第一次渲染的现有素材之上。

如果这听起来非常复杂,不要担心。就把它想象成一种文字精灵吧:我们可以多加一层(或者更多!)。然后让VIC-IV在硬件中为我们合成文本。

这是我用来在3D显示器上添加基于文本的字幕的功能。这是在介绍屏幕中使用的,可以在我制作的游戏核心视频中看到,我已经有了工作:

它的工作原理是告诉VIC-IV,屏幕的每一行都有80个字符,其中前40个字符排列在垂直条纹中,以便进行高效的纹理绘制。41个字符是";goto";令牌告诉VIC-IV返回到靠近左边缘,并在前40个字符的顶部开始绘制第42到80个字符。实际上每个字符有2个字节,每行总共160个字节,因为我们需要使用16位文本模式来访问此功能(并且屏幕上需要超过256个字符,这对于使用1000个不同字符的3D显示来说是必需的,以使我们的位图布局优化)。

因此,我们需要告诉VIC-IV,每行有160个字符。为此,我们将160写入$D058,将0写入$D059(因为每行少于160个字符。然后我们在$D05E中投入80美元,告诉VIC-IV在每行上绘制80个字符。对于16位文本模式,独立设置这两个值的能力是必需的。但它对于使屏幕大于显示区域也很强大,然后可以通过硬件平滑滚动。例如,这对于制作侧滚屏非常有用,因为整个地图可以设置为单个巨型屏幕,一次只能看到其中的一部分。

对于好奇的人来说,在main.c;中它们的名称中包含";overlaytext";的例程具有魔力。基本上,要计算出将字符放在哪里才能使它出现在正确的位置需要进行大量的地址计算。此外,还有一个很好的例程overlaytext_line_x_position()。这个例程很有趣,因为它使得设置叠层合成到栅格线上的水平位置变得很简单。也就是说,它可以用作一种逐行的硬件平滑滚动。这就是用来将标题屏幕文本从右侧滑动的位置。这就是用来将标题屏幕文本从右手边滑出的一种硬件平滑滚动方式。这就是用来将标题屏幕文本从右侧滑出的一种硬件平滑滚动方式。这就是用来将标题屏幕文本从右侧滑出的一种硬件平滑滚动方式。这就是用来将标题屏幕文本从右侧滑出的一种方式。

除了一般的VIC-IV和DMA黑客攻击,我们还需要游戏的随机数。因此,我编写了生成随机数的例程。这包括试图从FPGA收集热熵以生成真正的随机数的例程(尽管我们仍需要测试这是否可靠)。但是这个过程很慢,有时您需要可重现的数字序列,例如,为了一致地生成相同的随机迷宫。因此,我还创建了srand()用于播种伪随机数生成器,以及rand8()、rand16()和rand32()用于获取1、2或4字节的随机数。它的工作原理是使用硬件乘法器将随机数乘以所提供的参数,然后返回结果的高位字节。

你可能已经注意到在视频中游戏是从C64模式开始的。我们做的另一个小工作是创建了一个包装程序,它可以用来使任何面向C64模式的程序都可以直接从C65模式或C64模式启动。这本身就很有趣,所以当我有时间的时候,我会单独写博客的。但是可以说,当前版本的MEGA迷宫可以使用相同的程序从C64或C65模式启动。通过在D81上命名此AUTOBOOT.C65,我们甚至可以自动启动MEGA65来运行它,前提是它位于软盘驱动器(或默认挂载的D81映像)中的一个磁盘上。(注:Mega迷宫的当前版本可以使用相同的程序从C64或C65模式启动。)通过将其命名为D81上的AUTOBOOT.C65,我们甚至可以自动引导MEGA65来运行它。

最后,让音乐在游戏中播放有点有趣,因为在第一个64KB的RAM中没有足够的空间来存放SID文件。幸运的是,有一个中断处理程序的空间。所以我编写了一个很小的中断处理程序,它将包含SID文件的第二个64KB的RAM中的一部分存入银行,调用PLAY例程,然后在生成中断处理程序之前将所有内容放回原处。*它看起来如下所示:

uint8_t MUSIC_Irq[27]={0xa9,0xc0,*/lda#$c0*0xa2,0x11,*/ldx#$11,0xa0,0x00,*/ldy#$00*0xa3,0x00,*/ldz#$00*0xa3,0x00,*//map#$00*0xa3,0x00,*。*//*0xa8,*//nop*0xee、0x19、0xd0、//inct$D019用于攻击光栅中断0xxee。

我把它放在这样的C数组中,因为我将它复制到$0340的适当位置,而不是试图让中断处理程序调用C函数,这在CC65中是众所周知的混乱。例程本身非常简单:首先,将底部8KB的板片设置为要入库,并将库地址设置为+$1C000。这会导致存储在$1D000的SID文件映射到$1000(因为$1000+$1C000=$1D000)。然后它调用Play。

不过有一个诀窍:这个小例程也必须安装在$1C340,这样当第一个map指令执行时,在新的内存映射中可以看到例程的其余部分。处理所有这些的例程都在raycaster repo的music.c中。这些例程还处理从SID文件中解析出的播放例程地址,并修补上面的处理程序以使用正确的地址:

void MUSIC_START(Void){*ASM(";sei";);*PLAY_ADDR=lpeek(0x1cf8e)<;<;8;*PLAY_ADDR+=lpeek(0x1cf8f);*MUSIC_Irq[12]=PLAY_ADDR&;0xff;*MUSIC_IRQ[13]=PLAY_ADDR&>8;*MUSIC_Irt;8。0//将IRQ处理程序设置为音乐例程:Poke(0x0314,0x40);Poke(0x0315,0x03);//禁用CIA中断,启用光栅中断;Poke(0xDC0D,0x7f);Poke(0xD012,0xFF);Poke(0xD011,0x1B);Poke(0xD01A,0x81);Poke(0xD01A,0x81)。

我意识到这一切都有点像旋风之旅,而不是各种功能的深入研究示例。但希望这足以让你思考MEGA65上可能发生的一些事情,一些你可以查看的示例代码,以及指向它在https://github.com/mega65/mega65-user-guide存储库中的文档位置的指针。