wasm: Implement WASM support

This is an in-tree port of [sunpazed's excellent work][wasm].

The goal is to be able to run the simulator in a browser.
This builds with emscripten, as installed by the `emsdk`.
The `emsdk` is added as a submodule, and initialized on first build.

To build the browser WASM version of the simulator, run `make wasm`.

Fixes: #993

Signed-off-by: Christophe de Dinechin <christophe@dinechin.org>

[wasm]: https://github.com/sunpazed/db48x-wasm
This commit is contained in:
Christophe de Dinechin 2024-06-22 18:43:00 +02:00
parent 5c551ce194
commit 9bc7afd519
23 changed files with 713 additions and 54 deletions

3
.gitmodules vendored
View file

@ -7,3 +7,6 @@
[submodule "dmcp"]
path = dmcp
url = git@github.com:c3d/SDKdemo.git
[submodule "emsdk"]
path = emsdk
url = git@github.com:emscripten-core/emsdk.git

127
Makefile
View file

@ -101,6 +101,25 @@ sim: recorder/config.h \
keyboard \
.ALWAYS
WASM_TARGET=db48x.js
WASM_HTML=index.html
wasm: emsdk $(WASM_TARGET) $(WASM_HTML)
ifneq ($(VARIANT),wasm)
$(WASM_TARGET): $(SOURCES)
(source emsdk/emsdk_env.sh && \
make VARIANT=wasm TARGET=db48x PGM=js SDK=sim )
$(WASM_HTML): src/wasm/index.html
cp $< $@
endif
emsdk: emsdk/emsdk
emcc --version > /dev/null || \
(cd emsdk && ./emsdk install latest && ./emsdk activate latest)
emsdk/emsdk:
git submodule update --init --recursive
clangdb: sim/$(TARGET).mak .ALWAYS
cd sim && rm -f *.o && compiledb make -f $(TARGET).mak && mv compile_commands.json ..
@ -197,17 +216,12 @@ fastest-%:
$(MAKE) $* OPT=fastest
######################################
# System sources
######################################
C_INCLUDES += -I$(SDK)
C_SOURCES += $(SDK)/sys/pgm_syscalls.c
ASM_SOURCES = $(SDK)/startup_pgm.s
#######################################
# Custom section
#######################################
SOURCES=$(C_SOURCES) $(CXX_SOURCES)
# Includes
C_INCLUDES += -Isrc/$(VARIANT) -Isrc/$(PLATFORM) -Isrc
@ -271,7 +285,8 @@ CXX_SOURCES += \
src/unit.cc \
src/user_interface.cc \
src/util.cc \
src/variables.cc
src/variables.cc \
$(PLATFORM_SOURCES)
# ASM sources
#ASM_SOURCES += src/xxx.s
@ -285,16 +300,17 @@ DEFINES += \
$(DEFINES_$(VARIANT)) \
HELPFILE_NAME=\"/help/$(TARGET).md\"
DEFINES_debug=DEBUG
DEFINES_release=RELEASE
DEFINES_small=RELEASE
DEFINES_fast=RELEASE
DEFINES_faster=RELEASE
DEFINES_fastes=RELEASE
DEFINES_release=NDEBUG
DEFINES_small=NDEBUG
DEFINES_fast=NDEBUG
DEFINES_faster=NDEBUG
DEFINES_fastes=NDEBUG
DEFINES_dm32 = DM32 \
CONFIG_FIXED_BASED_OBJECTS \
DEOPTIMIZE_CATALOG
DEFINES_dm42 = DM42
DEFINES_wasm = $(DEFINES_dm32) SIMULATOR WASM
C_DEFS += $(DEFINES:%=-D%)
@ -309,6 +325,23 @@ $(BUILD)/recorder.o $(BUILD)/recorder_ring.o: recorder/config.h
#######################################
# binaries
#######################################
#------------------------------------------------------------------------------
ifeq ($(VARIANT),wasm)
#------------------------------------------------------------------------------
CC = emcc
CXX = emcc -x c++ -std=gnu++17
PLATFORM_SOURCES=src/wasp/dmcp.cc src/wasm/sim-screen.cc src/wasm/sim-window.cc
C_SOURCES += recorder/recorder.c
CFLAGS += -O3
LDFLAGS += -s MODULARIZE=0 \
-s EXPORT_NAME='Module' \
-s RESERVED_FUNCTION_POINTERS=20 \
--bind
#------------------------------------------------------------------------------
else
#------------------------------------------------------------------------------
CC = arm-none-eabi-gcc
CXX = arm-none-eabi-g++
AS = arm-none-eabi-gcc -x assembler-with-cpp
@ -317,6 +350,37 @@ AR = arm-none-eabi-ar
SIZE = arm-none-eabi-size
HEX = $(OBJCOPY) -O ihex
BIN = $(OBJCOPY) -O binary -S
CPUFLAGS += -mthumb -march=armv7e-m -mfloat-abi=hard -mfpu=fpv4-sp-d16
PLATFORM_FLAGS = -Wno-packed-bitfield-compat -Wall -fdata-sections -ffunction-sections
CXX_PLATFORM_FLAGS = -fno-rtti
C_LIST_FLAGS = -Wa,-a,-ad,-alms=$(BUILD)/$(notdir $(<:.c=.lst))
CXX_LIST_FLAGS = -Wa,-a,-ad,-alms=$(BUILD)/$(notdir $(<:.cc=.lst))
ASM_SOURCES = $(SDK)/startup_pgm.s
LINK_OPTS= \
-Wl,-Map=$(BUILD)/$(TARGET).map,--cref \
-Wl,--gc-sections \
-Wl,--wrap=_malloc_r
# System sources
C_INCLUDES += -I$(SDK)
C_SOURCES += $(SDK)/sys/pgm_syscalls.c
$(BUILD)/$(TARGET).elf: $(OBJECTS) Makefile
$(CC) $(OBJECTS) $(LDFLAGS) -o $@
$(TARGET).$(PGM): $(BUILD)/$(TARGET).elf Makefile $(CRCFIX)
$(OBJCOPY) --remove-section .qspi -O binary $< $(FLASH)
$(OBJCOPY) --remove-section .qspi -O ihex $< $(FLASH:.bin=.hex)
$(OBJCOPY) --only-section .qspi -O binary $< $(QSPI)
$(OBJCOPY) --only-section .qspi -O ihex $< $(QSPI:.bin=.hex)
$(TOOLS)/adjust_crc $(CRCFIX) $(QSPI)
$(TOOLS)/check_qspi_crc $(TARGET) $(BUILD)/$(TARGET)_qspi.bin src/$(VARIANT)/qspi_crc.h || ( rm -rf build/$(VARIANT) && exit 1)
$(TOOLS)/add_pgm_chsum $(BUILD)/$(TARGET)_flash.bin $@
$(SIZE) $<
wc -c $@
#------------------------------------------------------------------------------
endif
#------------------------------------------------------------------------------
#######################################
# CFLAGS
@ -326,12 +390,9 @@ AS_DEFS =
C_DEFS += -D__weak="__attribute__((weak))" -D__packed="__attribute__((__packed__))"
AS_INCLUDES =
CPUFLAGS += -mthumb -march=armv7e-m -mfloat-abi=hard -mfpu=fpv4-sp-d16
# compile gcc flags
ASFLAGS = $(CPUFLAGS) $(AS_DEFS) $(AS_INCLUDES) $(ASFLAGS_$(OPT)) -Wall -fdata-sections -ffunction-sections
CFLAGS = $(CPUFLAGS) $(C_DEFS) $(C_INCLUDES) $(CFLAGS_$(OPT)) -Wall -fdata-sections -ffunction-sections
ASFLAGS = $(CPUFLAGS) $(PLATFORM_FLAGS) $(AS_DEFS) $(AS_INCLUDES) $(ASFLAGS_$(OPT))
CFLAGS += $(CPUFLAGS) $(PLATFORM_FLAGS) $(C_DEFS) $(C_INCLUDES) $(CFLAGS_$(OPT))
CFLAGS += -Wno-misleading-indentation
DBGFLAGS = $(DBGFLAGS_$(OPT))
DBGFLAGS_debug = -g
@ -357,12 +418,7 @@ CFLAGS += -MD -MP -MF .dep/$(@F).d
# link script
LDSCRIPT = src/$(VARIANT)/stm32_program.ld
LIBDIR =
LDFLAGS = $(CPUFLAGS) -T$(LDSCRIPT) $(LIBDIR) $(LIBS) \
-Wl,-Map=$(BUILD)/$(TARGET).map,--cref \
-Wl,--gc-sections \
-Wl,--wrap=_malloc_r
LDFLAGS += $(CPUFLAGS) -T$(LDSCRIPT) $(LIBDIR) $(LIBS) $(LINK_OPTS)
#######################################
@ -378,29 +434,24 @@ vpath %.cc $(sort $(dir $(CXX_SOURCES)))
OBJECTS += $(addprefix $(BUILD)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))
CXXFLAGS = $(CFLAGS) -fno-exceptions -fno-rtti -Wno-packed-bitfield-compat
CXXFLAGS = $(CFLAGS) -fno-exceptions $(CXX_PLATFORM_FLAGS)
$(BUILD)/%.o: %.c Makefile | $(BUILD)/.exists
$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD)/$(notdir $(<:.c=.lst)) $< -o $@
$(CC) -c $(CFLAGS) $(C_LIST_FLAGS) $< -o $@
$(BUILD)/%.o: %.cc Makefile | $(BUILD)/.exists
$(CXX) -c $(CXXFLAGS) -Wa,-a,-ad,-alms=$(BUILD)/$(notdir $(<:.cc=.lst)) $< -o $@
$(CXX) -c $(CXXFLAGS) $(CXX_LIST_FLAGS) $< -o $@
$(BUILD)/%.o: %.cpp Makefile | $(BUILD)/.exists
$(CXX) -c $(CXXFLAGS) $(CXX_LIST_FLAGS) $< -o $@
$(BUILD)/%.o: %.s Makefile | $(BUILD)/.exists
$(AS) -c $(CFLAGS) $< -o $@
$(BUILD)/$(TARGET).elf: $(OBJECTS) Makefile
ifeq ($(VARIANT),wasm)
$(WASM_TARGET): $(OBJECTS) Makefile
$(CC) $(OBJECTS) $(LDFLAGS) -o $@
$(TARGET).$(PGM): $(BUILD)/$(TARGET).elf Makefile $(CRCFIX)
$(OBJCOPY) --remove-section .qspi -O binary $< $(FLASH)
$(OBJCOPY) --remove-section .qspi -O ihex $< $(FLASH:.bin=.hex)
$(OBJCOPY) --only-section .qspi -O binary $< $(QSPI)
$(OBJCOPY) --only-section .qspi -O ihex $< $(QSPI:.bin=.hex)
$(TOOLS)/adjust_crc $(CRCFIX) $(QSPI)
$(TOOLS)/check_qspi_crc $(TARGET) $(BUILD)/$(TARGET)_qspi.bin src/$(VARIANT)/qspi_crc.h || ( rm -rf build/$(VARIANT) && exit 1)
$(TOOLS)/add_pgm_chsum $(BUILD)/$(TARGET)_flash.bin $@
$(SIZE) $<
wc -c $@
endif
DECIMAL_CONSTANTS=pi e
DECIMAL_SOURCES=$(DECIMAL_CONSTANTS:%=src/decimal-%.h)

