Compare commits

...

3 Commits

Author SHA1 Message Date
c124e60168 Misc Go interop fixes 2024-04-27 12:16:04 -07:00
042f2dca79 Get it running in Chrome! 2024-04-22 21:10:09 -07:00
aae336ca66 Basic output system 2024-04-22 19:57:27 -07:00
29 changed files with 2895 additions and 113 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[alias]
run-wasm = "run --release --package run-wasm --"

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
// "go.lintTool": "golint"
"go.toolsEnvVars": {
"GOOS": "wasip1",
"GOARCH": "wasm"
}
}

2414
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,9 @@ anyhow = "1.0.82"
[workspace] [workspace]
members = [ members = [
"run-wasm",
"crates/editor", "crates/editor",
"crates/minifb_host", "crates/minifb_host",
"crates/player" "crates/player",
"crates/web_runner"
] ]

View File

@@ -4,3 +4,12 @@ To launch the editor:
$ cargo run --manifest-path crates\editor\Cargo.toml $ cargo run --manifest-path crates\editor\Cargo.toml
``` ```
To run the web runner:
```
$ cargo run-wasm --manifest-path crates\web_runner\Cargo.toml --release --package web_runner
```
To build the web runner:
```
$ wasm-pack build crates\web_runner
```

View File

@@ -7,5 +7,6 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.82" anyhow = "1.0.82"
minifb_host = { path = "../minifb_host" }
player = { path = "../player" } player = { path = "../player" }
viperid = { path = "../.." } viperid = { path = "../.." }

View File

