src/platform/3ds/main.c (view raw)
1/* Copyright (c) 2013-2015 Jeffrey Pfau
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7#include "gba/renderers/video-software.h"
8#include "gba/context/context.h"
9#include "gba/gui/gui-runner.h"
10#include "gba/video.h"
11#include "util/gui.h"
12#include "util/gui/file-select.h"
13#include "util/gui/font.h"
14#include "util/gui/menu.h"
15#include "util/memory.h"
16
17#include "3ds-vfs.h"
18#include "ctr-gpu.h"
19
20#include <3ds.h>
21#include <3ds/gpu/gx.h>
22
23static enum ScreenMode {
24 SM_PA_BOTTOM,
25 SM_AF_BOTTOM,
26 SM_SF_BOTTOM,
27 SM_PA_TOP,
28 SM_AF_TOP,
29 SM_SF_TOP,
30 SM_MAX
31} screenMode = SM_PA_TOP;
32
33#define _3DS_INPUT 0x3344534B
34
35#define AUDIO_SAMPLES 0x80
36#define AUDIO_SAMPLE_BUFFER (AUDIO_SAMPLES * 24)
37
38FS_Archive sdmcArchive;
39
40static struct GBA3DSRotationSource {
41 struct GBARotationSource d;
42 accelVector accel;
43 angularRate gyro;
44} rotation;
45
46static bool hasSound;
47// TODO: Move into context
48static struct GBAVideoSoftwareRenderer renderer;
49static struct GBAAVStream stream;
50static int16_t* audioLeft = 0;
51static int16_t* audioRight = 0;
52static size_t audioPos = 0;
53static struct ctrTexture gbaOutputTexture;
54static int guiDrawn;
55static int screenCleanup;
56
57enum {
58 GUI_ACTIVE = 1,
59 GUI_THIS_FRAME = 2,
60};
61
62enum {
63 SCREEN_CLEANUP_TOP_1 = 1,
64 SCREEN_CLEANUP_TOP_2 = 2,
65 SCREEN_CLEANUP_TOP = SCREEN_CLEANUP_TOP_1 | SCREEN_CLEANUP_TOP_2,
66 SCREEN_CLEANUP_BOTTOM_1 = 4,
67 SCREEN_CLEANUP_BOTTOM_2 = 8,
68 SCREEN_CLEANUP_BOTTOM = SCREEN_CLEANUP_BOTTOM_1 | SCREEN_CLEANUP_BOTTOM_2,
69};
70
71extern bool allocateRomBuffer(void);
72
73static void _map3DSKey(struct GBAInputMap* map, int ctrKey, enum GBAKey key) {
74 GBAInputBindKey(map, _3DS_INPUT, __builtin_ctz(ctrKey), key);
75}
76
77static void _csndPlaySound(u32 flags, u32 sampleRate, float vol, void* left, void* right, u32 size)
78{
79 u32 pleft = 0, pright = 0;
80
81 int loopMode = (flags >> 10) & 3;
82 if (!loopMode) {
83 flags |= SOUND_ONE_SHOT;
84 }
85
86 pleft = osConvertVirtToPhys(left);
87 pright = osConvertVirtToPhys(right);
88
89 u32 timer = CSND_TIMER(sampleRate);
90 if (timer < 0x0042) {
91 timer = 0x0042;
92 }
93 else if (timer > 0xFFFF) {
94 timer = 0xFFFF;
95 }
96 flags &= ~0xFFFF001F;
97 flags |= SOUND_ENABLE | (timer << 16);
98
99 u32 volumes = CSND_VOL(vol, -1.0);
100 CSND_SetChnRegs(flags | SOUND_CHANNEL(8), pleft, pleft, size, volumes, volumes);
101 volumes = CSND_VOL(vol, 1.0);
102 CSND_SetChnRegs(flags | SOUND_CHANNEL(9), pright, pright, size, volumes, volumes);
103}
104
105static void _postAudioBuffer(struct GBAAVStream* stream, struct GBAAudio* audio);
106
107static void _drawStart(void) {
108 ctrGpuBeginDrawing();
109 if (screenMode < SM_PA_TOP || (guiDrawn & GUI_ACTIVE)) {
110 ctrGpuBeginFrame(GFX_BOTTOM);
111 ctrSetViewportSize(320, 240);
112 } else {
113 ctrGpuBeginFrame(GFX_TOP);
114 ctrSetViewportSize(400, 240);
115 }
116 guiDrawn &= ~GUI_THIS_FRAME;
117}
118
119static void _drawEnd(void) {
120 int screen = screenMode < SM_PA_TOP ? GFX_BOTTOM : GFX_TOP;
121 u16 width = 0, height = 0;
122
123 void* outputFramebuffer = gfxGetFramebuffer(screen, GFX_LEFT, &height, &width);
124 ctrGpuEndFrame(screen, outputFramebuffer, width, height);
125
126 if (screen != GFX_BOTTOM) {
127 if (guiDrawn & (GUI_THIS_FRAME | GUI_ACTIVE)) {
128 void* outputFramebuffer = gfxGetFramebuffer(GFX_BOTTOM, GFX_LEFT, &height, &width);
129 ctrGpuEndFrame(GFX_BOTTOM, outputFramebuffer, width, height);
130 } else if (screenCleanup & SCREEN_CLEANUP_BOTTOM) {
131 ctrGpuBeginFrame(GFX_BOTTOM);
132 if (screenCleanup & SCREEN_CLEANUP_BOTTOM_1) {
133 screenCleanup &= ~SCREEN_CLEANUP_BOTTOM_1;
134 } else if (screenCleanup & SCREEN_CLEANUP_BOTTOM_2) {
135 screenCleanup &= ~SCREEN_CLEANUP_BOTTOM_2;
136 }
137 void* outputFramebuffer = gfxGetFramebuffer(GFX_BOTTOM, GFX_LEFT, &height, &width);
138 ctrGpuEndFrame(GFX_BOTTOM, outputFramebuffer, width, height);
139 }
140 }
141
142 if ((screenCleanup & SCREEN_CLEANUP_TOP) && screen != GFX_TOP) {
143 ctrGpuBeginFrame(GFX_TOP);
144 if (screenCleanup & SCREEN_CLEANUP_TOP_1) {
145 screenCleanup &= ~SCREEN_CLEANUP_TOP_1;
146 } else if (screenCleanup & SCREEN_CLEANUP_TOP_2) {
147 screenCleanup &= ~SCREEN_CLEANUP_TOP_2;
148 }
149 void* outputFramebuffer = gfxGetFramebuffer(GFX_TOP, GFX_LEFT, &height, &width);
150 ctrGpuEndFrame(GFX_TOP, outputFramebuffer, width, height);
151 }
152
153 ctrGpuEndDrawing();
154}
155
156static int _batteryState(void) {
157 u8 charge;
158 u8 adapter;
159 PTMU_GetBatteryLevel(&charge);
160 PTMU_GetBatteryChargeState(&adapter);
161 int state = 0;
162 if (adapter) {
163 state |= BATTERY_CHARGING;
164 }
165 if (charge > 0) {
166 --charge;
167 }
168 return state | charge;
169}
170
171static void _guiPrepare(void) {
172 guiDrawn = GUI_ACTIVE | GUI_THIS_FRAME;
173 int screen = screenMode < SM_PA_TOP ? GFX_BOTTOM : GFX_TOP;
174 if (screen == GFX_BOTTOM) {
175 return;
176 }
177
178 ctrFlushBatch();
179 ctrGpuBeginFrame(GFX_BOTTOM);
180 ctrSetViewportSize(320, 240);
181}
182
183static void _guiFinish(void) {
184 guiDrawn &= ~GUI_ACTIVE;
185 screenCleanup |= SCREEN_CLEANUP_BOTTOM;
186}
187
188static void _setup(struct GBAGUIRunner* runner) {
189 runner->context.gba->rotationSource = &rotation.d;
190 if (hasSound) {
191 runner->context.gba->stream = &stream;
192 }
193
194 _map3DSKey(&runner->context.inputMap, KEY_A, GBA_KEY_A);
195 _map3DSKey(&runner->context.inputMap, KEY_B, GBA_KEY_B);
196 _map3DSKey(&runner->context.inputMap, KEY_START, GBA_KEY_START);
197 _map3DSKey(&runner->context.inputMap, KEY_SELECT, GBA_KEY_SELECT);
198 _map3DSKey(&runner->context.inputMap, KEY_UP, GBA_KEY_UP);
199 _map3DSKey(&runner->context.inputMap, KEY_DOWN, GBA_KEY_DOWN);
200 _map3DSKey(&runner->context.inputMap, KEY_LEFT, GBA_KEY_LEFT);
201 _map3DSKey(&runner->context.inputMap, KEY_RIGHT, GBA_KEY_RIGHT);
202 _map3DSKey(&runner->context.inputMap, KEY_L, GBA_KEY_L);
203 _map3DSKey(&runner->context.inputMap, KEY_R, GBA_KEY_R);
204
205 GBAVideoSoftwareRendererCreate(&renderer);
206 renderer.outputBuffer = linearMemAlign(256 * VIDEO_VERTICAL_PIXELS * 2, 0x80);
207 renderer.outputBufferStride = 256;
208 runner->context.renderer = &renderer.d;
209
210 unsigned mode;
211 if (GBAConfigGetUIntValue(&runner->context.config, "screenMode", &mode) && mode < SM_MAX) {
212 screenMode = mode;
213 }
214
215 GBAAudioResizeBuffer(&runner->context.gba->audio, AUDIO_SAMPLES);
216}
217
218static void _gameLoaded(struct GBAGUIRunner* runner) {
219 if (runner->context.gba->memory.hw.devices & HW_TILT) {
220 HIDUSER_EnableAccelerometer();
221 }
222 if (runner->context.gba->memory.hw.devices & HW_GYRO) {
223 HIDUSER_EnableGyroscope();
224 }
225 osSetSpeedupEnable(true);
226
227#if RESAMPLE_LIBRARY == RESAMPLE_BLIP_BUF
228 double ratio = GBAAudioCalculateRatio(1, 59.8260982880808, 1);
229 blip_set_rates(runner->context.gba->audio.left, GBA_ARM7TDMI_FREQUENCY, 32768 * ratio);
230 blip_set_rates(runner->context.gba->audio.right, GBA_ARM7TDMI_FREQUENCY, 32768 * ratio);
231#endif
232 if (hasSound) {
233 memset(audioLeft, 0, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
234 memset(audioRight, 0, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
235 audioPos = 0;
236 _csndPlaySound(SOUND_REPEAT | SOUND_FORMAT_16BIT, 32768, 1.0, audioLeft, audioRight, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
237 csndExecCmds(false);
238 }
239 unsigned mode;
240 if (GBAConfigGetUIntValue(&runner->context.config, "screenMode", &mode) && mode != screenMode) {
241 screenMode = mode;
242 screenCleanup |= SCREEN_CLEANUP_BOTTOM | SCREEN_CLEANUP_TOP;
243 }
244}
245
246static void _gameUnloaded(struct GBAGUIRunner* runner) {
247 if (hasSound) {
248 CSND_SetPlayState(8, 0);
249 CSND_SetPlayState(9, 0);
250 csndExecCmds(false);
251 }
252 osSetSpeedupEnable(false);
253
254 if (runner->context.gba->memory.hw.devices & HW_TILT) {
255 HIDUSER_DisableAccelerometer();
256 }
257 if (runner->context.gba->memory.hw.devices & HW_GYRO) {
258 HIDUSER_DisableGyroscope();
259 }
260}
261
262static void _drawTex(bool faded) {
263 u32 color = faded ? 0x3FFFFFFF : 0xFFFFFFFF;
264
265 int screen_w = screenMode < SM_PA_TOP ? 320 : 400;
266 int screen_h = 240;
267
268 int w, h;
269
270 switch (screenMode) {
271 case SM_PA_TOP:
272 case SM_PA_BOTTOM:
273 default:
274 w = VIDEO_HORIZONTAL_PIXELS;
275 h = VIDEO_VERTICAL_PIXELS;
276 break;
277 case SM_AF_TOP:
278 w = 360;
279 h = 240;
280 break;
281 case SM_AF_BOTTOM:
282 // Largest possible size with 3:2 aspect ratio and integer dimensions
283 w = 318;
284 h = 212;
285 break;
286 case SM_SF_TOP:
287 case SM_SF_BOTTOM:
288 w = screen_w;
289 h = screen_h;
290 break;
291 }
292
293 int x = (screen_w - w) / 2;
294 int y = (screen_h - h) / 2;
295
296 ctrAddRectScaled(color, x, y, w, h, 0, 0, VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS);
297}
298
299static void _drawFrame(struct GBAGUIRunner* runner, bool faded) {
300 UNUSED(runner);
301
302 void* outputBuffer = renderer.outputBuffer;
303 struct ctrTexture* tex = &gbaOutputTexture;
304
305 GSPGPU_FlushDataCache(outputBuffer, 256 * VIDEO_VERTICAL_PIXELS * 2);
306 GX_DisplayTransfer(
307 outputBuffer, GX_BUFFER_DIM(256, VIDEO_VERTICAL_PIXELS),
308 tex->data, GX_BUFFER_DIM(256, 256),
309 GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB565) |
310 GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB565) |
311 GX_TRANSFER_OUT_TILED(1) | GX_TRANSFER_FLIP_VERT(1));
312
313#if RESAMPLE_LIBRARY == RESAMPLE_BLIP_BUF
314 if (!hasSound) {
315 blip_clear(runner->context.gba->audio.left);
316 blip_clear(runner->context.gba->audio.right);
317 }
318#endif
319
320 gspWaitForPPF();
321 ctrActivateTexture(tex);
322 _drawTex(faded);
323}
324
325static void _drawScreenshot(struct GBAGUIRunner* runner, const uint32_t* pixels, bool faded) {
326 UNUSED(runner);
327
328 struct ctrTexture* tex = &gbaOutputTexture;
329
330 u16* newPixels = linearMemAlign(256 * VIDEO_VERTICAL_PIXELS * sizeof(u32), 0x100);
331
332 // Convert image from RGBX8 to BGR565
333 for (unsigned y = 0; y < VIDEO_VERTICAL_PIXELS; ++y) {
334 for (unsigned x = 0; x < VIDEO_HORIZONTAL_PIXELS; ++x) {
335 // 0xXXBBGGRR -> 0bRRRRRGGGGGGBBBBB
336 u32 p = *pixels++;
337 newPixels[y * 256 + x] =
338 (p << 24 >> (24 + 3) << 11) | // R
339 (p << 16 >> (24 + 2) << 5) | // G
340 (p << 8 >> (24 + 3) << 0); // B
341 }
342 memset(&newPixels[y * 256 + VIDEO_HORIZONTAL_PIXELS], 0, (256 - VIDEO_HORIZONTAL_PIXELS) * sizeof(u32));
343 }
344
345 GSPGPU_FlushDataCache(newPixels, 256 * VIDEO_VERTICAL_PIXELS * sizeof(u32));
346 GX_DisplayTransfer(
347 (u32*) newPixels, GX_BUFFER_DIM(256, VIDEO_VERTICAL_PIXELS),
348 tex->data, GX_BUFFER_DIM(256, 256),
349 GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB565) |
350 GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB565) |
351 GX_TRANSFER_OUT_TILED(1) | GX_TRANSFER_FLIP_VERT(1));
352 gspWaitForPPF();
353 linearFree(newPixels);
354
355 ctrActivateTexture(tex);
356 _drawTex(faded);
357}
358
359static uint16_t _pollGameInput(struct GBAGUIRunner* runner) {
360 UNUSED(runner);
361
362 hidScanInput();
363 uint32_t activeKeys = hidKeysHeld();
364 uint16_t keys = GBAInputMapKeyBits(&runner->context.inputMap, _3DS_INPUT, activeKeys, 0);
365 keys |= (activeKeys >> 24) & 0xF0;
366 return keys;
367}
368
369static void _incrementScreenMode(struct GBAGUIRunner* runner) {
370 UNUSED(runner);
371 screenCleanup |= SCREEN_CLEANUP_TOP | SCREEN_CLEANUP_BOTTOM;
372 screenMode = (screenMode + 1) % SM_MAX;
373 GBAConfigSetUIntValue(&runner->context.config, "screenMode", screenMode);
374}
375
376static uint32_t _pollInput(void) {
377 hidScanInput();
378 uint32_t keys = 0;
379 int activeKeys = hidKeysHeld();
380 if (activeKeys & KEY_X) {
381 keys |= 1 << GUI_INPUT_CANCEL;
382 }
383 if (activeKeys & KEY_Y) {
384 keys |= 1 << GBA_GUI_INPUT_SCREEN_MODE;
385 }
386 if (activeKeys & KEY_B) {
387 keys |= 1 << GUI_INPUT_BACK;
388 }
389 if (activeKeys & KEY_A) {
390 keys |= 1 << GUI_INPUT_SELECT;
391 }
392 if (activeKeys & KEY_LEFT) {
393 keys |= 1 << GUI_INPUT_LEFT;
394 }
395 if (activeKeys & KEY_RIGHT) {
396 keys |= 1 << GUI_INPUT_RIGHT;
397 }
398 if (activeKeys & KEY_UP) {
399 keys |= 1 << GUI_INPUT_UP;
400 }
401 if (activeKeys & KEY_DOWN) {
402 keys |= 1 << GUI_INPUT_DOWN;
403 }
404 if (activeKeys & KEY_CSTICK_UP) {
405 keys |= 1 << GBA_GUI_INPUT_INCREASE_BRIGHTNESS;
406 }
407 if (activeKeys & KEY_CSTICK_DOWN) {
408 keys |= 1 << GBA_GUI_INPUT_DECREASE_BRIGHTNESS;
409 }
410 return keys;
411}
412
413static enum GUICursorState _pollCursor(unsigned* x, unsigned* y) {
414 hidScanInput();
415 if (!(hidKeysHeld() & KEY_TOUCH)) {
416 return GUI_CURSOR_NOT_PRESENT;
417 }
418 touchPosition pos;
419 hidTouchRead(&pos);
420 *x = pos.px;
421 *y = pos.py;
422 return GUI_CURSOR_DOWN;
423}
424
425static void _sampleRotation(struct GBARotationSource* source) {
426 struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
427 // Work around ctrulib getting the entries wrong
428 rotation->accel = *(accelVector*)& hidSharedMem[0x48];
429 rotation->gyro = *(angularRate*)& hidSharedMem[0x5C];
430}
431
432static int32_t _readTiltX(struct GBARotationSource* source) {
433 struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
434 return rotation->accel.x << 18L;
435}
436
437static int32_t _readTiltY(struct GBARotationSource* source) {
438 struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
439 return rotation->accel.y << 18L;
440}
441
442static int32_t _readGyroZ(struct GBARotationSource* source) {
443 struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
444 return rotation->gyro.y << 18L; // Yes, y
445}
446
447static void _postAudioBuffer(struct GBAAVStream* stream, struct GBAAudio* audio) {
448 UNUSED(stream);
449#if RESAMPLE_LIBRARY == RESAMPLE_BLIP_BUF
450 blip_read_samples(audio->left, &audioLeft[audioPos], AUDIO_SAMPLES, false);
451 blip_read_samples(audio->right, &audioRight[audioPos], AUDIO_SAMPLES, false);
452#elif RESAMPLE_LIBRARY == RESAMPLE_NN
453 GBAAudioCopy(audio, &audioLeft[audioPos], &audioRight[audioPos], AUDIO_SAMPLES);
454#endif
455 GSPGPU_FlushDataCache(&audioLeft[audioPos], AUDIO_SAMPLES * sizeof(int16_t));
456 GSPGPU_FlushDataCache(&audioRight[audioPos], AUDIO_SAMPLES * sizeof(int16_t));
457 audioPos = (audioPos + AUDIO_SAMPLES) % AUDIO_SAMPLE_BUFFER;
458 if (audioPos == AUDIO_SAMPLES * 3) {
459 u8 playing = 0;
460 csndIsPlaying(0x8, &playing);
461 if (!playing) {
462 CSND_SetPlayState(0x8, 1);
463 CSND_SetPlayState(0x9, 1);
464 csndExecCmds(false);
465 }
466 }
467}
468
469int main() {
470 rotation.d.sample = _sampleRotation;
471 rotation.d.readTiltX = _readTiltX;
472 rotation.d.readTiltY = _readTiltY;
473 rotation.d.readGyroZ = _readGyroZ;
474
475 stream.postVideoFrame = 0;
476 stream.postAudioFrame = 0;
477 stream.postAudioBuffer = _postAudioBuffer;
478
479 if (!allocateRomBuffer()) {
480 return 1;
481 }
482
483 ptmuInit();
484 hasSound = !csndInit();
485
486 if (hasSound) {
487 audioLeft = linearMemAlign(AUDIO_SAMPLE_BUFFER * sizeof(int16_t), 0x80);
488 audioRight = linearMemAlign(AUDIO_SAMPLE_BUFFER * sizeof(int16_t), 0x80);
489 }
490
491 gfxInit(GSP_BGR8_OES, GSP_BGR8_OES, false);
492
493 if (ctrInitGpu() < 0) {
494 gbaOutputTexture.data = 0;
495 goto cleanup;
496 }
497
498 ctrTexture_Init(&gbaOutputTexture);
499 gbaOutputTexture.format = GPU_RGB565;
500 gbaOutputTexture.filter = GPU_LINEAR;
501 gbaOutputTexture.width = 256;
502 gbaOutputTexture.height = 256;
503 gbaOutputTexture.data = vramAlloc(256 * 256 * 2);
504 void* outputTextureEnd = (u8*)gbaOutputTexture.data + 256 * 256 * 2;
505
506 if (!gbaOutputTexture.data) {
507 goto cleanup;
508 }
509
510 // Zero texture data to make sure no garbage around the border interferes with filtering
511 GX_MemoryFill(
512 gbaOutputTexture.data, 0x0000, outputTextureEnd, GX_FILL_16BIT_DEPTH | GX_FILL_TRIGGER,
513 NULL, 0, NULL, 0);
514 gspWaitForPSC0();
515
516 sdmcArchive = (FS_Archive) {
517 ARCHIVE_SDMC,
518 (FS_Path) { PATH_EMPTY, 1, "" },
519 0
520 };
521 FSUSER_OpenArchive(&sdmcArchive);
522
523 struct GUIFont* font = GUIFontCreate();
524
525 if (!font) {
526 goto cleanup;
527 }
528
529 struct GBAGUIRunner runner = {
530 .params = {
531 320, 240,
532 font, "/",
533 _drawStart, _drawEnd,
534 _pollInput, _pollCursor,
535 _batteryState,
536 _guiPrepare, _guiFinish,
537
538 GUI_PARAMS_TRAIL
539 },
540 .keySources = (struct GUIInputKeys[]) {
541 {
542 .name = "3DS Input",
543 .id = _3DS_INPUT,
544 .keyNames = (const char*[]) {
545 "A",
546 "B",
547 "Select",
548 "Start",
549 "D-Pad Right",
550 "D-Pad Left",
551 "D-Pad Up",
552 "D-Pad Down",
553 "R",
554 "L",
555 "X",
556 "Y",
557 0,
558 0,
559 "ZL",
560 "ZR",
561 0,
562 0,
563 0,
564 0,
565 0,
566 0,
567 0,
568 0,
569 "C-Stick Right",
570 "C-Stick Left",
571 "C-Stick Up",
572 "C-Stick Down",
573 },
574 .nKeys = 28
575 },
576 { .id = 0 }
577 },
578 .configExtra = (struct GUIMenuItem[]) {
579 {
580 .title = "Screen mode",
581 .data = "screenMode",
582 .submenu = 0,
583 .state = SM_PA_TOP,
584 .validStates = (const char*[]) {
585 "Pixel-Accurate/Bottom",
586 "Aspect-Ratio Fit/Bottom",
587 "Stretched/Bottom",
588 "Pixel-Accurate/Top",
589 "Aspect-Ratio Fit/Top",
590 "Stretched/Top",
591 },
592 .nStates = 6
593 }
594 },
595 .nConfigExtra = 1,
596 .setup = _setup,
597 .teardown = 0,
598 .gameLoaded = _gameLoaded,
599 .gameUnloaded = _gameUnloaded,
600 .prepareForFrame = 0,
601 .drawFrame = _drawFrame,
602 .drawScreenshot = _drawScreenshot,
603 .paused = _gameUnloaded,
604 .unpaused = _gameLoaded,
605 .incrementScreenMode = _incrementScreenMode,
606 .pollGameInput = _pollGameInput
607 };
608
609 GBAGUIInit(&runner, "3ds");
610 GBAGUIRunloop(&runner);
611 GBAGUIDeinit(&runner);
612
613cleanup:
614 if (renderer.outputBuffer) {
615 linearFree(renderer.outputBuffer);
616 }
617
618 if (gbaOutputTexture.data) {
619 ctrDeinitGpu();
620 vramFree(gbaOutputTexture.data);
621 }
622
623 gfxExit();
624
625 if (hasSound) {
626 linearFree(audioLeft);
627 linearFree(audioRight);
628 }
629
630 csndExit();
631 ptmuExit();
632 return 0;
633}