1
emsdk Submodule

@ -0,0 +1 @@
Subproject commit 4b9e83d629ec8a9468aa3bf388cec447ab771312

View file

@ -31,18 +31,44 @@
#include "dmcp.h"
#include "sim-dmcp.h"
#include <QBitmap>
#include <QGraphicsPixmapItem>
#include <QTimer>
#include <target.h>
SimScreen *SimScreen::theScreen = nullptr;
// A copy of the LCD buffer
pixword lcd_copy[sizeof(lcd_buffer) / sizeof(*lcd_buffer)];
#ifdef WASM
uintptr_t ui_update_pixmap()
// ----------------------------------------------------------------------------
// Recompute the pixmap
// ----------------------------------------------------------------------------
// This should be done on the RPL thread to get a consistent picture
{
// Monochrome screen
pixword mask = ~(~0U << color::BPP);
surface s(lcd_buffer, LCD_W, LCD_H, LCD_SCANLINE);
for (int y = 0; y < SIM_LCD_H; y++)
{
for (int xw = 0; xw < SIM_LCD_SCANLINE*color::BPP/32; xw++)
{
unsigned woffs = y * (SIM_LCD_SCANLINE*color::BPP/32) + xw;
if (uint32_t diffs = lcd_copy[woffs] ^ lcd_buffer[woffs])
lcd_copy[woffs] = lcd_buffer[woffs];
}
}
return uintptr_t(lcd_buffer);
}
#else // Qt simulator
#include <QBitmap>
#include <QGraphicsPixmapItem>
#include <QTimer>
SimScreen *SimScreen::theScreen = nullptr;
SimScreen::SimScreen(QWidget *parent)
// ----------------------------------------------------------------------------
@ -163,3 +189,4 @@ void SimScreen::refreshScreen()
QGraphicsView::update();
redraws++;
}
#endif // WASM

