Portierung von Quake auf den iPod Classic


Starten Sie Quake auf dem iPod Classic ( Video ).

TL; DR : Ich habe es geschafft, Quake auf einem MP3-Player auszuführen. Der Artikel beschreibt, wie dies passiert ist.

Ich habe einen Teil des letzten Sommers mit ein paar meiner Lieblingssachen verbracht: Rockbox und das Spiel Quake id Software. Ich hatte sogar die Möglichkeit, diese beiden Hobbys zu kombinieren, indem ich Quake auf Rockbox portierte ! Mehr konnte man sich nicht wünschen!

Dieser Beitrag erzählt die Geschichte, wie es funktioniert hat. Dies ist eine lange Geschichte, die sich über fast zwei Jahre erstreckt. Darüber hinaus ist dies mein erster Versuch, den Entwicklungsprozess detailliert und ungeschminkt zu dokumentieren, im Gegensatz zu der fertigen technischen Dokumentation, die ich in meinem Leben zu viel geschrieben habe. Der Artikel wird auch technische Details enthalten, aber zunächst werde ich versuchen, über den Denkprozess zu sprechen, der zur Erstellung des Codes geführt hat.

Leider ist es an der Zeit, sich zumindest kurzfristig von Rockbox und Quake zu verabschieden. Für einige Monate wird Freizeit für mich eine sehr knappe Ressource sein. Bevor ich zur Arbeit eile, beeile ich mich, meine Gedanken auszudrücken.

Rockbox


Rockbox ist ein merkwürdiges Open-Source-Projekt, das ich viel gehackt habe. Das Beste daran steht auf der Webseite: "Rockbox ist eine kostenlose Firmware für digitale Musikplayer." Das ist richtig - wir haben einen vollständigen Ersatz für die Werkssoftware erstellt, die mit Sandisk Sansa-Playern, dem Apple iPod und vielen anderen unterstützten Geräten geliefert wurde.

Wir bemühen uns nicht nur, die Funktionen der ursprünglichen Firmware wiederherzustellen, sondern implementieren auch Unterstützung für herunterladbare Erweiterungen, sogenannte Plug-Ins - kleine Programme, die auf einem MP3-Player ausgeführt werden. Rockbox hat bereits viele großartige Spiele und Demos, von denen die beeindruckendsten wahrscheinlich die Ego-Shooter Doom und Duke Nukem 3D 1 sind. Aber ich hatte das Gefühl, dass etwas in ihm fehlte.

Beben erscheint auf der Bühne


Quake ist ein vollständig dreidimensionaler Ego-Shooter. Mal sehen, was das bedeutet. Die Schlüsselwörter sind hier „vollständig dreidimensional . Im Gegensatz zu Doom und Duke Nukem 3D, allgemein als 2.5D bezeichnet (stellen Sie sich eine 2D-Karte mit einer optionalen Höhenkomponente vor), ist Quake in vollständigem 3D implementiert. Jeder Scheitelpunkt und jedes Polygon existiert im 3D-Raum. Dies bedeutet, dass alte Pseudo-3D-Tricks nicht mehr funktionieren - alles wird in 3D ausgeführt. Ich war jedoch abgelenkt. Kurz gesagt, Quake ist eine mächtige Sache.

Und Quake vergibt keine Witze. Unsere Untersuchungen haben gezeigt, dass Quake einen x86-Prozessor mit einer Frequenz von ca. 100 MHz und einer FPU sowie ca. 32 MB RAM „benötigt“. Bevor Sie anfangen zu kichern, denken Sie daran, dass die Zielplattformen für Rockbox nicht mit denen vergleichbar sind, auf die sich John Carmack beim Schreiben des Spiels konzentriert hat - Rockbox funktioniert sogar auf Geräten mit Prozessoren mit einer Frequenz von nur 11 MHz und 2 MB RAM (Quake sollte natürlich nicht funktionieren auf solchen Geräten). Vor diesem Hintergrund habe ich mir meine allmählich abnehmende Sammlung digitaler Audioplayer angesehen und den leistungsstärksten der Überlebenden ausgewählt: den Apple iPod Classic / 6G mit 216 MHz ARMv5E-Prozessor und 64 MB RAM (Index E)zeigt das Vorhandensein von ARM-DSP-Erweiterungen an - später wird dies für uns wichtig sein). Ernsthafte Spezifikationen, aber es gibt kaum genug, um Quake auszuführen.

Hafen


Es gibt eine wunderbare Version von Quake, die unter SDL ausgeführt werden kann . Es hat den logischen Namen SDLQuake . Glücklicherweise habe ich die SDL-Bibliothek bereits auf Rockbox portiert (dies ist ein Thema für einen anderen Artikel), sodass sich die Vorbereitung von Quake für die Kompilierung als ziemlich einfacher Vorgang herausstellte: Kopieren des Quellbaums; make;; wir korrigieren Fehler; spülen, einseifen, wiederholen. Ich bin wahrscheinlich hier, um ein paar langweilige Details neu zu streichen, aber stellen Sie sich meine Bewunderung vor, dass ich die ausführbare Quake-Datei erfolgreich kompilieren und verknüpfen kann. Ich war erfreut.