@@ -3,8 +3,7 @@ use std::{fs::File, io::Read, path::{Path, PathBuf}, process::Command};
use viperid::VResult; use viperid::VResult;
pub struct GoBuilder { pub struct GoBuilder {
project_directory: PathBuf, project_directory: PathBuf
entry_point: PathBuf
} }
impl GoBuilder { impl GoBuilder {
@@ -27,15 +26,9 @@ impl GoBuilder {
anyhow::bail!("project directory must exist: {}", project_directory.to_string_lossy()) anyhow::bail!("project directory must exist: {}", project_directory.to_string_lossy())
} }
let entry_point = project_directory.join("main.go");
if !entry_point.exists() {
anyhow::bail!("entry point must exist: {}", entry_point.to_string_lossy())
}
// OK, we should be able to do things with Go without any errors // OK, we should be able to do things with Go without any errors
Ok(GoBuilder { Ok(GoBuilder {
project_directory: PathBuf::from(project_directory), project_directory: PathBuf::from(project_directory),
entry_point
}) })
} }
@@ -47,24 +40,17 @@ impl GoBuilder {
self.project_directory.to_string_lossy() self.project_directory.to_string_lossy()
); );
} }
if !self.entry_point.exists() {
anyhow::bail!(
"entry point disappeared: {}",
self.entry_point.to_string_lossy()
);
}
let project_directory = &self.project_directory; let build_directory = self.project_directory.join("build");
let entry_point = &self.entry_point;
let build_directory = project_directory.join("build");
let wasm_name = build_directory.join("game.wasm"); let wasm_name = build_directory.join("game.wasm");
let mut child = Command::new("go") let mut child = Command::new("go")
.args(["build", "-o"]).arg(&wasm_name) .arg("-C").arg(self.project_directory.clone())
.args(["build", "-o"]).arg("build/game.wasm")
.env("GOOS", "wasip1") .env("GOOS", "wasip1")
.env("GOARCH", "wasm") .env("GOARCH", "wasm")
.arg("-trimpath") .arg("-trimpath")
.arg(entry_point) .arg(".")
.spawn()?; .spawn()?;
let status = child.wait()?; let status = child.wait()?;
if !status.success() { if !status.success() {

View File

@@ -1,5 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use player::HOSTED_DEVICE;
use viperid::VResult; use viperid::VResult;
use crate::go_builder::GoBuilder; use crate::go_builder::GoBuilder;
@@ -10,14 +11,14 @@ fn main() -> VResult<()> {
let builder = GoBuilder::new( let builder = GoBuilder::new(
&PathBuf::from("example_project"))?; &PathBuf::from("example_project"))?;
let wasm = builder.build()?; let wasm = builder.build()?;
let mut executor = player::Executor::new(&wasm)?; let mut executor = player::Executor::new(&wasm)?;
while executor.is_running() { minifb_host::host(HOSTED_DEVICE.with(|d| d.share()), || {
println!("update started");
executor.update(); executor.update();
println!("update completed");
executor.get_error()?; executor.get_error()?;
} Ok(())
})?;
Ok(()) Ok(())
} }

View File

@@ -6,3 +6,5 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
minifb = "0.25.0"
viperid = { path = "../.." }

View File

@@ -0,0 +1,27 @@
use minifb::{Window, WindowOptions};
use viperid::{Device, VResult, SCREEN_H, SCREEN_W};
pub fn host(
device: Device,
mut update: impl FnMut() -> VResult<()>
) -> VResult<()> {
let mut window = Window::new(
"viperid",
SCREEN_W,
SCREEN_H,
WindowOptions::default()
)?;
// limit to 60FPS
window.limit_update_rate(Some(std::time::Duration::from_micros(16600)));
let mut buffer: Vec<u32> = vec![0; SCREEN_W * SCREEN_H];
while window.is_open() {
update()?;
device.shared.screen.to_bitmap(&mut buffer);
window.update_with_buffer(&buffer, SCREEN_W, SCREEN_H)?;
}
Ok(())
}

View File

@@ -8,4 +8,5 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.82" anyhow = "1.0.82"
viperid = { path = "../.." } viperid = { path = "../.." }
wasm-timer = "0.2.5"
wasmi = "0.31.2" wasmi = "0.31.2"

View File

@@ -3,14 +3,23 @@ use std::fmt::Display;
use viperid::VResult; use viperid::VResult;
use wasmi::{core::{HostError, Trap}, Caller, Linker}; use wasmi::{core::{HostError, Trap}, Caller, Linker};
use crate::executor::ExecutorState; use crate::{executor::ExecutorState, HOSTED_DEVICE};
pub(crate) fn integrate(linker: &mut Linker<ExecutorState>) -> VResult<()> { pub(crate) fn integrate(linker: &mut Linker<ExecutorState>) -> VResult<()> {
linker.func_wrap("viperid", "Pset", pset)?;
linker.func_wrap("viperid", "YieldFrame", yield_frame)?; linker.func_wrap("viperid", "YieldFrame", yield_frame)?;
Ok(()) Ok(())
} }
fn pset(mut _caller: Caller<ExecutorState>, x: i32, y: i32, color: i32) -> Result<(), Trap> {
HOSTED_DEVICE.with(|dev| {
dev.shared.screen.pset(x, y, (color & 0xff) as u8);
Ok(())
})
}
fn yield_frame(_caller: Caller<ExecutorState>) -> Result<(), Trap> { fn yield_frame(_caller: Caller<ExecutorState>) -> Result<(), Trap> {
Err(Trap::from(YieldFrame {})) Err(Trap::from(YieldFrame {}))
} }

View File

@@ -11,13 +11,13 @@ pub struct Executor {
} }
pub(crate) struct ExecutorState { pub(crate) struct ExecutorState {
wasi: wasi::StockWasi wasi: wasi::StockWasi,
} }
impl ExecutorState { impl ExecutorState {
fn new() -> Self { fn new() -> Self {
ExecutorState { ExecutorState {
wasi: StockWasi::new() wasi: StockWasi::new(),
} }
} }

View File

@@ -0,0 +1,7 @@
use std::thread_local;
use viperid::Device;
thread_local! {
pub static HOSTED_DEVICE: Device = Device::new();
}

View File

@@ -1,5 +1,7 @@
mod engine_api; mod engine_api;
mod executor; mod executor;
mod wasi; mod wasi;
mod hosted_device;
pub use executor::Executor; pub use executor::Executor;
pub use hosted_device::HOSTED_DEVICE;

View File

@@ -1,5 +1,3 @@
use std::time::SystemTime;
use wasmi::core::Trap; use wasmi::core::Trap;
use super::MinimalPreview1; use super::MinimalPreview1;
@@ -50,7 +48,7 @@ impl MinimalPreview1 for StockWasi {
println!("clock_time_get: {} {} {}", id, precision, outptr); println!("clock_time_get: {} {} {}", id, precision, outptr);
// TODO: Actually fetch time // TODO: Actually fetch time
let duration = let duration =
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) wasm_timer::SystemTime::now().duration_since(wasm_timer::SystemTime::UNIX_EPOCH)
.expect("time must be post-epoch"); .expect("time must be post-epoch");
write_u64(memory, outptr, duration.as_nanos() as u64); write_u64(memory, outptr, duration.as_nanos() as u64);

View File

@@ -0,0 +1,30 @@
[package]
name = "web_runner"
version = "0.1.0"
edition = "2021"
[build]
target = "wasm32-unknown-unknown"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
optimize = ["log/release_max_level_warn"]
default = ["optimize"]
[dependencies]
error-iter = "0.4"
log = "0.4"
pixels = "0.13.0"
player = { path = "../player" }
viperid = { path = "../.." }
winit = "0.28"
winit_input_helper = "0.14"
console_error_panic_hook = "0.1"
console_log = "1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = ["GpuTextureFormat"] }
anyhow = "1.0.82"

View File

@@ -0,0 +1,128 @@
use std::rc::Rc;
use error_iter::ErrorIter as _;
use log::error;
use pixels::{Pixels, SurfaceTexture};
use player::HOSTED_DEVICE;
use viperid::{Device, VResult, SCREEN_H, SCREEN_W};
// https://github.com/parasyte/pixels/blob/main/examples/minimal-web/src/main.rs
use winit::{dpi::LogicalSize, event::Event, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder};
fn main() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Trace).expect("error initializing logger");
wasm_bindgen_futures::spawn_local(run());
}
async fn run() {
let wasm = include_bytes!("../../../example_project/build/game.wasm");
let mut executor =
player::Executor::new(wasm)
.expect("should have been able to create executor");
host(HOSTED_DEVICE.with(|d| d.share()), move || {
executor.update();
executor.get_error()?;
Ok(())
}).await.expect("error while hosting");
}
async fn host(
device: Device,
mut update: impl FnMut() -> VResult<()> + 'static
) -> VResult<()> {
let event_loop = EventLoop::new();
let size = LogicalSize::new(SCREEN_W as f64, SCREEN_H as f64);
let window = WindowBuilder::new()
.with_title("viperid")
.with_inner_size(size)
.with_min_inner_size(size)
.build(&event_loop)
.expect("WindowBuilder error");
let window = Rc::new(window);
#[cfg(target_arch = "wasm32")]
{
use wasm_bindgen::JsCast;
use winit::platform::web::WindowExtWebSys;
// Retrieve current width and height dimensions of browser client window
let get_window_size = || {
let client_window = web_sys::window().unwrap();
LogicalSize::new(
client_window.inner_width().unwrap().as_f64().unwrap(),
client_window.inner_height().unwrap().as_f64().unwrap(),
)
};
let window = Rc::clone(&window);
// Initialize winit window with current dimensions of browser client
// window.set_inner_size(get_window_size());
let client_window = web_sys::window().unwrap();
// Attach winit canvas to body element
web_sys::window()
.and_then(|win| win.document())
.and_then(|doc| doc.body())
.and_then(|body| {
body.append_child(&web_sys::Element::from(window.canvas()))
.ok()
})
.expect("couldn't append canvas to document body");
/*
// Listen for resize event on browser client. Adjust winit window dimensions
// on event trigger
let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |_e: web_sys::Event| {
let size = get_window_size();
window.set_inner_size(size)
}) as Box<dyn FnMut(_)>);
client_window
.add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
*/
}
// let mut input = WinitInputHelper::new();
let mut pixels = {
let window_size = window.inner_size();
let surface_texture =
SurfaceTexture::new(window_size.width, window_size.height, window.as_ref());
Pixels::new_async(SCREEN_W as u32, SCREEN_H as u32, surface_texture).await.expect("Pixels error")
};
event_loop.run(move |event, _, control_flow| {
if let Event::RedrawRequested(_) = event {
let frame = pixels.frame_mut();
if let Err(err) = update() {
log_anyhow_error("update", err);
*control_flow = ControlFlow::Exit;
return;
}
device.shared.screen.to_bitmap_slice(frame).expect("to_bitmap_slice error");
if let Err(err) = pixels.render() {
log_error("pixels.render", err);
*control_flow = ControlFlow::Exit;
return;
}
}
window.request_redraw();
})
}
fn log_error<E: std::error::Error + 'static>(method_name: &str, err: E) {
error!("{method_name}() failed: {err}");
for source in err.sources().skip(1) {
error!(" Caused by: {source}");
}
}
fn log_anyhow_error(method_name: &str, err: anyhow::Error) {
error!("{method_name}() failed: {err}");
}

