all repos — mgba @ bee854bb610c2dd6bffd5934783ad2e3c707e3ba

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