all repos — mgba @ d9079c06103ff22b8a14883b4a80341a306bb56f

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