diff --git a/wasm/Makefile b/wasm/Makefile index 81714db..ce0e44b 100644 --- a/wasm/Makefile +++ b/wasm/Makefile @@ -51,15 +51,14 @@ endif ifeq ($(CONF),native_release) override CONF := release -LDFLAGS += -march=native -mtune=native -CFLAGS += -march=native -mtune=native endif # Set compilation and linkage flags based on target, platform and configuration CFLAGS += -Werror -Wall -Wno-strict-aliasing -Wno-unknown-warning -Wno-unknown-warning-option -Wno-multichar -Wno-int-in-bool-context -std=gnu11 -D_GNU_SOURCE -DVERSION="$(VERSION)" -I. -D_USE_MATH_DEFINES +# CFLAGS += -DGB_INTERNAL=1 # get access to internal APIs CFLAGS += -I$(CORE_DIR) -CFLAGS += -s WASM=1 -s USE_SDL=2 --preload-file $(BOOTROMS_DIR)@/BootROMs +CFLAGS += -s WASM=1 -s USE_SDL=2 --preload-file $(BOOTROMS_DIR)@/BootROMs -s "EXTRA_EXPORTED_RUNTIME_METHODS=['FS']" # CFLAGS += -Wcast-align -Wover-aligned -s SAFE_HEAP=1 -s WARN_UNALIGNED=1 WASM_LDFLAGS := @@ -67,9 +66,11 @@ LDFLAGS += -lc -lm -ldl CFLAGS += -Wno-deprecated-declarations ifeq ($(CONF),debug) -CFLAGS += -g4 --profiling-funcs +CFLAGS += -g -g4 +CFLAGS += --cpuprofiler --memoryprofiler else ifeq ($(CONF), release) -CFLAGS += -O3 -DNDEBUG +CFLAGS += -O3 -DNDEBUG --emit-symbol-map +CFLAGS += --llvm-lto 3 # might be unstable else $(error Invalid value for CONF: $(CONF). Use "debug", "release" or "native_release") endif @@ -82,9 +83,6 @@ bootroms: $(BOOTROMS_DIR)/agb_boot.bin \ $(BOOTROMS_DIR)/sgb_boot.bin \ $(BOOTROMS_DIR)/sgb2_boot.bin -wasm: bootroms $(BIN)/SameBoy.js -all: wasm - # Get a list of our source files and their respective object file targets CORE_SOURCES_RAW := $(shell ls $(CORE_DIR)/Core/*.c) @@ -94,6 +92,12 @@ CORE_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(CORE_SOURCES)) WASM_SOURCES := $(shell ls *.c) WASM_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(WASM_SOURCES)) +WEB_SOURCES := $(shell ls ressources/.) +WEB_OBJECTS := $(patsubst %,$(BIN)/ressources/%,$(WEB_SOURCES)) + +wasm: bootroms $(BIN)/index.html $(WEB_OBJECTS) +all: wasm + # Automatic dependency generation ifneq ($(filter-out clean %.bin, $(MAKECMDGOALS)),) @@ -120,12 +124,21 @@ $(OBJ)/%.c.o: %.c -@$(MKDIR) -p $(dir $@) $(CC) $(CFLAGS) -c $< -o $@ -$(BIN)/SameBoy.js: $(CORE_OBJECTS) $(WASM_OBJECTS) +$(BIN)/ressources/%: -@$(MKDIR) -p $(dir $@) - cp -r web/* $(BIN) - $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(WASM_LDFLAGS) + cp -a $(patsubst $(BIN)/%,%,$@) $@ + +$(BIN)/index.html: $(CORE_OBJECTS) $(WASM_OBJECTS) index-shell.html main.js + -@$(MKDIR) -p $(dir $@) + $(CC) \ + $(CFLAGS) \ + $(filter %.o, $^) \ + -o $@ \ + --shell-file "index-shell.html" \ + --post-js "main.js" \ + $(LDFLAGS) \ + $(WASM_LDFLAGS) ifeq ($(CONF), release) - strip $@ endif $(CORE_DIR)/build/bin/BootROMs/%_boot.bin: diff --git a/wasm/index-shell.html b/wasm/index-shell.html new file mode 100644 index 0000000..4bc341c --- /dev/null +++ b/wasm/index-shell.html @@ -0,0 +1,176 @@ + + + + + + SameBoy + + + + + + +
+ +
+
Downloading...
+ + +
+ +
+
+ +
+
+ +
+ +
+ Resize canvas + Lock/hide mouse pointer +     + +
+ + + + {{{ SCRIPT }}} + + + + \ No newline at end of file diff --git a/wasm/main.c b/wasm/main.c index 7986e3e..7d59c2a 100644 --- a/wasm/main.c +++ b/wasm/main.c @@ -1,62 +1,20 @@ #include #include +#include +#include #include #include -#include + #include - -const char *resource_folder(void) -{ -#ifdef DATA_DIR - return DATA_DIR; -#else - static const char *ret = NULL; - if (!ret) { - ret = SDL_GetBasePath(); - if (!ret) { - ret = "./"; - } - } - return ret; -#endif -} - -char *resource_path(const char *filename) -{ - static char path[1024]; - snprintf(path, sizeof(path), "%s%s", resource_folder(), filename); - return path; -} - -char* concat(const char *s1, const char *s2) -{ - char *result = malloc(strlen(s1) + strlen(s2) + 1); // +1 for the null-terminator - - if (!result) { - fprintf(stderr, "Failed to allocate memory\n"); - exit(EXIT_FAILURE); - } - - strcpy(result, s1); - strcat(result, s2); - return result; -} +#include "main.h" +#include "utils.h" GB_gameboy_t gb; -#define VIDEO_WIDTH 160 -#define VIDEO_HEIGHT 144 -#define VIDEO_PIXELS (VIDEO_WIDTH * VIDEO_HEIGHT) - -#define SGB_VIDEO_WIDTH 256 -#define SGB_VIDEO_HEIGHT 224 -#define SGB_VIDEO_PIXELS (SGB_VIDEO_WIDTH * SGB_VIDEO_HEIGHT) - -#define FRAME_RATE 0 // let the browser schedule (usually 60 FPS), if absolutely needed define as (0x400000 / 70224.0) - SDL_Window *window; SDL_Renderer *renderer; +SDL_Surface *screen; SDL_Texture *texture; SDL_PixelFormat *pixel_format; SDL_AudioDeviceID device_id; @@ -65,45 +23,7 @@ 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; - -signed short soundbuf[1024 * 2]; - -typedef enum { - JOYPAD_AXISES_X, - JOYPAD_AXISES_Y, - JOYPAD_AXISES_MAX -} joypad_axis_t; - -typedef struct { - SDL_Scancode keys[9]; - GB_color_correction_mode_t color_correction_mode; - bool blend_frames; - - GB_highpass_mode_t highpass_mode; - - char filter[32]; - enum { - MODEL_DMG, - MODEL_CGB, - MODEL_AGB, - MODEL_SGB, - MODEL_MAX, - } model; - - /* v0.11 */ - uint32_t rewind_length; - SDL_Scancode keys_2[32]; /* Rewind and underclock, + padding for the future */ - uint8_t joypad_configuration[32]; /* 12 Keys + padding for the future*/; - uint8_t joypad_axises[JOYPAD_AXISES_MAX]; - - /* v0.12 */ - enum { - SGB_NTSC, - SGB_PAL, - SGB_2, - SGB_MAX - } sgb_revision; -} configuration_t; +static char *battery_save_path_ptr; configuration_t configuration = { @@ -147,11 +67,22 @@ configuration_t configuration = .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({ - var AudioContext = window.AudioContext || window.webkitAudioContext; - var ctx = new AudioContext(); - var sr = ctx.sampleRate; + const AudioContext = window.AudioContext || window.webkitAudioContext; + const ctx = new AudioContext(); + const sr = ctx.sampleRate; ctx.close(); return sr; }); @@ -177,6 +108,18 @@ void render_texture(void *pixels, void *previous) 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) { @@ -184,7 +127,7 @@ static void handle_events(GB_gameboy_t *gb) { } static void vblank(GB_gameboy_t *gb) { - if (1 == 0) { + if (configuration.blend_frames) { render_texture(active_pixel_buffer, previous_pixel_buffer); uint32_t *temp = active_pixel_buffer; active_pixel_buffer = previous_pixel_buffer; @@ -203,7 +146,7 @@ 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() { +void init_gb() { GB_model_t model; model = (GB_model_t []) @@ -273,13 +216,8 @@ void init() { error = GB_load_boot_rom(&gb, boot_rom_path); } -void run() { - GB_run_frame(&gb); -} - -int main(int argc, char **argv) -{ -#define str(x) #x +int EMSCRIPTEN_KEEPALIVE init() { + #define str(x) #x #define xstr(x) str(x) pixel_format = (SDL_PixelFormat *) malloc(sizeof(SDL_PixelFormat)); @@ -303,7 +241,7 @@ int main(int argc, char **argv) SDL_WINDOWPOS_UNDEFINED, VIDEO_WIDTH, VIDEO_HEIGHT, - SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI + SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS | SDL_WINDOW_ALLOW_HIGHDPI ); if (!window) { @@ -312,6 +250,7 @@ int main(int argc, char **argv) } SDL_SetWindowMinimumSize(window, VIDEO_WIDTH, VIDEO_HEIGHT); + SDL_SetWindowMaximumSize(window, VIDEO_WIDTH, VIDEO_HEIGHT); renderer = SDL_CreateRenderer( window, @@ -324,7 +263,7 @@ int main(int argc, char **argv) return EXIT_FAILURE; } - SDL_Surface *screen = SDL_CreateRGBSurface( + screen = SDL_CreateRGBSurface( 0, VIDEO_WIDTH, VIDEO_HEIGHT, @@ -363,23 +302,63 @@ int main(int argc, char **argv) fprintf(stderr, "Failed to open audio: %s", SDL_GetError()); } - init(); + EM_ASM({ + var AudioContext = window.AudioContext || window.webkitAudioContext; + var ctx = new AudioContext(); + + // unlock audio for iOS + if (ctx && ctx.currentTime == 0) { + var buffer = ctx.createBuffer(1, 1, 22050); + var source = ctx.createBufferSource(); + source.buffer = buffer; + source.connect(ctx.destination); + source.start(0); + } + + // Google audio enable work-around: + // https://github.com/emscripten-ports/SDL2/issues/57 + try { + if (!Module.SDL2 || !ctx || !ctx.resume) return; + ctx.resume(); + } catch (err) {} + + ctx.close(); + }); + + init_gb(); SDL_PauseAudioDevice(device_id, 0); - emscripten_set_main_loop( - run, // our main loop - FRAME_RATE, - true // infinite loop - ); + return EXIT_SUCCESS; +} +int EMSCRIPTEN_KEEPALIVE load_rom(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(); - - return EXIT_SUCCESS; +} + +void EMSCRIPTEN_KEEPALIVE run_frame() { + GB_run_frame(&gb); } diff --git a/wasm/main.h b/wasm/main.h new file mode 100644 index 0000000..6161ff4 --- /dev/null +++ b/wasm/main.h @@ -0,0 +1,49 @@ +#ifndef main_h +#define main_h + +#define VIDEO_WIDTH 160 +#define VIDEO_HEIGHT 144 +#define VIDEO_PIXELS (VIDEO_WIDTH * VIDEO_HEIGHT) + +#define SGB_VIDEO_WIDTH 256 +#define SGB_VIDEO_HEIGHT 224 +#define SGB_VIDEO_PIXELS (SGB_VIDEO_WIDTH * SGB_VIDEO_HEIGHT) + +typedef enum { + JOYPAD_AXISES_X, + JOYPAD_AXISES_Y, + JOYPAD_AXISES_MAX +} joypad_axis_t; + +typedef struct { + SDL_Scancode keys[9]; + GB_color_correction_mode_t color_correction_mode; + bool blend_frames; + + GB_highpass_mode_t highpass_mode; + + char filter[32]; + enum { + MODEL_DMG, + MODEL_CGB, + MODEL_AGB, + MODEL_SGB, + MODEL_MAX, + } model; + + /* v0.11 */ + uint32_t rewind_length; + SDL_Scancode keys_2[32]; /* Rewind and underclock, + padding for the future */ + uint8_t joypad_configuration[32]; /* 12 Keys + padding for the future*/; + uint8_t joypad_axises[JOYPAD_AXISES_MAX]; + + /* v0.12 */ + enum { + SGB_NTSC, + SGB_PAL, + SGB_2, + SGB_MAX + } sgb_revision; +} configuration_t; + +#endif /* main_h */ diff --git a/wasm/main.js b/wasm/main.js new file mode 100644 index 0000000..7c52926 --- /dev/null +++ b/wasm/main.js @@ -0,0 +1,150 @@ +const frame_rate = (0x400000 / 70224.0); +const ms_per_frame = 1000 / frame_rate; +let last_frame_time = 0; + +const stringHash = str => { + let hash = 0; + + if (str.length === 0) return hash; + + for (let i = 0; i < str.length; i++) { + let chr = str.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + + return hash; +} + +const run_frame = time => { + window.requestAnimationFrame(run_frame); + + const delta = time - last_frame_time; + + if (delta > ms_per_frame) { + Module._run_frame(); + + last_frame_time = time - (delta % ms_per_frame); + } +} + +const loadRomFromMemory = (name, data) => { + const pos = name.lastIndexOf('.'); + const battery_name = name.substr(0, pos < 0 ? name.length : pos) + '.sav'; + + try { + // try to create the virtual ROM folder + FS.mkdir('/rom'); + } catch (e) { } + + try { + // try to delete all previous ROM files + for (let file of FS.readdir('/rom').filter(f => f != '.' && f != '..')) { + FS.unlink(`/rom/${file}`) + } + } catch (e) { } + + // create a new virtual file from memory + Module['FS_createDataFile']('/rom/', name, new Uint8Array(data), true, true); + + const rom_path = allocate(intArrayFromString(`/rom/${name}`), 'i8', ALLOC_NORMAL); + const battery_path = allocate(intArrayFromString(`/persist/${battery_name}`), 'i8', ALLOC_NORMAL); + + Module._load_rom(rom_path, battery_path); + + // The ROM has been read into memory, we can unlink the file now + FS.unlink(`/rom/${name}`) + + window.requestAnimationFrame(run_frame) +} + +const loadROM = f => { + const reader = new FileReader(); + + reader.onload = (file => { + return event => { + loadRomFromMemory(file.name, event.target.result) + }; + })(f); + + reader.readAsArrayBuffer(f); +} + +const loadRemoteRom = url => { + const request = new Request(url); + + const name = (_ => { + const name = url.substring(url.lastIndexOf('/') + 1); + + if (name.endsWith('.gb') || name.endsWith('.gbc')) { + return name + } + else if (name.length) { + return `${name}.gb` + } + + return stringHash(url) + })() + + return fetch(request).then(response => { + if (!response.ok) { + throw new Error('HTTP error, status = ' + response.status); + } + + return response.arrayBuffer(); + }).then(buf => { + loadRomFromMemory(name, buf) + }) +} + +const handleFileSelect = (evt, files) => { + evt.stopPropagation(); + evt.preventDefault(); + + if (files.length) { + loadROM(files[0]); + } +} + +const handleDragOver = evt => { + evt.stopPropagation(); + evt.preventDefault(); + evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy. +} + +window.addEventListener('dragover', handleDragOver, false); + +window.addEventListener('drop', e => { + handleFileSelect(e, e.dataTransfer.files); +}, false); + +document.getElementById('file').addEventListener('change', e => { + handleFileSelect(e, e.target.files); +}, false); + +Module.onRuntimeInitialized = _ => { + FS.mkdir('/persist'); + FS.mount(IDBFS, { }, '/persist'); + + FS.syncfs(true, function (err) { + if (!err) { + console.log('Successfully loaded FS from persistent storage') + } + else { + console.error(err) + } + + // Call the exported init function + Module._init(); + }) +}; + +const romClickHandler = event => { + event.stopPropagation(); + event.preventDefault(); + loadRemoteRom(event.target.href); +} + +for (const anchor of document.querySelectorAll('#demo-roms a')) { + anchor.addEventListener('click', romClickHandler); +} \ No newline at end of file diff --git a/wasm/ressources/demos/gejmboj.gb b/wasm/ressources/demos/gejmboj.gb new file mode 100644 index 0000000..474bf5c Binary files /dev/null and b/wasm/ressources/demos/gejmboj.gb differ diff --git a/wasm/ressources/demos/mezase.gbc b/wasm/ressources/demos/mezase.gbc new file mode 100644 index 0000000..52b80b0 Binary files /dev/null and b/wasm/ressources/demos/mezase.gbc differ diff --git a/wasm/ressources/demos/oh.gb b/wasm/ressources/demos/oh.gb new file mode 100644 index 0000000..94ab594 Binary files /dev/null and b/wasm/ressources/demos/oh.gb differ diff --git a/wasm/ressources/demos/pht-mr.gbc b/wasm/ressources/demos/pht-mr.gbc new file mode 100644 index 0000000..5ba90f2 Binary files /dev/null and b/wasm/ressources/demos/pht-mr.gbc differ diff --git a/wasm/ressources/demos/pht-pz.gbc b/wasm/ressources/demos/pht-pz.gbc new file mode 100644 index 0000000..23cea2e Binary files /dev/null and b/wasm/ressources/demos/pht-pz.gbc differ diff --git a/wasm/ressources/demos/pocket.gb b/wasm/ressources/demos/pocket.gb new file mode 100644 index 0000000..382ed3e Binary files /dev/null and b/wasm/ressources/demos/pocket.gb differ diff --git a/wasm/ressources/demos/video.gbc b/wasm/ressources/demos/video.gbc new file mode 100644 index 0000000..b88cfe8 Binary files /dev/null and b/wasm/ressources/demos/video.gbc differ diff --git a/wasm/utils.c b/wasm/utils.c new file mode 100644 index 0000000..a8799e2 --- /dev/null +++ b/wasm/utils.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include "utils.h" + +const char *resource_folder(void) +{ +#ifdef DATA_DIR + return DATA_DIR; +#else + static const char *ret = NULL; + if (!ret) { + ret = SDL_GetBasePath(); + if (!ret) { + ret = "./"; + } + } + return ret; +#endif +} + +char *resource_path(const char *filename) +{ + static char path[1024]; + snprintf(path, sizeof(path), "%s%s", resource_folder(), filename); + return path; +} + +void replace_extension(const char *src, size_t length, char *dest, const char *ext) +{ + memcpy(dest, src, length); + dest[length] = 0; + + /* Remove extension */ + for (size_t i = length; i--;) { + if (dest[i] == '/') break; + if (dest[i] == '.') { + dest[i] = 0; + break; + } + } + + /* Add new extension */ + strcat(dest, ext); +} + +char* concat(const char *s1, const char *s2) +{ + char *result = malloc(strlen(s1) + strlen(s2) + 1); // +1 for the null-terminator + + if (!result) { + fprintf(stderr, "Failed to allocate memory\n"); + exit(EXIT_FAILURE); + } + + strcpy(result, s1); + strcat(result, s2); + return result; +} diff --git a/wasm/utils.h b/wasm/utils.h new file mode 100644 index 0000000..930d8aa --- /dev/null +++ b/wasm/utils.h @@ -0,0 +1,10 @@ +#ifndef utils_h +#define utils_h +#include + +const char *resource_folder(void); +char *resource_path(const char *filename); +void replace_extension(const char *src, size_t length, char *dest, const char *ext); +char* concat(const char *s1, const char *s2); + +#endif /* utils_h */ diff --git a/wasm/web/index.html b/wasm/web/index.html deleted file mode 100644 index 9449e7a..0000000 --- a/wasm/web/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - SameBoy - - - - - - - - -