all repos — mgba @ 41c96c79b42ef3f9966b56d12086364a04507ee0

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