all repos — mgba @ 789a84d2e25b2a6e7df0bb86351ec81726313eed

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