将Quake移植到iPod Classic


在iPod Classic上启动Quake(视频)。

TL; DR:我设法在MP3播放器上运行Quake。本文介绍了这种情况。

去年夏天,我花了一些我最喜欢的东西:Rockbox和游戏Quake id Software。通过将Quake移植 Rockbox,我什至有机会结合了这两个爱好!不可能期望更多!

这篇文章讲述了它如何运作的故事。这是一个漫长的故事,延续了将近两年。此外,这是我第一次尝试记录开发过程的细节和细节,与我一生编写过多的最终技术文档相反。本文还将介绍技术细节,但是首先,我将尝试讨论导致代码创建的思维过程。

las,现在是时候告别Rockbox和Quake了,至少是在短期内。几个月以来,空闲时间对我来说将是非常稀缺的资源,因此在着急工作之前,我急于表达自己的想法。

摇滚盒


Rockbox是一个好奇的开源项目,我花了很多时间进行黑客攻击。最好的事情写在网页上:“ Rockbox是数字音乐播放器的免费固件。” 没错-我们为Sandisk Sansa播放器,Apple iPod和许多其他受支持的设备随附的工厂软件创建了完整的替代品。

我们不仅努力重建原始固件的功能,而且还实现了对可下载扩展的支持,这些扩展称为插件 - 在MP3播放器上运行的小程序。Rockbox已经拥有许多出色的游戏和演示,其中最令人印象深刻的可能是第一人称射击游戏DoomDuke Nukem 3D 1但是我感到他身上缺少一些东西。

地震出现在舞台上


雷神之锤是完全三维的第一人称射击游戏。让我们看看这意味着什么。这里的关键词是“完全三维的”与Doom和Duke Nukem 3D通常称为2.5D(想象带有可选高度分量的2D地图)不同,Quake是在完整3D中实现的。每个顶点和多边形都存在于3D空间中。这意味着旧的伪3D技巧不再起作用-一切都以完整3D完成。但是,我分心了。简而言之,雷神之锤是强大的东西。

雷神之锤不会原谅笑话。我们的研究表明,Quake“需要”一个x86处理器,该处理器的频率约为100 MHz,FPU和约32 MB的RAM。在开始傻笑之前,请记住,Rockbox的目标平台与编写游戏时John Carmack专注的目标平台无法比拟-Rockbox甚至可以在处理器频率仅为11 MHz和2 MB RAM的设备上工作(当然,Quake不能工作在此类设备上)。考虑到这一点,我研究了逐渐减少的数字音频播放器,并选择了功能最强大的产品:Apple iPod Classic / 6G,带有216 MHz ARMv5E处理器和64 MB RAM(索引E表示存在ARM DSP扩展-稍后这对我们很重要)。规格很严格,但足以运行Quake。

港口


Quake有一个很棒的版本,可以在SDL上运行。它的逻辑名称为SDLQuake。幸运的是,我已经将SDL库移植到了Rockbox(这是另一篇文章的主题),因此准备Quake进行编译的过程非常简单:复制源代码树;复制源代码树。make;我们纠正错误;冲洗,肥皂,重复一遍。我可能在这里重画了许多无聊的细节,但是想像一下我对能够成功编译并链接Quake可执行文件感到钦佩。我很高兴。

“好吧,加载它!”我想。

它启动了!美丽的Quake控制台背景和菜单让我感到非常满意。一切都完美。但是,慢慢来!当我开始游戏时,出了点问题。 “介绍”级别似乎正常加载,但是玩家的生成位置完全不在地图上。我觉得很奇怪。我尝试了各种技巧,开始进行调试splashf,然后却徒劳无功-该错误对我来说太复杂了,或者在我看来像那样。

这种情况持续了几年。大概值得讨论一下时间安排。 2017年9月进行了首次启动Quake的尝试,此后我放弃了,直到2019年7月,来自Quake和Rockbox的我的科学怪人躺在架子上,收集灰尘。找到无聊和动力的完美结合之后,我决定继续完成我的工作。

我开始调试。我的流程状态是如此之差,以至于我几乎不记得自己在做什么,但我将尝试重新创建工作流程。

我发现Quake结构分为两个主要部分:C语言中的引擎代码和QuakeC语言中的游戏高级逻辑,QuakeC是一种字节码编译语言。由于总是出于调试他人代码的非理性恐惧,所以我总是试图远离QuakeC VM。但是现在我被迫投入其中。我隐约地记得疯狂的流媒体会话,在此期间我搜索了错误的来源。经过很多次grep,我找到了罪魁祸首:pr_cmds.c:PF_setorigin。此功能接收到三维矢量,该矢量可以在加载地图时设置玩家的新坐标,由于某些原因,这些坐标总是相等的(0, 0, 0)嗯...

我回溯了数据流,发现它来自哪里:从调用Q_atof()-经典的从字符串到浮点的转换函数。然后,洞察力浮现在我身上:我编写了一组包装器函数来重新定义Q_atof()Quake代码,而我的实现atof()可能是错误的。修复它非常容易。atof Quake代码中正确函数替换了错误的代码。瞧!著名的入门级建筑,三道走廊满载而没有任何问题,“ E1M1:滑门综合大楼”也是如此。音频输出听起来仍然像割草机坏了,但是我们仍然在MP3播放器上运行了Quake!

下兔子洞


这个项目终于成为我推迟的借口:学习ARM 2汇编语言

问题是混入速度敏感的声音snd_mix.c(还记得割草机的声音吗?)。

该函数SND_PaintChannelFrom8接收一个8位单声道音频样本的数组,并将它们混合为一个16位立体声流,该音频流的左右声道根据两个整数参数分别缩放。 GCC在优化饱和度算法方面做得很糟糕,所以我决定自己做。结果使我完全满意。

