all repos — mgba @ 8615defda763c928eadb4c0919081a74b6bd28fc

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