SameBoy/wasm/main.c

397 lines
11 KiB
C

#include <emscripten.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_opengl.h>
#include <SDL2/SDL_video.h>
#include <stdbool.h>
#include <stdio.h>
#include <Core/gb.h>
#include "main.h"
#include "utils.h"
GB_gameboy_t gb;
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Surface *screen;
SDL_Texture *texture;
SDL_PixelFormat *pixel_format;
SDL_AudioDeviceID device_id;
static SDL_AudioSpec want_aspec, have_aspec;
static uint32_t pixel_buffer_1[256 * 224], pixel_buffer_2[256 * 224];
static uint32_t *active_pixel_buffer = pixel_buffer_1;
static uint32_t *previous_pixel_buffer = pixel_buffer_2;
static char *battery_save_path_ptr;
configuration_t configuration =
{
.keys = {
SDL_SCANCODE_RIGHT,
SDL_SCANCODE_LEFT,
SDL_SCANCODE_UP,
SDL_SCANCODE_DOWN,
SDL_SCANCODE_X,
SDL_SCANCODE_Z,
SDL_SCANCODE_BACKSPACE,
SDL_SCANCODE_RETURN,
SDL_SCANCODE_SPACE
},
.keys_2 = {
SDL_SCANCODE_TAB,
SDL_SCANCODE_LSHIFT,
},
.joypad_configuration = {
13,
14,
11,
12,
0,
1,
9,
8,
10,
4,
-1,
5,
},
.joypad_axises = {
0,
1,
},
.color_correction_mode = GB_COLOR_CORRECTION_EMULATE_HARDWARE,
.highpass_mode = GB_HIGHPASS_ACCURATE,
.blend_frames = true,
.rewind_length = 60 * 2,
.model = MODEL_CGB
};
// Use this function instead of GB_save_battery()
int save_battery(GB_gameboy_t *gb, const char *path) {
int result = GB_save_battery(gb, path);
fprintf(stderr, "Saving battery: \"%s\": %d\n", path, result);
EM_ASM(Module.sync_fs());
return result;
}
unsigned query_sample_rate_of_audiocontexts() {
return EM_ASM_INT({
if (!Module.SDL2 || !Module.SDL2.audioContext) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioContext();
const sr = ctx.sampleRate;
ctx.close();
return sr;
}
return Module.SDL2.audioContext.sampleRate;
});
}
static void audio_callback(void *gb, Uint8 *stream, int len)
{
if (GB_is_inited(gb)) {
GB_apu_copy_buffer(gb, (GB_sample_t *) stream, len / sizeof(GB_sample_t));
}
else {
memset(stream, 0, len);
}
}
void render_texture(void *pixels, void *previous)
{
if (renderer) {
if (pixels) {
SDL_UpdateTexture(texture, NULL, pixels, 160 * sizeof (uint32_t));
}
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
/*else {
static void *_pixels = NULL;
if (pixels) {
_pixels = pixels;
}
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
render_bitmap_with_shader(&shader, _pixels, previous,
GB_get_screen_width(&gb), GB_get_screen_height(&gb),
rect.x, rect.y, rect.w, rect.h);
SDL_GL_SwapWindow(window);
}*/
}
static void handle_events(GB_gameboy_t *gb) {
GB_set_key_state(gb, GB_KEY_START, true);
}
static void vblank(GB_gameboy_t *gb) {
if (configuration.blend_frames) {
render_texture(active_pixel_buffer, previous_pixel_buffer);
uint32_t *temp = active_pixel_buffer;
active_pixel_buffer = previous_pixel_buffer;
previous_pixel_buffer = temp;
GB_set_pixels_output(gb, active_pixel_buffer);
}
else {
render_texture(active_pixel_buffer, NULL);
}
handle_events(gb);
}
static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
{
return SDL_MapRGB(pixel_format, r, g, b);
}
void init_gb() {
GB_model_t model;
model = (GB_model_t [])
{
[MODEL_DMG] = GB_MODEL_DMG_B,
[MODEL_CGB] = GB_MODEL_CGB_E,
[MODEL_AGB] = GB_MODEL_AGB,
[MODEL_SGB] = (GB_model_t [])
{
[SGB_NTSC] = GB_MODEL_SGB_NTSC,
[SGB_PAL] = GB_MODEL_SGB_PAL,
[SGB_2] = GB_MODEL_SGB2,
}[configuration.sgb_revision],
}[configuration.model];
fprintf(stderr, "Initializing ...\n");
if (GB_is_inited(&gb)) {
fprintf(stderr, "Already initialized, switching model ...\n");
GB_switch_model_and_reset(&gb, model);
}
else {
fprintf(stderr, "Initializing new GB ...\n");
GB_init(&gb, model);
GB_set_input_callback(&gb, NULL);
GB_set_async_input_callback(&gb, NULL);
GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank);
GB_set_pixels_output(&gb, active_pixel_buffer);
GB_set_rgb_encode_callback(&gb, rgb_encode);
GB_set_sample_rate(&gb, have_aspec.freq);
GB_set_color_correction_mode(&gb, configuration.color_correction_mode);
GB_set_highpass_filter_mode(&gb, configuration.highpass_mode);
GB_set_rewind_length(&gb, 0);
}
SDL_DestroyTexture(texture);
texture = SDL_CreateTexture(
renderer,
SDL_GetWindowPixelFormat(window),
SDL_TEXTUREACCESS_STREAMING,
GB_get_screen_width(&gb),
GB_get_screen_height(&gb)
);
SDL_SetWindowMinimumSize(window, GB_get_screen_width(&gb), GB_get_screen_height(&gb));
bool error = false;
const char * const boot_roms[] = {
"dmg_boot.bin",
"cgb_boot.bin",
"agb_boot.bin",
"sgb_boot.bin"
};
const char *boot_rom = boot_roms[configuration.model];
if (configuration.model == GB_MODEL_SGB && configuration.sgb_revision == SGB_2) {
boot_rom = "sgb2_boot.bin";
}
const char *boot_rom_path = resource_path(concat("BootROMs/", boot_rom));
fprintf(stderr, "Loading boot ROM: %s\n", boot_rom_path);
error = GB_load_boot_rom(&gb, boot_rom_path);
}
int EMSCRIPTEN_KEEPALIVE init() {
#define str(x) #x
#define xstr(x) str(x)
pixel_format = (SDL_PixelFormat *) malloc(sizeof(SDL_PixelFormat));
if (!pixel_format) {
fprintf(stderr, "Failed to allocate memory\n");
return EXIT_FAILURE;
}
// emscripten_sample_gamepad_data();
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) {
fprintf(stderr, "SDL_Init Error: %s\n", SDL_GetError());
return EXIT_FAILURE;
}
fprintf(stderr, "SameBoy v" xstr(VERSION) "\n");
window = SDL_CreateWindow(
"SameBoy v" xstr(VERSION),
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
VIDEO_WIDTH,
VIDEO_HEIGHT,
SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS | SDL_WINDOW_ALLOW_HIGHDPI
);
if (!window) {
printf("Could not create window: %s\n", SDL_GetError());
return EXIT_FAILURE;
}
SDL_SetWindowMinimumSize(window, VIDEO_WIDTH, VIDEO_HEIGHT);
SDL_SetWindowMaximumSize(window, VIDEO_WIDTH, VIDEO_HEIGHT);
renderer = SDL_CreateRenderer(
window,
-1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
);
if (!renderer) {
fprintf(stderr, "SDL_CreateRenderer Error: %s\n", SDL_GetError());
return EXIT_FAILURE;
}
screen = SDL_CreateRGBSurface(
0,
VIDEO_WIDTH,
VIDEO_HEIGHT,
32,
0, 0, 0, 0
);
if (!screen) {
SDL_Log("SDL_CreateRGBSurface() failed: %s", SDL_GetError());
exit(1);
}
pixel_format = screen->format;
texture = SDL_CreateTextureFromSurface(renderer, screen);
if (!texture) {
fprintf(stderr, "SDL_CreateTextureFromSurface Error: %s\n", SDL_GetError());
return EXIT_FAILURE;
}
unsigned audio_sample_rate = query_sample_rate_of_audiocontexts();
fprintf(stderr, "Sample rate: %u\n", audio_sample_rate);
memset(&want_aspec, 0, sizeof(want_aspec));
want_aspec.freq = audio_sample_rate;
want_aspec.format = AUDIO_S16SYS;
want_aspec.channels = 2;
want_aspec.samples = 2048;
want_aspec.callback = audio_callback;
want_aspec.userdata = &gb;
device_id = SDL_OpenAudioDevice(NULL, 0, &want_aspec, &have_aspec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
if (device_id == 0) {
fprintf(stderr, "Failed to open audio: %s", SDL_GetError());
}
fprintf(stderr, "WANT:\nfreq: %d\nchannels: %d\nsilence: %d\nsamples: %d\nsize: %d\nformat: %d\n", want_aspec.freq, want_aspec.channels, want_aspec.silence, want_aspec.samples, want_aspec.size, want_aspec.format);
fprintf(stderr, "HAVE:\nfreq: %d\nchannels: %d\nsilence: %d\nsamples: %d\nsize: %d\nformat: %d\n", have_aspec.freq, have_aspec.channels, have_aspec.silence, have_aspec.samples, have_aspec.size, have_aspec.format);
EM_ASM({
function audio_workaround(e) {
if (!Module.SDL2 || !Module.SDL2.audioContext || !Module.SDL2.audioContext.resume) return;
console.log('Applying audio workarounds...');
if (Module.SDL2.audioContext.state == 'suspended') {
Module.SDL2.audioContext.resume();
}
if (Module.SDL2.audioContext.state == 'running') {
document.removeEventListener('touchstart', audio_workaround);
document.removeEventListener('click', audio_workaround);
document.removeEventListener('keydown', audio_workaround);
if (Module.canvas) {
Module.canvas.removeEventListener('touchstart', audio_workaround);
Module.canvas.removeEventListener('click', audio_workaround);
Module.canvas.removeEventListener('keydown', audio_workaround);
}
}
else if (Module.SDL2.audioContext && Module.SDL2.audioContext.currentTime == 0) {
// unlock audio for iOS
let buffer = Module.SDL2.audioContext.createBuffer(1, 1, 22050);
let source = Module.SDL2.audioContext.createBufferSource();
source.buffer = buffer;
source.connect(Module.SDL2.audioContext.destination);
source.start(0);
}
}
document.addEventListener('touchstart', audio_workaround);
document.addEventListener('click', audio_workaround);
document.addEventListener('keydown', audio_workaround);
if (Module.canvas) {
Module.canvas.addEventListener('touchstart', audio_workaround);
Module.canvas.addEventListener('click', audio_workaround);
Module.canvas.addEventListener('keydown', audio_workaround);
}
audio_workaround();
});
init_gb();
SDL_PauseAudioDevice(device_id, 0);
return EXIT_SUCCESS;
}
int EMSCRIPTEN_KEEPALIVE load_boot_rom_from_file(char* filename) {
return GB_load_boot_rom(&gb, filename);
}
int EMSCRIPTEN_KEEPALIVE load_rom_from_file(char* filename, char* battery_save_path) {
int result = GB_load_rom(&gb, filename);
if (result == 0) {
battery_save_path_ptr = battery_save_path;
GB_load_battery(&gb, battery_save_path);
save_battery(&gb, battery_save_path_ptr);
}
return result;
}
void EMSCRIPTEN_KEEPALIVE quit() {
fprintf(stderr, "Quitting ...\n");
emscripten_set_main_loop(NULL, 0, false);
GB_free(&gb);
SDL_FreeSurface(screen);
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
void EMSCRIPTEN_KEEPALIVE run_frame() {
GB_run_frame(&gb);
}