Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
707 changes: 418 additions & 289 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ ratatui = { version = "0.30.0-beta.0", default-features = false, optional = true

# features=graphics
pixels = { git = "https://github.com/parasyte/pixels.git", features = [], optional = true }
game-loop = { version = "=1.1.0", features = ["winit"], optional = true }
winit_input_helper = { version = "=0.15", optional = true }
game-loop = { version = "=1.3.0", features = ["winit"], optional = true }
winit_input_helper = { version = "=0.17.0", optional = true }
winit = { version = "=0.30.12", optional = true }
pollster = { version = "0.4.0", optional = true }

# features=wasm
web-time = { version = "1.1.0", optional = true }

# features=run-wasm
cargo-run-wasm = { version = "0.4.0", optional = true }

Expand All @@ -57,11 +61,11 @@ debug = true

[features]
default = ["graphics", "tui", "pty", "demo", "vram-dump"]
wasm = ["graphics", "embed-rom", "ssu/wasm"]
wasm = ["graphics", "embed-rom", "ssu/wasm", "dep:web-time"]
pc-trace = []
pty = ["ssu/pty"]
tui = ["dep:ratatui", "ratatui/crossterm", "dep:i8051-debug-tui"]
graphics = ["dep:pixels", "dep:game-loop", "dep:winit_input_helper", "dep:pollster"]
graphics = ["dep:pixels", "dep:winit", "dep:winit_input_helper", "dep:pollster"]
embed-rom = []
run-wasm = ["dep:cargo-run-wasm"]
demo = ["dep:vt-push-parser", "dep:ratatui"]
Expand Down
69 changes: 61 additions & 8 deletions crates/ssu/src/session/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ extern "C" {}

pub struct WasmSession {
read_fn: js_sys::Function,
read_buffer: js_sys::Uint8Array,
read_buffer_index: u32,
write_fn: js_sys::Function,
}

Expand All @@ -27,21 +29,63 @@ fn to_io_error(e: impl AsRef<wasm_bindgen::JsValue>) -> io::Error {

impl WasmSession {
pub fn new(read_fn: String, write_fn: String) -> io::Result<WasmSession> {
let read_fn = js_sys::Function::from(js_sys::eval(&read_fn).map_err(to_io_error)?);
// Convert the async function into one that returns null if a promise is resolving.
let read_fn = js_sys::Function::from(
js_sys::eval(
&r#"
(function() {
const LOW_WATER_MARK = 2;
const async_fn = __FN__;
let promise = undefined;
let next = [];
let then = (value) => {
next.push(value);
if (next.length < LOW_WATER_MARK && promise === undefined) {
promise = async_fn().then(then);
} else {
promise = undefined;
}
};

return function() {
if (next.length === 0) {
if (promise === undefined) {
promise = async_fn().then(then);
}
return null;
}
const result = next.shift();
if (next.length < LOW_WATER_MARK && promise === undefined) {
promise = async_fn().then(then);
}
return result;
};
})();
"#
.replace("__FN__", &read_fn),
)
.map_err(to_io_error)?,
);
if !read_fn.is_function() {
return Err(io::Error::other("read_fn was not a function"));
}
let write_fn = js_sys::Function::from(js_sys::eval(&write_fn).map_err(to_io_error)?);
if !write_fn.is_function() {
return Err(io::Error::other("write_fn was not a function"));
}
Ok(Self { read_fn, write_fn })
Ok(Self {
read_fn,
write_fn,
read_buffer: js_sys::Uint8Array::new_with_length(0),
read_buffer_index: 0,
})
}

pub fn new_message_channel() -> io::Result<WasmSession> {
let array = js_sys::eval(
r#"
(function () {
const LOW_WATER_MARK = 2;
let messageChannel = new MessageChannel();
const interval = setInterval(function () {
window.parent.postMessage({ type: "ready" }, "*");
Expand All @@ -53,9 +97,7 @@ impl WasmSession {
port.onmessage = function (event) {
reading = false;
const data = new Uint8Array(event.data.data);
for (let i = 0; i < data.length; i++) {
readQueue.push(data[i]);
}
readQueue.push(data);
};

window.onmessage = function (event) {
Expand All @@ -66,7 +108,7 @@ impl WasmSession {
};

function js_read(data) {
if (readQueue.length === 0 && !reading) {
if (readQueue.length < LOW_WATER_MARK && !reading) {
reading = true;
port.postMessage({ type: "read" });
}
Expand All @@ -89,20 +131,31 @@ impl WasmSession {
if !write_fn.is_function() {
return Err(io::Error::other("write_fn was not a function"));
}
Ok(Self { read_fn, write_fn })
Ok(Self {
read_fn,
write_fn,
read_buffer: js_sys::Uint8Array::new_with_length(0),
read_buffer_index: 0,
})
}
}

impl SessionEndpoint for WasmSession {
fn recv(&mut self) -> Ticked {
if self.read_buffer_index < self.read_buffer.byte_length() as _ {
self.read_buffer_index += 1;
return Ticked::Byte(self.read_buffer.get_index(self.read_buffer_index - 1));
}
let b = self.read_fn.call0(&JsValue::UNDEFINED);
let Ok(b) = b else {
return Ticked::Idle;
};
if b.is_null_or_undefined() {
return Ticked::Idle;
}
Ticked::Byte(b.as_f64().unwrap_or_default() as u8)
self.read_buffer = js_sys::Uint8Array::from(b);
self.read_buffer_index = 1;
Ticked::Byte(self.read_buffer.get_index(0))
}

fn send(&mut self, b: u8) {
Expand Down
51 changes: 51 additions & 0 deletions examples/wasm/index-cross-origin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blaze: VT420 Emulator</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
}
iframe {
border: none;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<script src="./index.js"></script>
<iframe src="../../wasm/#message-channel" width="100%" height="100%" scrolling="no"></iframe>
<script>
const iframe = document.querySelector("iframe");
iframe.addEventListener("load", function () {
window.addEventListener("message", function (event) {
if (event.data.type === "ready") {
iframe.contentWindow.postMessage({ type: "ready" }, "*");
} else if (event.data.type === "open") {
const port = event.ports[0];
port.onmessage = function (event) {
if (event.data.type === "write") {
js_write(event.data.data);
} else if (event.data.type === "read") {
(async () => {
const data = await js_read();
port.postMessage({ type: "data", data: data.buffer }, [data.buffer]);
})();
} else {
console.warn("unknown message type", event.data.type);
}
};
}
});
});
</script>
</body>
</html>
26 changes: 1 addition & 25 deletions examples/wasm/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,6 @@
</head>
<body>
<script src="./index.js"></script>
<iframe src="../../wasm/#message-channel" width="100%" height="100%" scrolling="no"></iframe>
<script>
const iframe = document.querySelector("iframe");
iframe.addEventListener("load", function () {
window.addEventListener("message", function (event) {
if (event.data.type === "ready") {
iframe.contentWindow.postMessage({ type: "ready" }, "*");
} else if (event.data.type === "open") {
const port = event.ports[0];
port.onmessage = function (event) {
if (event.data.type === "write") {
js_write(event.data.data);
} else if (event.data.type === "read") {
(async () => {
const data = await js_read();
port.postMessage({ type: "data", data: data.buffer }, [data.buffer]);
})();
} else {
console.warn("unknown message type", event.data.type);
}
};
}
});
});
</script>
<iframe src="../../wasm/#wasm window.parent.js_read window.parent.js_write" width="100%" height="100%" scrolling="no"></iframe>
</body>
</html>
25 changes: 21 additions & 4 deletions examples/wasm/termtris/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
const TICK_INTERVAL = 52;
const BUFFER_HIGH_WATER_MARK = 256;

class Termtris {
_readQueue = [];
_readBufferSize = 0;
_readWaker = null;
_approxTime = 0;
_nextTime = 0;
_wasmInstance = null;
_startTime;
_missedTicks = 0;

constructor() {
this._startTime = Date.now();
Expand All @@ -16,24 +19,37 @@ class Termtris {
_tick() {
this._approxTime = Date.now() - this._startTime;
if (this._wasmInstance !== null) {
if (this._readQueue.length === 0) {
if (this._nextTime < this._approxTime) {
if (this._nextTime < this._approxTime) {
if (this._readBufferSize < BUFFER_HIGH_WATER_MARK) {
// We could use the next tick time here but it's fine to just
// call the game update more often instead.
this._nextTime = this._wasmInstance.exports._update(this._approxTime) + this._approxTime;
} else {
this._missedTicks++;
}
}
}
}

async read() {
if (this._readQueue.length > 0) {
return this._readQueue.shift();
return this._doSyncRead();
}
const readPromise = new Promise((resolve, reject) => this._readWaker = resolve);
await readPromise;
this._readWaker = null;
return this._readQueue.shift();
return this._doSyncRead();
}

_doSyncRead() {
const result = this._readQueue.shift();
this._readBufferSize -= result.length;
if (this._missedTicks > 0 && this._readBufferSize < BUFFER_HIGH_WATER_MARK) {
console.log("restoring tick", this._missedTicks);
this._missedTicks = 0;
this._tick();
}
return result;
}

write(byte) {
Expand Down Expand Up @@ -107,6 +123,7 @@ function loadTermtris() {
console.log(new TextDecoder().decode(bytes));
if (fd == 1) {
termtris._readQueue.push(bytes);
termtris._readBufferSize += bytes.length;
}
written += len;
}
Expand Down
2 changes: 1 addition & 1 deletion src/host/lk201/winit.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use game_loop::winit::keyboard::{Key, KeyCode};
use winit::keyboard::{Key, KeyCode};
use winit_input_helper::WinitInputHelper;

use lk201::{LK201Sender, SpecialKey};
Expand Down
2 changes: 1 addition & 1 deletion src/host/screen/framebuffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ impl FramebufferRender {
let width = if render.row_flags.is_80 { 10 } else { 6 };
let mut offset = render.row_offset;
for mut y in 0..render.row_flags.row_height as usize {
if render.row + y >= crate::host::wgpu::WIDTH as _ {
if render.row + y >= crate::host::wgpu::REAL_WIDTH as _ {
break;
}
if c == 0 && !render.row_flags.is_80 {
Expand Down
Loading