"Nun, lade es!" Ich dachte.

Und es hat gebootet! Ich wurde vom wunderschönen Hintergrund und Menü der Quake-Konsole begrüßt. Alles in Ordnung ist. Aber nimm dir Zeit! Als ich das Spiel startete, stimmte etwas nicht. Die Stufe "Einführung" schien normal zu laden, aber die Spawn-Position des Spielers war vollständig von der Karte entfernt. Seltsam , dachte ich. Ich habe verschiedene Tricks ausprobiert, angefangen zu debuggen und splashf, aber es war alles umsonst - der Fehler stellte sich als zu kompliziert für mich heraus, oder es schien mir so.

Und diese Situation hielt mehrere Jahre an. Wahrscheinlich ein kleines Gespräch über das Timing wert. Der erste Versuch, Quake zu starten, wurde im September 2017 unternommen. Danach gab ich auf und mein Frankenstein von Quake und Rockbox lag bis Juli 2019 im Regal und sammelte Staub. Nachdem ich die perfekte Kombination aus Langeweile und Motivation gefunden hatte, beschloss ich, mit dem Abschluss meiner Anfänge fortzufahren.

Ich fing an zu debuggen. Mein Zustand des Flusses war so, dass ich mich praktisch an keine Details über das, was ich tat, erinnere, aber ich werde versuchen, den Arbeitsverlauf nachzubilden.

Ich fand heraus, dass die Quake-Struktur in zwei Hauptteile unterteilt ist: den Engine-Code in C und die Logik des Spiels auf hoher Ebene in QuakeC, einer mit Bytecode kompilierten Sprache. Ich habe immer versucht, mich von QuakeC VM fernzuhalten, weil ich irrationale Angst hatte, den Code eines anderen zu debuggen. Aber jetzt war ich gezwungen, mich darauf einzulassen. Ich erinnere mich vage an die verrückte Streaming-Sitzung, in der ich nach der Quelle des Fehlers gesucht habe. Nach vielen grepfand ich den Täter : pr_cmds.c:PF_setorigin. Diese Funktion erhielt einen dreidimensionalen Vektor, der beim Laden der Karte die neuen Koordinaten des Spielers festlegt, die aus irgendeinem Grund immer gleich waren (0, 0, 0). Hm ...

Ich habe den Datenstrom zurückverfolgt und herausgefunden, woher er stammt: vom Aufruf Q_atof()- der klassischen Konvertierungsfunktion von String zu Float. Und dann dämmerte mir die Einsicht: Ich schrieb eine Reihe von Wrapper-Funktionen, die Q_atof()den Quake-Code neu definierten , und meine Implementierung atof()war wahrscheinlich falsch. Es war sehr einfach, das Problem zu beheben. Ich ersetzen meine irrtümlichen mit der atofrichtigen - Funktion aus dem Quake - Code. Und voila! Das berühmte Einstiegsmodell mit drei problemlos geladenen Korridoren sowie der „E1M1: The Slipgate Complex“. Die Audioausgabe klingt immer noch wie ein kaputter Rasenmäher, aber wir haben Quake immer noch auf dem MP3-Player ausgeführt!

Den Hasenbau hinunter


Dieses Projekt wurde schließlich zu einer Ausrede für das, was ich aufgeschoben hatte: das Erlernen der ARM 2- Assemblersprache .

Das Problem war der geschwindigkeitsabhängige Schallmischzyklus snd_mix.c(erinnern Sie sich an den Klang eines Rasenmähers?).

Die Funktion SND_PaintChannelFrom8empfängt ein Array von 8-Bit-Mono-Audio-Samples und mischt sie zu einem 16-Bit-Stereostream, dessen linker und rechter Kanal basierend auf zwei ganzzahligen Parametern separat skaliert werden. GCC hat einen miesen Job gemacht, um die Sättigungsarithmetik zu optimieren, also habe ich beschlossen, es selbst zu tun. Das Ergebnis hat mich rundum zufrieden gestellt.

Hier ist die Assembler-Version von dem, was ich bekommen habe (die C-Version ist unten dargestellt):

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

Hier gibt es knifflige Hacks, die es wert sind, erklärt zu werden. Ich verwende den DSP-Befehl des qaddARM-Prozessors, um eine kostengünstige Addition der Sättigung zu implementieren, aber qadder funktioniert nur mit 32-Bit-Wörtern, und das Spiel verwendet 16-Bit-Soundbeispiele. Der Hack ist, dass ich zuerst die Samples um 16 Bit nach links verschiebe; Ich kombiniere Samples mit qadd; und dann die Rückwärtsverschiebung machen. Also mache ich in einer Anweisung das, was der GCC sieben genommen hat. (Ja, es wäre überhaupt möglich, auf Hacks zu verzichten, wenn ich mit ARMv6 arbeiten würde, das eine MMX-ähnliche Sättigungsarithmetik enthält qadd16, aber leider ist das Leben nicht so einfach. Außerdem hat sich der Hack als cool herausgestellt!)

