all repos — mgba @ 9eb8faf1bab99e69e7c62fb8d7daa85a5cc07adf

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#ifdef M_CORE_GBA
  8#include "gba/gba.h"
  9#include "gba/input.h"
 10#include "gba/video.h"
 11#endif
 12#ifdef M_CORE_GB
 13#include "gb/gb.h"
 14#endif
 15#include "feature/gui/gui-runner.h"
 16#include "util/gui.h"
 17#include "util/gui/file-select.h"
 18#include "util/gui/font.h"
 19#include "util/gui/menu.h"
 20#include "util/memory.h"
 21
 22#include "3ds-vfs.h"
 23#include "ctr-gpu.h"
 24
 25#include <3ds.h>
 26#include <3ds/gpu/gx.h>
 27
 28static enum ScreenMode {
 29	SM_PA_BOTTOM,
 30	SM_AF_BOTTOM,
 31	SM_SF_BOTTOM,
 32	SM_PA_TOP,
 33	SM_AF_TOP,
 34	SM_SF_TOP,
 35	SM_MAX
 36} screenMode = SM_PA_TOP;
 37
 38#define _3DS_INPUT 0x3344534B
 39
 40#define AUDIO_SAMPLES 384
 41#define AUDIO_SAMPLE_BUFFER (AUDIO_SAMPLES * 16)
 42#define DSP_BUFFERS 4
 43
 44FS_Archive sdmcArchive;
 45
 46static struct GBA3DSRotationSource {
 47	struct mRotationSource d;
 48	accelVector accel;
 49	angularRate gyro;
 50} rotation;
 51
 52static enum {
 53	NO_SOUND,
 54	DSP_SUPPORTED,
 55	CSND_SUPPORTED
 56} hasSound;
 57
 58// TODO: Move into context
 59static void* outputBuffer;
 60static struct mAVStream stream;
 61static int16_t* audioLeft = 0;
 62static int16_t* audioRight = 0;
 63static size_t audioPos = 0;
 64static C3D_Tex outputTexture;
 65static ndspWaveBuf dspBuffer[DSP_BUFFERS];
 66static int bufferId = 0;
 67static bool frameLimiter = true;
 68
 69static C3D_RenderBuf bottomScreen;
 70static C3D_RenderBuf topScreen;
 71
 72static aptHookCookie cookie;
 73
 74extern bool allocateRomBuffer(void);
 75
 76static bool _initGpu(void) {
 77	if (!C3D_Init(C3D_DEFAULT_CMDBUF_SIZE)) {
 78		return false;
 79	}
 80
 81	if (!C3D_RenderBufInit(&topScreen, 240, 400, GPU_RB_RGB8, 0) || !C3D_RenderBufInit(&bottomScreen, 240, 320, GPU_RB_RGB8, 0)) {
 82		return false;
 83	}
 84
 85	return ctrInitGpu();
 86}
 87
 88static void _cleanup(void) {
 89	ctrDeinitGpu();
 90
 91	if (outputBuffer) {
 92		linearFree(outputBuffer);
 93	}
 94
 95	C3D_RenderBufDelete(&topScreen);
 96	C3D_RenderBufDelete(&bottomScreen);
 97	C3D_Fini();
 98
 99	gfxExit();
100
101	if (hasSound != NO_SOUND) {
102		linearFree(audioLeft);
103	}
104
105	if (hasSound == CSND_SUPPORTED) {
106		linearFree(audioRight);
107		csndExit();
108	}
109
110	if (hasSound == DSP_SUPPORTED) {
111		ndspExit();
112	}
113
114	csndExit();
115	ptmuExit();
116}
117
118static void _aptHook(APT_HookType hook, void* user) {
119	UNUSED(user);
120	switch (hook) {
121	case APTHOOK_ONSUSPEND:
122	case APTHOOK_ONSLEEP:
123		if (hasSound == CSND_SUPPORTED) {
124			CSND_SetPlayState(8, 0);
125			CSND_SetPlayState(9, 0);
126			csndExecCmds(false);
127		}
128		break;
129	case APTHOOK_ONEXIT:
130		if (hasSound == CSND_SUPPORTED) {
131			CSND_SetPlayState(8, 0);
132			CSND_SetPlayState(9, 0);
133			csndExecCmds(false);
134		}
135		_cleanup();
136		exit(0);
137		break;
138	default:
139		break;
140	}
141}
142
143static void _map3DSKey(struct mInputMap* map, int ctrKey, enum GBAKey key) {
144	mInputBindKey(map, _3DS_INPUT, __builtin_ctz(ctrKey), key);
145}
146
147static void _csndPlaySound(u32 flags, u32 sampleRate, float vol, void* left, void* right, u32 size) {
148	u32 pleft = 0, pright = 0;
149
150	int loopMode = (flags >> 10) & 3;
151	if (!loopMode) {
152		flags |= SOUND_ONE_SHOT;
153	}
154
155	pleft = osConvertVirtToPhys(left);
156	pright = osConvertVirtToPhys(right);
157
158	u32 timer = CSND_TIMER(sampleRate);
159	if (timer < 0x0042) {
160		timer = 0x0042;
161	}
162	else if (timer > 0xFFFF) {
163		timer = 0xFFFF;
164	}
165	flags &= ~0xFFFF001F;
166	flags |= SOUND_ENABLE | (timer << 16);
167
168	u32 volumes = CSND_VOL(vol, -1.0);
169	CSND_SetChnRegs(flags | SOUND_CHANNEL(8), pleft, pleft, size, volumes, volumes);
170	volumes = CSND_VOL(vol, 1.0);
171	CSND_SetChnRegs(flags | SOUND_CHANNEL(9), pright, pright, size, volumes, volumes);
172}
173
174static void _postAudioBuffer(struct mAVStream* stream, blip_t* left, blip_t* right);
175
176static void _drawStart(void) {
177	C3D_RenderBufClear(&bottomScreen);
178	C3D_RenderBufClear(&topScreen);
179}
180
181static void _drawEnd(void) {
182	ctrFinalize();
183	C3D_RenderBufTransfer(&topScreen, (u32*) gfxGetFramebuffer(GFX_TOP, GFX_LEFT, NULL, NULL), GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB8) | GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8));
184	C3D_RenderBufTransfer(&bottomScreen, (u32*) gfxGetFramebuffer(GFX_BOTTOM, GFX_LEFT, NULL, NULL), GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB8) | GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8));
185	gfxSwapBuffersGpu();
186	if (frameLimiter) {
187		gspWaitForEvent(GSPGPU_EVENT_VBlank0, false);
188	}
189}
190
191static int _batteryState(void) {
192	u8 charge;
193	u8 adapter;
194	PTMU_GetBatteryLevel(&charge);
195	PTMU_GetBatteryChargeState(&adapter);
196	int state = 0;
197	if (adapter) {
198		state |= BATTERY_CHARGING;
199	}
200	if (charge > 0) {
201		--charge;
202	}
203	return state | charge;
204}
205
206static void _guiPrepare(void) {
207	int screen = screenMode < SM_PA_TOP ? GFX_BOTTOM : GFX_TOP;
208	if (screen == GFX_BOTTOM) {
209		return;
210	}
211
212	C3D_RenderBufBind(&bottomScreen);
213	ctrSetViewportSize(320, 240);
214}
215
216static void _guiFinish(void) {
217	ctrFlushBatch();
218}
219
220static void _setup(struct mGUIRunner* runner) {
221	bool isNew3DS = false;
222	APT_CheckNew3DS(&isNew3DS);
223	if (isNew3DS && !envIsHomebrew()) {
224		mCoreConfigSetDefaultIntValue(&runner->config, "threadedVideo", 1);
225		mCoreLoadForeignConfig(runner->core, &runner->config);
226	}
227
228	runner->core->setRotation(runner->core, &rotation.d);
229	if (hasSound != NO_SOUND) {
230		runner->core->setAVStream(runner->core, &stream);
231	}
232
233	_map3DSKey(&runner->core->inputMap, KEY_A, GBA_KEY_A);
234	_map3DSKey(&runner->core->inputMap, KEY_B, GBA_KEY_B);
235	_map3DSKey(&runner->core->inputMap, KEY_START, GBA_KEY_START);
236	_map3DSKey(&runner->core->inputMap, KEY_SELECT, GBA_KEY_SELECT);
237	_map3DSKey(&runner->core->inputMap, KEY_UP, GBA_KEY_UP);
238	_map3DSKey(&runner->core->inputMap, KEY_DOWN, GBA_KEY_DOWN);
239	_map3DSKey(&runner->core->inputMap, KEY_LEFT, GBA_KEY_LEFT);
240	_map3DSKey(&runner->core->inputMap, KEY_RIGHT, GBA_KEY_RIGHT);
241	_map3DSKey(&runner->core->inputMap, KEY_L, GBA_KEY_L);
242	_map3DSKey(&runner->core->inputMap, KEY_R, GBA_KEY_R);
243
244	outputBuffer = linearMemAlign(256 * VIDEO_VERTICAL_PIXELS * 2, 0x80);
245	runner->core->setVideoBuffer(runner->core, outputBuffer, 256);
246
247	unsigned mode;
248	if (mCoreConfigGetUIntValue(&runner->config, "screenMode", &mode) && mode < SM_MAX) {
249		screenMode = mode;
250	}
251	frameLimiter = true;
252
253	runner->core->setAudioBufferSize(runner->core, AUDIO_SAMPLES);
254}
255
256static void _gameLoaded(struct mGUIRunner* runner) {
257	switch (runner->core->platform(runner->core)) {
258#ifdef M_CORE_GBA
259	case PLATFORM_GBA:
260		if (((struct GBA*) runner->core->board)->memory.hw.devices & HW_TILT) {
261			HIDUSER_EnableAccelerometer();
262		}
263		if (((struct GBA*) runner->core->board)->memory.hw.devices & HW_GYRO) {
264			HIDUSER_EnableGyroscope();
265		}
266		break;
267#endif
268#ifdef M_CORE_GB
269	case PLATFORM_GB:
270		if (((struct GB*) runner->core->board)->memory.mbcType == GB_MBC7) {
271			HIDUSER_EnableAccelerometer();
272		}
273		break;
274#endif
275	default:
276		break;
277	}
278	osSetSpeedupEnable(true);
279
280	double ratio = GBAAudioCalculateRatio(1, 59.8260982880808, 1);
281	blip_set_rates(runner->core->getAudioChannel(runner->core, 0), runner->core->frequency(runner->core), 32768 * ratio);
282	blip_set_rates(runner->core->getAudioChannel(runner->core, 1), runner->core->frequency(runner->core), 32768 * ratio);
283	if (hasSound != NO_SOUND) {
284		audioPos = 0;
285	}
286	if (hasSound == CSND_SUPPORTED) {
287		memset(audioLeft, 0, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
288		memset(audioRight, 0, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
289		_csndPlaySound(SOUND_REPEAT | SOUND_FORMAT_16BIT, 32768, 1.0, audioLeft, audioRight, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
290		csndExecCmds(false);
291	} else if (hasSound == DSP_SUPPORTED) {
292		memset(audioLeft, 0, AUDIO_SAMPLE_BUFFER * 2 * sizeof(int16_t));
293	}
294	unsigned mode;
295	if (mCoreConfigGetUIntValue(&runner->config, "screenMode", &mode) && mode != screenMode) {
296		screenMode = mode;
297	}
298}
299
300static void _gameUnloaded(struct mGUIRunner* runner) {
301	if (hasSound == CSND_SUPPORTED) {
302		CSND_SetPlayState(8, 0);
303		CSND_SetPlayState(9, 0);
304		csndExecCmds(false);
305	}
306	osSetSpeedupEnable(false);
307	frameLimiter = true;
308
309	switch (runner->core->platform(runner->core)) {
310#ifdef M_CORE_GBA
311	case PLATFORM_GBA:
312		if (((struct GBA*) runner->core->board)->memory.hw.devices & HW_TILT) {
313			HIDUSER_DisableAccelerometer();
314		}
315		if (((struct GBA*) runner->core->board)->memory.hw.devices & HW_GYRO) {
316			HIDUSER_DisableGyroscope();
317		}
318		break;
319#endif
320#ifdef M_CORE_GB
321	case PLATFORM_GB:
322		if (((struct GB*) runner->core->board)->memory.mbcType == GB_MBC7) {
323			HIDUSER_DisableAccelerometer();
324		}
325		break;
326#endif
327	default:
328		break;
329	}
330}
331
332static void _drawTex(struct mCore* core, bool faded) {
333	if (screenMode < SM_PA_TOP) {
334		C3D_RenderBufBind(&bottomScreen);
335		ctrSetViewportSize(320, 240);
336	} else {
337		C3D_RenderBufBind(&topScreen);
338		ctrSetViewportSize(400, 240);
339	}
340	ctrActivateTexture(&outputTexture);
341
342	u32 color = faded ? 0x3FFFFFFF : 0xFFFFFFFF;
343
344	int screen_w = screenMode < SM_PA_TOP ? 320 : 400;
345	int screen_h = 240;
346
347	unsigned corew, coreh;
348	core->desiredVideoDimensions(core, &corew, &coreh);
349
350	int w = corew;
351	int h = coreh;
352	// Get greatest common divisor
353	while (w != 0) {
354		int temp = h % w;
355		h = w;
356		w = temp;
357	}
358	int gcd = h;
359	int aspectw = corew / gcd;
360	int aspecth = coreh / gcd;
361
362	switch (screenMode) {
363	case SM_PA_TOP:
364	case SM_PA_BOTTOM:
365	default:
366		w = corew;
367		h = coreh;
368		break;
369	case SM_AF_TOP:
370	case SM_AF_BOTTOM:
371		w = screen_w / aspectw;
372		h = screen_h / aspecth;
373		if (w * aspecth > screen_h) {
374			w = aspectw * h;
375			h = aspecth * h;
376		} else {
377			h = aspecth * w;
378			w = aspectw * w;
379		}
380		break;
381	case SM_SF_TOP:
382	case SM_SF_BOTTOM:
383		w = screen_w;
384		h = screen_h;
385		break;
386	}
387
388	int x = (screen_w - w) / 2;
389	int y = (screen_h - h) / 2;
390
391	ctrAddRectScaled(color, x, y, w, h, 0, 0, corew, coreh);
392	ctrFlushBatch();
393}
394
395static void _drawFrame(struct mGUIRunner* runner, bool faded) {
396	UNUSED(runner);
397
398	C3D_Tex* tex = &outputTexture;
399
400	GSPGPU_FlushDataCache(outputBuffer, 256 * VIDEO_VERTICAL_PIXELS * 2);
401	C3D_SafeDisplayTransfer(
402			outputBuffer, GX_BUFFER_DIM(256, VIDEO_VERTICAL_PIXELS),
403			tex->data, GX_BUFFER_DIM(256, 256),
404			GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB565) |
405				GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB565) |
406				GX_TRANSFER_OUT_TILED(1) | GX_TRANSFER_FLIP_VERT(1));
407
408	if (hasSound == NO_SOUND) {
409		blip_clear(runner->core->getAudioChannel(runner->core, 0));
410		blip_clear(runner->core->getAudioChannel(runner->core, 1));
411	}
412
413	gspWaitForPPF();
414	_drawTex(runner->core, faded);
415}
416
417static void _drawScreenshot(struct mGUIRunner* runner, const uint32_t* pixels, unsigned width, unsigned height, bool faded) {
418
419	C3D_Tex* tex = &outputTexture;
420
421	u16* newPixels = linearMemAlign(256 * height * sizeof(u32), 0x100);
422
423	// Convert image from RGBX8 to BGR565
424	for (unsigned y = 0; y < height; ++y) {
425		for (unsigned x = 0; x < width; ++x) {
426			// 0xXXBBGGRR -> 0bRRRRRGGGGGGBBBBB
427			u32 p = *pixels++;
428			newPixels[y * 256 + x] =
429				(p << 24 >> (24 + 3) << 11) | // R
430				(p << 16 >> (24 + 2) <<  5) | // G
431				(p <<  8 >> (24 + 3) <<  0);  // B
432		}
433		memset(&newPixels[y * 256 + width], 0, (256 - width) * sizeof(u32));
434	}
435
436	GSPGPU_FlushDataCache(newPixels, 256 * height * sizeof(u32));
437	C3D_SafeDisplayTransfer(
438			(u32*) newPixels, GX_BUFFER_DIM(256, height),
439			tex->data, GX_BUFFER_DIM(256, 256),
440			GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB565) |
441				GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB565) |
442				GX_TRANSFER_OUT_TILED(1) | GX_TRANSFER_FLIP_VERT(1));
443	gspWaitForPPF();
444	linearFree(newPixels);
445
446	_drawTex(runner->core, faded);
447}
448
449static uint16_t _pollGameInput(struct mGUIRunner* runner) {
450	UNUSED(runner);
451
452	hidScanInput();
453	uint32_t activeKeys = hidKeysHeld();
454	uint16_t keys = mInputMapKeyBits(&runner->core->inputMap, _3DS_INPUT, activeKeys, 0);
455	keys |= (activeKeys >> 24) & 0xF0;
456	return keys;
457}
458
459static void _incrementScreenMode(struct mGUIRunner* runner) {
460	UNUSED(runner);
461	screenMode = (screenMode + 1) % SM_MAX;
462	mCoreConfigSetUIntValue(&runner->config, "screenMode", screenMode);
463
464	C3D_RenderBufClear(&bottomScreen);
465	C3D_RenderBufClear(&topScreen);
466}
467
468static void _setFrameLimiter(struct mGUIRunner* runner, bool limit) {
469	UNUSED(runner);
470	frameLimiter = limit;
471}
472
473static uint32_t _pollInput(const struct mInputMap* map) {
474	hidScanInput();
475	int activeKeys = hidKeysHeld();
476	return mInputMapKeyBits(map, _3DS_INPUT, activeKeys, 0);
477}
478
479static enum GUICursorState _pollCursor(unsigned* x, unsigned* y) {
480	hidScanInput();
481	if (!(hidKeysHeld() & KEY_TOUCH)) {
482		return GUI_CURSOR_NOT_PRESENT;
483	}
484	touchPosition pos;
485	hidTouchRead(&pos);
486	*x = pos.px;
487	*y = pos.py;
488	return GUI_CURSOR_DOWN;
489}
490
491static void _sampleRotation(struct mRotationSource* source) {
492	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
493	// Work around ctrulib getting the entries wrong
494	rotation->accel = *(accelVector*) &hidSharedMem[0x48];
495	rotation->gyro = *(angularRate*) &hidSharedMem[0x5C];
496}
497
498static int32_t _readTiltX(struct mRotationSource* source) {
499	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
500	return rotation->accel.x << 18L;
501}
502
503static int32_t _readTiltY(struct mRotationSource* source) {
504	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
505	return rotation->accel.y << 18L;
506}
507
508static int32_t _readGyroZ(struct mRotationSource* source) {
509	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
510	return rotation->gyro.y << 18L; // Yes, y
511}
512
513static void _postAudioBuffer(struct mAVStream* stream, blip_t* left, blip_t* right) {
514	UNUSED(stream);
515	if (hasSound == CSND_SUPPORTED) {
516		blip_read_samples(left, &audioLeft[audioPos], AUDIO_SAMPLES, false);
517		blip_read_samples(right, &audioRight[audioPos], AUDIO_SAMPLES, false);
518		GSPGPU_FlushDataCache(&audioLeft[audioPos], AUDIO_SAMPLES * sizeof(int16_t));
519		GSPGPU_FlushDataCache(&audioRight[audioPos], AUDIO_SAMPLES * sizeof(int16_t));
520		audioPos = (audioPos + AUDIO_SAMPLES) % AUDIO_SAMPLE_BUFFER;
521		if (audioPos == AUDIO_SAMPLES * 3) {
522			u8 playing = 0;
523			csndIsPlaying(0x8, &playing);
524			if (!playing) {
525				CSND_SetPlayState(0x8, 1);
526				CSND_SetPlayState(0x9, 1);
527				csndExecCmds(false);
528			}
529		}
530	} else if (hasSound == DSP_SUPPORTED) {
531		int startId = bufferId;
532		while (dspBuffer[bufferId].status == NDSP_WBUF_QUEUED || dspBuffer[bufferId].status == NDSP_WBUF_PLAYING) {
533			bufferId = (bufferId + 1) & (DSP_BUFFERS - 1);
534			if (bufferId == startId) {
535				blip_clear(left);
536				blip_clear(right);
537				return;
538			}
539		}
540		void* tmpBuf = dspBuffer[bufferId].data_pcm16;
541		memset(&dspBuffer[bufferId], 0, sizeof(dspBuffer[bufferId]));
542		dspBuffer[bufferId].data_pcm16 = tmpBuf;
543		dspBuffer[bufferId].nsamples = AUDIO_SAMPLES;
544		blip_read_samples(left, dspBuffer[bufferId].data_pcm16, AUDIO_SAMPLES, true);
545		blip_read_samples(right, dspBuffer[bufferId].data_pcm16 + 1, AUDIO_SAMPLES, true);
546		DSP_FlushDataCache(dspBuffer[bufferId].data_pcm16, AUDIO_SAMPLES * 2 * sizeof(int16_t));
547		ndspChnWaveBufAdd(0, &dspBuffer[bufferId]);
548	}
549}
550
551int main() {
552	rotation.d.sample = _sampleRotation;
553	rotation.d.readTiltX = _readTiltX;
554	rotation.d.readTiltY = _readTiltY;
555	rotation.d.readGyroZ = _readGyroZ;
556
557	stream.videoDimensionsChanged = 0;
558	stream.postVideoFrame = 0;
559	stream.postAudioFrame = 0;
560	stream.postAudioBuffer = _postAudioBuffer;
561
562	if (!allocateRomBuffer()) {
563		return 1;
564	}
565
566	aptHook(&cookie, _aptHook, 0);
567
568	ptmuInit();
569	hasSound = NO_SOUND;
570	if (!ndspInit()) {
571		hasSound = DSP_SUPPORTED;
572		ndspSetOutputMode(NDSP_OUTPUT_STEREO);
573		ndspSetOutputCount(1);
574		ndspChnReset(0);
575		ndspChnSetFormat(0, NDSP_FORMAT_STEREO_PCM16);
576		ndspChnSetInterp(0, NDSP_INTERP_NONE);
577		ndspChnSetRate(0, 0x8000);
578		ndspChnWaveBufClear(0);
579		audioLeft = linearMemAlign(AUDIO_SAMPLES * DSP_BUFFERS * 2 * sizeof(int16_t), 0x80);
580		memset(dspBuffer, 0, sizeof(dspBuffer));
581		int i;
582		for (i = 0; i < DSP_BUFFERS; ++i) {
583			dspBuffer[i].data_pcm16 = &audioLeft[AUDIO_SAMPLES * i * 2];
584			dspBuffer[i].nsamples = AUDIO_SAMPLES;
585		}
586	}
587
588	if (hasSound == NO_SOUND && !csndInit()) {
589		hasSound = CSND_SUPPORTED;
590		audioLeft = linearMemAlign(AUDIO_SAMPLE_BUFFER * sizeof(int16_t), 0x80);
591		audioRight = linearMemAlign(AUDIO_SAMPLE_BUFFER * sizeof(int16_t), 0x80);
592	}
593
594	gfxInit(GSP_BGR8_OES, GSP_BGR8_OES, true);
595
596	if (!_initGpu()) {
597		outputTexture.data = 0;
598		_cleanup();
599		return 1;
600	}
601
602	if (!C3D_TexInitVRAM(&outputTexture, 256, 256, GPU_RGB565)) {
603		_cleanup();
604		return 1;
605	}
606	C3D_TexSetWrap(&outputTexture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE);
607	C3D_TexSetFilter(&outputTexture, GPU_LINEAR, GPU_LINEAR);
608	void* outputTextureEnd = (u8*)outputTexture.data + 256 * 256 * 2;
609
610	// Zero texture data to make sure no garbage around the border interferes with filtering
611	GX_MemoryFill(
612			outputTexture.data, 0x0000, outputTextureEnd, GX_FILL_16BIT_DEPTH | GX_FILL_TRIGGER,
613			NULL, 0, NULL, 0);
614	gspWaitForPSC0();
615
616	FSUSER_OpenArchive(&sdmcArchive, ARCHIVE_SDMC, fsMakePath(PATH_EMPTY, ""));
617
618	struct GUIFont* font = GUIFontCreate();
619
620	if (!font) {
621		_cleanup();
622		return 1;
623	}
624
625	struct mGUIRunner runner = {
626		.params = {
627			320, 240,
628			font, "/",
629			_drawStart, _drawEnd,
630			_pollInput, _pollCursor,
631			_batteryState,
632			_guiPrepare, _guiFinish,
633
634			GUI_PARAMS_TRAIL
635		},
636		.keySources = (struct GUIInputKeys[]) {
637			{
638				.name = "3DS Input",
639				.id = _3DS_INPUT,
640				.keyNames = (const char*[]) {
641					"A",
642					"B",
643					"Select",
644					"Start",
645					"D-Pad Right",
646					"D-Pad Left",
647					"D-Pad Up",
648					"D-Pad Down",
649					"R",
650					"L",
651					"X",
652					"Y",
653					0,
654					0,
655					"ZL",
656					"ZR",
657					0,
658					0,
659					0,
660					0,
661					0,
662					0,
663					0,
664					0,
665					"C-Stick Right",
666					"C-Stick Left",
667					"C-Stick Up",
668					"C-Stick Down",
669				},
670				.nKeys = 28
671			},
672			{ .id = 0 }
673		},
674		.configExtra = (struct GUIMenuItem[]) {
675			{
676				.title = "Screen mode",
677				.data = "screenMode",
678				.submenu = 0,
679				.state = SM_PA_TOP,
680				.validStates = (const char*[]) {
681					"Pixel-Accurate/Bottom",
682					"Aspect-Ratio Fit/Bottom",
683					"Stretched/Bottom",
684					"Pixel-Accurate/Top",
685					"Aspect-Ratio Fit/Top",
686					"Stretched/Top",
687				},
688				.nStates = 6
689			}
690		},
691		.nConfigExtra = 1,
692		.setup = _setup,
693		.teardown = 0,
694		.gameLoaded = _gameLoaded,
695		.gameUnloaded = _gameUnloaded,
696		.prepareForFrame = 0,
697		.drawFrame = _drawFrame,
698		.drawScreenshot = _drawScreenshot,
699		.paused = _gameUnloaded,
700		.unpaused = _gameLoaded,
701		.incrementScreenMode = _incrementScreenMode,
702		.setFrameLimiter = _setFrameLimiter,
703		.pollGameInput = _pollGameInput
704	};
705
706	mGUIInit(&runner, "3ds");
707
708	_map3DSKey(&runner.params.keyMap, KEY_X, GUI_INPUT_CANCEL);
709	_map3DSKey(&runner.params.keyMap, KEY_Y, mGUI_INPUT_SCREEN_MODE);
710	_map3DSKey(&runner.params.keyMap, KEY_B, GUI_INPUT_BACK);
711	_map3DSKey(&runner.params.keyMap, KEY_A, GUI_INPUT_SELECT);
712	_map3DSKey(&runner.params.keyMap, KEY_UP, GUI_INPUT_UP);
713	_map3DSKey(&runner.params.keyMap, KEY_DOWN, GUI_INPUT_DOWN);
714	_map3DSKey(&runner.params.keyMap, KEY_LEFT, GUI_INPUT_LEFT);
715	_map3DSKey(&runner.params.keyMap, KEY_RIGHT, GUI_INPUT_RIGHT);
716	_map3DSKey(&runner.params.keyMap, KEY_CSTICK_UP, mGUI_INPUT_INCREASE_BRIGHTNESS);
717	_map3DSKey(&runner.params.keyMap, KEY_CSTICK_DOWN, mGUI_INPUT_DECREASE_BRIGHTNESS);
718
719	mGUIRunloop(&runner);
720	mGUIDeinit(&runner);
721
722	_cleanup();
723	return 0;
724}