Use Emscripten’s shell-file feature
This commit is contained in:
parent
3ad106d9c5
commit
7550707562
@ -51,15 +51,14 @@ endif
|
|||||||
|
|
||||||
ifeq ($(CONF),native_release)
|
ifeq ($(CONF),native_release)
|
||||||
override CONF := release
|
override CONF := release
|
||||||
LDFLAGS += -march=native -mtune=native
|
|
||||||
CFLAGS += -march=native -mtune=native
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Set compilation and linkage flags based on target, platform and configuration
|
# 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 += -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 += -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
|
# CFLAGS += -Wcast-align -Wover-aligned -s SAFE_HEAP=1 -s WARN_UNALIGNED=1
|
||||||
WASM_LDFLAGS :=
|
WASM_LDFLAGS :=
|
||||||
|
|
||||||
@ -67,9 +66,11 @@ LDFLAGS += -lc -lm -ldl
|
|||||||
CFLAGS += -Wno-deprecated-declarations
|
CFLAGS += -Wno-deprecated-declarations
|
||||||
|
|
||||||
ifeq ($(CONF),debug)
|
ifeq ($(CONF),debug)
|
||||||
CFLAGS += -g4 --profiling-funcs
|
CFLAGS += -g -g4
|
||||||
|
CFLAGS += --cpuprofiler --memoryprofiler
|
||||||
else ifeq ($(CONF), release)
|
else ifeq ($(CONF), release)
|
||||||
CFLAGS += -O3 -DNDEBUG
|
CFLAGS += -O3 -DNDEBUG --emit-symbol-map
|
||||||
|
CFLAGS += --llvm-lto 3 # might be unstable
|
||||||
else
|
else
|
||||||
$(error Invalid value for CONF: $(CONF). Use "debug", "release" or "native_release")
|
$(error Invalid value for CONF: $(CONF). Use "debug", "release" or "native_release")
|
||||||
endif
|
endif
|
||||||
@ -82,9 +83,6 @@ bootroms: $(BOOTROMS_DIR)/agb_boot.bin \
|
|||||||
$(BOOTROMS_DIR)/sgb_boot.bin \
|
$(BOOTROMS_DIR)/sgb_boot.bin \
|
||||||
$(BOOTROMS_DIR)/sgb2_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
|
# Get a list of our source files and their respective object file targets
|
||||||
|
|
||||||
CORE_SOURCES_RAW := $(shell ls $(CORE_DIR)/Core/*.c)
|
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_SOURCES := $(shell ls *.c)
|
||||||
WASM_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(WASM_SOURCES))
|
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
|
# Automatic dependency generation
|
||||||
|
|
||||||
ifneq ($(filter-out clean %.bin, $(MAKECMDGOALS)),)
|
ifneq ($(filter-out clean %.bin, $(MAKECMDGOALS)),)
|
||||||
@ -120,12 +124,21 @@ $(OBJ)/%.c.o: %.c
|
|||||||
-@$(MKDIR) -p $(dir $@)
|
-@$(MKDIR) -p $(dir $@)
|
||||||
$(CC) $(CFLAGS) -c $< -o $@
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
$(BIN)/SameBoy.js: $(CORE_OBJECTS) $(WASM_OBJECTS)
|
$(BIN)/ressources/%:
|
||||||
-@$(MKDIR) -p $(dir $@)
|
-@$(MKDIR) -p $(dir $@)
|
||||||
cp -r web/* $(BIN)
|
cp -a $(patsubst $(BIN)/%,%,$@) $@
|
||||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(WASM_LDFLAGS)
|
|
||||||
|
$(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)
|
ifeq ($(CONF), release)
|
||||||
strip $@
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
$(CORE_DIR)/build/bin/BootROMs/%_boot.bin:
|
$(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>
|
195
wasm/main.c
195
wasm/main.c
@ -1,62 +1,20 @@
|
|||||||
#include <emscripten.h>
|
#include <emscripten.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
#include <SDL2/SDL_opengl.h>
|
||||||
|
#include <SDL2/SDL_video.h>
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
|
||||||
#include <Core/gb.h>
|
#include <Core/gb.h>
|
||||||
|
#include "main.h"
|
||||||
const char *resource_folder(void)
|
#include "utils.h"
|
||||||
{
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
GB_gameboy_t gb;
|
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_Window *window;
|
||||||
SDL_Renderer *renderer;
|
SDL_Renderer *renderer;
|
||||||
|
SDL_Surface *screen;
|
||||||
SDL_Texture *texture;
|
SDL_Texture *texture;
|
||||||
SDL_PixelFormat *pixel_format;
|
SDL_PixelFormat *pixel_format;
|
||||||
SDL_AudioDeviceID device_id;
|
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 pixel_buffer_1[256 * 224], pixel_buffer_2[256 * 224];
|
||||||
static uint32_t *active_pixel_buffer = pixel_buffer_1;
|
static uint32_t *active_pixel_buffer = pixel_buffer_1;
|
||||||
static uint32_t *previous_pixel_buffer = pixel_buffer_2;
|
static uint32_t *previous_pixel_buffer = pixel_buffer_2;
|
||||||
|
static char *battery_save_path_ptr;
|
||||||
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;
|
|
||||||
|
|
||||||
configuration_t configuration =
|
configuration_t configuration =
|
||||||
{
|
{
|
||||||
@ -147,11 +67,22 @@ configuration_t configuration =
|
|||||||
.model = MODEL_CGB
|
.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() {
|
unsigned query_sample_rate_of_audiocontexts() {
|
||||||
return EM_ASM_INT({
|
return EM_ASM_INT({
|
||||||
var AudioContext = window.AudioContext || window.webkitAudioContext;
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
var ctx = new AudioContext();
|
const ctx = new AudioContext();
|
||||||
var sr = ctx.sampleRate;
|
const sr = ctx.sampleRate;
|
||||||
ctx.close();
|
ctx.close();
|
||||||
return sr;
|
return sr;
|
||||||
});
|
});
|
||||||
@ -177,6 +108,18 @@ void render_texture(void *pixels, void *previous)
|
|||||||
SDL_RenderCopy(renderer, texture, NULL, NULL);
|
SDL_RenderCopy(renderer, texture, NULL, NULL);
|
||||||
SDL_RenderPresent(renderer);
|
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) {
|
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) {
|
static void vblank(GB_gameboy_t *gb) {
|
||||||
if (1 == 0) {
|
if (configuration.blend_frames) {
|
||||||
render_texture(active_pixel_buffer, previous_pixel_buffer);
|
render_texture(active_pixel_buffer, previous_pixel_buffer);
|
||||||
uint32_t *temp = active_pixel_buffer;
|
uint32_t *temp = active_pixel_buffer;
|
||||||
active_pixel_buffer = previous_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);
|
return SDL_MapRGB(pixel_format, r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
void init() {
|
void init_gb() {
|
||||||
GB_model_t model;
|
GB_model_t model;
|
||||||
|
|
||||||
model = (GB_model_t [])
|
model = (GB_model_t [])
|
||||||
@ -273,12 +216,7 @@ void init() {
|
|||||||
error = GB_load_boot_rom(&gb, boot_rom_path);
|
error = GB_load_boot_rom(&gb, boot_rom_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void run() {
|
int EMSCRIPTEN_KEEPALIVE init() {
|
||||||
GB_run_frame(&gb);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
|
||||||
{
|
|
||||||
#define str(x) #x
|
#define str(x) #x
|
||||||
#define xstr(x) str(x)
|
#define xstr(x) str(x)
|
||||||
pixel_format = (SDL_PixelFormat *) malloc(sizeof(SDL_PixelFormat));
|
pixel_format = (SDL_PixelFormat *) malloc(sizeof(SDL_PixelFormat));
|
||||||
@ -303,7 +241,7 @@ int main(int argc, char **argv)
|
|||||||
SDL_WINDOWPOS_UNDEFINED,
|
SDL_WINDOWPOS_UNDEFINED,
|
||||||
VIDEO_WIDTH,
|
VIDEO_WIDTH,
|
||||||
VIDEO_HEIGHT,
|
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) {
|
if (!window) {
|
||||||
@ -312,6 +250,7 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
SDL_SetWindowMinimumSize(window, VIDEO_WIDTH, VIDEO_HEIGHT);
|
SDL_SetWindowMinimumSize(window, VIDEO_WIDTH, VIDEO_HEIGHT);
|
||||||
|
SDL_SetWindowMaximumSize(window, VIDEO_WIDTH, VIDEO_HEIGHT);
|
||||||
|
|
||||||
renderer = SDL_CreateRenderer(
|
renderer = SDL_CreateRenderer(
|
||||||
window,
|
window,
|
||||||
@ -324,7 +263,7 @@ int main(int argc, char **argv)
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Surface *screen = SDL_CreateRGBSurface(
|
screen = SDL_CreateRGBSurface(
|
||||||
0,
|
0,
|
||||||
VIDEO_WIDTH,
|
VIDEO_WIDTH,
|
||||||
VIDEO_HEIGHT,
|
VIDEO_HEIGHT,
|
||||||
@ -363,23 +302,63 @@ int main(int argc, char **argv)
|
|||||||
fprintf(stderr, "Failed to open audio: %s", SDL_GetError());
|
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);
|
SDL_PauseAudioDevice(device_id, 0);
|
||||||
|
|
||||||
emscripten_set_main_loop(
|
return EXIT_SUCCESS;
|
||||||
run, // our main loop
|
}
|
||||||
FRAME_RATE,
|
|
||||||
true // infinite loop
|
|
||||||
);
|
|
||||||
|
|
||||||
|
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");
|
fprintf(stderr, "Quitting ...\n");
|
||||||
|
|
||||||
|
emscripten_set_main_loop(NULL, 0, false);
|
||||||
|
|
||||||
|
GB_free(&gb);
|
||||||
|
|
||||||
SDL_FreeSurface(screen);
|
SDL_FreeSurface(screen);
|
||||||
SDL_DestroyTexture(texture);
|
SDL_DestroyTexture(texture);
|
||||||
SDL_DestroyRenderer(renderer);
|
SDL_DestroyRenderer(renderer);
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
SDL_Quit();
|
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