all repos — mgba @ 60b4a490e2de76cc43caaba7226087ccc75db202

mGBA Game Boy Advance Emulator

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}