all repos — mgba @ 47f3358186f8871ecff0e7514bda2f36124fcbef

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