all repos — mgba @ ca3a7f1895c4749f0dc8ebf5c3e7e3a1f232424d

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