all repos — mgba @ a2b8c4ae807ae92103e14961fd34377b28cbe219

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