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