all repos — mgba @ 00321dd9de6d8c93090c601e5b44fabde2550028

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