mirror of
https://codeberg.org/virtualxt/virtualxt.git
synced 2026-10-21 14:24:47 +00:00
Web UI: video button defaulted to EGA in HTML/JS while the runtime defaults to CGA, leaving the label out of sync at page load (first click was a silent no-op). Fixed by setting `videoState = 'CGA'`, the button label to `Video: CGA`, and reordering the cycle to `['CGA', 'HGC', 'EGA']`. Added a `Turbo` toggle (`Turbo: 4.77` default / `Turbo: 10.0`) live-applied via new `wasm_set_turbo` and the existing `?cpu=4.77` / `?cpu=10` URL parameter. Libretro: default CPU speed was 10 MHz (turbo enabled). Now defaults to 4.77 MHz (turbo disabled) to match the web build. Descriptor at `core.c:743` swapped to `disabled|enabled`; fall-through default at `core.c:1399` set to `CPU_FREQ_477`. First-time installs see the new default; existing users keep their saved option. The RPi bare-metal build inherits the new default because its env callback returns false for `GET_VARIABLE`. docs/STATUS.md: added entries and consolidated the Boot Diagnostics / Gotchas / EGA Notes / Known Limitations sections into a unified Notes section.
964 lines
31 KiB
C
964 lines
31 KiB
C
// Copyright (c) 2019-2026 Andreas T Jonsson <mail@andreasjonsson.se>
|
|
//
|
|
// This software is provided 'as-is', without any express or implied
|
|
// warranty. In no event will the authors be held liable for any damages
|
|
// arising from the use of this software.
|
|
//
|
|
// Permission is granted to anyone to use this software for any purpose,
|
|
// including commercial applications, and to alter it and redistribute it
|
|
// freely, subject to the following restrictions:
|
|
//
|
|
// 1. The origin of this software must not be misrepresented; you must not
|
|
// claim that you wrote the original software. If you use this software
|
|
// in a product, an acknowledgment in the product documentation would be
|
|
// appreciated but is not required.
|
|
//
|
|
// 2. Altered source versions must be plainly marked as such, and must not
|
|
// be misrepresented as being the original software.
|
|
//
|
|
// 3. This notice may not be removed or altered from any source
|
|
// distribution.
|
|
|
|
#include <emscripten.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
|
|
#include "libretro.h"
|
|
#include "vxt/machine.h"
|
|
#include "vxt/peripheral.h"
|
|
#include "modules/disk/disk.h"
|
|
#include "modules/hostfs/hostfs.h"
|
|
#include "modules/chipset/ppi.h"
|
|
#include "frontend/audio_mixer.h"
|
|
|
|
// ── Forward declarations from core.c ────────────────────────────────────
|
|
|
|
void retro_set_environment(retro_environment_t cb);
|
|
void retro_set_video_refresh(retro_video_refresh_t cb);
|
|
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb);
|
|
void retro_set_input_poll(retro_input_poll_t cb);
|
|
void retro_set_input_state(retro_input_state_t cb);
|
|
void retro_init(void);
|
|
void retro_deinit(void);
|
|
bool retro_load_game(const struct retro_game_info *info);
|
|
void retro_unload_game(void);
|
|
void retro_run(void);
|
|
void retro_reset(void);
|
|
|
|
// ── VFS: memory-backed file store ───────────────────────────────────────
|
|
|
|
typedef struct mem_file {
|
|
char path[256];
|
|
uint8_t *data;
|
|
int64_t size;
|
|
int64_t capacity;
|
|
int64_t pos;
|
|
unsigned mode;
|
|
struct mem_file *next;
|
|
} mem_file_t;
|
|
|
|
static mem_file_t *vfs_files = NULL;
|
|
|
|
// ── HostFS directory tree ───────────────────────────────────────────────
|
|
|
|
typedef struct tree_node {
|
|
char name[256];
|
|
bool is_dir;
|
|
struct tree_node *next_sibling;
|
|
struct tree_node *first_child;
|
|
} tree_node_t;
|
|
|
|
static tree_node_t *hostfs_tree = NULL;
|
|
|
|
static tree_node_t *tree_find_child(tree_node_t *parent, const char *name) {
|
|
tree_node_t *c = parent ? parent->first_child : hostfs_tree;
|
|
while (c) {
|
|
if (strcmp(c->name, name) == 0)
|
|
return c;
|
|
c = c->next_sibling;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static tree_node_t *tree_ensure_path(const char *path) {
|
|
if (!path || path[0] != '/') return NULL;
|
|
const char *p = path + 1;
|
|
tree_node_t *parent = NULL;
|
|
|
|
while (*p) {
|
|
const char *slash = strchr(p, '/');
|
|
size_t len = slash ? (size_t)(slash - p) : strlen(p);
|
|
if (len == 0) { p = slash ? slash + 1 : p; continue; }
|
|
|
|
char part[256];
|
|
if (len >= sizeof(part)) len = sizeof(part) - 1;
|
|
memcpy(part, p, len);
|
|
part[len] = '\0';
|
|
|
|
tree_node_t *node = tree_find_child(parent, part);
|
|
if (!node) {
|
|
node = (tree_node_t*)malloc(sizeof(tree_node_t));
|
|
memset(node, 0, sizeof(tree_node_t));
|
|
strncpy(node->name, part, sizeof(node->name) - 1);
|
|
node->name[sizeof(node->name) - 1] = '\0';
|
|
node->is_dir = (slash != NULL);
|
|
if (parent) {
|
|
node->next_sibling = parent->first_child;
|
|
parent->first_child = node;
|
|
} else {
|
|
node->next_sibling = hostfs_tree;
|
|
hostfs_tree = node;
|
|
}
|
|
}
|
|
parent = node;
|
|
p = slash ? slash + 1 : p + len;
|
|
}
|
|
return parent;
|
|
}
|
|
|
|
static tree_node_t *tree_lookup(const char *path) {
|
|
if (!path || path[0] != '/') return NULL;
|
|
const char *p = path + 1;
|
|
tree_node_t *parent = NULL;
|
|
|
|
while (*p) {
|
|
const char *slash = strchr(p, '/');
|
|
size_t len = slash ? (size_t)(slash - p) : strlen(p);
|
|
if (len == 0) { p = slash ? slash + 1 : p; continue; }
|
|
|
|
char part[256];
|
|
if (len >= sizeof(part)) len = sizeof(part) - 1;
|
|
memcpy(part, p, len);
|
|
part[len] = '\0';
|
|
|
|
tree_node_t *node = tree_find_child(parent, part);
|
|
if (!node) return NULL;
|
|
parent = node;
|
|
p = slash ? slash + 1 : p + len;
|
|
}
|
|
return parent;
|
|
}
|
|
|
|
static bool tree_is_dir(const char *path) {
|
|
tree_node_t *node = tree_lookup(path);
|
|
return node && node->is_dir;
|
|
}
|
|
|
|
static void free_tree_recursive(tree_node_t *node) {
|
|
if (!node) return;
|
|
free_tree_recursive(node->first_child);
|
|
free_tree_recursive(node->next_sibling);
|
|
free(node);
|
|
}
|
|
|
|
static void tree_clear_hostfs(void) {
|
|
free_tree_recursive(hostfs_tree);
|
|
hostfs_tree = NULL;
|
|
}
|
|
|
|
static void vfs_remove_hostfs_files(void) {
|
|
mem_file_t **prev = &vfs_files;
|
|
mem_file_t *f = vfs_files;
|
|
while (f) {
|
|
if (strncmp(f->path, "/hostfs/", 8) == 0) {
|
|
mem_file_t *next = f->next;
|
|
*prev = next;
|
|
free(f->data);
|
|
free(f);
|
|
f = next;
|
|
} else {
|
|
prev = &f->next;
|
|
f = f->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Directory iteration ─────────────────────────────────────────────────
|
|
|
|
typedef struct dir_iter {
|
|
tree_node_t *parent;
|
|
tree_node_t *current;
|
|
} dir_iter_t;
|
|
|
|
static void normalize_path(char *out, size_t out_size, const char *path) {
|
|
size_t i = 0, o = 0;
|
|
bool prev_slash = false;
|
|
while (path[i] && o < out_size - 1) {
|
|
if (path[i] == '/') {
|
|
if (!prev_slash) {
|
|
out[o++] = '/';
|
|
prev_slash = true;
|
|
}
|
|
} else {
|
|
out[o++] = path[i];
|
|
prev_slash = false;
|
|
}
|
|
i++;
|
|
}
|
|
out[o] = '\0';
|
|
}
|
|
|
|
static mem_file_t *vfs_find(const char *path) {
|
|
char norm[256];
|
|
normalize_path(norm, sizeof(norm), path);
|
|
mem_file_t *f = vfs_files;
|
|
while (f) {
|
|
if (strcmp(f->path, norm) == 0)
|
|
return f;
|
|
f = f->next;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static struct retro_vfs_file_handle *web_vfs_open(const char *path, unsigned mode, unsigned hints) {
|
|
(void)hints;
|
|
char norm[256];
|
|
normalize_path(norm, sizeof(norm), path);
|
|
mem_file_t *f = vfs_find(norm);
|
|
if (f) {
|
|
f->pos = 0;
|
|
f->mode = mode;
|
|
return (struct retro_vfs_file_handle *)f;
|
|
}
|
|
if ((mode & RETRO_VFS_FILE_ACCESS_READ) && !(mode & RETRO_VFS_FILE_ACCESS_WRITE))
|
|
return NULL;
|
|
|
|
f = (mem_file_t*)malloc(sizeof(mem_file_t));
|
|
memset(f, 0, sizeof(mem_file_t));
|
|
strncpy(f->path, norm, sizeof(f->path) - 1);
|
|
f->path[sizeof(f->path) - 1] = '\0';
|
|
f->mode = mode;
|
|
f->next = vfs_files;
|
|
vfs_files = f;
|
|
return (struct retro_vfs_file_handle *)f;
|
|
}
|
|
|
|
static int web_vfs_close(struct retro_vfs_file_handle *stream) {
|
|
(void)stream;
|
|
return 0;
|
|
}
|
|
|
|
static int64_t web_vfs_read(struct retro_vfs_file_handle *stream, void *buf, uint64_t len) {
|
|
mem_file_t *f = (mem_file_t*)stream;
|
|
if (!f) return -1;
|
|
int64_t remain = f->size - f->pos;
|
|
if (remain < 0) remain = 0;
|
|
if ((int64_t)len > remain) len = (uint64_t)remain;
|
|
memcpy(buf, f->data + f->pos, len);
|
|
f->pos += (int64_t)len;
|
|
return (int64_t)len;
|
|
}
|
|
|
|
static int64_t web_vfs_write(struct retro_vfs_file_handle *stream, const void *buf, uint64_t len) {
|
|
mem_file_t *f = (mem_file_t*)stream;
|
|
if (!f) return -1;
|
|
int64_t needed = f->pos + (int64_t)len;
|
|
if (needed > f->capacity) {
|
|
int64_t new_cap = f->capacity * 2;
|
|
if (new_cap < needed) new_cap = needed;
|
|
if (new_cap < 1024) new_cap = 1024;
|
|
uint8_t *new_data = (uint8_t*)realloc(f->data, (size_t)new_cap);
|
|
if (!new_data) return -1;
|
|
f->data = new_data;
|
|
f->capacity = new_cap;
|
|
}
|
|
memcpy(f->data + f->pos, buf, len);
|
|
EM_ASM_({
|
|
if (Module.onVfsWrite) {
|
|
var path = UTF8ToString($0);
|
|
var offset = Number($1);
|
|
var buf = new Uint8Array(HEAPU8.subarray(Number($2), Number($2) + Number($3)));
|
|
Module.onVfsWrite(path, offset, buf);
|
|
}
|
|
}, f->path, (int)f->pos, (int)(size_t)buf, (int)len);
|
|
f->pos += (int64_t)len;
|
|
if (f->pos > f->size) f->size = f->pos;
|
|
return (int64_t)len;
|
|
}
|
|
|
|
static int64_t web_vfs_seek(struct retro_vfs_file_handle *stream, int64_t offset, int whence) {
|
|
mem_file_t *f = (mem_file_t*)stream;
|
|
if (!f) return -1;
|
|
int64_t new_pos;
|
|
switch (whence) {
|
|
case SEEK_SET: new_pos = offset; break;
|
|
case SEEK_CUR: new_pos = f->pos + offset; break;
|
|
case SEEK_END: new_pos = f->size + offset; break;
|
|
default: return -1;
|
|
}
|
|
if (new_pos < 0) new_pos = 0;
|
|
f->pos = new_pos;
|
|
return 0; // Success (disk module expects 0 on success)
|
|
}
|
|
|
|
static int64_t web_vfs_tell(struct retro_vfs_file_handle *stream) {
|
|
mem_file_t *f = (mem_file_t*)stream;
|
|
if (!f) return -1;
|
|
return f->pos;
|
|
}
|
|
|
|
static int64_t web_vfs_size(struct retro_vfs_file_handle *stream) {
|
|
mem_file_t *f = (mem_file_t*)stream;
|
|
if (!f) return -1;
|
|
return f->size;
|
|
}
|
|
|
|
static int web_vfs_flush(struct retro_vfs_file_handle *stream) {
|
|
(void)stream;
|
|
return 0;
|
|
}
|
|
|
|
static int web_vfs_remove(const char *path) {
|
|
char norm[256];
|
|
normalize_path(norm, sizeof(norm), path);
|
|
mem_file_t **prev = &vfs_files;
|
|
mem_file_t *f = vfs_files;
|
|
while (f) {
|
|
if (strcmp(f->path, norm) == 0) {
|
|
*prev = f->next;
|
|
free(f->data);
|
|
free(f);
|
|
return 0;
|
|
}
|
|
prev = &f->next;
|
|
f = f->next;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int web_vfs_rename(const char *old_path, const char *new_path) {
|
|
char norm_old[256];
|
|
char norm_new[256];
|
|
normalize_path(norm_old, sizeof(norm_old), old_path);
|
|
normalize_path(norm_new, sizeof(norm_new), new_path);
|
|
mem_file_t *f = vfs_files;
|
|
while (f) {
|
|
if (strcmp(f->path, norm_old) == 0) {
|
|
strncpy(f->path, norm_new, sizeof(f->path) - 1);
|
|
f->path[sizeof(f->path) - 1] = '\0';
|
|
return 0;
|
|
}
|
|
f = f->next;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int web_vfs_stat(const char *path, int32_t *size) {
|
|
char norm[256];
|
|
normalize_path(norm, sizeof(norm), path);
|
|
mem_file_t *f = vfs_find(norm);
|
|
if (f) {
|
|
if (size) *size = (int32_t)f->size;
|
|
return RETRO_VFS_STAT_IS_VALID;
|
|
}
|
|
if (tree_is_dir(norm)) {
|
|
if (size) *size = 0;
|
|
return RETRO_VFS_STAT_IS_VALID | RETRO_VFS_STAT_IS_DIRECTORY;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int web_vfs_mkdir(const char *dir) {
|
|
tree_ensure_path(dir);
|
|
return 0;
|
|
}
|
|
|
|
static struct retro_vfs_dir_handle *web_vfs_opendir(const char *dir, bool include_hidden) {
|
|
(void)include_hidden;
|
|
tree_node_t *node = tree_lookup(dir);
|
|
if (!node || !node->is_dir) return NULL;
|
|
dir_iter_t *iter = (dir_iter_t*)malloc(sizeof(dir_iter_t));
|
|
if (!iter) return NULL;
|
|
iter->parent = node;
|
|
iter->current = NULL;
|
|
return (struct retro_vfs_dir_handle *)iter;
|
|
}
|
|
|
|
static bool web_vfs_readdir(struct retro_vfs_dir_handle *dirstream) {
|
|
dir_iter_t *iter = (dir_iter_t*)dirstream;
|
|
if (!iter || !iter->parent) return false;
|
|
if (!iter->current)
|
|
iter->current = iter->parent->first_child;
|
|
else
|
|
iter->current = iter->current->next_sibling;
|
|
return iter->current != NULL;
|
|
}
|
|
|
|
static const char *web_vfs_dirent_get_name(struct retro_vfs_dir_handle *dirstream) {
|
|
dir_iter_t *iter = (dir_iter_t*)dirstream;
|
|
if (!iter || !iter->current) return NULL;
|
|
return iter->current->name;
|
|
}
|
|
|
|
static bool web_vfs_dirent_is_dir(struct retro_vfs_dir_handle *dirstream) {
|
|
dir_iter_t *iter = (dir_iter_t*)dirstream;
|
|
if (!iter || !iter->current) return false;
|
|
return iter->current->is_dir;
|
|
}
|
|
|
|
static int web_vfs_closedir(struct retro_vfs_dir_handle *dirstream) {
|
|
free(dirstream);
|
|
return 0;
|
|
}
|
|
|
|
static const char *web_vfs_get_path(struct retro_vfs_file_handle *stream) {
|
|
mem_file_t *f = (mem_file_t*)stream;
|
|
if (!f) return NULL;
|
|
return f->path;
|
|
}
|
|
|
|
static int64_t web_vfs_truncate(struct retro_vfs_file_handle *stream, int64_t length) {
|
|
mem_file_t *f = (mem_file_t*)stream;
|
|
if (!f) return -1;
|
|
if (length > f->capacity) {
|
|
uint8_t *new_data = (uint8_t*)realloc(f->data, (size_t)length);
|
|
if (!new_data) return -1;
|
|
f->data = new_data;
|
|
f->capacity = length;
|
|
}
|
|
f->size = length;
|
|
if (f->pos > f->size) f->pos = f->size;
|
|
return 0;
|
|
}
|
|
|
|
static struct retro_vfs_interface vfs_interface = {
|
|
.get_path = web_vfs_get_path,
|
|
.open = web_vfs_open,
|
|
.close = web_vfs_close,
|
|
.size = web_vfs_size,
|
|
.tell = web_vfs_tell,
|
|
.seek = web_vfs_seek,
|
|
.read = web_vfs_read,
|
|
.write = web_vfs_write,
|
|
.flush = web_vfs_flush,
|
|
.remove = web_vfs_remove,
|
|
.rename = web_vfs_rename,
|
|
.truncate = web_vfs_truncate,
|
|
.stat = web_vfs_stat,
|
|
.mkdir = web_vfs_mkdir,
|
|
.opendir = web_vfs_opendir,
|
|
.readdir = web_vfs_readdir,
|
|
.dirent_get_name = web_vfs_dirent_get_name,
|
|
.dirent_is_dir = web_vfs_dirent_is_dir,
|
|
.closedir = web_vfs_closedir,
|
|
};
|
|
|
|
// ── URL parameter options ───────────────────────────────────────────────
|
|
|
|
#define MAX_URL_OPTIONS 16
|
|
#define MAX_OPT_KEY_LEN 32
|
|
#define MAX_OPT_VAL_LEN 32
|
|
|
|
static struct {
|
|
char key[MAX_OPT_KEY_LEN];
|
|
char value[MAX_OPT_VAL_LEN];
|
|
} url_options[MAX_URL_OPTIONS];
|
|
static int num_url_options = 0;
|
|
|
|
static void parse_url_options(const char *query) {
|
|
num_url_options = 0;
|
|
if (!query || !query[0]) return;
|
|
|
|
char buf[512];
|
|
strncpy(buf, query, sizeof(buf) - 1);
|
|
buf[sizeof(buf) - 1] = '\0';
|
|
|
|
char *p = buf;
|
|
while (p && num_url_options < MAX_URL_OPTIONS) {
|
|
char *next = strchr(p, '&');
|
|
if (next) *next++ = '\0';
|
|
|
|
char *eq = strchr(p, '=');
|
|
if (eq) {
|
|
*eq++ = '\0';
|
|
if (*p && *eq) {
|
|
strncpy(url_options[num_url_options].key, p, MAX_OPT_KEY_LEN - 1);
|
|
url_options[num_url_options].key[MAX_OPT_KEY_LEN - 1] = '\0';
|
|
strncpy(url_options[num_url_options].value, eq, MAX_OPT_VAL_LEN - 1);
|
|
url_options[num_url_options].value[MAX_OPT_VAL_LEN - 1] = '\0';
|
|
num_url_options++;
|
|
}
|
|
}
|
|
p = next;
|
|
}
|
|
}
|
|
|
|
static const char *get_url_option(const char *key) {
|
|
for (int i = 0; i < num_url_options; i++) {
|
|
if (strcmp(url_options[i].key, key) == 0)
|
|
return url_options[i].value;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// ── Host state ──────────────────────────────────────────────────────────
|
|
|
|
static retro_keyboard_event_t keyboard_event_cb = NULL;
|
|
static struct retro_frame_time_callback frame_time_cb = {0};
|
|
|
|
static uint32_t *framebuffer_ptr = NULL;
|
|
static unsigned framebuffer_width = 0;
|
|
static unsigned framebuffer_height = 0;
|
|
static size_t framebuffer_pitch = 0;
|
|
|
|
static int mouse_dx = 0, mouse_dy = 0;
|
|
static uint8_t mouse_buttons = 0;
|
|
|
|
static vxt_ppi_t *ppi = NULL;
|
|
|
|
// ── Runtime core option overrides ───────────────────────────────────────
|
|
|
|
static bool crt_override = false, crt_value = true;
|
|
static bool noise_override = false, noise_value = true;
|
|
static bool video_override = false;
|
|
static char video_value[4] = "CGA";
|
|
static bool turbo_override = false, turbo_value = false;
|
|
static bool options_dirty = false;
|
|
|
|
// ── Audio ring buffer ───────────────────────────────────────────────────
|
|
|
|
#define AUDIO_RING_FRAMES (44100 * 2) // 2 seconds of stereo
|
|
|
|
static int16_t audio_ring[AUDIO_RING_FRAMES * 2];
|
|
static volatile unsigned audio_ring_write = 0;
|
|
static volatile unsigned audio_ring_read = 0;
|
|
|
|
// ── Host callbacks ──────────────────────────────────────────────────────
|
|
|
|
static void web_log_cb(enum retro_log_level level, const char *fmt, ...) {
|
|
(void)level;
|
|
char buf[512];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
va_end(ap);
|
|
EM_ASM({ console.log(UTF8ToString($0)); }, buf);
|
|
}
|
|
|
|
static void web_video_cb(const void *data, unsigned width, unsigned height, size_t pitch) {
|
|
framebuffer_ptr = (uint32_t*)data;
|
|
framebuffer_width = width;
|
|
framebuffer_height = height;
|
|
framebuffer_pitch = pitch;
|
|
}
|
|
|
|
static size_t web_audio_cb(const int16_t *data, size_t frames) {
|
|
for (size_t i = 0; i < frames; i++) {
|
|
unsigned w = audio_ring_write;
|
|
audio_ring[w * 2] = data[i * 2];
|
|
audio_ring[w * 2 + 1] = data[i * 2 + 1];
|
|
audio_ring_write = (w + 1) % AUDIO_RING_FRAMES;
|
|
if (audio_ring_write == audio_ring_read) {
|
|
audio_ring_read = (audio_ring_read + 1) % AUDIO_RING_FRAMES;
|
|
}
|
|
}
|
|
return frames;
|
|
}
|
|
|
|
static void web_input_poll_cb(void) {
|
|
mouse_dx = 0;
|
|
mouse_dy = 0;
|
|
}
|
|
|
|
static int16_t web_input_state_cb(unsigned port, unsigned device, unsigned index, unsigned id) {
|
|
(void)port;
|
|
(void)index;
|
|
if (device == RETRO_DEVICE_MOUSE) {
|
|
switch (id) {
|
|
case RETRO_DEVICE_ID_MOUSE_X: return (int16_t)mouse_dx;
|
|
case RETRO_DEVICE_ID_MOUSE_Y: return (int16_t)mouse_dy;
|
|
case RETRO_DEVICE_ID_MOUSE_LEFT: return (mouse_buttons & 1) ? 1 : 0;
|
|
case RETRO_DEVICE_ID_MOUSE_RIGHT: return (mouse_buttons & 2) ? 1 : 0;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void web_led_cb(int led, int state) {
|
|
(void)led;
|
|
(void)state;
|
|
}
|
|
|
|
static bool web_env_cb(unsigned cmd, void *data) {
|
|
switch (cmd) {
|
|
case RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME: {
|
|
bool *b = (bool*)data;
|
|
*b = true;
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_GET_LOG_INTERFACE: {
|
|
struct retro_log_callback *log = (struct retro_log_callback*)data;
|
|
log->log = web_log_cb;
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_SET_VARIABLES: {
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_GET_VARIABLE: {
|
|
struct retro_variable *var = (struct retro_variable*)data;
|
|
const char *opt = NULL;
|
|
|
|
// Check runtime overrides first
|
|
if (crt_override && strcmp(var->key, "virtualxt_crt_filter") == 0) {
|
|
var->value = crt_value ? "enabled" : "disabled";
|
|
return true;
|
|
}
|
|
if (noise_override && strcmp(var->key, "virtualxt_pc_noise") == 0) {
|
|
var->value = noise_value ? "enabled" : "disabled";
|
|
return true;
|
|
}
|
|
if (video_override && strcmp(var->key, "virtualxt_video") == 0) {
|
|
var->value = video_value;
|
|
return true;
|
|
}
|
|
if (turbo_override && strcmp(var->key, "virtualxt_cpu_turbo") == 0) {
|
|
var->value = turbo_value ? "enabled" : "disabled";
|
|
return true;
|
|
}
|
|
|
|
// Map core option key to URL parameter short key
|
|
const char *short_key = NULL;
|
|
if (strcmp(var->key, "virtualxt_video") == 0) short_key = "video";
|
|
else if (strcmp(var->key, "virtualxt_crt_filter") == 0) short_key = "crt";
|
|
else if (strcmp(var->key, "virtualxt_pc_noise") == 0) short_key = "noise";
|
|
else if (strcmp(var->key, "virtualxt_cpu_turbo") == 0) short_key = "cpu";
|
|
else if (strcmp(var->key, "virtualxt_boot_priority") == 0) short_key = "boot";
|
|
else if (strcmp(var->key, "virtualxt_reset_default_disk") == 0) short_key = "reset";
|
|
|
|
// Check URL options first
|
|
if (short_key) opt = get_url_option(short_key);
|
|
|
|
if (opt) {
|
|
// Translate short parameter values to core option values
|
|
static char buf[MAX_OPT_VAL_LEN];
|
|
if (strcmp(short_key, "video") == 0) {
|
|
if (strcmp(opt, "cga") == 0) var->value = "CGA";
|
|
else if (strcmp(opt, "ega") == 0) var->value = "EGA";
|
|
else if (strcmp(opt, "hgc") == 0) var->value = "HGC";
|
|
else var->value = "EGA";
|
|
} else if (strcmp(short_key, "cpu") == 0) {
|
|
if (strcmp(opt, "4.77") == 0) var->value = "disabled";
|
|
else var->value = "enabled";
|
|
} else if (strcmp(short_key, "crt") == 0 ||
|
|
strcmp(short_key, "noise") == 0 ||
|
|
strcmp(short_key, "reset") == 0) {
|
|
if (strcmp(opt, "off") == 0) var->value = "disabled";
|
|
else var->value = "enabled";
|
|
} else if (strcmp(short_key, "boot") == 0) {
|
|
if (strcmp(opt, "fd") == 0) var->value = "FD";
|
|
else var->value = "HD";
|
|
} else {
|
|
var->value = opt;
|
|
}
|
|
} else {
|
|
// Hardcoded defaults
|
|
if (strcmp(var->key, "virtualxt_video") == 0) var->value = "CGA";
|
|
else if (strcmp(var->key, "virtualxt_crt_filter") == 0) var->value = "enabled";
|
|
else if (strcmp(var->key, "virtualxt_pc_noise") == 0) var->value = "enabled";
|
|
else if (strcmp(var->key, "virtualxt_cpu_turbo") == 0) var->value = "disabled";
|
|
else if (strcmp(var->key, "virtualxt_boot_priority") == 0) var->value = "HD";
|
|
else if (strcmp(var->key, "virtualxt_reset_default_disk") == 0) var->value = "disabled";
|
|
else if (strcmp(var->key, "virtualxt_hostfs") == 0) var->value = "enabled";
|
|
else if (strcmp(var->key, "virtualxt_ems") == 0) var->value = "enabled";
|
|
else if (strcmp(var->key, "virtualxt_serial_mouse") == 0) var->value = "enabled";
|
|
else if (strcmp(var->key, "virtualxt_show_game_focus") == 0) var->value = "disabled";
|
|
else if (strcmp(var->key, "virtualxt_rtc") == 0) var->value = "enabled";
|
|
else if (strcmp(var->key, "virtualxt_noise_volume") == 0) var->value = "100%";
|
|
else var->value = NULL;
|
|
}
|
|
return var->value != NULL;
|
|
}
|
|
case RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE: {
|
|
bool *updated = (bool*)data;
|
|
*updated = options_dirty;
|
|
options_dirty = false;
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_SET_PIXEL_FORMAT: {
|
|
enum retro_pixel_format *fmt = (enum retro_pixel_format*)data;
|
|
*fmt = RETRO_PIXEL_FORMAT_XRGB8888;
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK: {
|
|
frame_time_cb = *(struct retro_frame_time_callback*)data;
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_GET_VFS_INTERFACE: {
|
|
struct retro_vfs_interface_info *info = (struct retro_vfs_interface_info*)data;
|
|
info->required_interface_version = 3;
|
|
info->iface = &vfs_interface;
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_GET_LED_INTERFACE: {
|
|
struct retro_led_interface *led = (struct retro_led_interface*)data;
|
|
led->set_led_state = web_led_cb;
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY: {
|
|
const char **dir = (const char**)data;
|
|
*dir = "/save";
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE: {
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK: {
|
|
struct retro_keyboard_callback *kbcb = (struct retro_keyboard_callback*)data;
|
|
keyboard_event_cb = kbcb->callback;
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_SET_GEOMETRY: {
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_SET_MESSAGE: {
|
|
struct retro_message *msg = (struct retro_message*)data;
|
|
EM_ASM({ console.log(UTF8ToString($0)); }, msg->msg);
|
|
return true;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ── Exported functions ──────────────────────────────────────────────────
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_initialize(void) {
|
|
retro_set_environment(web_env_cb);
|
|
retro_set_video_refresh(web_video_cb);
|
|
retro_set_audio_sample_batch(web_audio_cb);
|
|
retro_set_input_poll(web_input_poll_cb);
|
|
retro_set_input_state(web_input_state_cb);
|
|
retro_init();
|
|
|
|
struct retro_game_info info = {0};
|
|
retro_load_game(&info);
|
|
|
|
ppi = (vxt_ppi_t*)vxt_peripheral_find_class(VXT_CLASS_PPI);
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_run(int delta_us) {
|
|
if (frame_time_cb.callback) {
|
|
frame_time_cb.callback((retro_usec_t)delta_us);
|
|
}
|
|
retro_run();
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_send_key(uint8_t scancode) {
|
|
if (ppi) {
|
|
vxt_ppi_push_event(ppi, scancode);
|
|
}
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_send_mouse(int dx, int dy, uint8_t buttons) {
|
|
mouse_dx = dx;
|
|
mouse_dy = dy;
|
|
mouse_buttons = buttons;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
bool wasm_create_vfs_file(const char *path, uint8_t *data, int size) {
|
|
mem_file_t *f = vfs_find(path);
|
|
if (!f) {
|
|
f = (mem_file_t*)malloc(sizeof(mem_file_t));
|
|
memset(f, 0, sizeof(mem_file_t));
|
|
strncpy(f->path, path, sizeof(f->path) - 1);
|
|
f->path[sizeof(f->path) - 1] = '\0';
|
|
f->next = vfs_files;
|
|
vfs_files = f;
|
|
}
|
|
free(f->data);
|
|
f->data = (uint8_t*)malloc(size);
|
|
memcpy(f->data, data, size);
|
|
f->size = size;
|
|
f->capacity = size;
|
|
f->pos = 0;
|
|
f->mode = RETRO_VFS_FILE_ACCESS_READ;
|
|
return true;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
bool wasm_mount_disk(const char *path, int drive_num) {
|
|
vxt_disk_t *disk = (vxt_disk_t*)vxt_peripheral_find_by_name("disk");
|
|
if (!disk) return false;
|
|
|
|
if (!path) {
|
|
vxt_disk_unmount(disk, drive_num);
|
|
return true;
|
|
}
|
|
|
|
unsigned mode = RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE;
|
|
struct retro_vfs_file_handle *fp = vfs_interface.open(path, mode, 0);
|
|
if (!fp) return false;
|
|
|
|
vxt_disk_unmount(disk, drive_num);
|
|
return vxt_disk_mount(disk, drive_num, fp);
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_load_hd(const char *path) {
|
|
retro_unload_game();
|
|
|
|
struct retro_game_info info = {
|
|
.path = path,
|
|
.data = NULL,
|
|
.size = 0,
|
|
.meta = NULL
|
|
};
|
|
|
|
retro_load_game(&info);
|
|
ppi = (vxt_ppi_t*)vxt_peripheral_find_class(VXT_CLASS_PPI);
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
uint32_t *wasm_framebuffer_ptr(void) {
|
|
return framebuffer_ptr;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
unsigned wasm_framebuffer_width(void) {
|
|
return framebuffer_width;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
unsigned wasm_framebuffer_height(void) {
|
|
return framebuffer_height;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
size_t wasm_framebuffer_pitch(void) {
|
|
return framebuffer_pitch;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
unsigned wasm_audio_read(int16_t *dst, unsigned max_frames) {
|
|
unsigned available;
|
|
unsigned write = audio_ring_write;
|
|
unsigned read = audio_ring_read;
|
|
if (write >= read) available = write - read;
|
|
else available = AUDIO_RING_FRAMES - read + write;
|
|
|
|
unsigned frames = (max_frames < available) ? max_frames : available;
|
|
for (unsigned i = 0; i < frames; i++) {
|
|
dst[i * 2] = audio_ring[read * 2];
|
|
dst[i * 2 + 1] = audio_ring[read * 2 + 1];
|
|
read = (read + 1) % AUDIO_RING_FRAMES;
|
|
}
|
|
audio_ring_read = read;
|
|
return frames;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
bool wasm_disk_led_active(void) {
|
|
return audio_mixer_led_active();
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_set_options(const char *query_string) {
|
|
parse_url_options(query_string);
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
char* wasm_string_alloc(const char *str) {
|
|
size_t len = strlen(str) + 1;
|
|
char *copy = (char*)malloc(len);
|
|
if (copy) memcpy(copy, str, len);
|
|
return copy;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_string_free(char *str) {
|
|
free(str);
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_set_crt(int on) {
|
|
crt_override = true;
|
|
crt_value = (on != 0);
|
|
options_dirty = true;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_set_noise(int on) {
|
|
noise_override = true;
|
|
noise_value = (on != 0);
|
|
options_dirty = true;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_set_turbo(int on) {
|
|
turbo_override = true;
|
|
turbo_value = (on != 0);
|
|
options_dirty = true;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_reset(void) {
|
|
retro_reset();
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_set_video(const char *video) {
|
|
video_override = true;
|
|
strncpy(video_value, video, sizeof(video_value) - 1);
|
|
video_value[sizeof(video_value) - 1] = '\0';
|
|
options_dirty = true;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_reload(void) {
|
|
retro_unload_game();
|
|
struct retro_game_info info = {0};
|
|
retro_load_game(&info);
|
|
ppi = (vxt_ppi_t*)vxt_peripheral_find_class(VXT_CLASS_PPI);
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_hostfs_clear(void) {
|
|
vfs_remove_hostfs_files();
|
|
tree_clear_hostfs();
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
bool wasm_hostfs_add_file(const char *path, uint8_t *data, int size) {
|
|
// Build directory tree path
|
|
tree_ensure_path(path);
|
|
// Create VFS file entry
|
|
return wasm_create_vfs_file(path, data, size);
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_hostfs_set_root(const char *path) {
|
|
vxt_hostfs_t *hfs = (vxt_hostfs_t*)vxt_peripheral_find_by_name("hostfs");
|
|
if (!hfs) return;
|
|
if (!path) {
|
|
hfs->root_path[0] = '\0';
|
|
return;
|
|
}
|
|
strncpy(hfs->root_path, path, HOSTFS_MAX_PATH - 1);
|
|
hfs->root_path[HOSTFS_MAX_PATH - 1] = '\0';
|
|
size_t len = strlen(hfs->root_path);
|
|
if (len > 0 && hfs->root_path[len - 1] != '/' && len < HOSTFS_MAX_PATH - 1) {
|
|
hfs->root_path[len] = '/';
|
|
hfs->root_path[len + 1] = '\0';
|
|
}
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void wasm_hostfs_set_launch_path(const char *path) {
|
|
vxt_hostfs_t *hfs = (vxt_hostfs_t*)vxt_peripheral_find_by_name("hostfs");
|
|
if (!hfs) return;
|
|
if (!path) {
|
|
hfs->launch_path[0] = '\0';
|
|
return;
|
|
}
|
|
strncpy(hfs->launch_path, path, HOSTFS_MAX_PATH - 1);
|
|
hfs->launch_path[HOSTFS_MAX_PATH - 1] = '\0';
|
|
}
|