Преглед на файлове

add duality project page

Thomas B преди 3 седмици
родител
ревизия
a67ed09166
променени са 35 файла, в които са добавени 683 реда и са изтрити 0 реда
  1. 683
    0
      input/projects/duality.md
  2. Двоични данни
      static/img/duality_cart_1.jpg
  3. Двоични данни
      static/img/duality_cart_1_small.jpg
  4. Двоични данни
      static/img/duality_cart_2.jpg
  5. Двоични данни
      static/img/duality_cart_2_small.jpg
  6. Двоични данни
      static/img/duality_cart_3.jpg
  7. Двоични данни
      static/img/duality_cart_3_small.jpg
  8. Двоични данни
      static/img/duality_print_anim.webm
  9. Двоични данни
      static/img/duality_print_anim_poster.png
  10. Двоични данни
      static/img/duality_print_anim_thumb.png
  11. Двоични данни
      static/img/duality_print_video.webm
  12. Двоични данни
      static/img/duality_print_video_poster.png
  13. Двоични данни
      static/img/duality_print_video_thumb.png
  14. Двоични данни
      static/img/duality_score_print.jpg
  15. Двоични данни
      static/img/duality_score_print.png
  16. Двоични данни
      static/img/duality_score_print_small.jpg
  17. Двоични данни
      static/img/duality_score_print_small.png
  18. Двоични данни
      static/img/duality_vram_1.png
  19. Двоични данни
      static/img/duality_vram_1_small.png
  20. Двоични данни
      static/img/duality_vram_2.png
  21. Двоични данни
      static/img/duality_vram_2_small.png
  22. Двоични данни
      static/img/duality_vram_3.png
  23. Двоични данни
      static/img/duality_vram_3_small.png
  24. Двоични данни
      static/img/duality_vram_4.png
  25. Двоични данни
      static/img/duality_vram_4_small.png
  26. Двоични данни
      static/img/duality_vram_5.png
  27. Двоични данни
      static/img/duality_vram_5_small.png
  28. Двоични данни
      static/img/duality_vram_6.png
  29. Двоични данни
      static/img/duality_vram_6_small.png
  30. Двоични данни
      static/img/duality_vram_7.png
  31. Двоични данни
      static/img/duality_vram_7_small.png
  32. Двоични данни
      static/img/duality_vram_8.png
  33. Двоични данни
      static/img/duality_vram_8_small.png
  34. Двоични данни
      static/img/rockshp_spr24.png
  35. Двоични данни
      static/img/rockshp_spr24_small.png

+ 683
- 0
input/projects/duality.md Целия файл

