Files
Andreas T Jonsson fc6c3fb473 fix(web,libretro): align default video adapter and CPU speed across builds
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.
2026-06-10 10:10:50 +02:00

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';
}