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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
-
-
-
-
-
-
-
-
-