@@ -0,0 +1,683 @@
1
+title: Duality
2
+description: Game Boy Color game based on a GTA:SA Arcade machine
3
+parent: projects
4
+git: https://codeberg.org/xythobuz/Duality
5
+github: https://github.com/xythobuz/Duality
6
+date: 2025-08-19
7
+comments: true
8
+favicon: https://xythobuz.github.io/Duality/favicon.png
9
+auto_toc: true
10
+---
11
+
12
+## Introduction
13
+
14
+I regularly watch the livestreams of fulltime GTA:SA speedrunner [Joshimuz](https://www.twitch.tv/joshimuz/).
15
+A while ago he did a [PS2 Retro Achievements run](https://www.youtube.com/playlist?list=PLv1eoin737hDEcLhbiLIccTPr9qQEaXU9).
16
+As part of these he has to get a Top 5 score in the in-game arcade machine Duality in both the black and white highscore table.
17
+In [Part 10 at 04:02:16](https://youtu.be/PIAo_3YPYO8?t=14536), when Josh starts playing Duality, chat user `Caffie_` mentions how this could be a Game Boy game.
18
+So this inspired me to try to port the game to the Game Boy Color.
19
+Here are the results.
20
+
21
+You can either [download the ROM](https://xythobuz.github.io/Duality/duality.gb) or try it out right here, if you have JavaScript enabled, thanks to [EmulatorJS](https://emulatorjs.org/).
22
+
23
+<div id="duality_wrap" class="border">
24
+    <div id="duality_game"></div>
25
+    <noscript>Enable JavaScript to play the game right here in your browser.</noscript>
26
+</div>
27
+
28
+On PCs use keyboard input with the keys given below. On mobile devices a touch overlay should automatically appear over the emulator.
29
+
30
+<!--%
31
+tableHelper([ "align-center monospaced", "align-center monospaced", "align-center" ],
32
+    [ "Button", "Key", "Action" ], [
33
+        [ "D-Pad Left", "Arrow Left", "Rotate Left" ],
34
+        [ "D-Pad Right", "Arrow Right", "Rotate Right" ],
35
+        [ "A", "S", "Accelerate" ],
36
+        [ "B", "A", "Shoot" ],
37
+        [ "Start", "Enter", "Play / Pause" ],
38
+        [ "Select", "Space", "Config / About" ],
39
+    ]
40
+)
41
+%-->
42
+
43
+<p style="font-size: smaller; font-style: italic;">
44
+Although you can use EmulatorJS I recommend a native emulator for your target device if you experience stuttering music or bad performance with the emulator on this page.
45
+</p>
46
+
47
+### Quick Start Guide
48
+
49
+Press `Left` or `Right` on the title screen to show either the black or white highscores.
50
+Press `Select` to show the about screen and build info.
51
+In-game press `Start` to pause and resume.
52
+While paused press `Select` to return to the menu.
53
+
54
+Collect small white spheres to get +5 white score.
55
+Collect small black spheres to get +5 black score.
56
+The opposite color will reduce your score when collected.
57
+Shooting while you have a white score will reduce it by one.
58
+Large black holes will attract you and damage your ship when touched.
59
+Large white spheres will repel you and replenish your health when touched.
60
+Accelerating will reduce your fuel, which will recharge when not accelerating.
61
+You can shoot large spheres for +10 points.
62
+
63
+For a more detailed description of the original game check out the [Duality article on GTA Wiki](https://gta.fandom.com/wiki/Duality) 😛
64
+
65
+### Links
66
+
67
+The code is of course [freely available](https://codeberg.org/xythobuz/Duality) under the GPL license.
68
+Also check out the automatically generated [project website](https://xythobuz.github.io/Duality) on GitHub Pages.
69
+
70
+<!--%
71
+lightgallery([
72
+    [ "https://xythobuz.github.io/Duality/cartridge.png", "cartridge artwork" ],
73
+    [ "img/duality_cart_1.jpg", "flash carts (top)" ],
74
+    [ "img/duality_cart_2.jpg", "flash carts (side)" ],
75
+])
76
+%-->
77
+
78
+## Toolchain
79
+
80
+Fortunately the Game Boy is probably one of the retro hardware platforms with the best community-created documentation.
81
+The most important piece from this community is [Pan Docs](https://gbdev.io/pandocs/), a living document that has been extended and revised over the years that basically _is_ the reference manual for the platform.
82
+
83
+I wrote the whole game in C, which is very easy thanks to the [GBDK-2020](https://github.com/gbdk-2020/gbdk-2020/).
84
+It uses SDCC as the compiler with some custom tools to link the final ROM file.
85
+Additionally it also includes a tool that can convert graphics to the proper hardware sprite tile and map format.
86
+The GBDK also comes with [good documentation](https://gbdk.org/docs/api/) and lots of [examples](https://github.com/gbdk-2020/gbdk-2020/tree/develop/gbdk-lib/examples) that really make it easy to get started.
87
+
88
+Over the course of the project my `Makefile` grew relatively big and customized.
89
+Of course I've added the usual stuff like dependency file generation and git version information.
90
+I'm also automatically generating the graphics data with the `png2asset` tool.
91
+To convert sound samples I've modified the `cvtsample.py` tool from the GBDK examples.
92
+And to pre-calculate some speed vector tables for different angles I wrote my own `gen_angles.py` script.
93
+
94
+To find out how to convert the graphics assets I'm encoding the mode in the filename of the input images.
95
+
96
+<!--%
97
+include_sourcecode_slice(
98
+    "makefile", (146, 168), "Makefile",
99
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
100
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
101
+)
102
+%-->
103
+
104
+Like with the documentation, the emulation ecosystem for the Game Boy is also very healthy.
105
+
106
+  * I've mostly been using [Gearboy](https://github.com/drhelius/Gearboy) which includes some nice visualization tools for the hardware state.
107
+  * To test the Super Game Boy borders I used [SameBoy](https://sameboy.github.io/).
108
+  * A very nice symbolic C debbuger integration is available in [Emulicious](https://emulicious.net/).
109
+  * [GBE+](https://github.com/shonumi/gbe-plus) can also emulate a Game Boy Printer.
110
+  * [NO$GMB](https://problemkaputt.de/gmb.htm) is able to emulate multiple linked Game Boy systems.
111
+  * While testing debugging I also played around with [BGB](https://bgb.bircd.org/) for a bit.
112
+  * And for one especially hard debugging session I used the reverse-time-step feature of [GameRoy](https://github.com/Rodrigodd/gameroy) with good success (to actually see the faulty jump to the wrong bank after it happened).
113
+
114
+The Windows-only emulators all ran fine using Wine.
115
+
116
+I described the Emulicious debugger integration to Kate (or VSCode I guess) in more detail [in the README](https://codeberg.org/xythobuz/Duality#ide-integration).
117
+
118
+## Software
119
+
120
+The game is really made for the Game Boy Color (GBC).
121
+On the monochrome Game Boy (DMG) and the Super Game Boy (SGB) it runs at half-speed and the black and white spheres are hard to differentiate.
122
+Or, to put it another way, I was not able to optimize the code enough that it run's at the DMG clock speed of ~1MHz.
123
+So I had to cheat by putting the GBC CPU into a double-clock mode.
124
+
125
+<!--%
126
+include_sourcecode_slice(
127
+    "c", (646, 649), "src/main.c",
128
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
129
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
130
+)
131
+%-->
132
+
133
+To get some randomness into the gameplay I'm initially showing a splash screen after reset, where the user has to press `Start`.
134
+The timing of this button-press is used to initialize a random number generator that's later used to determine spawning positions of objects in the world map.
135
+
136
+<!--%
137
+include_sourcecode_slice(
138
+    "c", (662, 676), "src/main.c",
139
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
140
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
141
+)
142
+%-->
143
+
144
+The splash screen in `main.c` also handles the configuration and debug menus (try the Konami code).
145
+
146
+The main game loop in `game.c` is pretty straight-forward, like with most game engines.
147
+It basically does the following things:
148
+
149
+  1. read key inputs from user
150
+  2. modify player and world state (according to input and time)
151
+  3. draw the output graphics
152
+
153
+The world state is kept track of in `obj.c`, where each shot and colored sphere is represented as an entry in an object list.
154
+
155
+Background music is played automatically on the pulse channels (1 and 2), as well as the noise channel (4).
156
+Only the sample channel (3) is used for sound effects like shots or explosions.
157
+Both of these are handled by interrupts, so they should never stutter or crackle (instead gameplay slows down).
158
+For timekeeping I'm also configuring the internal timer and handle all of these tasks in the same ISR.
159
+
160
+<!--%
161
+include_sourcecode_slice(
162
+    "c", (29, 33), "src/timer.c",
163
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
164
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
165
+)
166
+%-->
167
+
168
+One pattern I've used repeatedly is storing (`const`) lists of "things" in ROM to be able to use them easily in other places.
169
+This is used for sprites, background maps, sound samples and music.
170
+For example, this is the start of the list of sprite graphics.
171
+
172
+<!--%
173
+include_sourcecode_slice(
174
+    "c", (56, 79), "src/sprite_data.c",
175
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
176
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
177
+)
178
+%-->
179
+
180
+By then calling the sprite functions the correct data is automatically loaded into VRAM and used accordingly.
181
+The sprite and map lists are not `const` because the offsets are calculated dynamically when loaded.
182
+The music and sample lists are both `const` though.
183
+
184
+<!--%
185
+include_sourcecode_slice(
186
+    "c", (73, 76), "src/sprites.h",
187
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
188
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
189
+)
190
+%-->
191
+
192
+It's not possible to rotate sprites by arbitrary angles, only the X and Y axis can be flipped individually.
193
+So for each desired rotation a sprite has to be prepared manually.
194
+
195
+I decided to split the circle into 16 different angles, or 22.5 degree steps.
196
+When taking advantage of the tile flipping we can get away with five different sprite rotations, from 0 degrees to 90 degrees.
197
+
198
+<!--%
199
+lightgallery([
200
+    [ "img/rockshp_spr24.png", "ship spritesheet" ],
201
+])
202
+%-->
203
+
204
+To get the proper movement and shot velocity vectors for the current angle I'm also pre-calculating these tables.
205
+
206
+<pre class="sh_c" skip_line_no>
207
+const int8_t table_speed_move[table_speed_move_SIZE] = {
208
+    0, 23, // 0.0
209
+    9, 21, // 22.5
210
+    16, 16, // 45.0
211
+    21, 9, // 67.5
212
+    23, 0, // 90.0
213
+    21, -9, // 112.5
214
+    16, -16, // 135.0
215
+    9, -21, // 157.5
216
+    0, -23, // 180.0
217
+    -9, -21, // 202.5
218
+    -16, -16, // 225.0
219
+    -21, -9, // 247.5
220
+    -23, 0, // 270.0
221
+    -21, 9, // 292.5
222
+    -16, 16, // 315.0
223
+    -9, 21, // 337.5
224
+};
225
+</pre>
226
+
227
+### Graphics
228
+
229
+To easily visualize the data in VRAM here are some screenshots from [Gearboy](https://github.com/drhelius/Gearboy).
230
+
231
+There's not enough space to fit all graphics data at once, so I'm loading different subsets in the menu and in-game.
232
+
233
+<!--%
234
+lightgallery([
235
+    [ "img/duality_vram_3.png", "menu sprites" ],
236
+    [ "img/duality_vram_2.png", "menu tile data" ],
237
+    [ "img/duality_vram_1.png", "menu background map" ],
238
+    [ "img/duality_vram_4.png", "menu palettes" ],
239
+])
240
+%-->
241
+
242
+  1. The first image shows the sprite objects in OAM. These are instances of tiles from the tile data.
243
+  2. Tile data is shown in the upper left part of the second image.
244
+     The middle and lower left parts of the second image contain the tile data for the background maps and window.
245
+     The right half of the second image shows a second bank only available on GBC, which I use for a smaller font.
246
+  3. The third image shows the background map, a large map of tiles that can be scrolled easily.
247
+  4. The fourth image shows the palettes to colorize the tiles on GBC hardware.
248
+
249
+<!--%
250
+lightgallery([
251
+    [ "img/duality_vram_7.png", "in-game sprites" ],
252
+    [ "img/duality_vram_6.png", "in-game tile data" ],
253
+    [ "img/duality_vram_5.png", "in-game background map" ],
254
+    [ "img/duality_vram_8.png", "in-game palettes" ],
255
+])
256
+%-->
257
+
258
+You can only store a maximum of 40 tile instances in [OAM](https://gbdev.io/pandocs/OAM.html) at once, but there are [more complicated rules](https://gbdev.io/pandocs/OAM.html#object-priority-and-conflicts) on when they appear.
259
+To colorize tiles the GBC adds a palette index to the attributes.
260
+
261
+The background map works similar to the OAM in that it indexes the background tile map, but the positions are fixed and [the GBC has attributes](https://gbdev.io/pandocs/Tile_Maps.html#bg-map-attributes-cgb-mode-only) for the map that don't exist on DMG.
262
+
263
+### Sound
264
+
265
+The background music is simply stored as long lists of notes.
266
+Here are some example excerpts from the score-screen music.
267
+
268
+<!--%
269
+include_sourcecode_slice(
270
+    "c", [
271
+        (26, 42),
272
+        (105, 131),
273
+        (159, 177),
274
+        (191, 204),
275
+    ], "src/sound_over.c",
276
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
277
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
278
+)
279
+%-->
280
+
281
+`over_notes` and `over_notes2` are the frequencies for the two pulse channels.
282
+`over_drums` has the IDs of different pre-defined settings for the noise channel.
283
+
284
+The `snd_play()` function in `sound.c` is then walking through this list after the note duration has elapsed, filling the sound hardware registers as needed.
285
+
286
+<!--%
287
+include_sourcecode_slice(
288
+    "c", (37, 219), "src/sound.c",
289
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
290
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
291
+)
292
+%-->
293
+
294
+For the sound effect sample player I've transcribed the assembly ISR from the GBDK examples to C, which was a fun little exercise in SM83 assembly.
295
+
296
+<!--%
297
+include_sourcecode_slice(
298
+    "c", (88, 184), "src/sample.c",
299
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
300
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
301
+)
302
+%-->
303
+
304
+### Banking
305
+
306
+Probably the most important topic in Game Boy software development is memory banking.
307
+Take a look at the memory map of the system.
308
+
309
+<!--%
310
+tableHelper([ "align-center monospaced", "align-center monospaced", "align-left"],
311
+    [ "Start", "End", "Description" ], [
312
+        [ "0x0000", "0x3FFF", "16 KiB ROM bank 00", "style='font-size: larger; font-weight: bold; line-height: 4lh'" ],
313
+        [ "0x4000", "0x7FFF", "16 KiB ROM Bank 01 – NN", "style='font-size: larger; font-weight: bold; line-height: 4lh'" ],
314
+        [ "0x8000", "0x9FFF", "8 KiB Video RAM (VRAM)", "style='line-height: 2lh'" ],
315
+        [ "0xA000", "0xBFFF", "8 KiB External RAM (SRAM)", "style='font-size: larger; font-weight: bold; line-height: 2lh'" ],
316
+        [ "0xC000", "0xCFFF", "4 KiB Work RAM (WRAM)", "style='line-height: 1lh'" ],
317
+        [ "0xD000", "0xDFFF", "4 KiB Work RAM (WRAM)", "style='line-height: 1lh'" ],
318
+        [ "0xE000", "0xFDFF", "Echo RAM (mirrors WRAM)", "style='line-height: 2lh'" ],
319
+        [ "0xFE00", "0xFE9F", "Object attribute memory (OAM)", "style='font-size: smaller'" ],
320
+        [ "0xFEA0", "0xFEFF", "Not Usable", "style='font-size: small'" ],
321
+        [ "0xFF00", "0xFF7F", "I/O Registers", "style='font-size: smaller'" ],
322
+        [ "0xFF80", "0xFFFE", "High RAM (HRAM)", "style='font-size: smaller'" ],
323
+        [ "0xFFFF", "0xFFFF", "Interrupt Enable register (IE)", "style='font-size: x-small'" ],
324
+    ]
325
+)
326
+%-->
327
+
328
+Only the 32KiB ROM bank areas at `0x0000 - 0x7FFF` and the 8KiB RAM at `0xA000 - 0xBFFF` are coming from the cartridge.
329
+The cartridge RAM is usually used to store persistent savegames and configs on a battery backed SRAM.
330
+The code and data live in the first 32KiB.
331
+
332
+Of course for many games these 32KiB are not enough, so they use some special hardware in the cartridge to map different memory chips to the same address range, depending on a configuration register.
333
+This is accomplished by Nintendos [Memory Bank Controllers](https://gbdev.io/pandocs/MBCs.html), most commonly (and for this game) the [MBC5](https://gbdev.io/pandocs/MBC5.html).
334
+
335
+Each bank has a size of 16KiB, with the first bank (0) always mapped to the first 16KiB of the address space.
336
+The next half of the ROM area is for the switchable bank, which can be selected by writing to the MBC-internal registers.
337
+
338
+Fortunately GBDK already contains a bunch of helper functions that make this all a bit easier.
339
+For the ROM banks we have to differentiate the following cases.
340
+
341
+  1. const data in a bank
342
+  2. functions in bank 0 (non-banked)
343
+  3. non-static functions in a bank
344
+  4. static functions (in a bank)
345
+
346
+Also note that with the GBDK and SDCC every compilation unit (so each `.c` file) can only be part of one bank.
347
+Which unit goes into which bank can be decided automatically (autobanking).
348
+
349
+First enable autobanking by passing the proper compiler flag `-Wm-yoA`.
350
+Now, at the start of each `.c` and `.h` files, declare a reference to the bank of this compilation unit.
351
+
352
+<pre class="sh_c" skip_line_no>
353
+/* in some_name.c */
354
+BANKREF(some_name)
355
+
356
+/* in some_name.h */
357
+BANKREF_EXTERN(some_name)
358
+</pre>
359
+
360
+Of course each unit needs a unique `some_name` (does not need to be the filename).
361
+
362
+Now you can declare your non-static functions with an attribute to place them in the correct bank.
363
+
364
+<pre class="sh_c" skip_line_no>
365
+void foo(void) BANKED;
366
+void bar(void) NONBANKED;
367
+</pre>
368
+
369
+When you call a `BANKED` function a trampoline in bank 0 automatically takes care of proper bank switching for you.
370
+Only when calling static functions not marked as `BANKED` (case 4 from above) you need to make sure you're either already in the correct bank (by coming from a `BANKED` function from the same compilation unit), or to switch banks manually (when coming from a `NONBANKED` function).
371
+
372
+To easily bank-switch I made some small helper macros.
373
+
374
+<!--%
375
+include_sourcecode_slice(
376
+    "c", (25, 27), "src/banks.h",
377
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
378
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
379
+)
380
+%-->
381
+
382
+`const` data has the same restrictions, so when you need to read `const` data from one bank in a function from another compilation unit you may need to add a small `NONBANKED` helper function.
383
+
384
+<!--%
385
+include_sourcecode_slice(
386
+    "c", (209, 215), "src/window.c",
387
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
388
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
389
+)
390
+%-->
391
+
392
+Of course you can never directly switch banks inside a function that is not `NONBANKED` as this would replace the currently executed opcodes.
393
+
394
+Initially I started out with lots of functions marked as `NONBANKED`, but this turned out to be unnecessary.
395
+By properly modularizing your code and liberally using `BANKED` you can get the compiler to do most of the work for you.
396
+
397
+It's also relatively easy to spot banking errors.
398
+Most of the time the VRAM will quickly fill with some regular pattern, like horizontal or vertical lines.
399
+Or a debugger shows you suddenly in the middle of nowhere.
400
+But I also had some devious cases, where the control flow jumped into some legitimate code that caused strange effects (wrong sound effects, no objects appearing, but the game still ran).
401
+These took me a while to figure out, until I noticed a `return` that skipped the final `END_ROM_BANK` of a function, thereby forgetting to switch-back the bank.
402
+
403
+RAM banks, in comparison, can be handled more easily.
404
+I'm simply enabling and setting RAM bank 0 at the beginning, before reading the config from there, and always keep it enabled while the game is running.
405
+
406
+<!--%
407
+include_sourcecode_slice(
408
+    "c", (51, 72), "src/config.ba0.c",
409
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
410
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
411
+)
412
+%-->
413
+
414
+Similar to the `png2asset` calls the RAM bank of a compilation unit is specified in the filename (`foo.baN.c` where N is the RAM bank number).
415
+
416
+<!--%
417
+include_sourcecode_slice(
418
+    "makefile", (170, 174), "Makefile",
419
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
420
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
421
+)
422
+%-->
423
+
424
+This is what the memory usage for my game looks like at the moment.
425
+
426
+    Bank         Range                Size     Used  Used%     Free  Free% 
427
+    --------     ----------------  -------  -------  -----  -------  -----
428
+    ROM_0        0x0000 -> 0x3FFF    16384    10466    64%     5918    36%
429
+    ROM_1        0x4000 -> 0x7FFF    16384    16383   100%        1     0%
430
+    ROM_2        0x4000 -> 0x7FFF    16384    16377   100%        7     0%
431
+    ROM_3        0x4000 -> 0x7FFF    16384    16383   100%        1     0%
432
+    ROM_4        0x4000 -> 0x7FFF    16384     5903    36%    10481    64%
433
+    SRAM_0       0xA000 -> 0xBFFF     8192      427     5%     7765    95%
434
+    WRAM_LO      0xC000 -> 0xCFFF     4096     1650    40%     2446    60%
435
+
436
+So I'm using five ROM banks in total for code and data (which has to be rounded up to eight banks, or 128KiB, as cartridges only can [speficy ROM bank counts](https://gbdev.io/pandocs/The_Cartridge_Header.html#0148--rom-size) that are a power of two).
437
+And a single SRAM bank (8KiB) for persistent highscores, configs and a savegame, which is the smallest number of [SRAM banks possible](https://gbdev.io/pandocs/The_Cartridge_Header.html#0149--ram-size).
438
+Take care not to fill ROM bank 0 [too close to the limit](https://gbdk.org/docs/api/docs_faq.html#faq_bank_overflow_errors), as the ROM header is not always properly taken into account in all tools.
439
+
440
+### Game Boy Printer
441
+
442
+The GBDK already comes with a [Game Boy Printer example](https://github.com/gbdk-2020/gbdk-2020/tree/develop/gbdk-lib/examples/gb/gbprinter) which I cleaned up and modified.
443
+Instead of reading a converted image I'm directly getting the tile data from VRAM to print either the window or background map.
444
+The data is read in blocks of two tile rows which are blacked out after transmission as a progress indicator.
445
+
446
+<!--%
447
+include_sourcecode_slice(
448
+    "c", (176, 249), "src/gbprinter.c",
449
+    "https://codeberg.org/xythobuz/Duality/raw/commit/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
450
+    "https://raw.githubusercontent.com/xythobuz/Duality/4315e0f8c17c29cfeb3be8a3eda745ff6a51b450/",
451
+)
452
+%-->
453
+
454
+Here is a screencast of printing in the [GBE+](https://github.com/shonumi/gbe-plus) emulator and a copy of the results.
455
+
456
+<!--%
457
+lightgallery([
458
+    [ "img/duality_print_anim.webm", "video/webm", "", "", "print screen animation" ],
459
+    [ "img/duality_score_print.png", "digital score printout" ],
460
+])
461
+%-->
462
+
463
+And this is a recording of my actual old childhood GB Printer in action.
464
+
465
+<!--%
466
+lightgallery([
467
+    [ "img/duality_print_video.webm", "video/webm", "", "", "printing process" ],
468
+    [ "img/duality_score_print.jpg", "physical score printout" ],
469
+])
470
+%-->
471
+
472
+## Physical Cartridges
473
+
474
+To build physical cartridges I ordered two [flashcarts](https://de.aliexpress.com/item/1005008596849158.html) as well as [cartridge cases](https://de.aliexpress.com/item/1005006340286928.html) in black and white.
475
+
476
+Unfortunately the flash carts use FRAM and a custom CPLD implementation to replace the mapper chip.
477
+It seems the timings or voltages or some other electrical or physical parameters are not quite correct.
478
+I can't get them to work in the [GB Interceptor](https://github.com/Staacks/gbinterceptor) for example.
479
+But in my real DMG and GBC they work fine.
480
+
481
+I'm using [this clone writer](https://de.aliexpress.com/item/1005007652321830.html), although I wouldn't really recommend it.
482
+It's very slow and can't be updated, probably due to being a chinese clone.
483
+Also the ["upstream repo"](https://github.com/simonkwng/GBFlash) is completely empty.
484
+Unfortunately all other writers supported by [FlashGBX](https://github.com/lesserkuma/FlashGBX) are not really free hardware or software either.
485
+
486
+To write to the carts I'm using the `DIY cart with MX29LV640 @ WR` setting of FlashGBX.
487
+
488
+I ordered three pieces of the label printed on foil, with the rounded corners cut out, at [Klebefisch](https://www.klebefisch.de/eigenes-motiv/aufkleber-drucken), for 30€ including postage, or about 10€ per label.
489
+The parameters were:
490
+
491
+  * White foil
492
+  * Cut outline
493
+  * Not mirrored
494
+  * 42mm by 37mm
495
+  * 1.5mm corner radius
496
+
497
+The size and finish of the sticker are great, but unfortunately the print quality is pretty bad with a low resolution (even though I sent high-res files with 2100px x 1850px).
498
+
499
+<!--%
500
+lightgallery([
501
+    [ "img/duality_cart_3.jpg", "printed cartridge label" ],
502
+])
503
+%-->
504
+
505
+But still the finished cartridges look and feel great!
506
+It's nice to have something physical in hand instead of "just" software.
507
+
508
+The cost for these came out to around 15€ per piece from AliExpress, excluding the label and working hours.
509
+There are other manufacturers of new carts for homebrew games, like [insideGadgets](https://shop.insidegadgets.com/product/custom-gameboy-flash-cart/), but surprisingly they are not really much cheaper, even for larger production runs.
510
+
511
+It's also possible to manufacture your own cartridge PCBs, but there are some caveats with this.
512
+The main problem is the memory bank controller, in my case the most common `MBC5` from Nintendo.
513
+They are no longer manufactured and can not be sourced new, so you either have to salvage them from original old donor games (which hurts the archivist in me too much) or find some kind of replacement.
514
+
515
+Different people came up with implementations based on CPLDs or microcontrollers.
516
+Some of them are even open-source, like the [MBC5 CPLD code from insideGadgets](https://github.com/insidegadgets/Gameboy-MBC5-MBC1-Hybrid) or [Allison's Bootleg Cart](https://abc.decontextualize.com/).
517
+In a [modern design](https://github.com/sillyhatday/GAMEBOY-CPLD-FRAM-2MB) you can use this with an FRAM chip to avoid the need for a backup battery for savegames, although this comes with some timing incompatibilities that may give problems with some games.
518
+You can also [use a design](https://github.com/sillyhatday/GAMEBOY-CPLD-SRAM-2MB) with an SRAM and a coin cell.
519
+Many different flash chips are compatible in theory, though you need to make sure it can handle 5V or add voltage translation circuitry.
520
+
521
+Unfortunately, doing some back-of-the-envelope cost calculation, this all comes out as more expensive for small production runs.
522
+It's cheaper and faster to just buy the chinese flash carts.
523
+
524
+But to be honest, I'm not sure how much interest people would have in buying physical copies of this game anyway, so I shelved this idea for now.
525
+Of course you can always easily make your own if you'd like.
526
+
527
+## Asset Recreation
528
+
529
+In order to get some inspiration from the original versions I extracted the relevant assets from the GTA:SA game files, like sprites, backgrounds, sound effects and music.
530
+I then re-created these in scaled-down versions that fit the Game Boy hardware.
531
+
532
+Extrating the audio files can be done with [Alci's SAAT GUI FrontEnd](https://www.gtagarage.com/mods/show.php?id=5777).
533
+The graphics can be extracted from `.txd` files with the [TXD Workshop](https://www.gtagarage.com/mods/show.php?id=8320).
534
+Both of these run fine in Wine.
535
+
536
+For the menu music I've re-created the San Andreas Theme, the game-over screen uses the victory fanfare from Final Fantasy VII.
537
+Fortunately both of these have spread very widely and there's lots of MIDI interpretations available.
538
+I used [MuseScore](https://musescore.org/en) to convert these MIDI files to sheet music and then transcribed the notes into my note lists in the source code.
539
+
540
+The Duality in-game theme song was more difficult.
541
+It consists more of noises and some LFOs instead of clearly defined notes, so transcribing was pretty hard.
542
+In the end I used a trial version of [AnthemScore](https://www.lunaverus.com/) in Wine to get an approximate idea of the notes, but the result is not great.
543
+
544
+<!--%
545
+lightgallery([
546
+    [ "https://www.youtube.com/watch?v=7qfbi3HACV8", "San Andreas Theme" ],
547
+    [ "https://www.youtube.com/watch?v=rgUksX6eM0Y", "Final Fantasy VII Victory Fanfare" ],
548
+    [ "https://www.youtube.com/watch?v=duiUhk5ZkaA", "Duality Theme" ],
549
+])
550
+%-->
551
+
552
+## Summary
553
+
554
+I started working on this project at the end of May 2025, and basically finished it (to the state described here) at the end of July 2025, so in a span of about two months.
555
+Of course this was only a side-project in my free time, outside my real job that pays the bills.
556
+For this relatively short time I'm pretty happy with the results.
557
+
558
+As always there is still lots of room for improvements.
559
+
560
+The code needs to be optimized so it runs full-speed on the DMG, maybe by improving object handling and rendering, which has some 𝒪(𝓃²) behavior.
561
+
562
+I also started working on a multiplayer mode, but due to me only having a single GBC and DMG to test each, I didn't really progress much there.
563
+
564
+And the background map scrolling could be improved.
565
+I played around with mirroring the map when overflowing on the sides, like some kind of endless scrolling, but haven't gotten far unfortunately.
566
+
567
+Working on a well-documented and well-designed retro game system like this was really lots of fun.
568
+In the future I hope to also take a closer look at other retro platforms.
569
+
570
+## License
571
+
572
+[The Duality source code](https://codeberg.org/xythobuz/Duality) is licensed under the [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.en.html).
573
+
574
+    Copyright (C) 2025 Thomas Buck <thomas@xythobuz.de>
575
+
576
+    This program is free software: you can redistribute it and/or modify
577
+    it under the terms of the GNU General Public License as published by
578
+    the Free Software Foundation, either version 3 of the License, or
579
+    (at your option) any later version.
580
+
581
+    This program is distributed in the hope that it will be useful,
582
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
583
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
584
+    GNU General Public License for more details.
585
+
586
+    See <http://www.gnu.org/licenses/>.
587
+
588
+Duality uses the [GBDK-2020](https://gbdk.org) libraries and is based on their example code.
589
+The files `sgb_border.c` and `sgb_border.h` are copied directly from their `sgb_border` example.
590
+
591
+The `util/cvtsample.py` script is based on a [GBDK example](https://github.com/gbdk-2020/gbdk-2020/blob/develop/gbdk-lib/examples/gb/wav_sample/utils/cvtsample.py).
592
+
593
+The [8x8 font](https://github.com/DavidDiPaola/font_vincent) is public domain.
594
+
595
+The included cartridge label graphic in `artwork/cart_label.xcf` is based on the ['Cartridge-Label-Templates' by Dinierto](https://github.com/Dinierto/Cartridge-Label-Templates) licensed as CC0.
596
+
597
+The included cartridge graphic in `artwork/cartridge.xcf` is based on the ['Front-End-Assets' by Duimon](https://github.com/Duimon/Front-End-Assets).
598
+
599
+<!--% #################################################################### %-->
600
+
601
+<style>
602
+    #duality_wrap {
603
+        width: 90%;
604
+        aspect-ratio: 160 / 144;
605
+        margin: 0px auto;
606
+        border-radius: 5px;
607
+        padding: 5px;
608
+        display: block;
609
+        resize: horizontal;
610
+    }
611
+
612
+    @media (min-width: 1000px) {
613
+        #duality_wrap {
614
+            width: 70%;
615
+        }
616
+    }
617
+
618
+    @media (min-width: 1500px) {
619
+        #duality_wrap {
620
+            width: 50%;
621
+        }
622
+    }
623
+</style>
624
+<script>
625
+    dw = document.getElementById("duality_wrap");
626
+    dw.addEventListener('resize', function(event) {
627
+        event.target.style.width = `${event.target.clientWidth}px`;
628
+        event.target.style.height = `${event.target.clientWidth * 160 / 144}px`;
629
+    });
630
+
631
+    EJS_player = "#duality_game";
632
+    EJS_core = "gb";
633
+    EJS_pathtodata = "https://cdn.emulatorjs.org/stable/data/";
634
+    EJS_gameUrl = "https://xythobuz.github.io/Duality/duality.gb";
635
+    EJS_alignStartButton = "center";
636
+    EJS_backgroundImage = "https://xythobuz.github.io/Duality/cartridge.png";
637
+    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
638
+        EJS_backgroundColor = "#111111";
639
+    } else {
640
+        EJS_backgroundColor = "#DDDDDD";
641
+    }
642
+    EJS_defaultControls = {
643
+        0: {
644
+            0: {
645
+                'value': 'a',
646
+                'value2': 'BUTTON_2'
647
+            },
648
+            2: {
649
+                'value': 'space',
650
+                'value2': 'SELECT'
651
+            },
652
+            3: {
653
+                'value': 'enter',
654
+                'value2': 'START'
655
+            },
656
+            4: {
657
+                'value': 'up arrow',
658
+                'value2': 'DPAD_UP'
659
+            },
660
+            5: {
661
+                'value': 'down arrow',
662
+                'value2': 'DPAD_DOWN'
663
+            },
664
+            6: {
665
+                'value': 'left arrow',
666
+                'value2': 'DPAD_LEFT'
667
+            },
668
+            7: {
669
+                'value': 'right arrow',
670
+                'value2': 'DPAD_RIGHT'
671
+            },
672
+            8: {
673
+                'value': 's',
674
+                'value2': 'BUTTON_1'
675
+            }
676
+        },
677
+        1: {},
678
+        2: {},
679
+        3: {}
680
+    };
681
+    EJS_startButtonName = "Start Duality";
682
+</script>
683
+<script src="https://cdn.emulatorjs.org/stable/data/loader.js" async defer></script>

Двоични данни
static/img/duality_cart_1.jpg Целия файл


Двоични данни
static/img/duality_cart_1_small.jpg Целия файл


Двоични данни
static/img/duality_cart_2.jpg Целия файл


Двоични данни
static/img/duality_cart_2_small.jpg Целия файл


Двоични данни
static/img/duality_cart_3.jpg Целия файл


Двоични данни
static/img/duality_cart_3_small.jpg Целия файл


Двоични данни
static/img/duality_print_anim.webm Целия файл


Двоични данни
static/img/duality_print_anim_poster.png Целия файл


Двоични данни
static/img/duality_print_anim_thumb.png Целия файл


Двоични данни
static/img/duality_print_video.webm Целия файл


Двоични данни
static/img/duality_print_video_poster.png Целия файл


Двоични данни
static/img/duality_print_video_thumb.png Целия файл


Двоични данни
static/img/duality_score_print.jpg Целия файл


Двоични данни
static/img/duality_score_print.png Целия файл


Двоични данни
static/img/duality_score_print_small.jpg Целия файл


Двоични данни
static/img/duality_score_print_small.png Целия файл


Двоични данни
static/img/duality_vram_1.png Целия файл


Двоични данни
static/img/duality_vram_1_small.png Целия файл


Двоични данни
static/img/duality_vram_2.png Целия файл


Двоични данни
static/img/duality_vram_2_small.png Целия файл


Двоични данни
static/img/duality_vram_3.png Целия файл


Двоични данни
static/img/duality_vram_3_small.png Целия файл


Двоични данни
static/img/duality_vram_4.png Целия файл


Двоични данни
static/img/duality_vram_4_small.png Целия файл


Двоични данни
static/img/duality_vram_5.png Целия файл


Двоични данни
static/img/duality_vram_5_small.png Целия файл


Двоични данни
static/img/duality_vram_6.png Целия файл


Двоични данни
static/img/duality_vram_6_small.png Целия файл


Двоични данни
static/img/duality_vram_7.png Целия файл


Двоични данни
static/img/duality_vram_7_small.png Целия файл


Двоични данни
static/img/duality_vram_8.png Целия файл


Двоични данни
static/img/duality_vram_8_small.png Целия файл


Двоични данни
static/img/rockshp_spr24.png Целия файл


Двоични данни
static/img/rockshp_spr24_small.png Целия файл


Loading…
Отказ
Запис