all repos — mgba @ cc06e177bbab013167c620a9bd860f4f6c120564

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