all repos — mgba @ 73010a4121c978fb96a162380cee77995def0d22

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