all repos — mgba @ 7097d249af677d955f7f58a56242a2e35253423e

mGBA Game Boy Advance Emulator

src/platform/3ds/main.c (view raw)

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