View file

@ -29,6 +29,8 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// ****************************************************************************
#ifndef WASM
#include <QGraphicsView>
#include <QGraphicsPixmapItem>
@ -70,4 +72,6 @@ public:
static uint redraw_count() { return theScreen->redraws; }
};
#endif // WASM
#endif // SIMSCREEN_H

View file

@ -33,11 +33,14 @@
#include "main.h"
#include "recorder.h"
#include "sim-dmcp.h"
#include "sim-rpl.h"
#include "target.h"
#include "tests.h"
#include "ui_sim-window.h"
#ifdef WASM
#include "emcc.h"
#else
#include "sim-rpl.h"
#include "ui_sim-window.h"
#include <iostream>
#include <QAudioDevice>
#include <QAudioFormat>
@ -53,6 +56,8 @@
#include <QtCore>
#include <QtGui>
#include <QtMath>
#endif // WASM
RECORDER(sim_keys, 16, "Recorder keys from the simulator");
RECORDER(sim_audio, 16, "Recorder keys from the simulator");
@ -61,6 +66,9 @@ extern bool run_tests;
extern bool db48x_keyboard;
extern bool shift_held;
extern bool alt_held;
#ifndef WASM
MainWindow *MainWindow::mainWindow = nullptr;
qreal MainWindow::devicePixelRatio = 1.0;
@ -1044,3 +1052,170 @@ bool tests::image_match(cstring file, int x, int y, int w, int h, bool force)
}
return data.toImage() == img.toImage();
}
#else // WASM
// ============================================================================
//
// Platform support on WASM
//
// ============================================================================
void ui_refresh()
// ----------------------------------------------------------------------------
// Request a refresh of the LCD
// ----------------------------------------------------------------------------
{
}
uint ui_refresh_count()
// ----------------------------------------------------------------------------
// Return the number of times the display was actually udpated
// ----------------------------------------------------------------------------
{
return 0;
}
void ui_screenshot()
// ----------------------------------------------------------------------------
// Take a screen snapshot
// ----------------------------------------------------------------------------
{
}
void ui_push_key(int k)
// ----------------------------------------------------------------------------
// Update display when pushing a key
// ----------------------------------------------------------------------------
{
// key_push(k);
}
void ui_ms_sleep(uint ms_delay)
// ----------------------------------------------------------------------------
// Suspend the current thread for the given interval in milliseconds
// ----------------------------------------------------------------------------
{
}
int ui_file_selector(const char *title,
const char *base_dir,
const char *ext,
file_sel_fn callback,
void *data,
int disp_new,
int overwrite_check)
// ----------------------------------------------------------------------------
// File selector function
// ----------------------------------------------------------------------------
{
return 0;
}
void ui_save_setting(const char *name, const char *value)
// ----------------------------------------------------------------------------
// Save some settings
// ----------------------------------------------------------------------------
{
}
size_t ui_read_setting(const char *name, char *value, size_t maxlen)
// ----------------------------------------------------------------------------
// Save some settings
// ----------------------------------------------------------------------------
{
return 0;
}
uint last_battery_ms = 0;
uint battery = 1000;
bool charging = false;
uint ui_battery()
// ----------------------------------------------------------------------------
// Return the battery voltage
// ----------------------------------------------------------------------------
{
uint now = sys_current_ms();
if (last_battery_ms < now - 1000)
last_battery_ms = now - 1000;
if (charging)
{
battery += (1000 - battery) * (now - last_battery_ms) / 6000;
if (battery >= 990)
charging = false;
}
else
{
battery -= (now - last_battery_ms) / 10;
uint v = battery * (BATTERY_VMAX - BATTERY_VMIN) / 1000 + BATTERY_VMIN;
if (v < BATTERY_VLOW)
charging = true;
}
last_battery_ms = now;
return battery;
}
bool ui_charging()
// ----------------------------------------------------------------------------
// Return true if USB-powered or not
// ----------------------------------------------------------------------------
{
return charging;
}
void ui_start_buzzer(uint frequency)
// ----------------------------------------------------------------------------
// Start buzzer at given frequency
// ----------------------------------------------------------------------------
{
}
void ui_stop_buzzer()
// ----------------------------------------------------------------------------
// Stop buzzer in simulator
// ----------------------------------------------------------------------------
{
}
void ui_run_rpl()
// ----------------------------------------------------------------------------
// Thread entry point
// ----------------------------------------------------------------------------
{
program_main();
}
int ui_wrap_io(file_sel_fn callback, const char *path, void *data, bool)
// ----------------------------------------------------------------------------
// Wrap I/Os into thread safety / file sync
// ----------------------------------------------------------------------------
{
cstring name = path;
for (cstring p = path; *p; p++)
if (*p == '/' || *p == '\\')
name = p + 1;
return callback(path, name, data);
}
#endif // WASM

