Use Emscripten’s shell-file feature
This commit is contained in:
parent
3ad106d9c5
commit
7550707562
@ -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:
|
||||
|
176
wasm/index-shell.html
Normal file
176
wasm/index-shell.html
Normal file
@ -0,0 +1,176 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"/>
|
||||
<title>SameBoy</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
border: 1px solid black;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
|
||||
.canvas-wrapper > canvas {
|
||||
border: 0px none;
|
||||
background-color: black;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.crisp {
|
||||
image-rendering: optimizeSpeed;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -o-crisp-edges;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
-ms-interpolation-mode: nearest-neighbor;
|
||||
}
|
||||
|
||||
#end {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* if an element has been placed after the end marker it is the CPU profiler */
|
||||
#end + div > div {
|
||||
margin-bottom: 10px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* if an element has been placed after the CPU profiler it is the memory profiler */
|
||||
#end + div + div > div {
|
||||
margin-bottom: 10px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ul id="demo-roms">
|
||||
<li><a href="ressources/demos/oh.gb">Oh!</a></li>
|
||||
<li><a href="ressources/demos/pocket.gb">Is that a demo in your pocket?</a></li>
|
||||
<li><a href="ressources/demos/gejmboj.gb">Gejmboj</a></li>
|
||||
<li><a href="ressources/demos/video.gbc">GBVP1</a></li>
|
||||
<li><a href="ressources/demos/mezase.gbc">GBVP2</a></li>
|
||||
<li><a href="ressources/demos/pht-mr.gbc">Mental Respirator</a></li>
|
||||
<li><a href="ressources/demos/pht-pz.gbc">It came from planet Zilog</a></li>
|
||||
</ul>
|
||||
<input type="file" id="file" name="file" accept=".gb,.gbc,.bin">
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="center">
|
||||
<div id="status">Downloading...</div>
|
||||
|
||||
<progress value="0" max="100" id="progress" hidden=1></progress>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<div class="canvas-wrapper">
|
||||
<canvas class="crisp" id="canvas" oncontextmenu="event.preventDefault()"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="center">
|
||||
<input type="checkbox" id="resize">Resize canvas
|
||||
<input type="checkbox" id="pointerLock" checked>Lock/hide mouse pointer
|
||||
|
||||
<input type="button" value="Fullscreen" onclick="Module.requestFullscreen(document.querySelector('#pointerLock').checked, document.querySelector('#resize').checked)">
|
||||
</div>
|
||||
|
||||
<script type='text/javascript'>
|
||||
const statusElement = document.querySelector('#status');
|
||||
const progressElement = document.querySelector('#progress');
|
||||
|
||||
var Module = {
|
||||
sync_fs: function () {
|
||||
console.log("Syncing file system ...");
|
||||
|
||||
FS.syncfs(function (err) {
|
||||
if (!err) {
|
||||
console.log("File system synchronized.")
|
||||
}
|
||||
else {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
preRun: [],
|
||||
postRun: [],
|
||||
print: function () {
|
||||
const text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.log(text);
|
||||
},
|
||||
printErr: function() {
|
||||
const text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.error(text);
|
||||
},
|
||||
canvas: (function() {
|
||||
const canvas = document.querySelector('#canvas');
|
||||
|
||||
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
|
||||
// application robust, you may want to override this behavior before shipping!
|
||||
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
|
||||
canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
|
||||
|
||||
return canvas;
|
||||
})(),
|
||||
setStatus: function (text) {
|
||||
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
|
||||
if (text === Module.setStatus.last.text) return;
|
||||
const m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
|
||||
const now = Date.now();
|
||||
if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
|
||||
Module.setStatus.last.time = now;
|
||||
Module.setStatus.last.text = text;
|
||||
if (m) {
|
||||
text = m[1];
|
||||
progressElement.value = parseInt(m[2])*100;
|
||||
progressElement.max = parseInt(m[4])*100;
|
||||
progressElement.hidden = false;
|
||||
}
|
||||
else {
|
||||
progressElement.value = null;
|
||||
progressElement.max = null;
|
||||
progressElement.hidden = true;
|
||||
}
|
||||
statusElement.innerHTML = text;
|
||||
},
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: function (left) {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
|
||||
}
|
||||
};
|
||||
|
||||
Module.setStatus('Downloading...');
|
||||
|
||||
window.onerror = function() {
|
||||
Module.setStatus('Exception thrown, see JavaScript console');
|
||||
|
||||
Module.setStatus = function(text) {
|
||||
if (text) Module.printErr('[post-exception status] ' + text);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
{{{ SCRIPT }}}
|
||||
|
||||
<a href="#bottom" id="end"></a>
|
||||
</body>
|
||||
</html>
|
197
wasm/main.c
197
wasm/main.c
@ -1,62 +1,20 @@
|
||||
#include <emscripten.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_opengl.h>
|
||||
#include <SDL2/SDL_video.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <Core/gb.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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
49
wasm/main.h
Normal file
49
wasm/main.h
Normal file
@ -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 */
|
150
wasm/main.js
Normal file
150
wasm/main.js
Normal file
@ -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);
|
||||
}
|
BIN
wasm/ressources/demos/gejmboj.gb
Normal file
BIN
wasm/ressources/demos/gejmboj.gb
Normal file
Binary file not shown.
BIN
wasm/ressources/demos/mezase.gbc
Normal file
BIN
wasm/ressources/demos/mezase.gbc
Normal file
Binary file not shown.
BIN
wasm/ressources/demos/oh.gb
Normal file
BIN
wasm/ressources/demos/oh.gb
Normal file
Binary file not shown.
BIN
wasm/ressources/demos/pht-mr.gbc
Normal file
BIN
wasm/ressources/demos/pht-mr.gbc
Normal file
Binary file not shown.
BIN
wasm/ressources/demos/pht-pz.gbc
Normal file
BIN
wasm/ressources/demos/pht-pz.gbc
Normal file
Binary file not shown.
BIN
wasm/ressources/demos/pocket.gb
Normal file
BIN
wasm/ressources/demos/pocket.gb
Normal file
Binary file not shown.
BIN
wasm/ressources/demos/video.gbc
Normal file
BIN
wasm/ressources/demos/video.gbc
Normal file
Binary file not shown.
59
wasm/utils.c
Normal file
59
wasm/utils.c
Normal file
@ -0,0 +1,59 @@
|
||||
#include <SDL2/SDL.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
10
wasm/utils.h
Normal file
10
wasm/utils.h
Normal file
@ -0,0 +1,10 @@
|
||||
#ifndef utils_h
|
||||
#define utils_h
|
||||
#include <stddef.h>
|
||||
|
||||
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 */
|
@ -1,59 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>SameBoy</title>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script>
|
||||
var Module = {
|
||||
canvas: document.querySelector('#canvas')
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="SameBoy.js"></script>
|
||||
<script>
|
||||
const loadROM = f => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (file => {
|
||||
return e => {
|
||||
Module.load_rom(romName, e.target.result);
|
||||
};
|
||||
})(f);
|
||||
|
||||
reader.readAsArrayBuffer(f);
|
||||
}
|
||||
|
||||
const handleFileSelect = (evt, files) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
|
||||
if (files.length) {
|
||||
load_rom(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 = _ => {
|
||||
console.log(Module)
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user