Beachten Sie auch das Ich lese zwei Stereo-Samples gleichzeitig (mit Worten ldrundstr), um noch ein paar Zyklen zu sparen.

Unten finden Sie eine C-Version als Referenz:

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);
        }
}

Ich habe berechnet, dass die Anzahl der Anweisungen pro Probe im Vergleich zur optimierten C-Version um 60% gesunken ist. Die meisten Schleifen wurden durch Verwendung von qaddSättigungs- und Packspeicheroperationen für die Arithmetik gespeichert.

Verschwörung von "Primzahlen"


Hier ist ein weiterer interessanter Fehler, den ich dabei gefunden habe. In der Assembler-Codeliste gibt es neben der Anweisung bgt(Zweig „wenn mehr als“) einen Kommentar, dass bne(Zweig „wenn nicht gleich“) aufgrund eines Grenzfalls, der das Programm mit einer Anzahl von Abtastwerten von 1 verlangsamt, nicht verwendet werden kann. Dies führt zu einer zyklischen Übertragung Ganzzahl ein 0xFFFFFFFFund eine extrem lange Verzögerung (die schließlich endet).

Dieser Grenzfall wird durch einen bestimmten Ton mit einer Länge von 7325 Samples 3 ausgelöst . Was ist das Besondere an 7325? Versuchen wir, den Rest seiner Teilung durch eine Zweierpotenz zu finden:

73251(mod2)73251(mod4)73255(mod8)732513(mod16)732529(mod32)732529(mod64)732529(mod128)7325157(mod256)7325157(mod512)7325157(mod1024)73251181(mod2048)73253229(mod4096)


5, 13, 29, 157 ...

Haben Sie etwas bemerkt? Zufällig ist 7325 eine "Primzahl", wenn durch eine Zweierpotenz dividiert wird. Dies führt irgendwie (ich verstehe nicht wie) dazu, dass ein Array aus einem Sample in den Sound-Mixing-Code übertragen wird, ein Grenzfall ausgelöst wird und es hängt.

Ich habe mindestens einen Tag damit verbracht, die Ursachen dieses Fehlers zu identifizieren, als ich herausfand, dass alles auf eine falsche Anweisung zurückzuführen ist. Manchmal passiert es im Leben, oder?

Abschied


Ich habe diesen Port schließlich als Patch verpackt und ihn mit dem Hauptzweig von Rockbox zusammengeführt, in dem er sich heute befindet. In Rockbox Version 3.15 und höher wird es in Assemblys für die meisten ARM-Zielplattformen mit 4 Farbdisplays geliefert . Wenn Sie keine unterstützte Plattform haben, können Sie die Demo user890104 sehen .

Aus Platzgründen habe ich einige interessante Punkte übersehen. Zum Beispiel gibt es eine Rennbedingung, die nur auftritt, wenn ein Zombie in Fleischstücke zerbricht, wenn die Abtastrate 44,1 kHz beträgt. (Dies war das Ergebnis des Soundstroms, der versuchte, den Sound zu laden - eine Explosion, und des Modellladers, der versuchte, ein Stück Fleischmodell zu laden. Diese beiden Codeabschnitte verwenden eine Funktion, die eine globale Variable verwendet.) Und es gibt auch viele Bestellprobleme (ich liebe dich, ARM! ) und eine Reihe von Rendering-Mikrooptimierungen, die ich erstellt habe, um ein paar weitere Frames aus dem Gerät herauszupressen. Aber ich werde sie ein anderes Mal verlassen. Und jetzt ist es Zeit, sich von Quake zu verabschieden - diese Erfahrung hat mir gefallen.

Alles Gute und danke für den Fisch!



Anmerkungen


  1. Duke Nukem 3D , runtime Rockbox SDL, . , user890104.
  2. ARM, Tonc: Whirlwind Tour of ARM Assembly — ( GBA) . , ARM Quick Reference Card.
  3. , 100 .
  4. Ehrlich gesagt kann ich mich nicht erinnern, welche spezifischen Zielplattformen Quake unterstützen und welche nicht. Wenn Sie neugierig sind, besuchen Sie die Rockbox-Website und versuchen Sie, den Build für Ihre Plattform zu installieren. Und lassen Sie mich auf der Mail wissen, wie es funktioniert! Neuere Versionen von Rockbox Utility (ab 1.4.1 und höher) unterstützen auch die automatische Installation der Shareware-Version von Quake.

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


All Articles