View file

@ -29,6 +29,8 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// ****************************************************************************
#ifndef WASM
#include "sim-rpl.h"
#include "tests.h"
#include "ui_sim-window.h"
@ -190,12 +192,5 @@ static void postToThread(F && fun, QThread *thread = qApp->thread())
QMetaObject::invokeMethod(obj, std::forward<F>(fun));
}
// ============================================================================
//
// Audio support (lifted from QT audiooutput example)
//
// ============================================================================
#endif // WASM
#endif // SIM_WINDOW_H

View file

@ -316,6 +316,25 @@ bool power_check(bool draw_off_image)
}
#ifdef WASM
uint memory_size = 100;
volatile uint test_command = 0;
bool noisy_tests = false;
bool tests::running = false;
void ui_init()
// ----------------------------------------------------------------------------
// Initialization for the JavaScript version
// ----------------------------------------------------------------------------
{
program_init();
redraw_lcd(true);
last_keystroke_time = sys_current_ms();
}
#endif // WASM
extern "C" void program_main()
// ----------------------------------------------------------------------------
// DMCP main entry point and main loop

View file

@ -101,4 +101,10 @@ int ui_wrap_io(file_sel_fn callback,
void *data,
bool writing);
#ifdef WASM
void ui_init();
uintptr_t ui_update_pixmap();
void ui_run_rpl();
# endif // WASM
//
#endif // SIM_DMCP

