all repos — mgba @ f7a9fe8e64c3d537e3d3342d06ed7e096d20b33b

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 color_t* pixels, unsigned width, unsigned height, bool faded) {
536
537	C3D_Tex* tex = &outputTexture;
538
539	color_t* newPixels = linearMemAlign(256 * height * sizeof(color_t), 0x100);
540
541	unsigned y;
542	for (y = 0; y < height; ++y) {
543		memcpy(&newPixels[y * 256], &pixels[y * width], width * sizeof(color_t));
544		memset(&newPixels[y * 256 + width], 0, (256 - width) * sizeof(color_t));
545	}
546
547	GSPGPU_FlushDataCache(newPixels, 256 * height * sizeof(u32));
548	C3D_SafeDisplayTransfer(
549			(u32*) newPixels, GX_BUFFER_DIM(256, height),
550			tex->data, GX_BUFFER_DIM(256, 256),
551			GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB565) |
552				GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB565) |
553				GX_TRANSFER_OUT_TILED(1) | GX_TRANSFER_FLIP_VERT(1));
554	gspWaitForPPF();
555	linearFree(newPixels);
556
557	_drawTex(runner->core, faded);
558}
559
560static uint16_t _pollGameInput(struct mGUIRunner* runner) {
561	UNUSED(runner);
562
563	hidScanInput();
564	uint32_t activeKeys = hidKeysHeld();
565	uint16_t keys = mInputMapKeyBits(&runner->core->inputMap, _3DS_INPUT, activeKeys, 0);
566	keys |= (activeKeys >> 24) & 0xF0;
567	return keys;
568}
569
570static void _incrementScreenMode(struct mGUIRunner* runner) {
571	UNUSED(runner);
572	screenMode = (screenMode + 1) % SM_MAX;
573	mCoreConfigSetUIntValue(&runner->config, "screenMode", screenMode);
574
575	C3D_RenderBufClear(&bottomScreen);
576	C3D_RenderBufClear(&topScreen);
577}
578
579static void _setFrameLimiter(struct mGUIRunner* runner, bool limit) {
580	UNUSED(runner);
581	frameLimiter = limit;
582}
583
584static uint32_t _pollInput(const struct mInputMap* map) {
585	hidScanInput();
586	int activeKeys = hidKeysHeld();
587	return mInputMapKeyBits(map, _3DS_INPUT, activeKeys, 0);
588}
589
590static enum GUICursorState _pollCursor(unsigned* x, unsigned* y) {
591	hidScanInput();
592	if (!(hidKeysHeld() & KEY_TOUCH)) {
593		return GUI_CURSOR_NOT_PRESENT;
594	}
595	touchPosition pos;
596	hidTouchRead(&pos);
597	*x = pos.px;
598	*y = pos.py;
599	return GUI_CURSOR_DOWN;
600}
601
602static void _sampleRotation(struct mRotationSource* source) {
603	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
604	// Work around ctrulib getting the entries wrong
605	rotation->accel = *(accelVector*) &hidSharedMem[0x48];
606	rotation->gyro = *(angularRate*) &hidSharedMem[0x5C];
607}
608
609static int32_t _readTiltX(struct mRotationSource* source) {
610	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
611	return rotation->accel.x << 18L;
612}
613
614static int32_t _readTiltY(struct mRotationSource* source) {
615	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
616	return rotation->accel.y << 18L;
617}
618
619static int32_t _readGyroZ(struct mRotationSource* source) {
620	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
621	return rotation->gyro.y << 18L; // Yes, y
622}
623
624static void _postAudioBuffer(struct mAVStream* stream, blip_t* left, blip_t* right) {
625	UNUSED(stream);
626	if (hasSound == CSND_SUPPORTED) {
627		blip_read_samples(left, &audioLeft[audioPos], AUDIO_SAMPLES, false);
628		blip_read_samples(right, &audioRight[audioPos], AUDIO_SAMPLES, false);
629		GSPGPU_FlushDataCache(&audioLeft[audioPos], AUDIO_SAMPLES * sizeof(int16_t));
630		GSPGPU_FlushDataCache(&audioRight[audioPos], AUDIO_SAMPLES * sizeof(int16_t));
631		audioPos = (audioPos + AUDIO_SAMPLES) % AUDIO_SAMPLE_BUFFER;
632		if (audioPos == AUDIO_SAMPLES * 3) {
633			u8 playing = 0;
634			csndIsPlaying(0x8, &playing);
635			if (!playing) {
636				CSND_SetPlayState(0x8, 1);
637				CSND_SetPlayState(0x9, 1);
638				csndExecCmds(false);
639			}
640		}
641	} else if (hasSound == DSP_SUPPORTED) {
642		int startId = bufferId;
643		while (dspBuffer[bufferId].status == NDSP_WBUF_QUEUED || dspBuffer[bufferId].status == NDSP_WBUF_PLAYING) {
644			bufferId = (bufferId + 1) & (DSP_BUFFERS - 1);
645			if (bufferId == startId) {
646				blip_clear(left);
647				blip_clear(right);
648				return;
649			}
650		}
651		void* tmpBuf = dspBuffer[bufferId].data_pcm16;
652		memset(&dspBuffer[bufferId], 0, sizeof(dspBuffer[bufferId]));
653		dspBuffer[bufferId].data_pcm16 = tmpBuf;
654		dspBuffer[bufferId].nsamples = AUDIO_SAMPLES;
655		blip_read_samples(left, dspBuffer[bufferId].data_pcm16, AUDIO_SAMPLES, true);
656		blip_read_samples(right, dspBuffer[bufferId].data_pcm16 + 1, AUDIO_SAMPLES, true);
657		DSP_FlushDataCache(dspBuffer[bufferId].data_pcm16, AUDIO_SAMPLES * 2 * sizeof(int16_t));
658		ndspChnWaveBufAdd(0, &dspBuffer[bufferId]);
659	}
660}
661
662int main() {
663	rotation.d.sample = _sampleRotation;
664	rotation.d.readTiltX = _readTiltX;
665	rotation.d.readTiltY = _readTiltY;
666	rotation.d.readGyroZ = _readGyroZ;
667
668	stream.videoDimensionsChanged = 0;
669	stream.postVideoFrame = 0;
670	stream.postAudioFrame = 0;
671	stream.postAudioBuffer = _postAudioBuffer;
672
673	if (!allocateRomBuffer()) {
674		return 1;
675	}
676
677	aptHook(&cookie, _aptHook, 0);
678
679	ptmuInit();
680	hasSound = NO_SOUND;
681	if (!ndspInit()) {
682		hasSound = DSP_SUPPORTED;
683		ndspSetOutputMode(NDSP_OUTPUT_STEREO);
684		ndspSetOutputCount(1);
685		ndspChnReset(0);
686		ndspChnSetFormat(0, NDSP_FORMAT_STEREO_PCM16);
687		ndspChnSetInterp(0, NDSP_INTERP_NONE);
688		ndspChnSetRate(0, 0x8000);
689		ndspChnWaveBufClear(0);
690		audioLeft = linearMemAlign(AUDIO_SAMPLES * DSP_BUFFERS * 2 * sizeof(int16_t), 0x80);
691		memset(dspBuffer, 0, sizeof(dspBuffer));
692		int i;
693		for (i = 0; i < DSP_BUFFERS; ++i) {
694			dspBuffer[i].data_pcm16 = &audioLeft[AUDIO_SAMPLES * i * 2];
695			dspBuffer[i].nsamples = AUDIO_SAMPLES;
696		}
697	}
698
699	if (hasSound == NO_SOUND && !csndInit()) {
700		hasSound = CSND_SUPPORTED;
701		audioLeft = linearMemAlign(AUDIO_SAMPLE_BUFFER * sizeof(int16_t), 0x80);
702		audioRight = linearMemAlign(AUDIO_SAMPLE_BUFFER * sizeof(int16_t), 0x80);
703	}
704
705	gfxInit(GSP_BGR8_OES, GSP_BGR8_OES, true);
706
707	if (!_initGpu()) {
708		outputTexture.data = 0;
709		_cleanup();
710		return 1;
711	}
712
713	if (!C3D_TexInitVRAM(&outputTexture, 256, 256, GPU_RGB565)) {
714		_cleanup();
715		return 1;
716	}
717	C3D_TexSetWrap(&outputTexture, GPU_CLAMP_TO_EDGE, GPU_CLAMP_TO_EDGE);
718	C3D_TexSetFilter(&outputTexture, GPU_NEAREST, GPU_NEAREST);
719	C3D_TexSetFilter(&upscaleBuffer.colorBuf, GPU_LINEAR, GPU_LINEAR);
720	void* outputTextureEnd = (u8*)outputTexture.data + 256 * 256 * 2;
721
722	// Zero texture data to make sure no garbage around the border interferes with filtering
723	GX_MemoryFill(
724			outputTexture.data, 0x0000, outputTextureEnd, GX_FILL_16BIT_DEPTH | GX_FILL_TRIGGER,
725			NULL, 0, NULL, 0);
726	gspWaitForPSC0();
727
728	FSUSER_OpenArchive(&sdmcArchive, ARCHIVE_SDMC, fsMakePath(PATH_EMPTY, ""));
729
730	struct GUIFont* font = GUIFontCreate();
731
732	if (!font) {
733		_cleanup();
734		return 1;
735	}
736
737	struct mGUIRunner runner = {
738		.params = {
739			320, 240,
740			font, "/",
741			_drawStart, _drawEnd,
742			_pollInput, _pollCursor,
743			_batteryState,
744			_guiPrepare, _guiFinish,
745
746			GUI_PARAMS_TRAIL
747		},
748		.keySources = (struct GUIInputKeys[]) {
749			{
750				.name = "3DS Input",
751				.id = _3DS_INPUT,
752				.keyNames = (const char*[]) {
753					"A",
754					"B",
755					"Select",
756					"Start",
757					"D-Pad Right",
758					"D-Pad Left",
759					"D-Pad Up",
760					"D-Pad Down",
761					"R",
762					"L",
763					"X",
764					"Y",
765					0,
766					0,
767					"ZL",
768					"ZR",
769					0,
770					0,
771					0,
772					0,
773					0,
774					0,
775					0,
776					0,
777					"C-Stick Right",
778					"C-Stick Left",
779					"C-Stick Up",
780					"C-Stick Down",
781				},
782				.nKeys = 28
783			},
784			{ .id = 0 }
785		},
786		.configExtra = (struct GUIMenuItem[]) {
787			{
788				.title = "Screen mode",
789				.data = "screenMode",
790				.submenu = 0,
791				.state = SM_PA_TOP,
792				.validStates = (const char*[]) {
793					"Pixel-Accurate/Bottom",
794					"Aspect-Ratio Fit/Bottom",
795					"Stretched/Bottom",
796					"Pixel-Accurate/Top",
797					"Aspect-Ratio Fit/Top",
798					"Stretched/Top",
799				},
800				.nStates = 6
801			},
802			{
803				.title = "Filtering",
804				.data = "filterMode",
805				.submenu = 0,
806				.state = FM_LINEAR_2x,
807				.validStates = (const char*[]) {
808					NULL, // Disable choosing nearest neighbor; it always looks bad
809					"Bilinear (smoother)",
810					"Bilinear (pixelated)",
811				},
812				.nStates = 3
813			},
814			{
815				.title = "Screen darkening",
816				.data = "darkenMode",
817				.submenu = 0,
818				.state = DM_NATIVE,
819				.validStates = (const char*[]) {
820					"None",
821					"Dark",
822					"Very dark",
823					"Grayed",
824				},
825				.nStates = 4
826			}
827		},
828		.nConfigExtra = 3,
829		.setup = _setup,
830		.teardown = 0,
831		.gameLoaded = _gameLoaded,
832		.gameUnloaded = _gameUnloaded,
833		.prepareForFrame = 0,
834		.drawFrame = _drawFrame,
835		.drawScreenshot = _drawScreenshot,
836		.paused = _gameUnloaded,
837		.unpaused = _gameLoaded,
838		.incrementScreenMode = _incrementScreenMode,
839		.setFrameLimiter = _setFrameLimiter,
840		.pollGameInput = _pollGameInput
841	};
842
843	mGUIInit(&runner, "3ds");
844
845	_map3DSKey(&runner.params.keyMap, KEY_X, GUI_INPUT_CANCEL);
846	_map3DSKey(&runner.params.keyMap, KEY_Y, mGUI_INPUT_SCREEN_MODE);
847	_map3DSKey(&runner.params.keyMap, KEY_B, GUI_INPUT_BACK);
848	_map3DSKey(&runner.params.keyMap, KEY_A, GUI_INPUT_SELECT);
849	_map3DSKey(&runner.params.keyMap, KEY_UP, GUI_INPUT_UP);
850	_map3DSKey(&runner.params.keyMap, KEY_DOWN, GUI_INPUT_DOWN);
851	_map3DSKey(&runner.params.keyMap, KEY_LEFT, GUI_INPUT_LEFT);
852	_map3DSKey(&runner.params.keyMap, KEY_RIGHT, GUI_INPUT_RIGHT);
853	_map3DSKey(&runner.params.keyMap, KEY_CSTICK_UP, mGUI_INPUT_INCREASE_BRIGHTNESS);
854	_map3DSKey(&runner.params.keyMap, KEY_CSTICK_DOWN, mGUI_INPUT_DECREASE_BRIGHTNESS);
855
856	mGUIRunloop(&runner);
857	mGUIDeinit(&runner);
858
859	_cleanup();
860	return 0;
861}