这是我所得到的汇编器版本(C版本如下所示):

SND_PaintChannelFrom8:
        ;; r0: int true_lvol
        ;; r1: int true_rvol
        ;; r2: char *sfx
        ;; r3: int count

        stmfd sp!, {r4, r5, r6, r7, r8, sl}

        ldr ip, =paintbuffer
        ldr ip, [ip]

        mov r0, r0, asl #16                 ; prescale by 2^16
        mov r1, r1, asl #16

        sub r3, r3, #1                      ; count backwards

        ldrh sl, =0xffff                    ; halfword mask

1:
        ldrsb r4, [r2, r3]                  ; load input sample
        ldr r8, [ip, r3, lsl #2]                ; load output sample pair from paintbuffer
                                ; (left:right in memory -> right:left in register)
        ;; right channel (high half)
        mul r5, r4, r1                      ; scaledright = sfx[i] * (true_rvol << 16) -- bottom half is zero
        qadd r7, r5, r8                     ; right = scaledright + right (in high half of word)
        bic r7, r7, sl                      ; zero bottom half of r7

        ;; left channel (low half)
        mul r5, r4, r0                      ; scaledleft = sfx[i] * (true_rvol << 16)
        mov r8, r8, lsl #16                 ; extract original left channel from paintbuffer
        qadd r8, r5, r8                     ; left = scaledleft + left

        orr r7, r7, r8, lsr #16                 ; combine right:left in r7
        str r7, [ip, r3, lsl #2]                ; write right:left to output buffer
        subs r3, r3, #1                         ; decrement and loop

        bgt 1b                          ; must use bgt instead of bne in case count=1

        ldmfd sp!, {r4, r5, r6, r7, r8, sl}

        bx lr

这里有一些棘手的骇客值得解释。我使用qaddARM处理器DSP指令来实现低成本的饱和度加法运算,但qadd它仅适用于32位单词,并且游戏使用16位声音样本。 hack是我先将样本左移16位;我将样品与qadd;然后进行反向移位。因此,在一项指示中,我做了GCC采取的七项措施。 (是的,如果我使用ARMv6,完全可以没有黑客qadd16,但ARMv6具有类似MMX的压缩饱和算法,但可惜,生活并不是那么简单。此外,黑客证明很酷!)

也请注意我一次读取两个立体声样本(使用单词ldrstr)以节省更多的周期。

以下是C版本供参考:

void SND_PaintChannelFrom8 (int true_lvol, int true_rvol, signed char *sfx, int count)
{
        int     data;
        int             i;

        // we have 8-bit sound in sfx[], which we want to scale to
        // 16bit and take the volume into account
        for (i=0 ; i<count ; i++)
        {
            // We could use the QADD16 instruction on ARMv6+
            // or just 32-bit QADD with pre-shifted arguments
            data = sfx[i];
            paintbuffer[2*i+0] = CLAMPADD(paintbuffer[2*i+0], data * true_lvol); // need saturation
            paintbuffer[2*i+1] = CLAMPADD(paintbuffer[2*i+1], data * true_rvol);
        }
}

我计算出,与优化的C版本相比,每个样本的指令数量减少了60%。通过使用qadd饱和和压缩存储操作进行算术运算,大多数循环得以保存

“素数”的阴谋


这是我在此过程中发现的另一个有趣的错误。在汇编代码列表中,指令bgt(分支“如果大于”)旁边有一条注释,指出bne(分支“如果不等于”)由于边界情况使采样数等于1的程序变慢而无法使用。这导致循环传输整数打开,0xFFFFFFFF并且有一个非常长的延迟(最终将结束)。

这种临界情况是由一种长度为7325个样本3的特定声音触发的7325有什么特别之处?让我们尝试通过除以2的任意幂来找到除法的余数:

73251个273251个47325587325十三十六732529日32732529日64732529日1287325157256732515751273251571024732511812048732532294096


5、13、29、157 ...

您有没有注意到?即-巧合的是,当除以2的任意幂时,7325是一个“素数”。在某种程度上(我不知道如何)导致的事实,从一个样本的阵列转移到混音代码,极限情况触发,它挂起。

我花了至少一天的时间来确定此错误的原因,因为发现了所有这些都归结为一条错误的指令。有时候它在生活中发生,对吧?

离别


我最终将此端口打包为补丁,并将其与今天的Rockbox主分支合并。在Rockbox 3.15版及更高版本中,它随大多数带有4种彩色显示屏的ARM目标平台一同提供。如果您没有受支持的平台,则可以看到演示用户890104

为了节省空间,我错过了一些有趣的观点。例如,只有在采样率为44.1 kHz的情况下,僵尸闯入肉块时,才会出现种族条件。(这是声音流试图加载声音的结果-爆炸,而模型加载器试图加载一块肉模型。这两个代码段使用一个使用一个全局变量的函数。)还有很多排序问题(爱您,ARM! )和我创建的一堆渲染微优化,以从设备中挤出更多帧。但我会再离开他们。现在是时候与Quake道别了-我喜欢这种经历。

祝一切顺利,并感谢您的钓鱼!



笔记


  1. Duke Nukem 3D , runtime Rockbox SDL, . , user890104.
  2. ARM, Tonc: Whirlwind Tour of ARM Assembly — ( GBA) . , ARM Quick Reference Card.
  3. , 100 .
  4. 老实说,我不记得哪个特定目标平台支持和不支持Quake。如果您好奇,请访问Rockbox网站,然后尝试为您的平台安装该版本。在邮件中让我知道它的工作原理!较新版本的Rockbox Utility(1.4.1和更高版本)还支持自动安装Quake共享软件版本。

Source: https://habr.com/ru/post/undefined/


All Articles