all repos — mgba @ d752df421f09a993092563df349736acdedbb7f5

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