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