all repos — mgba @ a58458b9438354970adb306d79caa53735c90807

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 "gba/renderers/video-software.h"
  8#include "gba/context/context.h"
  9#include "gba/gui/gui-runner.h"
 10#include "gba/video.h"
 11#include "util/gui.h"
 12#include "util/gui/file-select.h"
 13#include "util/gui/font.h"
 14#include "util/gui/menu.h"
 15#include "util/memory.h"
 16
 17#include "3ds-vfs.h"
 18#include "ctr-gpu.h"
 19
 20#include <3ds.h>
 21
 22static enum ScreenMode {
 23	SM_PA_BOTTOM,
 24	SM_AF_BOTTOM,
 25	SM_SF_BOTTOM,
 26	SM_PA_TOP,
 27	SM_AF_TOP,
 28	SM_SF_TOP,
 29	SM_MAX
 30} screenMode = SM_PA_TOP;
 31
 32#define AUDIO_SAMPLES 0x80
 33#define AUDIO_SAMPLE_BUFFER (AUDIO_SAMPLES * 24)
 34
 35FS_archive sdmcArchive;
 36
 37static struct GBA3DSRotationSource {
 38	struct GBARotationSource d;
 39	accelVector accel;
 40	angularRate gyro;
 41} rotation;
 42
 43static bool hasSound;
 44// TODO: Move into context
 45static struct GBAVideoSoftwareRenderer renderer;
 46static struct GBAAVStream stream;
 47static int16_t* audioLeft = 0;
 48static int16_t* audioRight = 0;
 49static size_t audioPos = 0;
 50static struct ctrTexture gbaOutputTexture;
 51static int guiDrawn;
 52
 53enum {
 54	GUI_ACTIVE = 1,
 55	GUI_THIS_FRAME = 2
 56};
 57
 58extern bool allocateRomBuffer(void);
 59
 60static void _postAudioBuffer(struct GBAAVStream* stream, struct GBAAudio* audio);
 61
 62static void _drawStart(void) {
 63	ctrGpuBeginDrawing();
 64	if (screenMode < SM_PA_TOP || (guiDrawn & GUI_ACTIVE)) {
 65		ctrGpuBeginFrame(GFX_BOTTOM);
 66		ctrSetViewportSize(320, 240);
 67	} else {
 68		ctrGpuBeginFrame(GFX_TOP);
 69		ctrSetViewportSize(400, 240);
 70	}
 71	guiDrawn &= ~GUI_THIS_FRAME;
 72}
 73
 74static void _drawEnd(void) {
 75	int screen = screenMode < SM_PA_TOP ? GFX_BOTTOM : GFX_TOP;
 76	u16 width = 0, height = 0;
 77	if (guiDrawn & GUI_ACTIVE) {
 78		screen = GFX_BOTTOM;
 79	}
 80
 81	if (!(guiDrawn & GUI_THIS_FRAME) || screen == GFX_BOTTOM) {
 82		void* outputFramebuffer = gfxGetFramebuffer(screen, GFX_LEFT, &height, &width);
 83		ctrGpuEndFrame(screen, outputFramebuffer, width, height);
 84	}
 85
 86	ctrGpuEndDrawing();
 87}
 88
 89static int _batteryState(void) {
 90	u8 charge;
 91	u8 adapter;
 92	PTMU_GetBatteryLevel(0, &charge);
 93	PTMU_GetBatteryChargeState(0, &adapter);
 94	int state = 0;
 95	if (adapter) {
 96		state |= BATTERY_CHARGING;
 97	}
 98	if (charge > 0) {
 99		--charge;
100	}
101	return state | charge;
102}
103
104static void _guiPrepare(void) {
105	guiDrawn = GUI_ACTIVE | GUI_THIS_FRAME;
106	int screen = screenMode < SM_PA_TOP ? GFX_BOTTOM : GFX_TOP;
107	if (screen == GFX_BOTTOM) {
108		return;
109	}
110
111	u16 width = 0, height = 0;
112
113	void* outputFramebuffer = gfxGetFramebuffer(screen, GFX_LEFT, &height, &width);
114	ctrGpuEndFrame(screen, outputFramebuffer, width, height);
115
116	ctrGpuBeginFrame(GFX_BOTTOM);
117	ctrSetViewportSize(320, 240);
118}
119
120static void _guiFinish(void) {
121	guiDrawn &= ~GUI_ACTIVE;
122	int screen = screenMode < SM_PA_TOP ? GFX_BOTTOM : GFX_TOP;
123	if (screen == GFX_BOTTOM) {
124		return;
125	}
126
127	u16 width = 0, height = 0;
128	void* outputFramebuffer = gfxGetFramebuffer(GFX_BOTTOM, GFX_LEFT, &height, &width);
129	ctrGpuEndFrame(GFX_BOTTOM, outputFramebuffer, width, height);
130}
131
132static void _setup(struct GBAGUIRunner* runner) {
133	runner->context.gba->rotationSource = &rotation.d;
134	if (hasSound) {
135		runner->context.gba->stream = &stream;
136	}
137
138	GBAVideoSoftwareRendererCreate(&renderer);
139	renderer.outputBuffer = linearMemAlign(256 * VIDEO_VERTICAL_PIXELS * 2, 0x80);
140	renderer.outputBufferStride = 256;
141	runner->context.renderer = &renderer.d;
142
143	unsigned mode;
144	if (GBAConfigGetUIntValue(&runner->context.config, "screenMode", &mode) && mode < SM_MAX) {
145		screenMode = mode;
146	}
147
148	GBAAudioResizeBuffer(&runner->context.gba->audio, AUDIO_SAMPLES);
149}
150
151static void _gameLoaded(struct GBAGUIRunner* runner) {
152	if (runner->context.gba->memory.hw.devices & HW_TILT) {
153		HIDUSER_EnableAccelerometer();
154	}
155	if (runner->context.gba->memory.hw.devices & HW_GYRO) {
156		HIDUSER_EnableGyroscope();
157	}
158
159#if RESAMPLE_LIBRARY == RESAMPLE_BLIP_BUF
160	double ratio = GBAAudioCalculateRatio(1, 59.8260982880808, 1);
161	blip_set_rates(runner->context.gba->audio.left,  GBA_ARM7TDMI_FREQUENCY, 32768 * ratio);
162	blip_set_rates(runner->context.gba->audio.right, GBA_ARM7TDMI_FREQUENCY, 32768 * ratio);
163#endif
164	if (hasSound) {
165		memset(audioLeft, 0, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
166		memset(audioRight, 0, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
167		audioPos = 0;
168		csndPlaySound(0x8, SOUND_REPEAT | SOUND_FORMAT_16BIT, 32768, 1.0, -1.0, audioLeft, audioLeft, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
169		csndPlaySound(0x9, SOUND_REPEAT | SOUND_FORMAT_16BIT, 32768, 1.0, 1.0, audioRight, audioRight, AUDIO_SAMPLE_BUFFER * sizeof(int16_t));
170	}
171}
172
173static void _gameUnloaded(struct GBAGUIRunner* runner) {
174	if (hasSound) {
175		CSND_SetPlayState(8, 0);
176		CSND_SetPlayState(9, 0);
177		csndExecCmds(false);
178	}
179
180	if (runner->context.gba->memory.hw.devices & HW_TILT) {
181		HIDUSER_DisableAccelerometer();
182	}
183	if (runner->context.gba->memory.hw.devices & HW_GYRO) {
184		HIDUSER_DisableGyroscope();
185	}
186}
187
188static void _drawTex(bool faded) {
189	u32 color = faded ? 0x3FFFFFFF : 0xFFFFFFFF;
190
191	int screen_w = screenMode < SM_PA_TOP ? 320 : 400;
192	int screen_h = 240;
193
194	int w, h;
195
196	switch (screenMode) {
197	case SM_PA_TOP:
198	case SM_PA_BOTTOM:
199	default:
200		w = VIDEO_HORIZONTAL_PIXELS;
201		h = VIDEO_VERTICAL_PIXELS;
202		break;
203	case SM_AF_TOP:
204		w = 360;
205		h = 240;
206		break;
207	case SM_AF_BOTTOM:
208		// Largest possible size with 3:2 aspect ratio and integer dimensions
209		w = 318;
210		h = 212;
211		break;
212	case SM_SF_TOP:
213	case SM_SF_BOTTOM:
214		w = screen_w;
215		h = screen_h;
216		break;
217	}
218
219	int x = (screen_w - w) / 2;
220	int y = (screen_h - h) / 2;
221
222	ctrAddRectScaled(color, x, y, w, h, 0, 0, VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS);
223}
224
225static void _drawFrame(struct GBAGUIRunner* runner, bool faded) {
226	UNUSED(runner);
227
228	void* outputBuffer = renderer.outputBuffer;
229	struct ctrTexture* tex = &gbaOutputTexture;
230
231	GSPGPU_FlushDataCache(NULL, outputBuffer, 256 * VIDEO_VERTICAL_PIXELS * 2);
232	GX_SetDisplayTransfer(NULL,
233			outputBuffer, GX_BUFFER_DIM(256, VIDEO_VERTICAL_PIXELS),
234			tex->data, GX_BUFFER_DIM(256, 256),
235			GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB565) |
236				GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB565) |
237				GX_TRANSFER_OUT_TILED(1) | GX_TRANSFER_FLIP_VERT(1));
238
239#if RESAMPLE_LIBRARY == RESAMPLE_BLIP_BUF
240	if (!hasSound) {
241		blip_clear(runner->context.gba->audio.left);
242		blip_clear(runner->context.gba->audio.right);
243	}
244#endif
245
246	gspWaitForPPF();
247	ctrActivateTexture(tex);
248	_drawTex(faded);
249}
250
251static void _drawScreenshot(struct GBAGUIRunner* runner, const uint32_t* pixels, bool faded) {
252	UNUSED(runner);
253
254	struct ctrTexture* tex = &gbaOutputTexture;
255
256	u16* newPixels = linearMemAlign(256 * VIDEO_VERTICAL_PIXELS * sizeof(u32), 0x100);
257
258	// Convert image from RGBX8 to BGR565
259	for (unsigned y = 0; y < VIDEO_VERTICAL_PIXELS; ++y) {
260		for (unsigned x = 0; x < VIDEO_HORIZONTAL_PIXELS; ++x) {
261			// 0xXXBBGGRR -> 0bRRRRRGGGGGGBBBBB
262			u32 p = *pixels++;
263			newPixels[y * 256 + x] =
264				(p << 24 >> (24 + 3) << 11) | // R
265				(p << 16 >> (24 + 2) <<  5) | // G
266				(p <<  8 >> (24 + 3) <<  0);  // B
267		}
268		memset(&newPixels[y * 256 + VIDEO_HORIZONTAL_PIXELS], 0, (256 - VIDEO_HORIZONTAL_PIXELS) * sizeof(u32));
269	}
270
271	GSPGPU_FlushDataCache(NULL, (void*)newPixels, 256 * VIDEO_VERTICAL_PIXELS * sizeof(u32));
272	GX_SetDisplayTransfer(NULL,
273			(void*)newPixels, GX_BUFFER_DIM(256, VIDEO_VERTICAL_PIXELS),
274			tex->data, GX_BUFFER_DIM(256, 256),
275			GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB565) |
276				GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB565) |
277				GX_TRANSFER_OUT_TILED(1) | GX_TRANSFER_FLIP_VERT(1));
278	gspWaitForPPF();
279	linearFree(newPixels);
280
281	ctrActivateTexture(tex);
282	_drawTex(faded);
283}
284
285static uint16_t _pollGameInput(struct GBAGUIRunner* runner) {
286	UNUSED(runner);
287
288	hidScanInput();
289	uint32_t activeKeys = hidKeysHeld() & 0xF00003FF;
290	activeKeys |= activeKeys >> 24;
291	return activeKeys;
292}
293
294static void _incrementScreenMode(struct GBAGUIRunner* runner) {
295	UNUSED(runner);
296	// Clear the buffer
297	_drawStart();
298	_drawEnd();
299	_drawStart();
300	_drawEnd();
301	unsigned mode;
302	if (GBAConfigGetUIntValue(&runner->context.config, "screenMode", &mode) && mode != screenMode) {
303		screenMode = mode;
304	} else {
305		screenMode = (screenMode + 1) % SM_MAX;
306		GBAConfigSetUIntValue(&runner->context.config, "screenMode", screenMode);
307	}
308}
309
310static uint32_t _pollInput(void) {
311	hidScanInput();
312	uint32_t keys = 0;
313	int activeKeys = hidKeysHeld();
314	if (activeKeys & KEY_X) {
315		keys |= 1 << GUI_INPUT_CANCEL;
316	}
317	if (activeKeys & KEY_Y) {
318		keys |= 1 << GBA_GUI_INPUT_SCREEN_MODE;
319	}
320	if (activeKeys & KEY_B) {
321		keys |= 1 << GUI_INPUT_BACK;
322	}
323	if (activeKeys & KEY_A) {
324		keys |= 1 << GUI_INPUT_SELECT;
325	}
326	if (activeKeys & KEY_LEFT) {
327		keys |= 1 << GUI_INPUT_LEFT;
328	}
329	if (activeKeys & KEY_RIGHT) {
330		keys |= 1 << GUI_INPUT_RIGHT;
331	}
332	if (activeKeys & KEY_UP) {
333		keys |= 1 << GUI_INPUT_UP;
334	}
335	if (activeKeys & KEY_DOWN) {
336		keys |= 1 << GUI_INPUT_DOWN;
337	}
338	if (activeKeys & KEY_CSTICK_UP) {
339		keys |= 1 << GBA_GUI_INPUT_INCREASE_BRIGHTNESS;
340	}
341	if (activeKeys & KEY_CSTICK_DOWN) {
342		keys |= 1 << GBA_GUI_INPUT_DECREASE_BRIGHTNESS;
343	}
344	return keys;
345}
346
347static enum GUICursorState _pollCursor(int* x, int* y) {
348	hidScanInput();
349	if (!(hidKeysHeld() & KEY_TOUCH)) {
350		return GUI_CURSOR_NOT_PRESENT;
351	}
352	touchPosition pos;
353	hidTouchRead(&pos);
354	*x = pos.px;
355	*y = pos.py;
356	return GUI_CURSOR_DOWN;
357}
358
359static void _sampleRotation(struct GBARotationSource* source) {
360	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
361	// Work around ctrulib getting the entries wrong
362	rotation->accel = *(accelVector*)& hidSharedMem[0x48];
363	rotation->gyro = *(angularRate*)& hidSharedMem[0x5C];
364}
365
366static int32_t _readTiltX(struct GBARotationSource* source) {
367	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
368	return rotation->accel.x << 18L;
369}
370
371static int32_t _readTiltY(struct GBARotationSource* source) {
372	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
373	return rotation->accel.y << 18L;
374}
375
376static int32_t _readGyroZ(struct GBARotationSource* source) {
377	struct GBA3DSRotationSource* rotation = (struct GBA3DSRotationSource*) source;
378	return rotation->gyro.y << 18L; // Yes, y
379}
380
381static void _postAudioBuffer(struct GBAAVStream* stream, struct GBAAudio* audio) {
382	UNUSED(stream);
383#if RESAMPLE_LIBRARY == RESAMPLE_BLIP_BUF
384	blip_read_samples(audio->left, &audioLeft[audioPos], AUDIO_SAMPLES, false);
385	blip_read_samples(audio->right, &audioRight[audioPos], AUDIO_SAMPLES, false);
386#elif RESAMPLE_LIBRARY == RESAMPLE_NN
387	GBAAudioCopy(audio, &audioLeft[audioPos], &audioRight[audioPos], AUDIO_SAMPLES);
388#endif
389	GSPGPU_FlushDataCache(0, (void*) &audioLeft[audioPos], AUDIO_SAMPLES * sizeof(int16_t));
390	GSPGPU_FlushDataCache(0, (void*) &audioRight[audioPos], AUDIO_SAMPLES * sizeof(int16_t));
391	audioPos = (audioPos + AUDIO_SAMPLES) % AUDIO_SAMPLE_BUFFER;
392	if (audioPos == AUDIO_SAMPLES * 3) {
393		u8 playing = 0;
394		csndIsPlaying(0x8, &playing);
395		if (!playing) {
396			CSND_SetPlayState(0x8, 1);
397			CSND_SetPlayState(0x9, 1);
398			csndExecCmds(false);
399		}
400	}
401}
402
403int main() {
404	ptmInit();
405	hasSound = !csndInit();
406
407	rotation.d.sample = _sampleRotation;
408	rotation.d.readTiltX = _readTiltX;
409	rotation.d.readTiltY = _readTiltY;
410	rotation.d.readGyroZ = _readGyroZ;
411
412	stream.postVideoFrame = 0;
413	stream.postAudioFrame = 0;
414	stream.postAudioBuffer = _postAudioBuffer;
415
416	if (!allocateRomBuffer()) {
417		return 1;
418	}
419
420	if (hasSound) {
421		audioLeft = linearMemAlign(AUDIO_SAMPLE_BUFFER * sizeof(int16_t), 0x80);
422		audioRight = linearMemAlign(AUDIO_SAMPLE_BUFFER * sizeof(int16_t), 0x80);
423	}
424
425	gfxInit(GSP_BGR8_OES, GSP_BGR8_OES, false);
426
427	if (ctrInitGpu() < 0) {
428		goto cleanup;
429	}
430
431	ctrTexture_Init(&gbaOutputTexture);
432	gbaOutputTexture.format = GPU_RGB565;
433	gbaOutputTexture.filter = GPU_LINEAR;
434	gbaOutputTexture.width = 256;
435	gbaOutputTexture.height = 256;
436	gbaOutputTexture.data = vramAlloc(256 * 256 * 2);
437	void* outputTextureEnd = (u8*)gbaOutputTexture.data + 256 * 256 * 2;
438
439	// Zero texture data to make sure no garbage around the border interferes with filtering
440	GX_SetMemoryFill(NULL,
441			gbaOutputTexture.data, 0x0000, outputTextureEnd, GX_FILL_16BIT_DEPTH | GX_FILL_TRIGGER,
442			NULL, 0, NULL, 0);
443	gspWaitForPSC0();
444
445	sdmcArchive = (FS_archive) {
446		ARCH_SDMC,
447		(FS_path) { PATH_EMPTY, 1, (const u8*)"" },
448		0, 0
449	};
450	FSUSER_OpenArchive(0, &sdmcArchive);
451
452	struct GUIFont* font = GUIFontCreate();
453
454	if (!font) {
455		goto cleanup;
456	}
457
458	struct GBAGUIRunner runner = {
459		.params = {
460			320, 240,
461			font, "/",
462			_drawStart, _drawEnd,
463			_pollInput, _pollCursor,
464			_batteryState,
465			_guiPrepare, _guiFinish,
466
467			GUI_PARAMS_TRAIL
468		},
469		.configExtra = (struct GUIMenuItem[]) {
470			{
471				.title = "Screen mode",
472				.data = "screenMode",
473				.submenu = 0,
474				.state = SM_PA_TOP,
475				.validStates = (const char*[]) {
476					"Pixel-Accurate/Bottom",
477					"Aspect-Ratio Fit/Bottom",
478					"Stretched/Bottom",
479					"Pixel-Accurate/Top",
480					"Aspect-Ratio Fit/Top",
481					"Stretched/Top",
482					0
483				}
484			}
485		},
486		.nConfigExtra = 1,
487		.setup = _setup,
488		.teardown = 0,
489		.gameLoaded = _gameLoaded,
490		.gameUnloaded = _gameUnloaded,
491		.prepareForFrame = 0,
492		.drawFrame = _drawFrame,
493		.drawScreenshot = _drawScreenshot,
494		.paused = _gameUnloaded,
495		.unpaused = _gameLoaded,
496		.incrementScreenMode = _incrementScreenMode,
497		.pollGameInput = _pollGameInput
498	};
499
500	GBAGUIInit(&runner, "3ds");
501	GBAGUIRunloop(&runner);
502	GBAGUIDeinit(&runner);
503
504cleanup:
505	linearFree(renderer.outputBuffer);
506
507	ctrDeinitGpu();
508	vramFree(gbaOutputTexture.data);
509
510	gfxExit();
511
512	if (hasSound) {
513		linearFree(audioLeft);
514		linearFree(audioRight);
515	}
516	csndExit();
517	ptmExit();
518	return 0;
519}