1
src/wasm/dmcp.cc Symbolic link
View file

@ -0,0 +1 @@
../../sim/dmcp.cpp

1
src/wasm/dmcp.h Symbolic link
View file

@ -0,0 +1 @@
../../sim/dmcp.h

1
src/wasm/dmcp_fonts.c Symbolic link
View file

@ -0,0 +1 @@
../../sim/dmcp_fonts.c

10
src/wasm/emcc.h Normal file
View file

@ -0,0 +1,10 @@
#include <emscripten/bind.h>
using namespace emscripten;
EMSCRIPTEN_BINDINGS(my_module) {
function("ui_battery", &ui_battery);
function("ui_update_pixmap", &ui_update_pixmap);
function("ui_run_rpl", &ui_run_rpl);
function("ui_init", &ui_init);
function("ui_push_key", &key_push);
}

1
src/wasm/ff_ifc.h Symbolic link
View file

@ -0,0 +1 @@
../../sim/ff_ifc.h

356
src/wasm/index.html Normal file
View file

@ -0,0 +1,356 @@
<!DOCTYPE html>
<html>
<head>
<title>DB48X</title>
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=0.9, maximum-scale=1.0" />
<link rel="manifest" href="manifest.json">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="apple-touch-icon" href="./logo.png">
<script src="./db48x.js"></script>
<style>
html, body {
padding: 0;
margin: auto auto;
background-color: black;
overflow: hidden;
display: flex;
height: 100vh;
width: 100vw;
flex-direction: row;
align-items: center;
justify-items: center;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
canvas {
display: block;
margin: 0 auto;
}
#keyCanvas {
background-image: url('keyboard-db48x.png');
background-repeat: no-repeat;
background-size: 100% 100%;
margin: 0 auto;
align-self: center;
width: 100%;
}
#lcdCanvas {
align-self: center;
width: 100%;
/* IE, only works on <img> tags */
-ms-interpolation-mode: nearest-neighbor;
/* Firefox */
image-rendering: crisp-edges;
/* Chromium + Safari */
image-rendering: pixelated;
}
#calcContainer {
margin: auto;
grid-column: 1 / 1;
grid-row: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-items: center;
}
</style>
</head>
<body>
<div id="calcContainer">
<canvas id="lcdCanvas"></canvas>
<canvas id="keyCanvas"></canvas>
</div>
<script>
document.getElementsByTagName('body')[0].onscroll = function(event) {
event.preventDefault();
};
const DEBUG = false;
const SIM_LCD_H = 240; // Height of the LCD
const SIM_LCD_SCANLINE = 416; // Width of the LCD scanline
const SIM_LCD_OFFSET = 16;
const BPP = 1; // Bits per pixel
const WORD_SIZE = 32; // Size of the word in bits
const MASK = (1 << BPP) - 1; // Mask to extract BPP bits
const KB_RATIO = 1.3; // Keyboard aspect ratio
let keyBuffer = []; // keyBuffer
let threadSince = 0;
let threadInterval = 50; // 50ms
// Keyboard mapping with key areas
const key_map = [
[ 38, 0.03, 0.15, 0.03, 0.10 ], // Qt::Key_F1,
[ 39, 0.20, 0.32, 0.03, 0.10 ], // Qt::Key_F2,
[ 40, 0.345, 0.47, 0.03, 0.10 ], // Qt::Key_F3,
[ 41, 0.52, 0.63, 0.03, 0.10 ], // Qt::Key_F4,
[ 42, 0.68, 0.80, 0.03, 0.10 ], // Qt::Key_F5,
[ 43, 0.83, 0.95, 0.03, 0.10 ], // Qt::Key_F6,
[ 1, 0.03, 0.15, 0.15, 0.22 ], // Qt::Key_A,
[ 2, 0.20, 0.32, 0.15, 0.22 ], // Qt::Key_B,
[ 3, 0.345, 0.47, 0.15, 0.22 ], // Qt::Key_C,
[ 4, 0.52, 0.63, 0.15, 0.22 ], // Qt::Key_D,
[ 5, 0.68, 0.80, 0.15, 0.22 ], // Qt::Key_E,
[ 6, 0.83, 0.95, 0.15, 0.22 ], // Qt::Key_F,
[ 7, 0.03, 0.15, 0.275, 0.345 ], // Qt::Key_G,
[ 8, 0.20, 0.32, 0.275, 0.345 ], // Qt::Key_H,
[ 9, 0.345, 0.47, 0.275, 0.345 ], // Qt::Key_I,
[ 10, 0.52, 0.63, 0.275, 0.345 ], // Qt::Key_J,
[ 11, 0.68, 0.80, 0.275, 0.345 ], // Qt::Key_K,
[ 12, 0.83, 0.95, 0.275, 0.345 ], // Qt::Key_L,
[ 13, 0.03, 0.32, 0.40, 0.47 ], // Qt::Key_Return,
[ 14, 0.345, 0.47, 0.40, 0.47 ], // Qt::Key_M,
[ 15, 0.51, 0.64, 0.40, 0.47 ], // Qt::Key_N,
[ 16, 0.68, 0.80, 0.40, 0.47 ], // Qt::Key_O,
[ 17, 0.83, 0.95, 0.40, 0.47 ], // Qt::Key_Backspace
[ 18, 0.03, 0.15, 0.52, 0.59 ], // Qt::Key_Up,
[ 19, 0.23, 0.36, 0.52, 0.59 ], // Qt::Key_7,
[ 20, 0.42, 0.56, 0.52, 0.59 ], // Qt::Key_8,
[ 21, 0.62, 0.75, 0.52, 0.59 ], // Qt::Key_9,
[ 22, 0.81, 0.95, 0.52, 0.59 ], // Qt::Key_Slash,
[ 23, 0.03, 0.15, 0.645, 0.715 ], // Qt::Key_Down,
[ 24, 0.23, 0.36, 0.645, 0.715 ], // Qt::Key_4,
[ 25, 0.42, 0.56, 0.645, 0.715 ], // Qt::Key_5,
[ 26, 0.62, 0.75, 0.645, 0.715 ], // Qt::Key_6,
[ 27, 0.81, 0.95, 0.645, 0.715 ], // Qt::Key_Asterisk,
[ 28, 0.028, 0.145, 0.77, 0.84 ], // Qt::Key_Control,
[ 29, 0.23, 0.36, 0.77, 0.84 ], // Qt::Key_1,
[ 30, 0.42, 0.56, 0.77, 0.84 ], // Qt::Key_2,
[ 31, 0.62, 0.75, 0.77, 0.84 ], // Qt::Key_3,
[ 32, 0.81, 0.95, 0.77, 0.84 ], // Qt::Key_Minus,
[ 33, 0.03, 0.15, 0.89, 0.97 ], // Qt::Key_Escape,
[ 34, 0.23, 0.36, 0.89, 0.97 ], // Qt::Key_0,
[ 35, 0.42, 0.55, 0.89, 0.97 ], // Qt::Key_Period,
[ 36, 0.62, 0.74, 0.89, 0.97 ], // Qt::Key_Question,
[ 37, 0.81, 0.95, 0.89, 0.97 ], // Qt::Key_Plus,
];
const keystroke_map = {
"0": 34,
"1": 29,
"2": 30,
"3": 31,
"4": 24,
"5": 25,
"6": 26,
"7": 19,
"8": 20,
"9": 21,
".": 35,
"+": 37,
"-": 32,
"*": 27,
"/": 22,
"'": 6,
"=": 36,
" ": 36,
"`": 1,
"Alt": 1,
"Meta": 1,
"Enter": 13,
"Backspace": 17,
"Escape": 33,
"F1": 38,
"F2": 39,
"F3": 40,
"F4": 41,
"F5": 42,
"F6": 43,
"Up": 18,
"Down": 23,
"Shift": 28,
"Control": 28,
"a": 1,
"b": 2,
"c": 3,
"d": 4,
"e": 5,
"f": 6,
"g": 7,
"h": 8,
"i": 9,
"j": 10,
"k": 11,
"l": 12,
"m": 14,
"n": 15,
"o": 16,
"p": 36,
"q": 20,
"r": 21,
"s": 22,
"t": 24,
"u": 25,
"v": 26,
"w": 27,
"x": 29,
"y": 30,
"z": 31,
"ArrowLeft": 18,
"ArrowRight": 23,
"ArrowUp": 18,
"ArrowDown": 23,
};
// canvas for the keyboard
const canvas_key = document.getElementById('keyCanvas');
const KEY_H = SIM_LCD_SCANLINE * KB_RATIO;
const KEY_W = SIM_LCD_SCANLINE - SIM_LCD_OFFSET;
canvas_key.width = KEY_W;
canvas_key.height = KEY_H;
const ctx_key = canvas_key.getContext('2d');
ctx_key.clearRect( 0, 0, KEY_W, KEY_H);
// debug presses
if (DEBUG) {
ctx_key.strokeStyle = "red";
for (let i=0; i<key_map.length; i++) {
ctx_key.beginPath();
ctx_key.moveTo(key_map[i][1]*KEY_W, key_map[i][3]*KEY_H);
ctx_key.lineTo(key_map[i][1]*KEY_W, key_map[i][4]*KEY_H);
ctx_key.lineTo(key_map[i][2]*KEY_W, key_map[i][4]*KEY_H);
ctx_key.lineTo(key_map[i][2]*KEY_W, key_map[i][3]*KEY_H);
ctx_key.lineTo(key_map[i][1]*KEY_W, key_map[i][3]*KEY_H);
ctx_key.stroke();
}
}
// get the mouse/touch position
const getMousePosition = function(canvas, event) {
let rect = canvas.getBoundingClientRect();
let x = event.clientX - rect.left;
let y = event.clientY - rect.top;
if (DEBUG) {
console.log("clicked x: " + x, " y: " + y);
}
// should use html elements for the keys in future
for (let i=0; i<key_map.length; i++) {
if (x > key_map[i][1]*KEY_W && x < key_map[i][2]*KEY_W && y > key_map[i][3]*KEY_H && y < key_map[i][4]*KEY_H) {
if (DEBUG) {
console.log("clicked:", key_map[i][0]);
}
keyBuffer.push(key_map[i][0]);
}
}
}
const processKey = function() {
if (keyBuffer.length > 0) {
let key = keyBuffer.shift();
if (DEBUG) {
console.log("processed key:", key);
}
Module.ui_push_key(key);
Module.ui_push_key(0);
}
}
// add an event listener to the canvas so we can parse the key presses
canvas_key.addEventListener("mousedown", function (e) {
getMousePosition(canvas_key, e);
});
// add an event listener to the canvas so we can parse the key presses
document.addEventListener("keyup", (event)=>{
// check that event.key matches a key in the keystroke_map
if (Object.keys(keystroke_map).includes(event.key)) {
keyBuffer.push(keystroke_map[event.key]);
} else {
if (DEBUG) {
console.log("Key not found in keystroke_map: ", event.key);
}
}
});
// draw the LCD panel
const canvas = document.getElementById('lcdCanvas');
canvas.width = SIM_LCD_SCANLINE - SIM_LCD_OFFSET;
canvas.height = SIM_LCD_H;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(canvas.width, canvas.height);
// parse the LCD data and draw to the canvas
const drawBitmap = function(canvasId, dataArray, width, height) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
canvas.width = SIM_LCD_SCANLINE - SIM_LCD_OFFSET;
canvas.height = SIM_LCD_H;
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
for (let i = 0; i < dataArray.length; i++) {
const bitmask = dataArray[i];
for (let bit = 0; bit < 32; bit++) {
const value = (bitmask >> bit) & 1;
const pixelIndex = i * 32 + bit;
const x = pixelIndex % width;
const y = Math.floor(pixelIndex / width);
const mirroredX = width - 1 - x; // Mirroring the x position
const index = (y * width + mirroredX) * 4;
// const index = (y * width + x) * 4;
const color = value === 1 ? 213 : 0;
data[index] = color;
data[index + 1] = color;
data[index + 2] = color;
data[index + 3] = 255; // Alpha channel
}
}
// offset -16 pixels to the left
ctx.putImageData(imageData, -SIM_LCD_OFFSET, 0);
}
Module.onRuntimeInitialized = function() {
// initialise the calculator
console.log("loaded wasm...");
console.log("ui_init()",Module.ui_init());
// fetch and display the LCD
const getLCD = function() {
var heap = Module.ui_update_pixmap();
const lcd_buffer = []
for (let v=0; v < (13*240); v++) {
lcd_buffer.push( Module.HEAP32[heap/Int32Array.BYTES_PER_ELEMENT+v] )
}
drawBitmap('lcdCanvas', lcd_buffer, SIM_LCD_SCANLINE, SIM_LCD_H);
}
// thread to draw the LCD and run the calculator
const drawThread = function() {
let now = new Date().getTime();
let dt = now - threadSince;
processKey();
if (dt > threadInterval) {
Module.ui_run_rpl();
getLCD();
threadSince = now;
}
window.requestAnimationFrame(drawThread);
}
window.requestAnimationFrame(drawThread);
}
</script>
</body>
</html>

1
src/wasm/lft_ifc.h Symbolic link
View file

@ -0,0 +1 @@
../../sim/lft_ifc.h

1
src/wasm/main.h Symbolic link
View file

@ -0,0 +1 @@
../dm32/main.h

1
src/wasm/qspi_crc.h Symbolic link
View file

@ -0,0 +1 @@
../dm32/qspi_crc.h

1
src/wasm/sim-dmcp.h Symbolic link
View file

@ -0,0 +1 @@
../dmcp/sim-dmcp.h

1
src/wasm/sim-screen.cc Symbolic link
View file

@ -0,0 +1 @@
../../sim/sim-screen.cpp

1
src/wasm/sim-screen.h Symbolic link
View file

@ -0,0 +1 @@
../../sim/sim-screen.h

1
src/wasm/sim-window.cc Symbolic link
View file

@ -0,0 +1 @@
../../sim/sim-window.cpp

1
src/wasm/sim-window.h Symbolic link
View file

@ -0,0 +1 @@
../../sim/sim-window.h