all repos — mgba @ a87fe56ec691df7a865b858cde4033a382178c77

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