all repos — mgba @ 63ed9bfe9138691759d3759364cb182ca09c0571

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