View File

@@ -1 +1,3 @@
go 1.21.5 go 1.21.5
use .

View File

@@ -1,21 +1,25 @@
package main package main
import ( import "example_project/viperid"
"fmt"
)
func main() { func main() {
count := 0 t := 0
for { for {
fmt.Printf("Hello! %d", count) for y := 0; y < 120; y++ {
count++ for x := 0; x < 160; x++ {
viperid.Pset(
x,
y,
(x+y+t)%64,
)
}
}
// fmt.Printf("Hello! %d", count)
// count++
/* /*
debug.PrintStack() debug.PrintStack()
*/ */
YieldFrame() viperid.YieldFrame()
t += 1
} }
} }
//go:wasmimport viperid YieldFrame
//go:noescape
func YieldFrame()

View File

@@ -0,0 +1,13 @@
package viperid
func YieldFrame() {
yieldFrame()
}
func Pset(x int, y int, color int) {
pset(int32(x), int32(y), int32(color))
}
func Pget(x int, y int) int {
return int(pget(int32(x), int32(y)))
}

View File

@@ -0,0 +1,15 @@
//lint:file-ignore U1000 Ignore all unused code, it's generated
package viperid
//go:wasmimport viperid YieldFrame
//go:noescape
func yieldFrame()
//go:wasmimport viperid Pset
//go:noescape
func pset(x int32, y int32, color int32)
//go:wasmimport viperid Pget
//go:noescape
func pget(x int32, y int32) int32

7
run-wasm/Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[package]
name = "run-wasm"
version = "0.1.0"
edition = "2021"
[dependencies]
cargo-run-wasm = "0.3"

9
run-wasm/src/main.rs Normal file
View File

@@ -0,0 +1,9 @@
fn main() {
let css = r#"body {
background-color: #000;
margin: 0;
overflow: hidden;
}"#;
cargo_run_wasm::run_wasm_with_css(css);
}

View File

@@ -182,14 +182,18 @@ function dump(fname, colors, width, height, scalar) {
function dumpRust(fname, colors) { function dumpRust(fname, colors) {
var paletteString = ""; var paletteString = "";
paletteString += "pub const PALETTE: [[u8; 3]; " + palette.length + "] = [\n" paletteString += "pub const PALETTE: [u32; " + palette.length + "] = [\n"
for (var i = 0; i < colors.length; i++) { for (var i = 0; i < colors.length; i++) {
var c = colors[i]; var c = colors[i];
paletteString += (" [" + paletteString += (
clamp(255 * c.srgb.r) + ", " + " " +
clamp(255 * c.srgb.g) + ", " + (
clamp(255 * c.srgb.b) + clamp(255 * c.srgb.r) * 256 * 256 +
"],\n"); clamp(255 * c.srgb.g) * 256 +
clamp(255 * c.srgb.b)
) +
",\n"
);
} }
paletteString += "];\n" paletteString += "];\n"
fs.writeFileSync(fname, paletteString); fs.writeFileSync(fname, paletteString);

35
src/device.rs Normal file
View File

@@ -0,0 +1,35 @@
use std::rc::Rc;
use crate::screen::Screen;
pub struct DeviceT {
pub screen: Screen
}
impl DeviceT {
fn new() -> Self {
let screen = Screen::new();
DeviceT {
screen
}
}
}
pub struct Device {
pub shared: Rc<DeviceT>
}
impl Device {
pub fn new() -> Self {
return Device {
shared: Rc::new(DeviceT::new())
}
}
pub fn share(&self) -> Self {
return Device {
shared: self.shared.clone()
}
}
}

View File

@@ -1,4 +1,9 @@
mod device;
mod palette; mod palette;
mod screen;
pub use device::Device;
pub use palette::PALETTE; pub use palette::PALETTE;
pub use screen::{Screen, SCREEN_W, SCREEN_H};
pub type VResult<T> = Result<T, anyhow::Error>; pub type VResult<T> = Result<T, anyhow::Error>;

View File

@@ -1,66 +1,66 @@
pub const PALETTE: [[u8; 3]; 64] = [ pub const PALETTE: [u32; 64] = [
[0, 0, 0], 0,
[0, 0, 255], 255,
[0, 255, 0], 65280,
[0, 255, 255], 65535,
[255, 0, 0], 16711680,
[255, 0, 255], 16711935,
[255, 255, 0], 16776960,
[255, 255, 255], 16777215,
[32, 45, 44], 2108716,
[41, 60, 67], 2702403,
[18, 22, 32], 1185312,
[40, 34, 49], 2630193,
[27, 17, 29], 1773853,
[54, 42, 47], 3549743,
[46, 33, 23], 3023127,
[66, 60, 43], 4340779,
[39, 92, 148], 2579604,
[55, 83, 107], 3625835,
[110, 45, 90], 7220570,
[79, 50, 74], 5190218,
[130, 103, 52], 8546100,
[100, 85, 68], 6575428,
[80, 133, 85], 5277013,
[88, 106, 80], 5794384,
[59, 115, 206], 3896270,
[76, 108, 153], 5008537,
[165, 53, 114], 10827122,
[124, 70, 99], 8144483,
[188, 157, 22], 12360982,
[154, 137, 89], 10127705,
[57, 194, 137], 3785353,
[109, 161, 129], 7184769,
[39, 187, 255], 2604031,
[123, 67, 240], 8078320,
[186, 66, 177], 12206769,
[239, 60, 80], 15678544,
[255, 170, 48], 16755248,
[186, 255, 0], 12254976,
[69, 255, 115], 4587379,
[2, 241, 228], 192996,
[150, 190, 255], 9879295,
[185, 113, 226], 12153314,
[220, 118, 175], 14448303,
[249, 134, 85], 16352853,
[242, 204, 114], 15912050,
[194, 255, 116], 12779380,
[100, 255, 173], 6619053,
[105, 242, 252], 6943484,
[201, 169, 248], 13216248,
[221, 193, 228], 14533092,
[250, 156, 143], 16424079,
[240, 197, 180], 15779252,
[230, 237, 137], 15134089,
[233, 243, 193], 15332289,
[116, 253, 234], 7667178,
[182, 247, 242], 11991026,
[243, 222, 238], 15982318,
[237, 228, 236], 15590636,
[254, 235, 224], 16706528,
[245, 241, 240], 16118256,
[245, 254, 225], 16121569,
[250, 253, 246], 16449014,
[211, 250, 250], 13892346,
[230, 247, 244], 15136756,
]; ];

62
src/screen.rs Normal file
View File

@@ -0,0 +1,62 @@
use std::cell::RefCell;
use crate::{VResult, PALETTE};
pub const SCREEN_W: usize = 640;
pub const SCREEN_H: usize = 480;
const PIXEL_SCALAR: usize = 4;
// const CURSES_SNAP: usize = 8;
pub struct Screen {
// NOTE: There are better data structures we could use than this
pixels: RefCell<Vec<u8>>
}
impl Screen {
pub fn new() -> Screen {
Screen {
pixels: RefCell::new(vec![0; SCREEN_W * SCREEN_H])
}
}
pub fn to_bitmap(&self, bitmap: &mut Vec<u32>) {
bitmap.resize(SCREEN_W * SCREEN_H, 0);
let pxl = self.pixels.borrow();
for i in 0..pxl.len() {
bitmap[i] = PALETTE[pxl[i] as usize];
}
}
pub fn to_bitmap_slice(&self, slice: &mut [u8]) -> VResult<()> {
if slice.len() != SCREEN_W * SCREEN_H * 4 {
anyhow::bail!("slice should be {} x {} in length", SCREEN_W, SCREEN_H);
}
let pxl = self.pixels.borrow();
for i in 0..pxl.len() {
slice[i * 4..i * 4 + 4].copy_from_slice(&(0xff000000 | PALETTE[pxl[i] as usize]).to_le_bytes());
}
Ok(())
}
pub fn pset(&self, x: i32, y: i32, color: u8) {
let px0 = x * PIXEL_SCALAR as i32;
let py0 = y * PIXEL_SCALAR as i32;
if px0 < 0 || py0 < 0 || px0 >= SCREEN_W as i32 || py0 >= SCREEN_H as i32 {
return;
}
let px0 = px0 as usize;
let py0 = py0 as usize;
let mut pxl = self.pixels.borrow_mut();
for py in py0..py0 + PIXEL_SCALAR {
for px in px0..px0 + PIXEL_SCALAR {
pxl[py * SCREEN_W + px] = color;
}
}
}
}