20 Commits

Author SHA1 Message Date
00d21b4745 add include dir to release script 2022-02-28 23:59:51 +01:00
d11b46576a prepare for 0.1.2 release 2022-02-28 23:19:58 +01:00
add49a1f8b fix #2: Crash when drawing zero sized line 2022-02-27 22:09:54 +01:00
8815a8e02e add full dependencies to file watcher 2022-02-27 21:50:33 +01:00
eb7c33d412 update curlywas, use 'include "microw8-api.cwa"' in examples 2022-02-26 23:41:15 +01:00
47ad3b4f30 add command to uw8-tool to write api include file 2022-02-26 22:13:15 +01:00
0f668fb6e9 fix typo in atan2 docs, change 'i' character in font 2022-02-26 21:01:57 +01:00
f876f59e80 Merge branch 'browser-run' into next 2022-02-26 21:00:58 +01:00
44b8656f29 make both native and browser runtime optional 2022-02-21 22:54:14 +01:00
90467f7c5b add some more polish to web runner 2022-02-20 23:54:28 +01:00
c56196bd2e actual run --browser instead of just optimized technotunnel 2022-02-20 18:26:57 +01:00
266493ca1c run --browser is working (very unpolished) 2022-02-19 23:58:13 +01:00
4c75ba2e44 start implementing run --browser 2022-02-19 16:21:02 +01:00
f1493ebded Split core web runtime into separate js 2022-02-17 23:27:57 +01:00
5d41733142 add download links to 0.1.1 2022-02-04 23:23:29 +01:00
c25a52b61b Prepare 0.1.1 release
add devkit mode to web runtime
add unpack and compile commands to uw8
2022-02-04 22:25:17 +01:00
619ea903ba add support for table/element section in pack command 2022-02-02 00:02:55 +01:00
9b900a49e4 add screenshot on F9, increase video rate + docs 2022-01-30 20:03:13 +01:00
f21497dd2e implement more robust file watcher 2022-01-29 15:22:14 +01:00
381eaf970f Add first basic logo version 2022-01-29 15:21:35 +01:00
49 changed files with 2668 additions and 819 deletions

905
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,26 @@
[package] [package]
name = "uw8" name = "uw8"
version = "0.1.0" version = "0.1.2"
edition = "2021" 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
[features]
default = ["native", "browser"]
native = ["wasmtime"]
browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
[dependencies] [dependencies]
wasmtime = "0.30" wasmtime = { version = "0.30", optional = true }
anyhow = "1" anyhow = "1"
minifb = { version = "0.20", default-features = false, features = ["x11"] } minifb = { version = "0.20", default-features = false, features = ["x11"] }
notify = "4" notify = "4"
pico-args = "0.4" pico-args = "0.4"
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "196719b" } curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "89638565" }
wat = "1" wat = "1"
uw8-tool = { path = "uw8-tool" } uw8-tool = { path = "uw8-tool" }
same-file = "1"
warp = { version = "0.3.2", optional = true }
tokio = { version = "1.17.0", features = ["sync", "rt"], optional = true }
tokio-stream = { version = "0.1.8", features = ["sync"], optional = true }
webbrowser = { version = "0.6.0", optional = true }

View File

@@ -15,9 +15,9 @@ See [here](https://exoticorn.github.io/microw8/) for more information and docs.
## Downloads ## Downloads
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-linux.tgz) * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-macos.tgz) * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-windows.zip) * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-windows.zip)
The download includes The download includes
@@ -35,6 +35,7 @@ Runs <file> which can be a binary WebAssembly module, an `.uw8` cart, a wat (Web
Options: Options:
-b, --browser : Run in browser instead of using native runtime
-t, --timeout FRAMES : Sets the timeout in frames (1/60s) -t, --timeout FRAMES : Sets the timeout in frames (1/60s)
-w, --watch : Reloads the given file every time it changes on disk. -w, --watch : Reloads the given file every time it changes on disk.
-p, --pack : Pack the file into an .uw8 cart before running it and print the resulting size. -p, --pack : Pack the file into an .uw8 cart before running it and print the resulting size.
@@ -53,6 +54,21 @@ Options:
-l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow. -l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow.
uw8 unpack <infile> <outfile>
Unpacks a MicroW8 module into a standard WebAssembly module.
uw8 compile [<options>] <infile> <outfile>
Compiles a CurlyWas source file to a standard WebAssembly module. Most useful together with
the --debug option to get a module that works well in the Chrome debugger.
Options:
-d, --debug : Generate a name section to help debugging
uw8 filter-exports <infile> <outfile> uw8 filter-exports <infile> <outfile>
Reads a binary WebAssembly module, removes all exports not used by the MicroW8 platform + everything that is unreachable without those exports and writes the resulting module to <outfile>. Reads a binary WebAssembly module, removes all exports not used by the MicroW8 platform + everything that is unreachable without those exports and writes the resulting module to <outfile>.

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
clang -O2 -Wno-incompatible-library-redeclaration --no-standard-libraries -ffast-math -Xclang -target-feature -Xclang +nontrapping-fptoint -Wl,--no-entry -Wl,--export-all -Wl,--import-memory -Wl,--initial-memory=262144 -Wl,-zstack-size=90000 -o cart.wasm cart.c --target=wasm32 && \ clang -O2 -Wno-incompatible-library-redeclaration --no-standard-libraries -ffast-math -Xclang -target-feature -Xclang +nontrapping-fptoint -Wl,--no-entry,--export-all,--import-memory,--initial-memory=262144,--global-base=81920,-zstack-size=4096 -o cart.wasm cart.c --target=wasm32 && \
uw8 filter-exports cart.wasm cart.wasm && \ uw8 filter-exports cart.wasm cart.wasm && \
wasm-opt -Oz --fast-math --strip-producers -o cart.wasm cart.wasm && \ wasm-opt -Oz --fast-math --strip-producers -o cart.wasm cart.wasm && \
uw8 pack -l 9 cart.wasm cart.uw8 uw8 pack -l 9 cart.wasm cart.uw8

View File

@@ -1,5 +1,4 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.printString" fn printString(i32);
export fn upd() { export fn upd() {
printString(0x20000); printString(0x20000);

View File

@@ -1,11 +1,4 @@
import "env.time" fn time() -> f32; include "../include/microw8-api.cwa"
import "env.circle" fn circle(f32, f32, f32, i32);
import "env.cls" fn cls(i32);
import "env.randomSeed" fn seed(i32);
import "env.randomf" fn randomf() -> f32;
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
import "env.fmod" fn fmod(f32, f32) -> f32;
export fn upd() { export fn upd() {
cls(0); cls(0);
@@ -15,10 +8,10 @@ export fn upd() {
let inline rocket = i #>> 9; let inline rocket = i #>> 9;
let lazy local_time = fmod(time() + rocket as f32 / 5 as f32, 2 as f32); let lazy local_time = fmod(time() + rocket as f32 / 5 as f32, 2 as f32);
let lazy rocket = rocket + nearest(time() - local_time) as i32 * 10; let lazy rocket = rocket + nearest(time() - local_time) as i32 * 10;
seed(rocket); randomSeed(rocket);
let inline x = randomf() * 645 as f32; let inline x = randomf() * 645 as f32;
let y = randomf() * 133 as f32; let y = randomf() * 133 as f32;
let lazy angle = { seed(i); randomf() } * 44 as f32; let lazy angle = { randomSeed(i); randomf() } * 44 as f32;
let inline dx = sin(angle); let inline dx = sin(angle);
let inline dy = cos(angle); let inline dy = cos(angle);
let lazy dist = local_time * (randomf() * 44 as f32); let lazy dist = local_time * (randomf() * 44 as f32);

View File

@@ -1,38 +1,30 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.cls" fn cls(i32);
import "env.printString" fn printString(i32);
import "env.printChar" fn printChar(i32);
import "env.setCursorPosition" fn setCursor(i32, i32);
import "env.setTextColor" fn setTextColor(i32);
import "env.line" fn line(f32, f32, f32, f32, i32);
import "env.isButtonTriggered" fn triggered(i32) -> i32;
global mut mode: i32 = 0; global mut mode: i32 = 0;
export fn upd() { export fn upd() {
cls(0); cls(0);
if triggered(4) { if isButtonTriggered(BUTTON_A) {
mode = !mode; mode = !mode;
} }
setTextColor(15); setTextColor(15);
printString(mode * 0x20000); printString(mode * USER_MEM);
let y: i32; let y: i32;
loop y { loop y {
line(0 as f32, (y * 9 + 39) as f32, (14+16*9) as f32, (y * 9 + 39) as f32, 1); line(0 as f32, (y * 9 + 39) as f32, (14+16*9) as f32, (y * 9 + 39) as f32, 1);
line((y * 9 + 15) as f32, 24 as f32, (y * 9 + 15) as f32, (38+16*9) as f32, 1); line((y * 9 + 15) as f32, 24 as f32, (y * 9 + 15) as f32, (38+16*9) as f32, 1);
setTextColor(15); setTextColor(15);
setCursor(y * 9 + 16, 24); setCursorPosition(y * 9 + 16, 24);
let lazy hexChar = select(y < 10, y + 48, y + 87); let lazy hexChar = select(y < 10, y + 48, y + 87);
printChar(hexChar); printChar(hexChar);
setCursor(0, y * 9 + 24+16); setCursorPosition(0, y * 9 + 24+16);
printChar(hexChar); printChar(hexChar);
let x = 0; let x = 0;
loop x { loop x {
setCursor(x * 9 + 16, y * 9 + 24+16); setCursorPosition(x * 9 + 16, y * 9 + 24+16);
setTextColor(select(mode, x + y * 16, -9)); setTextColor(select(mode, x + y * 16, -9));
if y >= 2 | mode { if y >= 2 | mode {
printChar(select(mode, 0xa4, x + y * 16)); printChar(select(mode, 0xa4, x + y * 16));
@@ -47,6 +39,6 @@ data 0 {
"Default font: (press " i8(0xcc) " for palette)" i8(5, 0) "Default font: (press " i8(0xcc) " for palette)" i8(5, 0)
} }
data 0x20000 { data USER_MEM {
"Default palette: (press " i8(0xcc) " for font)" i8(5, 0) "Default palette: (press " i8(0xcc) " for font)" i8(5, 0)
} }

View File

@@ -1,102 +1,15 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.cls" fn cls(i32);
import "env.setPixel" fn setPixel(i32, i32, i32);
import "env.time" fn time() -> f32;
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
let swapTmp: f32;
if x1 > x2 {
swapTmp = x1;
x1 = x2;
x2 = swapTmp;
swapTmp = y1;
y1 = y2;
y2 = swapTmp;
}
if x1 < 0 as f32 & x2 >= 0 as f32 {
y1 = y1 + (y2 - y1) * -x1 / (x2 - x1);
x1 = 0 as f32;
}
if x1 < 320 as f32 & x2 >= 320 as f32 {
y2 = y2 + (y2 - y1) * (320 as f32 - x2) / (x2 - x1);
x2 = 320 as f32;
}
if y1 > y2 {
swapTmp = x1;
x1 = x2;
x2 = swapTmp;
swapTmp = y1;
y1 = y2;
y2 = swapTmp;
}
if y1 < 0 as f32 & y2 >= 0 as f32 {
x1 = x1 + (x2 - x1) * -y1 / (y2 - y1);
y1 = 0 as f32;
}
if y1 < 240 as f32 & y2 >= 240 as f32 {
x2 = x2 + (x2 - x1) * (240 as f32 - y2) / (y2 - y1);
y2 = 240 as f32;
}
let lazy dx = x2 - x1;
let lazy dy = y2 - y1;
let max_axis: f32;
let p: f32;
if abs(dx) >= dy {
max_axis = dx;
p = x1;
} else {
max_axis = dy;
p = y1;
}
let steps = floor(p + max_axis) as i32 - floor(p) as i32;
p = floor(p) + 0.5 - p;
if max_axis < 0 as f32 {
steps = -steps;
p = -p;
max_axis = -max_axis;
}
dx = dx / max_axis;
dy = dy / max_axis;
let f = min(max_axis, max(0 as f32, p));
setPixel((x1 + f * dx) as i32, (y1 + f * dy) as i32, col);
if !steps {
return;
}
x1 = x1 + (1 as f32 + p) * dx;
y1 = y1 + (1 as f32 + p) * dy;
p = p + steps as f32;
loop pixels {
if steps := steps - 1 {
setPixel(x1 as i32, y1 as i32, col);
x1 = x1 + dx;
y1 = y1 + dy;
branch pixels;
}
}
f = min(max_axis, p) - p;
setPixel((x1 + f * dx) as i32, (y1 + f * dy) as i32, col);
}
export fn upd() { export fn upd() {
cls(0); cls(0);
// line(0.0, 4.0, 7.0, -2.0, 15);
// return;
let i: i32; let i: i32;
loop lines { loop lines {
let angle = i as f32 * (3.1415 / 25.0) + time() * 0.1; let angle = i as f32 * (3.1415 / 25.0) + time() * 0.125;
line(160.0, 120.0, 160.0 + sin(angle) * 100.0, 120.0 + cos(angle) * 100.0, 47); line(
160 as f32, 120 as f32,
160 as f32 + sin(angle) * 100 as f32,
120 as f32 + cos(angle) * 100 as f32,
47);
branch_if (i := i + 1) < 50: lines; branch_if (i := i + 1) < 50: lines;
} }
} }

View File

@@ -1,12 +1,4 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.rectangle" fn rect(f32, f32, f32, f32, i32);
import "env.circle" fn circle(f32, f32, f32, i32);
import "env.isButtonPressed" fn btn(i32) -> i32;
import "env.random" fn random() -> i32;
import "env.randomSeed" fn randomSeed(i32);
import "env.cls" fn cls(i32);
import "env.printInt" fn printInt(i32);
global mut pz: i32 = 4; global mut pz: i32 = 4;
global mut px: f32 = 2.0; global mut px: f32 = 2.0;
@@ -19,7 +11,7 @@ export fn upd() {
let inline zero = 0.0; let inline zero = 0.0;
let lazy control_speed = 0.03125; let lazy control_speed = 0.03125;
s = s + 0.1875 - (f + control_speed) * btn(4 <| cls(4)) as f32; s = s + 0.1875 - (f + control_speed) * isButtonPressed(4 <| cls(4)) as f32;
f = f * 0.5625; f = f * 0.5625;
printInt(pz); printInt(pz);
@@ -33,8 +25,8 @@ export fn upd() {
let inline c = (z & 1) * -2; let inline c = (z & 1) * -2;
let inline yf = y as f32; let inline yf = y as f32;
rect(rx, yf, rw, yf / 6 as f32, c + 1); rectangle(rx, yf, rw, yf / 6 as f32, c + 1);
rect(rx, yf, rw, 1 as f32, c - 4); rectangle(rx, yf, rw, 1 as f32, c - 4);
if y == 180 & py > zero { if y == 180 & py > zero {
if x > w | x < zero { if x > w | x < zero {
@@ -51,7 +43,7 @@ export fn upd() {
circle(160 as f32, 160 as f32 + py, 22 as f32, -28); circle(160 as f32, 160 as f32 + py, 22 as f32, -28);
circle((160 - 6) as f32, (160 - 6) as f32 + py, 6 as f32, -26); circle((160 - 6) as f32, (160 - 6) as f32 + py, 6 as f32, -26);
px = px + (btn(3) - btn(2)) as f32 * control_speed; px = px + (isButtonPressed(3) - isButtonPressed(2)) as f32 * control_speed;
py = py + s; py = py + s;
pz = pz + 1; pz = pz + 1;
} }

View File

@@ -1,25 +1,26 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.sin" fn sin(f32) -> f32;
import "env.time" fn time() -> f32;
export fn upd() { export fn upd() {
let i: i32; let x: i32;
let y: i32;
loop screen { loop screen {
let inline t = time() / 2 as f32; let inline t = time() / 2 as f32;
let lazy o = sin(t) * 0.8; let lazy o = sin(t) * 0.75;
let lazy q = (i % 320) as f32 - 160.1; let inline q = x as f32 - 160.5;
let lazy w = (i / 320 - 120) as f32; let inline w = (y - 120) as f32;
let lazy r = sqrt(q*q + w*w); let lazy r = sqrt(q*q + w*w);
let lazy z = q / r; let lazy z = q / r;
let lazy s = z * o + sqrt(z * z * o * o + 1 as f32 - o * o); let lazy s = z * o + sqrt(z * z * o * o + 1 as f32 - o * o);
let lazy q2 = (z * s - o) * 10 as f32 + t; let inline q2 = (z * s - o) * 10 as f32 + t;
let lazy w2 = w / r * s * 10 as f32 + t; let inline w2 = w / r * s * 10 as f32 + t;
let lazy s2 = s * 100 as f32 / r; let inline s2 = s * 100 as f32 / r;
i?120 = max( let inline color = max(
0 as f32, 0 as f32,
((q2 as i32 ^ w2 as i32 & ((s2 + time()) * 10 as f32) as i32) & 5) as f32 * ((q2 as i32 ^ w2 as i32 & ((s2 + time()) * 10 as f32) as i32) & 5) as f32 *
(4 as f32 - s2) as f32 (4 as f32 - s2) as f32
) as i32 - 32; ) as i32 - 32;
branch_if (i := i + 1) < 320*240: screen setPixel(x, y, color);
branch_if x := (x + 1) % 320: screen;
branch_if y := (y + 1) % 320: screen;
} }
} }

View File

@@ -1,7 +1,4 @@
import "env.memory" memory(2); include "../include/microw8-api.cwa"
import "env.fmod" fn fmod(f32, f32) -> f32;
import "env.time" fn time() -> f32;
export fn upd() { export fn upd() {
let i: i32; let i: i32;

View File

@@ -1,7 +1,4 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.atan2" fn atan2(f32, f32) -> f32;
import "env.time" fn time() -> f32;
export fn upd() { export fn upd() {
let i: i32; let i: i32;
@@ -12,7 +9,7 @@ export fn upd() {
let inline d = 40000 as f32 / sqrt(x * x + y * y); let inline d = 40000 as f32 / sqrt(x * x + y * y);
let inline u = atan2(x, y) * (512.0 / 3.141); let inline u = atan2(x, y) * (512.0 / 3.141);
let inline c = ((i32.trunc_sat_f32_s(d + t * 2 as f32) ^ i32.trunc_sat_f32_s(u + t)) & 255) >> 4; let inline c = ((i32.trunc_sat_f32_s(d + t * 2 as f32) ^ i32.trunc_sat_f32_s(u + t)) & 255) >> 4;
i?120 = c; i?FRAMEBUFFER = c;
branch_if (i := i + 1) < 320*240: pixels; branch_if (i := i + 1) < 320*240: pixels;
} }

View File

@@ -0,0 +1,50 @@
// MicroW8 APIs, to be `include`d in CurlyWas sources
import "env.memory" memory(4);
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
import "env.tan" fn tan(f32) -> f32;
import "env.asin" fn asin(f32) -> f32;
import "env.acos" fn acos(f32) -> f32;
import "env.atan" fn atan(f32) -> f32;
import "env.atan2" fn atan2(f32, f32) -> f32;
import "env.pow" fn pow(f32, f32) -> f32;
import "env.log" fn log(f32) -> f32;
import "env.fmod" fn fmod(f32, f32) -> f32;
import "env.random" fn random() -> i32;
import "env.randomf" fn randomf() -> f32;
import "env.randomSeed" fn randomSeed(i32);
import "env.cls" fn cls(i32);
import "env.setPixel" fn setPixel(i32, i32, i32);
import "env.getPixel" fn getPixel(i32, i32) -> i32;
import "env.hline" fn hline(i32, i32, i32, i32);
import "env.rectangle" fn rectangle(f32, f32, f32, f32, i32);
import "env.circle" fn circle(f32, f32, f32, i32);
import "env.line" fn line(f32, f32, f32, f32, i32);
import "env.time" fn time() -> f32;
import "env.isButtonPressed" fn isButtonPressed(i32) -> i32;
import "env.isButtonTriggered" fn isButtonTriggered(i32) -> i32;
import "env.printChar" fn printChar(i32);
import "env.printString" fn printString(i32);
import "env.printInt" fn printInt(i32);
import "env.setTextColor" fn setTextColor(i32);
import "env.setBackgroundColor" fn setBackgroundColor(i32);
import "env.setCursorPosition" fn setCursorPosition(i32, i32);
import "env.rectangle_outline" fn rectangle_outline(f32, f32, f32, f32, i32);
import "env.circle_outline" fn circle_outline(f32, f32, f32, i32);
import "env.exp" fn exp(f32) -> f32;
const TIME_MS = 0x40;
const GAMEPAD = 0x44;
const FRAMEBUFFER = 0x78;
const PALETTE = 0x13000;
const FONT = 0x13400;
const USER_MEM = 0x14000;
const BUTTON_UP = 0x0;
const BUTTON_DOWN = 0x1;
const BUTTON_LEFT = 0x2;
const BUTTON_RIGHT = 0x3;
const BUTTON_A = 0x4;
const BUTTON_B = 0x5;
const BUTTON_X = 0x6;
const BUTTON_Y = 0x7;

View File

@@ -0,0 +1,52 @@
;; MicroW8 APIs, in WAT (Wasm Text) format
(import "env" "memory" (memory 4))
(import "env" "sin" (func $sin (param f32) (result f32)))
(import "env" "cos" (func $cos (param f32) (result f32)))
(import "env" "tan" (func $tan (param f32) (result f32)))
(import "env" "asin" (func $asin (param f32) (result f32)))
(import "env" "acos" (func $acos (param f32) (result f32)))
(import "env" "atan" (func $atan (param f32) (result f32)))
(import "env" "atan2" (func $atan2 (param f32) (param f32) (result f32)))
(import "env" "pow" (func $pow (param f32) (param f32) (result f32)))
(import "env" "log" (func $log (param f32) (result f32)))
(import "env" "fmod" (func $fmod (param f32) (param f32) (result f32)))
(import "env" "random" (func $random (result i32)))
(import "env" "randomf" (func $randomf (result f32)))
(import "env" "randomSeed" (func $randomSeed (param i32)))
(import "env" "cls" (func $cls (param i32)))
(import "env" "setPixel" (func $setPixel (param i32) (param i32) (param i32)))
(import "env" "getPixel" (func $getPixel (param i32) (param i32) (result i32)))
(import "env" "hline" (func $hline (param i32) (param i32) (param i32) (param i32)))
(import "env" "rectangle" (func $rectangle (param f32) (param f32) (param f32) (param f32) (param i32)))
(import "env" "circle" (func $circle (param f32) (param f32) (param f32) (param i32)))
(import "env" "line" (func $line (param f32) (param f32) (param f32) (param f32) (param i32)))
(import "env" "time" (func $time (result f32)))
(import "env" "isButtonPressed" (func $isButtonPressed (param i32) (result i32)))
(import "env" "isButtonTriggered" (func $isButtonTriggered (param i32) (result i32)))
(import "env" "printChar" (func $printChar (param i32)))
(import "env" "printString" (func $printString (param i32)))
(import "env" "printInt" (func $printInt (param i32)))
(import "env" "setTextColor" (func $setTextColor (param i32)))
(import "env" "setBackgroundColor" (func $setBackgroundColor (param i32)))
(import "env" "setCursorPosition" (func $setCursorPosition (param i32) (param i32)))
(import "env" "rectangle_outline" (func $rectangle_outline (param f32) (param f32) (param f32) (param f32) (param i32)))
(import "env" "circle_outline" (func $circle_outline (param f32) (param f32) (param f32) (param i32)))
(import "env" "exp" (func $exp (param f32) (result f32)))
;; to use defines, include this file with a preprocessor
;; like gpp (https://logological.org/gpp).
#define TIME_MS 0x40;
#define GAMEPAD 0x44;
#define FRAMEBUFFER 0x78;
#define PALETTE 0x13000;
#define FONT 0x13400;
#define USER_MEM 0x14000;
#define BUTTON_UP 0x0;
#define BUTTON_DOWN 0x1;
#define BUTTON_LEFT 0x2;
#define BUTTON_RIGHT 0x3;
#define BUTTON_A 0x4;
#define BUTTON_B 0x5;
#define BUTTON_X 0x6;
#define BUTTON_Y 0x7;

65
logo.svg Normal file
View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="120"
height="120"
viewBox="0 0 120 120"
version="1.1"
id="svg5"
sodipodi:docname="logo.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="3.6041667"
inkscape:cx="21.780347"
inkscape:cy="63.260116"
inkscape:window-width="1916"
inkscape:window-height="1041"
inkscape:window-x="0"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#85a6b2;fill-rule:evenodd;fill-opacity:1"
id="rect31"
width="112.05161"
height="109.64198"
x="3.9731982"
y="6.3613" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:117.333px;line-height:1.25;font-family:'Fira Code';-inkscape-font-specification:'Fira Code Bold';fill:#ffffff;fill-opacity:1;stroke:none"
x="-6.2686396"
y="117.70291"
id="text2691"><tspan
sodipodi:role="line"
id="tspan2689"
x="-6.2686396"
y="117.70291">W8</tspan></text>
<circle
style="fill:#ffffff;stroke-width:1.32327"
id="path121"
cx="60.962471"
cy="6.7644148"
r="14.855442" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

127
platform/Cargo.lock generated
View File

@@ -79,9 +79,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chumsky" name = "chumsky"
version = "0.5.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d3efff85e8572b1c3fa0127706af58c4fff8458f8d9436d54b1e97573c7a3f" checksum = "8d02796e4586c6c41aeb68eae9bfb4558a522c35f1430c14b40136c3706e09e4"
dependencies = [ dependencies = [
"ahash 0.3.8", "ahash 0.3.8",
] ]
@@ -146,14 +146,14 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]] [[package]]
name = "curlywas" name = "curlywas"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/exoticorn/curlywas.git?rev=196719b#196719b35ef377cb7e001554b27ac5de013dcf2b" source = "git+https://github.com/exoticorn/curlywas.git?rev=89638565#896385654ab2c089200920b6dea4abec641c88d6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ariadne", "ariadne",
"chumsky", "chumsky",
"pico-args", "pico-args",
"wasm-encoder", "wasm-encoder 0.10.0",
"wasmparser", "wasmparser 0.83.0",
] ]
[[package]] [[package]]
@@ -197,6 +197,21 @@ dependencies = [
"ahash 0.7.6", "ahash 0.7.6",
] ]
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "id-arena"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@@ -227,6 +242,15 @@ dependencies = [
"rgb", "rgb",
] ]
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.4.4" version = "0.4.4"
@@ -286,6 +310,24 @@ version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
[[package]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.31" version = "0.8.31"
@@ -304,6 +346,17 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "syn"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.43" version = "0.1.43"
@@ -323,6 +376,18 @@ dependencies = [
"crunchy", "crunchy",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]] [[package]]
name = "upkr" name = "upkr"
version = "0.1.0" version = "0.1.0"
@@ -342,8 +407,9 @@ dependencies = [
"pbr", "pbr",
"pico-args", "pico-args",
"upkr", "upkr",
"wasm-encoder", "walrus",
"wasmparser", "wasm-encoder 0.8.0",
"wasmparser 0.81.0",
] ]
[[package]] [[package]]
@@ -352,6 +418,32 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walrus"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb08e48cde54c05f363d984bb54ce374f49e242def9468d2e1b6c2372d291f8"
dependencies = [
"anyhow",
"id-arena",
"leb128",
"log",
"walrus-macro",
"wasmparser 0.77.0",
]
[[package]]
name = "walrus-macro"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e5bd22c71e77d60140b0bd5be56155a37e5bd14e24f5f87298040d0cc40d7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.2+wasi-snapshot-preview1" version = "0.10.2+wasi-snapshot-preview1"
@@ -367,12 +459,33 @@ dependencies = [
"leb128", "leb128",
] ]
[[package]]
name = "wasm-encoder"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9d9bf45fc46f71c407837c9b30b1e874197f2dc357588430b21e5017d290ab"
dependencies = [
"leb128",
]
[[package]]
name = "wasmparser"
version = "0.77.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35c86d22e720a07d954ebbed772d01180501afe7d03d464f413bb5f8914a8d6"
[[package]] [[package]]
name = "wasmparser" name = "wasmparser"
version = "0.81.0" version = "0.81.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98930446519f63d00a836efdc22f67766ceae8dbcc1571379f2bcabc6b2b9abc" checksum = "98930446519f63d00a836efdc22f67766ceae8dbcc1571379f2bcabc6b2b9abc"
[[package]]
name = "wasmparser"
version = "0.83.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "718ed7c55c2add6548cca3ddd6383d738cd73b892df400e96b9aa876f0141d7a"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@@ -6,7 +6,7 @@ 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]
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="196719b" } curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="89638565" }
uw8-tool = { path="../uw8-tool" } uw8-tool = { path="../uw8-tool" }
anyhow = "1" anyhow = "1"
lodepng = "3.4" lodepng = "3.4"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

View File

@@ -12,15 +12,15 @@ fn main() -> Result<()> {
println!("Compiling loader module"); println!("Compiling loader module");
let loader = curlywas::compile_file("src/loader.cwa", curlywas::Options::default())?; let loader = curlywas::compile_file("src/loader.cwa", curlywas::Options::default())?;
File::create("bin/loader.wasm")?.write_all(&loader)?; File::create("bin/loader.wasm")?.write_all(&loader.wasm)?;
println!("Loader (including base module): {} bytes", loader.len()); println!("Loader (including base module): {} bytes", loader.wasm.len());
println!("Compiling platform module"); println!("Compiling platform module");
let platform = curlywas::compile_file("src/platform.cwa", curlywas::Options::default())?; let platform = curlywas::compile_file("src/platform.cwa", curlywas::Options::default())?;
println!("Compressing platform module"); println!("Compressing platform module");
let platform = uw8_tool::pack( let platform = uw8_tool::pack(
&platform, &platform.wasm,
&uw8_tool::PackConfig::default().with_compression_level(4), &uw8_tool::PackConfig::default().with_compression_level(4),
)?; )?;
File::create("bin/platform.uw8")?.write_all(&platform)?; File::create("bin/platform.uw8")?.write_all(&platform)?;

View File

@@ -256,6 +256,11 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
p = y1; p = y1;
} }
if max_axis == 0 as f32 {
setPixel(x1 as i32, y1 as i32, col);
return;
}
let steps = floor(p + max_axis) as i32 - floor(p) as i32; let steps = floor(p + max_axis) as i32 - floor(p) as i32;
p = floor(p) + 0.5 - p; p = floor(p) + 0.5 - p;
if max_axis < 0 as f32 { if max_axis < 0 as f32 {
@@ -267,7 +272,7 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
dy = dy / max_axis; dy = dy / max_axis;
let f = min(max_axis, max(0 as f32, p)); let f = min(max_axis, max(0 as f32, p));
setPixel((x1 + f * dx) as i32, (y1 + f * dy) as i32, col); setPixel(i32.trunc_sat_f32_s(x1 + f * dx), i32.trunc_sat_f32_s(y1 + f * dy), col);
if !steps { if !steps {
return; return;
@@ -280,7 +285,7 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
loop pixels { loop pixels {
if steps := steps - 1 { if steps := steps - 1 {
setPixel(x1 as i32, y1 as i32, col); setPixel(i32.trunc_sat_f32_s(x1), i32.trunc_sat_f32_s(y1), col);
x1 = x1 + dx; x1 = x1 + dx;
y1 = y1 + dy; y1 = y1 + dy;
branch pixels; branch pixels;
@@ -288,7 +293,7 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
} }
f = min(max_axis, p) - p; f = min(max_axis, p) - p;
setPixel((x1 + f * dx) as i32, (y1 + f * dy) as i32, col); setPixel(i32.trunc_sat_f32_s(x1 + f * dx), i32.trunc_sat_f32_s(y1 + f * dy), col);
} }
////////// //////////

View File

@@ -36,6 +36,8 @@ for dir in build/*; do
cp $example $dir/examples cp $example $dir/examples
done done
cp -r ../examples/include $dir/include
mkdir $dir/carts mkdir $dir/carts
for example in $dir/examples/*; do for example in $dir/examples/*; do
build/microw8-linux/uw8 pack -l 9 $example $dir/carts/$(basename ${example%.*}).uw8 build/microw8-linux/uw8 pack -l 9 $example $dir/carts/$(basename ${example%.*}).uw8

View File

@@ -18,7 +18,7 @@ highlight_theme = "ascetic-white"
[extra] [extra]
# Put all your custom variables here # Put all your custom variables here
juice_logo_name = "MicroW8" juice_logo_name = "MicroW8"
juice_logo_path = "microw8.svg" juice_logo_path = "img/microw8.svg"
juice_extra_menu = [ juice_extra_menu = [
{ title = "Github", link = "https://github.com/exoticorn/microw8" } { title = "Github", link = "https://github.com/exoticorn/microw8" }
] ]

View File

@@ -15,17 +15,52 @@ The initial motivation behind MicroW8 was to explore whether there was a way to
* Gamepad input (D-Pad + 4 Buttons) * Gamepad input (D-Pad + 4 Buttons)
## Examples ## Examples
* [Fireworks](v0.1pre5#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022. * [Fireworks](v0.1.2#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022.
* [Skip Ahead](v0.1pre5#AgyfpZ80wkW28kiUZ9VIK4v+RPnVxqjK1dz2BcDoNyQPsS2g4OgEzkTe6jyoAfFOmqKrS8SM2aRljBal9mjNn8i4fP9eBK+RehQKxxGtJa9FqftvqEnh3ez1YaYxqj7jgTdzJ/WAYVmKMovBT1myrX3FamqKSOgMsNedLhVTLAhQup3sNcYEjGNo8b0HZ5+AgMgCwYRGCe//XQOMAaAAzqDILgmpEZ/43RKHcQpHEQwbURfNQJpadJe2sz3q5FlQnTGXQ9oSMokidhlC+aR/IpNHieuBGLhFZ2GfnwVQ0geBbQpTPA==) (229 bytes): A port of my [TIC-80 256byte game](http://tic80.com/play?cart=1735) from LoveByte'21 * [Skip Ahead](v0.1.2#AgyfpZ80wkW28kiUZ9VIK4v+RPnVxqjK1dz2BcDoNyQPsS2g4OgEzkTe6jyoAfFOmqKrS8SM2aRljBal9mjNn8i4fP9eBK+RehQKxxGtJa9FqftvqEnh3ez1YaYxqj7jgTdzJ/WAYVmKMovBT1myrX3FamqKSOgMsNedLhVTLAhQup3sNcYEjGNo8b0HZ5+AgMgCwYRGCe//XQOMAaAAzqDILgmpEZ/43RKHcQpHEQwbURfNQJpadJe2sz3q5FlQnTGXQ9oSMokidhlC+aR/IpNHieuBGLhFZ2GfnwVQ0geBbQpTPA==) (229 bytes): A port of my [TIC-80 256byte game](http://tic80.com/play?cart=1735) from LoveByte'21
* [OhNoAnotherTunnel](v0.1pre4#Ag95rdCB5Ww5NofyQaKF4P1mrNRso4azgiem4hK99Gh8OMzSpFq3NsNDo7O7pqln10D11l9uXr/ritw7OEzKwbEfCdvaRnS2Z0Kz0iDEZt/gIqOdvFmxsL1MjPQ4XInPbUJpQUonhQq29oP2omFabnQxn0bzoK7mZjcwc5GetHG+hGajkJcRr8oOnjfCol8RD+ha33GYtPnut+GLe4ktzf5UxZwGs6oT9qqC61lRDakN) (177 bytes): A port of my [entry](http://tic80.com/play?cart=1871) in the Outline'21 bytebattle final * [OhNoAnotherTunnel](v0.1.2#AgPP1oEFvPzY/rBZwTumtYn37zeMFgpir1Bkn91jsNcp26VzoUpkAOOJTtnzVBfW+/dGnnIdbq/irBUJztY5wuua80DORTYZndgdwZHcSk15ajc4nyO0g1A6kGWyW56oZk0iPYJA9WtUmoj0Plvy1CGwIZrMe57X7QZcdqc3u6zjTA41Tpiqi9vnO3xbhi8o594Vx0XPXwVzpYq1ZCTYenfAGaXKkDmAFJqiVIsiCg==) (175 bytes): A port of my [entry](http://tic80.com/play?cart=1871) in the Outline'21 bytebattle final
* [Technotunnel](v0.1pre4#AqL8HeK1M9dn2nWNIF5vaq/Vh64pMt5nJIFoFKpBMPUsGtDtpqjo1JbT9LzPhAxCqJ7Yh4TA6oTGd4xhLowf+cWZMY73+7AZmfXJJsBi4cej/hH+4wlAgxFIrnOYnr/18IpnZbsHf0eGm1BhahX74+cVR0TRmNQmYC7GhCNS3mv/3MJn74lCj7t28aBJPjEZhP9fGXdG2u5Egh/Tjdg=) (158 bytes): A port of my [entry](https://tic80.com/play?cart=1873) in the Outline'21 bytebattle quater final * [Technotunnel](v0.1.2#AhPXpq894LaUhp5+HQf39f39/Jc8g5zUrBSc0uyKh36ivskczhY84h55zL8gWpkdvKuRQI+KIt80isKzh8jkM8nILcx0RUvyk8yjE8TgNsgkcORVI0RY5k3qE4ySjaycxa2DVZH61UWZuLsCouuwT7I80TbmmetQSbMywJ/avrrCZIAH0UzQfvOiCJNG48NI0FFY1vjB7a7dcp8Uqg==) (157 bytes): A port of my [entry](https://tic80.com/play?cart=1873) in the Outline'21 bytebattle quater final
* [Font & Palette](v0.1pre4#AgKaeeOuwg5gCKvFIeiitEwMpUI2rymEcu+DDB1vMu9uBoufvUxIr4Y5p4Jj2ukoNO4PE7QS5cN1ZyDMCRfSzYIGZxKlN2J6NKEWK7KVPk9wVUgn1Ip+hsMinWgEO8ETKfPuHoIa4kjI+ULFOMad7vd3rt/lh1Vy9w+R2MXG/7T61d3c7C6KY+eQNS0eW3ys4iU8R6SycuWZuuZ2Sg3Qxp826s+Kt+2qBojpzNOSoyFqyrVyYMTKEkSl0BZOj59Cs1hPm5bq0F1MmVhGAzMhW9V4YeAe): Just a simple viewer for the default font and palette. * [Font & Palette](v0.1.2#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ): Just a simple viewer for the default font and palette.
Examplers for older versions:
* [Technotunnel B/W](v0.1pre2#AQrDAQHAAQIBfwp9A0AgAUEAsiABQcACb7JDmhkgQ5MiBCAEIASUIAFBwAJtQfgAa7IiBSAFlJKRIgaVIgcgByAAskHQD7KVIgIQAEPNzEw/lCIDlCAHIAeUIAOUIAOUQQGykiADIAOUk5GSIgiUIAOTQQqylCACkiIJqCAFIAaVIAiUQQqylCACkiIKqHMgCEEyspQgBpUiCyACkkEUspSocUEFcbJBArIgC5OUQRaylJeoOgB4IAFBAWoiAUGA2ARIDQALCw==) (199 bytes uncompressed): A port of my [entry](https://tic80.com/play?cart=1873) in the Outline'21 bytebattle quater final (older MicroW8 version with monochrome palette) * [Technotunnel B/W](v0.1pre2#AQrDAQHAAQIBfwp9A0AgAUEAsiABQcACb7JDmhkgQ5MiBCAEIASUIAFBwAJtQfgAa7IiBSAFlJKRIgaVIgcgByAAskHQD7KVIgIQAEPNzEw/lCIDlCAHIAeUIAOUIAOUQQGykiADIAOUk5GSIgiUIAOTQQqylCACkiIJqCAFIAaVIAiUQQqylCACkiIKqHMgCEEyspQgBpUiCyACkkEUspSocUEFcbJBArIgC5OUQRaylJeoOgB4IAFBAWoiAUGA2ARIDQALCw==) (199 bytes uncompressed): A port of my [entry](https://tic80.com/play?cart=1873) in the Outline'21 bytebattle quater final (older MicroW8 version with monochrome palette)
* [XorScroll](v0.1pre2#AQovAS0BAX8DQCABIAFBwAJvIABBCm1qIAFBwAJtczoAeCABQQFqIgFBgNgESA0ACws=) (50 bytes uncompressed): A simple scrolling XOR pattern. Fun fact: This is the pre-loaded effect when entering a bytebattle. * [XorScroll](v0.1pre2#AQovAS0BAX8DQCABIAFBwAJvIABBCm1qIAFBwAJtczoAeCABQQFqIgFBgNgESA0ACws=) (50 bytes uncompressed): A simple scrolling XOR pattern. Fun fact: This is the pre-loaded effect when entering a bytebattle.
* [CircleWorm](v0.1pre2#AQp7AXkCAX8CfUEgEA0DQCABskEEspUiAkECspUgALJBiCeylSIDQQWylJIQAEEBspJBoAGylCACQQOylSADQQSylJIQAEEBspJB+ACylCADQRGylCACQQKylJIQAEECspJBELKUIAFBAmxBP2oQEiABQQFqIgFBP0gNAAsL) (126 bytes uncompressed): Just a test for the circle fill function. * [CircleWorm](v0.1pre2#AQp7AXkCAX8CfUEgEA0DQCABskEEspUiAkECspUgALJBiCeylSIDQQWylJIQAEEBspJBoAGylCACQQOylSADQQSylJIQAEEBspJB+ACylCADQRGylCACQQKylJIQAEECspJBELKUIAFBAmxBP2oQEiABQQFqIgFBP0gNAAsL) (126 bytes uncompressed): Just a test for the circle fill function.
## Versions ## Versions
### v0.1.2
* [Web runtime](v0.1.2)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-windows.zip)
Changes:
* add option to `uw8 run` to run the cart in the browser using the web runtime
* CurlyWas: implement `include` support
* CurlyWas: implement support for constants
* fix crash when trying to draw zero sized line
### v0.1.1
* [Web runtime](v0.1.1)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-windows.zip)
Changes:
* implement more robust file watcher
* add basic video recording on F10 in web runtime
* add screenshot on F9
* add watchdog to interrupt hanging update in native runtime
* add devkit mode to web runtime
* add unpack and compile commands to uw8
* add support for table/element section in pack command
* disable wayland support (caused missing window decorations in gnome)
### v0.1.0 ### v0.1.0
* [Web runtime](v0.1.0) * [Web runtime](v0.1.0)

View File

@@ -47,7 +47,7 @@ Returns the arccosine of `x`.
Returns the arctangent of `x`. Returns the arctangent of `x`.
### fn atan2(y: f32, y: f32) -> f32 ### fn atan2(y: f32, x: f32) -> f32
Returns the angle between the point `(x, y)` and the positive x-axis. Returns the angle between the point `(x, y)` and the positive x-axis.
@@ -105,7 +105,7 @@ as a cheap random-access PRNG (aka noise function).
## Graphics ## Graphics
The default palette can be seen [here](../v0.1pre4#AgKaeeOuwg5gCKvFIeiitEwMpUI2rymEcu+DDB1vMu9uBoufvUxIr4Y5p4Jj2ukoNO4PE7QS5cN1ZyDMCRfSzYIGZxKlN2J6NKEWK7KVPk9wVUgn1Ip+hsMinWgEO8ETKfPuHoIa4kjI+ULFOMad7vd3rt/lh1Vy9w+R2MXG/7T61d3c7C6KY+eQNS0eW3ys4iU8R6SycuWZuuZ2Sg3Qxp826s+Kt+2qBojpzNOSoyFqyrVyYMTKEkSl0BZOj59Cs1hPm5bq0F1MmVhGAzMhW9V4YeAe). (Press Z on the keyboard to switch to palette.) The default palette can be seen [here](../v0.1.0#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ). (Press Z on the keyboard to switch to palette.)
The palette can be changed by writing 32bit rgba colors to addresses 0x13000-0x13400. The palette can be changed by writing 32bit rgba colors to addresses 0x13000-0x13400.
@@ -192,7 +192,7 @@ The integer time in milliseconds can also be read at address 0x40.
## Text output ## Text output
The default font can be seen [here](../v0.1pre4#AgKaeeOuwg5gCKvFIeiitEwMpUI2rymEcu+DDB1vMu9uBoufvUxIr4Y5p4Jj2ukoNO4PE7QS5cN1ZyDMCRfSzYIGZxKlN2J6NKEWK7KVPk9wVUgn1Ip+hsMinWgEO8ETKfPuHoIa4kjI+ULFOMad7vd3rt/lh1Vy9w+R2MXG/7T61d3c7C6KY+eQNS0eW3ys4iU8R6SycuWZuuZ2Sg3Qxp826s+Kt+2qBojpzNOSoyFqyrVyYMTKEkSl0BZOj59Cs1hPm5bq0F1MmVhGAzMhW9V4YeAe). The default font can be seen [here](../v0.1.0#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ).
The font can be changed by writing 1bpp 8x8 characters to addresses 0x13400-0x13c00. The font can be changed by writing 1bpp 8x8 characters to addresses 0x13400-0x13c00.
@@ -284,7 +284,8 @@ Runs `<file>` which can be a binary WebAssembly module, an `.uw8` cart, a wat (W
Options: Options:
* `-t FRAMES`, `--timeout FRAMES` : Sets the timeout in frames (1/60s). If the start or update function runs longer than this it is forcibly interupted * `-b`, `--browser`: Run in browser instead of using native runtime
* `-t FRAMES`, `--timeout FRAMES`: Sets the timeout in frames (1/60s). If the start or update function runs longer than this it is forcibly interupted
and execution of the cart is stopped. Defaults to 30 (0.5s) and execution of the cart is stopped. Defaults to 30 (0.5s)
* `-w`, `--watch`: Reloads the given file every time it changes on disk. * `-w`, `--watch`: Reloads the given file every time it changes on disk.
* `-p`, `--pack`: Pack the file into an `.uw8` cart before running it and print the resulting size. * `-p`, `--pack`: Pack the file into an `.uw8` cart before running it and print the resulting size.
@@ -305,6 +306,27 @@ Options:
* `-u`, `--uncompressed`: Use the uncompressed `uw8` format for packing. * `-u`, `--uncompressed`: Use the uncompressed `uw8` format for packing.
* `-l LEVEL`, `--level LEVEL`: Compression level (0-9). Higher compression levels are really slow. * `-l LEVEL`, `--level LEVEL`: Compression level (0-9). Higher compression levels are really slow.
## `uw8 unpack`
Usage:
`uw8 unpack <infile> <outfile>`
Unpacks a MicroW8 module into a standard WebAssembly module.
## `uw8 compile`
Usage:
`uw8 compile [<options>] <infile> <outfile>`
Compiles a [CurlyWas](https://github.com/exoticorn/curlywas) source file to a standard WebAssembly module. Most useful together with
the `--debug` option to get a module that works well in the Chrome debugger.
Options:
* `-d`, `--debug`: Generate a name section to help debugging
## `uw8 filter-exports` ## `uw8 filter-exports`
Usage: Usage:
@@ -390,3 +412,33 @@ the first function in the file under the name `upd`.
Same as version `01` except everything after the first byte is compressed Same as version `01` except everything after the first byte is compressed
using a [custom LZ compression scheme](https://github.com/exoticorn/upkr). using a [custom LZ compression scheme](https://github.com/exoticorn/upkr).
# The web runtime
Load carts into the web runtime either by using the "Load cart..." button, or by dragging the file
onto the screen area.
## Input
For input, you can either use a standard gamepad or keyboard. On a keyboard use the arrow keys and the keys Z, X, A and S to emulate the A, B, X and Y buttons.
## Video recording
Press F10 to start recording, press again to stop. Then a download dialog will open for the video file.
The file might miss some metadata needed to load in some video editing tools, in that case you can run
it through ffmpeg like this `ffmpeg -i IN_NAME.webm -c copy -o OUT_NAME.webm to fix it up.
To convert it to 1280x720, for example for a lovebyte upload, you can use:
```
ffmpeg -i IN.webm -vf "scale=960:720:flags=neighbor,pad=1280:720:160:0" -r 60 OUT.mp4
```
## Screenshot
Pressing F9 opens a download dialog with a screenshot.
## Devkit mode
Append `#devkit` to the web runtime url in order to switch to devkit mode. In devkit mode, standard web assembly modules
are loaded bypassing the loader, removing all size restrictions. At the same time, the memory limit is increased to 1GB.

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="120" height="120" version="1.1" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
<path d="m3.9727 6.3613v27.945h7.8691l2.8281 66.549 6.9199-56.922h16.969l6.4375 56.922 4.1523-66.549h17.148l-9.5488 81.697h27.195c-4.1759-2.1972-7.3595-5.016-9.5234-8.4707-2.2464-3.6905-3.3691-7.6805-3.3691-11.973 0-4.7735 1.3845-8.9251 4.1523-12.455 2.7679-3.53 6.4981-6.3194 11.191-8.3652-4.2521-2.7277-7.3402-5.4948-9.2656-8.3027-1.8854-2.8481-2.8281-6.5783-2.8281-11.191 0-4.7334 1.223-8.8256 3.6699-12.275 2.4469-3.4498 5.7559-6.0981 9.9277-7.9434 4.212-1.8854 8.9647-2.8262 14.26-2.8262 5.2203 0 9.8397 0.84602 13.867 2.5234v-28.363h-40.227a14.855 14.855 0 0 1 0.019531 0.40234 14.855 14.855 0 0 1-14.855 14.855 14.855 14.855 0 0 1-14.855-14.855 14.855 14.855 0 0 1 0.052734-0.40234zm98.186 38.775c-3.0888 0-5.4939 0.863-7.2187 2.5879-1.6848 1.6848-2.5273 4.192-2.5273 7.5215 0 2.9283 0.98169 5.3538 2.9473 7.2793 1.9656 1.8854 5.0352 3.6113 9.207 5.1758 2.7277-2.0057 4.5928-3.971 5.5957-5.8965 1.0028-1.9656 1.5039-4.1129 1.5039-6.4395 0-2.9684-0.8017-5.4144-2.4062-7.3398-1.5644-1.9255-3.9326-2.8887-7.1016-2.8887zm-72.203 12.938-6.4902 57.93h12.393zm68.113 21.781c-2.4469 1.5644-4.3938 3.5502-5.8379 5.957-1.4441 2.4068-2.166 5.2741-2.166 8.6035 0 3.4498 1.0041 6.2781 3.0098 8.4844 2.0458 2.1662 5.0726 3.25 9.084 3.25 4.1317 0 7.142-1.1043 9.0274-3.3106 1.8854-2.2464 2.8281-4.8723 2.8281-7.8809 0-2.7679-0.56235-5.0153-1.6856-6.7402-1.0831-1.7249-2.7886-3.2096-5.1152-4.4531-2.3266-1.2836-5.3738-2.5864-9.1445-3.9102z" fill="#85a6b2" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<section> <section>
<h1 class="text-center heading-text">A WebAssembly based fantasy console</h1> <h1 class="text-center heading-text">A WebAssembly based fantasy console</h1>
</section> </section>
<a href="v0.1.0"> <a href="v0.1.1">
<img class="demonstration-gif" style="width:640px;height:480px;image-rendering:pixelated" src="img/technotunnel.png"></img> <img class="demonstration-gif" style="width:640px;height:480px;image-rendering:pixelated" src="img/technotunnel.png"></img>
</a> </a>
</div> </div>

55
src/filewatcher.rs Normal file
View File

@@ -0,0 +1,55 @@
use anyhow::{anyhow, bail, Result};
use notify::{DebouncedEvent, RecommendedWatcher, Watcher};
use std::{collections::BTreeSet, path::PathBuf, sync::mpsc, time::Duration};
pub struct FileWatcher {
watcher: RecommendedWatcher,
watched_files: BTreeSet<PathBuf>,
directories: BTreeSet<PathBuf>,
rx: mpsc::Receiver<DebouncedEvent>,
}
impl FileWatcher {
pub fn new() -> Result<FileWatcher> {
let (tx, rx) = mpsc::channel();
let watcher = notify::watcher(tx, Duration::from_millis(100))?;
Ok(FileWatcher {
watcher,
watched_files: BTreeSet::new(),
directories: BTreeSet::new(),
rx,
})
}
pub fn add_file<P: Into<PathBuf>>(&mut self, path: P) -> Result<()> {
let path = path.into();
let parent = path.parent().ok_or_else(|| anyhow!("File has no parent"))?;
if !self.directories.contains(parent) {
self.watcher
.watch(parent, notify::RecursiveMode::NonRecursive)?;
self.directories.insert(parent.to_path_buf());
}
self.watched_files.insert(path);
Ok(())
}
pub fn poll_changed_file(&self) -> Result<Option<PathBuf>> {
let event = self.rx.try_recv();
match event {
Ok(DebouncedEvent::Create(path) | DebouncedEvent::Write(path)) => {
let handle = same_file::Handle::from_path(&path)?;
for file in &self.watched_files {
if handle == same_file::Handle::from_path(file)? {
return Ok(Some(path));
}
}
}
Err(mpsc::TryRecvError::Disconnected) => bail!("File watcher disconnected"),
_ => (),
}
Ok(None)
}
}

View File

@@ -1,266 +1,22 @@
use std::io::prelude::*; mod filewatcher;
use std::path::Path; #[cfg(feature = "native")]
use std::sync::{Arc, Mutex}; mod run_native;
use std::time::Duration; #[cfg(feature = "browser")]
use std::{fs::File, thread, time::Instant}; mod run_web;
pub use filewatcher::FileWatcher;
#[cfg(feature = "native")]
pub use run_native::MicroW8;
#[cfg(feature = "browser")]
pub use run_web::RunWebServer;
use anyhow::Result; use anyhow::Result;
use minifb::{Key, Window, WindowOptions};
use wasmtime::{
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
};
static GAMEPAD_KEYS: &'static [Key] = &[ pub trait Runtime {
Key::Up, fn is_open(&self) -> bool;
Key::Down, fn set_timeout(&mut self, _timeout: u32) {
Key::Left, eprintln!("Warning: runtime doesn't support timeout");
Key::Right,
Key::Z,
Key::X,
Key::A,
Key::S,
];
pub struct MicroW8 {
engine: Engine,
loader_module: Module,
window: Window,
window_buffer: Vec<u32>,
instance: Option<UW8Instance>,
timeout: u32,
}
struct UW8Instance {
store: Store<()>,
memory: Memory,
end_frame: TypedFunc<(), ()>,
update: TypedFunc<(), ()>,
start_time: Instant,
module: Vec<u8>,
watchdog: Arc<Mutex<UW8WatchDog>>,
}
impl Drop for UW8Instance {
fn drop(&mut self) {
if let Ok(mut watchdog) = self.watchdog.lock() {
watchdog.stop = true;
}
} }
} fn load(&mut self, module_data: &[u8]) -> Result<()>;
fn run_frame(&mut self) -> Result<()>;
struct UW8WatchDog { }
interupt: wasmtime::InterruptHandle,
timeout: u32,
stop: bool,
}
impl MicroW8 {
pub fn new() -> Result<MicroW8> {
let engine = wasmtime::Engine::new(wasmtime::Config::new().interruptable(true))?;
let loader_module =
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
let mut options = WindowOptions::default();
options.scale = minifb::Scale::X2;
options.scale_mode = minifb::ScaleMode::AspectRatioStretch;
options.resize = true;
let mut window = Window::new("MicroW8", 320, 240, options)?;
window.limit_update_rate(Some(std::time::Duration::from_micros(16666)));
Ok(MicroW8 {
engine,
loader_module,
window,
window_buffer: vec![0u32; 320 * 240],
instance: None,
timeout: 30,
})
}
pub fn is_open(&self) -> bool {
self.window.is_open() && !self.window.is_key_down(Key::Escape)
}
pub fn set_timeout(&mut self, timeout: u32) {
self.timeout = timeout;
}
fn reset(&mut self) {
self.instance = None;
for v in &mut self.window_buffer {
*v = 0;
}
}
pub fn load_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.reset();
let mut module = vec![];
File::open(path)?.read_to_end(&mut module)?;
self.load_from_memory(&module)
}
pub fn load_from_memory(&mut self, module_data: &[u8]) -> Result<()> {
self.reset();
let mut store = wasmtime::Store::new(&self.engine, ());
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
let mut linker = wasmtime::Linker::new(&self.engine);
linker.define("env", "memory", memory.clone())?;
let loader_instance = linker.instantiate(&mut store, &self.loader_module)?;
let load_uw8 = loader_instance.get_typed_func::<i32, i32, _>(&mut store, "load_uw8")?;
let platform_data = include_bytes!("../platform/bin/platform.uw8");
memory.data_mut(&mut store)[..platform_data.len()].copy_from_slice(platform_data);
let platform_length =
load_uw8.call(&mut store, platform_data.len() as i32)? as u32 as usize;
let platform_module =
wasmtime::Module::new(&self.engine, &memory.data(&store)[..platform_length])?;
memory.data_mut(&mut store)[..module_data.len()].copy_from_slice(module_data);
let module_length = load_uw8.call(&mut store, module_data.len() as i32)? as u32 as usize;
let module = wasmtime::Module::new(&self.engine, &memory.data(&store)[..module_length])?;
linker.func_wrap("env", "acos", |v: f32| v.acos())?;
linker.func_wrap("env", "asin", |v: f32| v.asin())?;
linker.func_wrap("env", "atan", |v: f32| v.atan())?;
linker.func_wrap("env", "atan2", |x: f32, y: f32| x.atan2(y))?;
linker.func_wrap("env", "cos", |v: f32| v.cos())?;
linker.func_wrap("env", "exp", |v: f32| v.exp())?;
linker.func_wrap("env", "log", |v: f32| v.ln())?;
linker.func_wrap("env", "sin", |v: f32| v.sin())?;
linker.func_wrap("env", "tan", |v: f32| v.tan())?;
linker.func_wrap("env", "pow", |a: f32, b: f32| a.powf(b))?;
for i in 9..64 {
linker.func_wrap("env", &format!("reserved{}", i), || {})?;
}
for i in 0..16 {
linker.define(
"env",
&format!("g_reserved{}", i),
wasmtime::Global::new(
&mut store,
GlobalType::new(ValType::I32, Mutability::Const),
0.into(),
)?,
)?;
}
let platform_instance = linker.instantiate(&mut store, &platform_module)?;
for export in platform_instance.exports(&mut store) {
linker.define(
"env",
export.name(),
export
.into_func()
.expect("platform surely only exports functions"),
)?;
}
let watchdog = Arc::new(Mutex::new(UW8WatchDog {
interupt: store.interrupt_handle()?,
timeout: self.timeout,
stop: false,
}));
{
let watchdog = watchdog.clone();
thread::spawn(move || loop {
thread::sleep(Duration::from_millis(17));
if let Ok(mut watchdog) = watchdog.lock() {
if watchdog.stop {
break;
}
if watchdog.timeout > 0 {
watchdog.timeout -= 1;
if watchdog.timeout == 0 {
watchdog.interupt.interrupt();
}
}
} else {
break;
}
});
}
let instance = linker.instantiate(&mut store, &module)?;
if let Ok(mut watchdog) = watchdog.lock() {
watchdog.timeout = 0;
}
let end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?;
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd")?;
self.instance = Some(UW8Instance {
store,
memory,
end_frame,
update,
start_time: Instant::now(),
module: module_data.into(),
watchdog,
});
Ok(())
}
pub fn run_frame(&mut self) -> Result<()> {
let mut result = Ok(());
if let Some(mut instance) = self.instance.take() {
{
let time = instance.start_time.elapsed().as_millis() as i32;
let mut gamepad: u32 = 0;
for key in self.window.get_keys() {
if let Some(index) = GAMEPAD_KEYS
.iter()
.enumerate()
.find(|(_, &k)| k == key)
.map(|(i, _)| i)
{
gamepad |= 1 << index;
}
}
let mem = instance.memory.data_mut(&mut instance.store);
mem[64..68].copy_from_slice(&time.to_le_bytes());
mem[68..72].copy_from_slice(&gamepad.to_le_bytes());
}
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = self.timeout;
}
result = instance.update.call(&mut instance.store, ());
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = 0;
}
instance.end_frame.call(&mut instance.store, ())?;
let memory = instance.memory.data(&instance.store);
let framebuffer = &memory[120..];
let palette = &memory[0x13000..];
for i in 0..320 * 240 {
let offset = framebuffer[i] as usize * 4;
self.window_buffer[i] = 0xff000000
| ((palette[offset + 0] as u32) << 16)
| ((palette[offset + 1] as u32) << 8)
| palette[offset + 2] as u32;
}
if self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No) {
self.load_from_memory(&instance.module)?;
} else if result.is_ok() {
self.instance = Some(instance);
}
}
self.window
.update_with_buffer(&self.window_buffer, 320, 240)?;
result?;
Ok(())
}
}

View File

@@ -1,35 +1,40 @@
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::process; use std::process;
use std::sync::mpsc;
use std::time::Duration;
use std::{
path::{Path, PathBuf},
process::exit,
};
use anyhow::{bail, Result}; use anyhow::Result;
use notify::{DebouncedEvent, Watcher};
use pico_args::Arguments; use pico_args::Arguments;
#[cfg(feature = "native")]
use uw8::MicroW8; use uw8::MicroW8;
#[cfg(feature = "browser")]
use uw8::RunWebServer;
#[cfg(any(feature = "native", feature = "browser"))]
use uw8::Runtime;
fn main() -> Result<()> { fn main() -> Result<()> {
let mut args = Arguments::from_env(); let mut args = Arguments::from_env();
match args.subcommand()?.as_ref().map(|s| s.as_str()) { match args.subcommand()?.as_deref() {
Some("version") => { Some("version") => {
println!("{}", env!("CARGO_PKG_VERSION")); println!("{}", env!("CARGO_PKG_VERSION"));
Ok(()) Ok(())
} }
#[cfg(any(feature = "native", feature = "browser"))]
Some("run") => run(args), Some("run") => run(args),
Some("pack") => pack(args), Some("pack") => pack(args),
Some("unpack") => unpack(args),
Some("compile") => compile(args),
Some("filter-exports") => filter_exports(args), Some("filter-exports") => filter_exports(args),
Some("help") | None => { Some("help") | None => {
println!("uw8 {}", env!("CARGO_PKG_VERSION")); println!("uw8 {}", env!("CARGO_PKG_VERSION"));
println!(); println!();
println!("Usage:"); println!("Usage:");
println!(" uw8 run [-t/--timeout <frames>] [-w/--watch] [-p/--pack] [-u/--uncompressed] [-l/--level] [-o/--output <out-file>] <file>"); #[cfg(any(feature = "native", feature = "browser"))]
println!(" uw8 run [-t/--timeout <frames>] [--b/--browser] [-w/--watch] [-p/--pack] [-u/--uncompressed] [-l/--level] [-o/--output <out-file>] <file>");
println!(" uw8 pack [-u/--uncompressed] [-l/--level] <in-file> <out-file>"); println!(" uw8 pack [-u/--uncompressed] [-l/--level] <in-file> <out-file>");
println!(" uw8 unpack <in-file> <out-file>");
println!(" uw8 compile [-d/--debug] <in-file> <out-file>");
println!(" uw8 filter-exports <in-wasm> <out-wasm>"); println!(" uw8 filter-exports <in-wasm> <out-wasm>");
Ok(()) Ok(())
} }
@@ -40,6 +45,7 @@ fn main() -> Result<()> {
} }
} }
#[cfg(any(feature = "native", feature = "browser"))]
fn run(mut args: Arguments) -> Result<()> { fn run(mut args: Arguments) -> Result<()> {
let watch_mode = args.contains(["-w", "--watch"]); let watch_mode = args.contains(["-w", "--watch"]);
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?; let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
@@ -64,40 +70,56 @@ fn run(mut args: Arguments) -> Result<()> {
config.output_path = Some(path); config.output_path = Some(path);
} }
#[cfg(feature = "native")]
let run_browser = args.contains(["-b", "--browser"]);
#[cfg(not(feature = "native"))]
let run_browser = args.contains(["-b", "--browser"]) || true;
let filename = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?; let filename = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let mut uw8 = MicroW8::new()?; let mut watcher = uw8::FileWatcher::new()?;
use std::process::exit;
let mut runtime: Box<dyn Runtime> = if !run_browser {
#[cfg(not(feature = "native"))]
unimplemented!();
#[cfg(feature = "native")]
Box::new(MicroW8::new()?)
} else {
#[cfg(not(feature = "browser"))]
unimplemented!();
#[cfg(feature = "browser")]
Box::new(RunWebServer::new())
};
if let Some(timeout) = timeout { if let Some(timeout) = timeout {
uw8.set_timeout(timeout); runtime.set_timeout(timeout);
} }
let (tx, rx) = mpsc::channel(); let mut first_run = true;
let mut watcher = notify::watcher(tx, Duration::from_millis(100))?;
if watch_mode { while runtime.is_open() {
watcher.watch(&filename, notify::RecursiveMode::NonRecursive)?; if first_run || watcher.poll_changed_file()?.is_some() {
} match start_cart(&filename, &mut *runtime, &config) {
Ok(dependencies) => {
if let Err(err) = start_cart(&filename, &mut uw8, &config) { if watch_mode {
eprintln!("Load error: {}", err); for dep in dependencies {
if !watch_mode { watcher.add_file(dep)?;
exit(1); }
} }
} }
Err(err) => {
while uw8.is_open() {
match rx.try_recv() {
Ok(DebouncedEvent::Create(_) | DebouncedEvent::Write(_)) => {
if let Err(err) = start_cart(&filename, &mut uw8, &config) {
eprintln!("Load error: {}", err); eprintln!("Load error: {}", err);
if !watch_mode {
exit(1);
}
} }
} }
Err(mpsc::TryRecvError::Disconnected) => bail!("File watcher disconnected"), first_run = false;
_ => (),
} }
if let Err(err) = uw8.run_frame() { if let Err(err) = runtime.run_frame() {
eprintln!("Runtime error: {}", err); eprintln!("Runtime error: {}", err);
if !watch_mode { if !watch_mode {
exit(1); exit(1);
@@ -114,39 +136,83 @@ struct Config {
output_path: Option<PathBuf>, output_path: Option<PathBuf>,
} }
fn load_cart(filename: &Path, pack: &Option<uw8_tool::PackConfig>) -> Result<Vec<u8>> { fn load_cart(filename: &Path, config: &Config) -> Result<(Vec<u8>, Vec<PathBuf>)> {
let mut cart = vec![]; let mut dependencies = Vec::new();
File::open(filename)?.read_to_end(&mut cart)?; let mut cart = match SourceType::of_file(filename)? {
SourceType::Binary => {
let mut cart = vec![];
File::open(filename)?.read_to_end(&mut cart)?;
dependencies.push(filename.to_path_buf());
cart
}
SourceType::Wat => {
let cart = wat::parse_file(filename)?;
dependencies.push(filename.to_path_buf());
cart
}
SourceType::CurlyWas => {
let module = curlywas::compile_file(filename, curlywas::Options::default())?;
dependencies = module.dependencies;
module.wasm
}
};
if cart[0] >= 10 { if let Some(ref pack_config) = config.pack {
let src = String::from_utf8(cart)?;
cart = if src.chars().find(|c| !c.is_whitespace()) == Some('(') {
wat::parse_str(src)?
} else {
curlywas::compile_str(&src, filename, curlywas::Options::default())?
};
}
if let Some(pack_config) = pack {
cart = uw8_tool::pack(&cart, pack_config)?; cart = uw8_tool::pack(&cart, pack_config)?;
println!("packed size: {} bytes", cart.len()); println!("packed size: {} bytes", cart.len());
} }
Ok(cart)
}
fn start_cart(filename: &Path, uw8: &mut MicroW8, config: &Config) -> Result<()> {
let cart = load_cart(filename, &config.pack)?;
if let Some(ref path) = config.output_path { if let Some(ref path) = config.output_path {
File::create(path)?.write_all(&cart)?; File::create(path)?.write_all(&cart)?;
} }
if let Err(err) = uw8.load_from_memory(&cart) { Ok((cart, dependencies))
}
enum SourceType {
Binary,
Wat,
CurlyWas,
}
impl SourceType {
fn of_file(filename: &Path) -> Result<SourceType> {
if let Some(extension) = filename.extension() {
if extension == "uw8" || extension == "wasm" {
return Ok(SourceType::Binary);
} else if extension == "wat" || extension == "wast" {
return Ok(SourceType::Wat);
} else if extension == "cwa" {
return Ok(SourceType::CurlyWas);
}
}
let mut cart = vec![];
File::open(filename)?.read_to_end(&mut cart)?;
let ty = if cart[0] < 10 {
SourceType::Binary
} else {
let src = String::from_utf8(cart)?;
if src.chars().find(|&c| !c.is_whitespace() && c != ';') == Some('(') {
SourceType::Wat
} else {
SourceType::CurlyWas
}
};
Ok(ty)
}
}
#[cfg(any(feature = "native", feature = "browser"))]
fn start_cart(filename: &Path, runtime: &mut dyn Runtime, config: &Config) -> Result<Vec<PathBuf>> {
let cart = load_cart(filename, config)?;
if let Err(err) = runtime.load(&cart.0) {
eprintln!("Load error: {}", err); eprintln!("Load error: {}", err);
Err(err) Err(err)
} else { } else {
Ok(()) Ok(cart.1)
} }
} }
@@ -165,13 +231,41 @@ fn pack(mut args: Arguments) -> Result<()> {
let out_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?; let out_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let cart = load_cart(&in_file, &Some(pack_config))?; let (cart, _) = load_cart(
&in_file,
&Config {
pack: Some(pack_config),
output_path: None,
},
)?;
File::create(out_file)?.write_all(&cart)?; File::create(out_file)?.write_all(&cart)?;
Ok(()) Ok(())
} }
fn unpack(mut args: Arguments) -> Result<()> {
let in_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let out_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
uw8_tool::unpack_file(&in_file, &out_file)
}
fn compile(mut args: Arguments) -> Result<()> {
let mut options = curlywas::Options::default();
if args.contains(["-d", "--debug"]) {
options = options.with_debug();
}
let in_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let out_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let module = curlywas::compile_file(in_file, options)?;
File::create(out_file)?.write_all(&module.wasm)?;
Ok(())
}
fn filter_exports(mut args: Arguments) -> Result<()> { fn filter_exports(mut args: Arguments) -> Result<()> {
let in_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?; let in_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let out_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?; let out_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;

1
src/run-web.html Normal file

File diff suppressed because one or more lines are too long

260
src/run_native.rs Normal file
View File

@@ -0,0 +1,260 @@
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{thread, time::Instant};
use anyhow::Result;
use minifb::{Key, Window, WindowOptions};
use wasmtime::{
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
};
static GAMEPAD_KEYS: &[Key] = &[
Key::Up,
Key::Down,
Key::Left,
Key::Right,
Key::Z,
Key::X,
Key::A,
Key::S,
];
pub struct MicroW8 {
engine: Engine,
loader_module: Module,
window: Window,
window_buffer: Vec<u32>,
instance: Option<UW8Instance>,
timeout: u32,
}
struct UW8Instance {
store: Store<()>,
memory: Memory,
end_frame: TypedFunc<(), ()>,
update: TypedFunc<(), ()>,
start_time: Instant,
module: Vec<u8>,
watchdog: Arc<Mutex<UW8WatchDog>>,
}
impl Drop for UW8Instance {
fn drop(&mut self) {
if let Ok(mut watchdog) = self.watchdog.lock() {
watchdog.stop = true;
}
}
}
struct UW8WatchDog {
interupt: wasmtime::InterruptHandle,
timeout: u32,
stop: bool,
}
impl MicroW8 {
pub fn new() -> Result<MicroW8> {
let engine = wasmtime::Engine::new(wasmtime::Config::new().interruptable(true))?;
let loader_module =
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
let options = WindowOptions {
scale: minifb::Scale::X2,
scale_mode: minifb::ScaleMode::AspectRatioStretch,
resize: true,
..Default::default()
};
let mut window = Window::new("MicroW8", 320, 240, options)?;
window.limit_update_rate(Some(std::time::Duration::from_micros(16666)));
Ok(MicroW8 {
engine,
loader_module,
window,
window_buffer: vec![0u32; 320 * 240],
instance: None,
timeout: 30,
})
}
fn reset(&mut self) {
self.instance = None;
for v in &mut self.window_buffer {
*v = 0;
}
}
}
impl super::Runtime for MicroW8 {
fn is_open(&self) -> bool {
self.window.is_open() && !self.window.is_key_down(Key::Escape)
}
fn set_timeout(&mut self, timeout: u32) {
self.timeout = timeout;
}
fn load(&mut self, module_data: &[u8]) -> Result<()> {
self.reset();
let mut store = wasmtime::Store::new(&self.engine, ());
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
let mut linker = wasmtime::Linker::new(&self.engine);
linker.define("env", "memory", memory)?;
let loader_instance = linker.instantiate(&mut store, &self.loader_module)?;
let load_uw8 = loader_instance.get_typed_func::<i32, i32, _>(&mut store, "load_uw8")?;
let platform_data = include_bytes!("../platform/bin/platform.uw8");
memory.data_mut(&mut store)[..platform_data.len()].copy_from_slice(platform_data);
let platform_length =
load_uw8.call(&mut store, platform_data.len() as i32)? as u32 as usize;
let platform_module =
wasmtime::Module::new(&self.engine, &memory.data(&store)[..platform_length])?;
memory.data_mut(&mut store)[..module_data.len()].copy_from_slice(module_data);
let module_length = load_uw8.call(&mut store, module_data.len() as i32)? as u32 as usize;
let module = wasmtime::Module::new(&self.engine, &memory.data(&store)[..module_length])?;
linker.func_wrap("env", "acos", |v: f32| v.acos())?;
linker.func_wrap("env", "asin", |v: f32| v.asin())?;
linker.func_wrap("env", "atan", |v: f32| v.atan())?;
linker.func_wrap("env", "atan2", |x: f32, y: f32| x.atan2(y))?;
linker.func_wrap("env", "cos", |v: f32| v.cos())?;
linker.func_wrap("env", "exp", |v: f32| v.exp())?;
linker.func_wrap("env", "log", |v: f32| v.ln())?;
linker.func_wrap("env", "sin", |v: f32| v.sin())?;
linker.func_wrap("env", "tan", |v: f32| v.tan())?;
linker.func_wrap("env", "pow", |a: f32, b: f32| a.powf(b))?;
for i in 9..64 {
linker.func_wrap("env", &format!("reserved{}", i), || {})?;
}
for i in 0..16 {
linker.define(
"env",
&format!("g_reserved{}", i),
wasmtime::Global::new(
&mut store,
GlobalType::new(ValType::I32, Mutability::Const),
0.into(),
)?,
)?;
}
let platform_instance = linker.instantiate(&mut store, &platform_module)?;
for export in platform_instance.exports(&mut store) {
linker.define(
"env",
export.name(),
export
.into_func()
.expect("platform surely only exports functions"),
)?;
}
let watchdog = Arc::new(Mutex::new(UW8WatchDog {
interupt: store.interrupt_handle()?,
timeout: self.timeout,
stop: false,
}));
{
let watchdog = watchdog.clone();
thread::spawn(move || loop {
thread::sleep(Duration::from_millis(17));
if let Ok(mut watchdog) = watchdog.lock() {
if watchdog.stop {
break;
}
if watchdog.timeout > 0 {
watchdog.timeout -= 1;
if watchdog.timeout == 0 {
watchdog.interupt.interrupt();
}
}
} else {
break;
}
});
}
let instance = linker.instantiate(&mut store, &module)?;
if let Ok(mut watchdog) = watchdog.lock() {
watchdog.timeout = 0;
}
let end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?;
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd")?;
self.instance = Some(UW8Instance {
store,
memory,
end_frame,
update,
start_time: Instant::now(),
module: module_data.into(),
watchdog,
});
Ok(())
}
fn run_frame(&mut self) -> Result<()> {
let mut result = Ok(());
if let Some(mut instance) = self.instance.take() {
{
let time = instance.start_time.elapsed().as_millis() as i32;
let mut gamepad: u32 = 0;
for key in self.window.get_keys() {
if let Some(index) = GAMEPAD_KEYS
.iter()
.enumerate()
.find(|(_, &k)| k == key)
.map(|(i, _)| i)
{
gamepad |= 1 << index;
}
}
let mem = instance.memory.data_mut(&mut instance.store);
mem[64..68].copy_from_slice(&time.to_le_bytes());
mem[68..72].copy_from_slice(&gamepad.to_le_bytes());
}
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = self.timeout;
}
result = instance.update.call(&mut instance.store, ());
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = 0;
}
instance.end_frame.call(&mut instance.store, ())?;
let memory = instance.memory.data(&instance.store);
let framebuffer = &memory[120..(120 + 320 * 240)];
let palette = &memory[0x13000..];
for (i, &color_index) in framebuffer.iter().enumerate() {
let offset = color_index as usize * 4;
self.window_buffer[i] = 0xff000000
| ((palette[offset] as u32) << 16)
| ((palette[offset + 1] as u32) << 8)
| palette[offset + 2] as u32;
}
if self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No) {
self.load(&instance.module)?;
} else if result.is_ok() {
self.instance = Some(instance);
}
}
self.window
.update_with_buffer(&self.window_buffer, 320, 240)?;
result?;
Ok(())
}
}

89
src/run_web.rs Normal file
View File

@@ -0,0 +1,89 @@
use anyhow::Result;
use std::{
net::SocketAddr,
sync::{Arc, Mutex},
thread,
};
use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, Stream, StreamExt};
use warp::{http::Response, Filter};
pub struct RunWebServer {
cart: Arc<Mutex<Vec<u8>>>,
tx: broadcast::Sender<()>,
}
impl RunWebServer {
pub fn new() -> RunWebServer {
let cart = Arc::new(Mutex::new(Vec::new()));
let (tx, _) = broadcast::channel(1);
let server_cart = cart.clone();
let server_tx = tx.clone();
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.expect("Failed to create tokio runtime");
rt.block_on(async {
let html = warp::path::end().map(|| {
Response::builder()
.header("Content-Type", "text/html")
.body(include_str!("run-web.html"))
});
let cart = warp::path("cart")
.map(move || server_cart.lock().map_or(Vec::new(), |c| c.clone()));
let events = warp::path("events").and(warp::get()).map(move || {
fn event_stream(
tx: &broadcast::Sender<()>,
) -> impl Stream<Item = Result<warp::sse::Event, std::convert::Infallible>>
{
BroadcastStream::new(tx.subscribe())
.map(|_| Ok(warp::sse::Event::default().data("L")))
}
warp::sse::reply(warp::sse::keep_alive().stream(event_stream(&server_tx)))
});
let socket_addr = "127.0.0.1:3030"
.parse::<SocketAddr>()
.expect("Failed to parse socket address");
let server_future = warp::serve(html.or(cart).or(events)).bind(socket_addr);
println!("Point browser at http://{}", socket_addr);
let _ignore_result = webbrowser::open(&format!("http://{}", socket_addr));
server_future.await
});
});
RunWebServer { cart, tx }
}
}
impl super::Runtime for RunWebServer {
fn load(&mut self, module_data: &[u8]) -> Result<()> {
if let Ok(mut lock) = self.cart.lock() {
lock.clear();
lock.extend_from_slice(module_data);
}
let _ignore_result = self.tx.send(());
Ok(())
}
fn is_open(&self) -> bool {
true
}
fn run_frame(&mut self) -> Result<()> {
std::thread::sleep(std::time::Duration::from_millis(100));
Ok(())
}
}
impl Default for RunWebServer {
fn default() -> RunWebServer {
RunWebServer::new()
}
}

13
test.cwa Normal file
View File

@@ -0,0 +1,13 @@
import "env.memory" memory(4);
import "env.printString" fn print(i32);
export fn upd() {
}
start fn start() {
print(0);
}
data 0 {
"Press " i8(0xe0) " and " i8(0xe1) " to adjust, " i8(0xcc) " to commit." i8(0)
}

BIN
test.wasm Normal file

Binary file not shown.

View File

@@ -71,26 +71,100 @@ impl BaseModule {
add_function(&mut functions, &type_map, "randomSeed", &[I32], None); add_function(&mut functions, &type_map, "randomSeed", &[I32], None);
add_function(&mut functions, &type_map, "cls", &[I32], None); add_function(&mut functions, &type_map, "cls", &[I32], None);
add_function(&mut functions, &type_map, "setPixel", &[I32, I32, I32], None); add_function(
add_function(&mut functions, &type_map, "getPixel", &[I32, I32], Some(I32)); &mut functions,
add_function(&mut functions, &type_map, "hline", &[I32, I32, I32, I32], None); &type_map,
add_function(&mut functions, &type_map, "rectangle", &[F32, F32, F32, F32, I32], None); "setPixel",
add_function(&mut functions, &type_map, "circle", &[F32, F32, F32, I32], None); &[I32, I32, I32],
add_function(&mut functions, &type_map, "line", &[F32, F32, F32, F32, I32], None); None,
);
add_function(
&mut functions,
&type_map,
"getPixel",
&[I32, I32],
Some(I32),
);
add_function(
&mut functions,
&type_map,
"hline",
&[I32, I32, I32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"rectangle",
&[F32, F32, F32, F32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"circle",
&[F32, F32, F32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"line",
&[F32, F32, F32, F32, I32],
None,
);
add_function(&mut functions, &type_map, "time", &[], Some(F32)); add_function(&mut functions, &type_map, "time", &[], Some(F32));
add_function(&mut functions, &type_map, "isButtonPressed", &[I32], Some(I32)); add_function(
add_function(&mut functions, &type_map, "isButtonTriggered", &[I32], Some(I32)); &mut functions,
&type_map,
"isButtonPressed",
&[I32],
Some(I32),
);
add_function(
&mut functions,
&type_map,
"isButtonTriggered",
&[I32],
Some(I32),
);
add_function(&mut functions, &type_map, "printChar", &[I32], None); add_function(&mut functions, &type_map, "printChar", &[I32], None);
add_function(&mut functions, &type_map, "printString", &[I32], None); add_function(&mut functions, &type_map, "printString", &[I32], None);
add_function(&mut functions, &type_map, "printInt", &[I32], None); add_function(&mut functions, &type_map, "printInt", &[I32], None);
add_function(&mut functions, &type_map, "setTextColor", &[I32], None); add_function(&mut functions, &type_map, "setTextColor", &[I32], None);
add_function(&mut functions, &type_map, "setBackgroundColor", &[I32], None); add_function(
add_function(&mut functions, &type_map, "setCursorPosition", &[I32, I32], None); &mut functions,
&type_map,
"setBackgroundColor",
&[I32],
None,
);
add_function(
&mut functions,
&type_map,
"setCursorPosition",
&[I32, I32],
None,
);
add_function(&mut functions, &type_map, "rectangle_outline", &[F32, F32, F32, F32, I32], None); add_function(
add_function(&mut functions, &type_map, "circle_outline", &[F32, F32, F32, I32], None); &mut functions,
&type_map,
"rectangle_outline",
&[F32, F32, F32, F32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"circle_outline",
&[F32, F32, F32, I32],
None,
);
add_function(&mut functions, &type_map, "exp", &[F32], Some(F32));
for i in functions.len()..64 { for i in functions.len()..64 {
add_function( add_function(
@@ -214,6 +288,68 @@ impl BaseModule {
File::create(path)?.write_all(&data)?; File::create(path)?.write_all(&data)?;
Ok(()) Ok(())
} }
pub fn write_as_cwa<P: AsRef<Path>>(&self, path: P) -> Result<()> {
fn inner(mut file: File, base: &BaseModule) -> Result<()> {
writeln!(file, "// MicroW8 APIs, to be `include`d in CurlyWas sources")?;
writeln!(file, "import \"env.memory\" memory({});", base.memory)?;
writeln!(file)?;
for &(module, ref name, type_id) in &base.function_imports {
if !name.contains("reserved") {
let ty = &base.types[type_id as usize];
let params: Vec<&str> = ty.params.iter().copied().map(type_to_str).collect();
write!(
file,
"import \"{}.{}\" fn {}({})",
module,
name,
name,
params.join(", ")
)?;
if let Some(result) = ty.result {
write!(file, " -> {}", type_to_str(result))?;
}
writeln!(file, ";")?;
}
}
writeln!(file)?;
for &(name, value) in CONSTANTS {
writeln!(file, "const {} = 0x{:x};", name, value)?;
}
Ok(())
}
inner(File::create(path)?, self)
}
pub fn write_as_wat<P: AsRef<Path>>(&self, path: P) -> Result<()> {
fn inner(mut file: File, base: &BaseModule) -> Result<()> {
writeln!(file, ";; MicroW8 APIs, in WAT (Wasm Text) format")?;
writeln!(file, "(import \"env\" \"memory\" (memory {}))", base.memory)?;
writeln!(file)?;
for &(module, ref name, type_id) in &base.function_imports {
if !name.contains("reserved") {
let ty = &base.types[type_id as usize];
write!(file, "(import \"{}\" \"{}\" (func ${}", module, name, name)?;
for &param in &ty.params {
write!(file, " (param {})", type_to_str(param))?;
}
if let Some(result) = ty.result {
write!(file, " (result {})", type_to_str(result))?;
}
writeln!(file, "))")?;
}
}
writeln!(file)?;
writeln!(file, ";; to use defines, include this file with a preprocessor\n;; like gpp (https://logological.org/gpp).")?;
for &(name, value) in CONSTANTS {
writeln!(file, "#define {} 0x{:x};", name, value)?;
}
Ok(())
}
inner(File::create(path)?, self)
}
} }
fn add_function( fn add_function(
@@ -241,3 +377,30 @@ fn lookup_type(
}; };
*type_map.get(&key).unwrap() *type_map.get(&key).unwrap()
} }
fn type_to_str(ty: ValType) -> &'static str {
match ty {
ValType::I32 => "i32",
ValType::I64 => "i64",
ValType::F32 => "f32",
ValType::F64 => "f64",
_ => unimplemented!(),
}
}
const CONSTANTS: &[(&str, u32)] = &[
("TIME_MS", 0x40),
("GAMEPAD", 0x44),
("FRAMEBUFFER", 0x78),
("PALETTE", 0x13000),
("FONT", 0x13400),
("USER_MEM", 0x14000),
("BUTTON_UP", 0),
("BUTTON_DOWN", 1),
("BUTTON_LEFT", 2),
("BUTTON_RIGHT", 3),
("BUTTON_A", 4),
("BUTTON_B", 5),
("BUTTON_X", 6),
("BUTTON_Y", 7)
];

View File

@@ -1,8 +1,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use uw8_tool::BaseModule;
use pico_args::Arguments; use pico_args::Arguments;
use uw8_tool::BaseModule;
fn main() -> Result<()> { fn main() -> Result<()> {
let mut args = Arguments::from_env(); let mut args = Arguments::from_env();
@@ -32,6 +32,14 @@ fn main() -> Result<()> {
let dest: PathBuf = args.free_from_str()?; let dest: PathBuf = args.free_from_str()?;
uw8_tool::filter_exports(&source, &dest)?; uw8_tool::filter_exports(&source, &dest)?;
} }
"base-cwa" => {
let path: PathBuf = args.free_from_str()?;
BaseModule::for_format_version(1)?.write_as_cwa(path)?;
}
"base-wat" => {
let path: PathBuf = args.free_from_str()?;
BaseModule::for_format_version(1)?.write_as_wat(path)?;
}
_ => { _ => {
eprintln!("Unknown subcommand '{}'", cmd); eprintln!("Unknown subcommand '{}'", cmd);
print_help(); print_help();

View File

@@ -10,7 +10,7 @@ use std::{
use wasm_encoder as enc; use wasm_encoder as enc;
use wasmparser::{ use wasmparser::{
BinaryReader, ExportSectionReader, ExternalKind, FunctionBody, FunctionSectionReader, BinaryReader, ExportSectionReader, ExternalKind, FunctionBody, FunctionSectionReader,
ImportSectionEntryType, ImportSectionReader, TypeSectionReader, ImportSectionEntryType, ImportSectionReader, TableSectionReader, TypeSectionReader,
}; };
pub struct PackConfig { pub struct PackConfig {
@@ -31,7 +31,9 @@ impl PackConfig {
impl Default for PackConfig { impl Default for PackConfig {
fn default() -> PackConfig { fn default() -> PackConfig {
PackConfig { compression: Some(2) } PackConfig {
compression: Some(2),
}
} }
} }
@@ -156,6 +158,8 @@ struct ParsedModule<'a> {
start_section: Option<u32>, start_section: Option<u32>,
function_bodies: Vec<wasmparser::FunctionBody<'a>>, function_bodies: Vec<wasmparser::FunctionBody<'a>>,
data_section: Option<Section<()>>, data_section: Option<Section<()>>,
table_section: Option<Section<()>>,
element_section: Option<Vec<Element>>,
} }
impl<'a> ParsedModule<'a> { impl<'a> ParsedModule<'a> {
@@ -170,6 +174,8 @@ impl<'a> ParsedModule<'a> {
let mut start_section = None; let mut start_section = None;
let mut function_bodies = Vec::new(); let mut function_bodies = Vec::new();
let mut data_section = None; let mut data_section = None;
let mut table_section = None;
let mut element_section = None;
let mut offset = 0; let mut offset = 0;
@@ -209,6 +215,17 @@ impl<'a> ParsedModule<'a> {
Payload::DataSection(_) => { Payload::DataSection(_) => {
data_section = Some(Section::new(range, ())); data_section = Some(Section::new(range, ()));
} }
Payload::TableSection(reader) => {
validate_table_section(reader)?;
table_section = Some(Section::new(range, ()));
}
Payload::ElementSection(mut reader) => {
let mut elements = Vec::with_capacity(reader.get_count() as usize);
for _ in 0..reader.get_count() {
elements.push(Element::parse(reader.read()?)?);
}
element_section = Some(elements);
}
Payload::CodeSectionStart { .. } => (), Payload::CodeSectionStart { .. } => (),
Payload::CodeSectionEntry(body) => function_bodies.push(body), Payload::CodeSectionEntry(body) => function_bodies.push(body),
Payload::CustomSection { .. } => (), Payload::CustomSection { .. } => (),
@@ -229,6 +246,8 @@ impl<'a> ParsedModule<'a> {
start_section, start_section,
function_bodies, function_bodies,
data_section, data_section,
table_section,
element_section,
}) })
} }
@@ -405,6 +424,10 @@ impl<'a> ParsedModule<'a> {
module.section(&function_section); module.section(&function_section);
} }
if let Some(tables) = self.table_section {
copy_section(&mut module, &self.data[tables.range.clone()])?;
}
if let Some(ref globals) = self.globals { if let Some(ref globals) = self.globals {
copy_section(&mut module, &self.data[globals.range.clone()])?; copy_section(&mut module, &self.data[globals.range.clone()])?;
for i in 0..globals.data { for i in 0..globals.data {
@@ -446,6 +469,25 @@ impl<'a> ParsedModule<'a> {
}); });
} }
if let Some(elements) = self.element_section {
let mut element_section = wasm_encoder::ElementSection::new();
for element in elements {
let mut functions = Vec::with_capacity(element.functions.len());
for index in element.functions {
functions.push(*function_map.get(&index).ok_or_else(|| {
anyhow!("Function index {} not found in function map", index)
})?);
}
element_section.active(
None,
&wasm_encoder::Instruction::I32Const(element.start_index as i32),
ValType::FuncRef,
wasm_encoder::Elements::Functions(&functions),
);
}
module.section(&element_section);
}
{ {
let mut code_section = enc::CodeSection::new(); let mut code_section = enc::CodeSection::new();
@@ -502,6 +544,19 @@ fn read_type_section(reader: TypeSectionReader) -> Result<Vec<base_module::Funct
Ok(function_types) Ok(function_types)
} }
fn validate_table_section(mut reader: TableSectionReader) -> Result<()> {
if reader.get_count() != 1 {
bail!("Only up to one table supported");
}
let type_ = reader.read()?;
if type_.element_type != wasmparser::Type::FuncRef {
bail!("Only one funcref table is supported");
}
Ok(())
}
#[derive(Debug)] #[derive(Debug)]
struct Section<T> { struct Section<T> {
range: std::ops::Range<usize>, range: std::ops::Range<usize>,
@@ -579,6 +634,51 @@ impl ImportSection {
} }
} }
#[derive(Debug)]
struct Element {
start_index: u32,
functions: Vec<u32>,
}
impl Element {
fn parse(element: wasmparser::Element) -> Result<Element> {
if element.ty != wasmparser::Type::FuncRef {
bail!("Table element type is not FuncRef");
}
let start_index = if let wasmparser::ElementKind::Active {
init_expr,
table_index: 0,
} = element.kind
{
let mut init_reader = init_expr.get_operators_reader();
if let wasmparser::Operator::I32Const { value: start_index } = init_reader.read()? {
start_index as u32
} else {
bail!("Table element start index is not a integer constant");
}
} else {
bail!("Unsupported table element kind");
};
let mut items_reader = element.items.get_items_reader()?;
let mut functions = Vec::with_capacity(items_reader.get_count() as usize);
for _ in 0..items_reader.get_count() {
if let wasmparser::ElementItem::Func(index) = items_reader.read()? {
functions.push(index);
} else {
bail!("Table element item is not a function");
}
}
Ok(Element {
start_index,
functions,
})
}
}
#[derive(Debug)] #[derive(Debug)]
struct FunctionImport { struct FunctionImport {
module: String, module: String,
@@ -678,8 +778,13 @@ fn remap_function(
.get(&function_index) .get(&function_index)
.ok_or_else(|| anyhow!("Function index out of range: {}", function_index))?, .ok_or_else(|| anyhow!("Function index out of range: {}", function_index))?,
), ),
De::CallIndirect { .. } De::CallIndirect { index, table_index } => En::CallIndirect {
| De::ReturnCall { .. } ty: *type_map
.get(&index)
.ok_or_else(|| anyhow!("Unknown function type in call indirect"))?,
table: table_index,
},
De::ReturnCall { .. }
| De::ReturnCallIndirect { .. } | De::ReturnCallIndirect { .. }
| De::Delegate { .. } | De::Delegate { .. }
| De::CatchAll => todo!(), | De::CatchAll => todo!(),

2
web/build-run-web Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
rm -rf .parcel-cache && yarn parcel build src/run-web.html && cp dist/run-web.html ../src/

View File

@@ -10,7 +10,7 @@
</head> </head>
<body> <body>
<div id="uw8"> <div id="uw8">
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.1.0 <a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.1.2
</div> </div>
<div id="centered"> <div id="centered">
<canvas id="screen" width="320" height="240"> <canvas id="screen" width="320" height="240">

View File

@@ -1,5 +1,4 @@
import loaderUrl from "data-url:../../platform/bin/loader.wasm"; import MicroW8 from './microw8.js';
import platformUrl from "data-url:../../platform/bin/platform.uw8";
function setMessage(size, error) { function setMessage(size, error) {
let html = size ? `${size} bytes` : 'Insert cart'; let html = size ? `${size} bytes` : 'Insert cart';
@@ -9,291 +8,27 @@ function setMessage(size, error) {
document.getElementById('message').innerHTML = html; document.getElementById('message').innerHTML = html;
} }
let screen = document.getElementById('screen'); let uw8 = MicroW8(document.getElementById('screen'), {
let canvasCtx = screen.getContext('2d'); setMessage,
let imageData = canvasCtx.createImageData(320, 240); keyboardElement: window,
timerElement: document.getElementById("timer"),
let cancelFunction; });
let currentData;
let U8 = (d) => new Uint8Array(d);
let U32 = (d) => new Uint32Array(d);
let pad = 0;
let keyHandler = (e) => {
let isKeyDown = e.type == 'keydown';
let mask;
switch (e.code) {
case 'ArrowUp':
mask = 1;
break;
case 'ArrowDown':
mask = 2;
break;
case 'ArrowLeft':
mask = 4;
break;
case 'ArrowRight':
mask = 8;
break;
case 'KeyZ':
mask = 16;
break;
case 'KeyX':
mask = 32;
break;
case 'KeyA':
mask = 64;
break;
case 'KeyS':
mask = 128;
break;
case 'KeyR':
if (isKeyDown) {
runModule(currentData);
}
break;
case 'F10':
if(isKeyDown) {
recordVideo();
}
e.preventDefault();
break;
}
if (isKeyDown) {
pad |= mask;
} else {
pad &= ~mask;
}
};
window.onkeydown = keyHandler;
window.onkeyup = keyHandler;
async function runModule(data, keepUrl) {
if (cancelFunction) {
cancelFunction();
cancelFunction = null;
}
let cartridgeSize = data.byteLength;
setMessage(cartridgeSize);
if (cartridgeSize == 0) {
return;
}
currentData = data;
let newURL = window.location.pathname;
if (cartridgeSize <= 1024 && !keepUrl) {
let dataString = '';
for (let byte of U8(data)) {
dataString += String.fromCharCode(byte);
}
newURL += '#' + btoa(dataString);
if (newURL != window.location.pathname + window.location.hash) {
history.pushState(null, null, newURL);
}
}
screen.width = screen.width;
try {
let memory = new WebAssembly.Memory({ initial: 4, maximum: 4 });
let memU8 = U8(memory.buffer);
let importObject = {
env: {
memory
},
};
let loader;
let loadModuleData = (data) => {
if (U8(data)[0] != 0) {
memU8.set(U8(data));
let length = loader.exports.load_uw8(data.byteLength);
data = new ArrayBuffer(length);
U8(data).set(memU8.slice(0, length));
}
return data;
}
let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance;
let loadModuleURL = async (url) => instantiate(loadModuleData(await (await fetch(url)).arrayBuffer()));
loader = await loadModuleURL(loaderUrl);
for (let n of ['acos', 'asin', 'atan', 'atan2', 'cos', 'exp', 'log', 'sin', 'tan', 'pow']) {
importObject.env[n] = Math[n];
}
for (let i = 9; i < 64; ++i) {
importObject.env['reserved' + i] = () => { };
}
for (let i = 0; i < 16; ++i) {
importObject.env['g_reserved' + i] = 0;
}
data = loadModuleData(data);
let platform_instance = await loadModuleURL(platformUrl);
for (let name in platform_instance.exports) {
importObject.env[name] = platform_instance.exports[name]
}
let instance = await instantiate(data);
let buffer = U32(imageData.data.buffer);
let startTime = Date.now();
let keepRunning = true;
cancelFunction = () => keepRunning = false;
const timePerFrame = 1000 / 60;
let nextFrame = startTime;
function mainloop() {
if (!keepRunning) {
return;
}
try {
let now = Date.now();
let restart = false;
if (now >= nextFrame) {
let gamepads = navigator.getGamepads();
let gamepad = 0;
for (let i = 0; i < 4; ++i) {
let pad = gamepads[i];
if (!pad) {
continue;
}
for (let j = 0; j < 8; ++j) {
let buttonIdx = (j + 12) % 16;
if (pad.buttons.length > buttonIdx && pad.buttons[buttonIdx].pressed) {
gamepad |= 1 << (i * 8 + j);
}
}
if (pad.axes.length > 1) {
for (let j = 0; j < 4; ++j) {
let v = pad.axes[1 - (j >> 1)];
if (((j & 1) ? v : -v) > 0.5) {
gamepad |= 1 << (i * 8 + j);
}
}
}
if (pad.buttons.length > 9 && pad.buttons[9].pressed) {
restart = true;
}
}
let u32Mem = U32(memory.buffer);
u32Mem[16] = now - startTime;
u32Mem[17] = pad | gamepad;
instance.exports.upd();
platform_instance.exports.endFrame();
let palette = U32(memory.buffer.slice(0x13000, 0x13000 + 1024));
for (let i = 0; i < 320 * 240; ++i) {
buffer[i] = palette[memU8[i + 120]] | 0xff000000;
}
canvasCtx.putImageData(imageData, 0, 0);
nextFrame = Math.max(nextFrame + timePerFrame, now);
}
if (restart) {
runModule(currentData);
} else {
window.requestAnimationFrame(mainloop);
}
} catch (err) {
setMessage(cartridgeSize, err.toString());
}
}
mainloop();
} catch (err) {
setMessage(cartridgeSize, err.toString());
}
}
let videoRecorder;
let videoStartTime;
function recordVideo() {
if(videoRecorder) {
videoRecorder.stop();
videoRecorder = null;
return;
}
videoRecorder = new MediaRecorder(screen.captureStream(), {
mimeType: 'video/webm',
videoBitsPerSecond: 5000000
});
let chunks = [];
videoRecorder.ondataavailable = e => {
chunks.push(e.data);
};
let timer = document.getElementById("timer");
timer.hidden = false;
timer.innerText = "00:00";
videoRecorder.onstop = () => {
timer.hidden = true;
let a = document.createElement('a');
a.href = URL.createObjectURL(new Blob(chunks, {type: 'video/webm'}));
a.download = 'microw8_' + new Date().toISOString() + 'webm';
a.click();
URL.revokeObjectURL(a.href);
};
videoRecorder.start();
videoStartTime = Date.now();
function updateTimer() {
if(!videoStartTime) {
return;
}
let duration = Math.floor((Date.now() - videoStartTime) / 1000);
timer.innerText = Math.floor(duration / 60).toString().padStart(2, '0') + ':' + (duration % 60).toString().padStart(2, '0');
setTimeout(updateTimer, 1000);
}
setTimeout(updateTimer, 1000);
}
async function runModuleFromURL(url, keepUrl) {
let response = await fetch(url);
let type = response.headers.get('Content-Type');
if(type && type.includes('html')) {
throw false;
}
runModule(await response.arrayBuffer(), keepUrl);
}
function runModuleFromHash() { function runModuleFromHash() {
let hash = window.location.hash.slice(1); let hash = window.location.hash.slice(1);
if(hash == 'devkit') {
uw8.setDevkitMode(true);
return;
}
uw8.setDevkitMode(false);
if (hash.length > 0) { if (hash.length > 0) {
if (hash.startsWith("url=")) { if (hash.startsWith("url=")) {
runModuleFromURL(hash.slice(4), true); uw8.runModuleFromURL(hash.slice(4), true);
} else { } else {
runModuleFromURL('data:;base64,' + hash); uw8.runModuleFromURL('data:;base64,' + hash);
} }
} else { } else {
runModule(new ArrayBuffer(0)); uw8.runModule(new ArrayBuffer(0));
} }
} }
@@ -308,7 +43,7 @@ let setupLoad = () => {
fileInput.accept = '.wasm,.uw8,application/wasm'; fileInput.accept = '.wasm,.uw8,application/wasm';
fileInput.onchange = () => { fileInput.onchange = () => {
if (fileInput.files.length > 0) { if (fileInput.files.length > 0) {
runModuleFromURL(URL.createObjectURL(fileInput.files[0])); uw8.runModuleFromURL(URL.createObjectURL(fileInput.files[0]));
} }
}; };
fileInput.click(); fileInput.click();
@@ -322,7 +57,7 @@ let setupLoad = () => {
let files = e.dataTransfer && e.dataTransfer.files; let files = e.dataTransfer && e.dataTransfer.files;
if(files && files.length == 1) { if(files && files.length == 1) {
e.preventDefault(); e.preventDefault();
runModuleFromURL(URL.createObjectURL(e.dataTransfer.files[0])); uw8.runModuleFromURL(URL.createObjectURL(e.dataTransfer.files[0]));
} }
} }
@@ -344,7 +79,7 @@ if(location.hash.length != 0) {
url += 'cart.uw8'; url += 'cart.uw8';
} }
try { try {
await runModuleFromURL(url, true); await uw8.runModuleFromURL(url, true);
} catch(e) { } catch(e) {
setupLoad(); setupLoad();
} }

319
web/src/microw8.js Normal file
View File

@@ -0,0 +1,319 @@
import loaderUrl from "data-url:../../platform/bin/loader.wasm";
import platformUrl from "data-url:../../platform/bin/platform.uw8";
export default function MicroW8(screen, config = {}) {
if(!config.setMessage) {
config.setMessage = (s, e) => {
if(e) {
console.log('error: ' + e);
}
}
}
let canvasCtx = screen.getContext('2d');
let imageData = canvasCtx.createImageData(320, 240);
let devkitMode = config.devkitMode;
let cancelFunction;
let currentData;
let U8 = (d) => new Uint8Array(d);
let U32 = (d) => new Uint32Array(d);
let pad = 0;
let keyboardElement = config.keyboardElement == undefined ? screen : config.keyboardElement;
if(keyboardElement) {
let keyHandler = (e) => {
let isKeyDown = e.type == 'keydown';
let mask;
switch (e.code) {
case 'ArrowUp':
mask = 1;
break;
case 'ArrowDown':
mask = 2;
break;
case 'ArrowLeft':
mask = 4;
break;
case 'ArrowRight':
mask = 8;
break;
case 'KeyZ':
mask = 16;
break;
case 'KeyX':
mask = 32;
break;
case 'KeyA':
mask = 64;
break;
case 'KeyS':
mask = 128;
break;
case 'KeyR':
if (isKeyDown) {
runModule(currentData, true);
}
break;
case 'F9':
if(isKeyDown) {
screen.toBlob(blob => {
downloadBlob(blob, '.png');
});
}
e.preventDefault();
break;
case 'F10':
if(isKeyDown) {
recordVideo();
}
e.preventDefault();
break;
}
if (isKeyDown) {
pad |= mask;
} else {
pad &= ~mask;
}
};
keyboardElement.onkeydown = keyHandler;
keyboardElement.onkeyup = keyHandler;
}
async function runModule(data, keepUrl) {
if (cancelFunction) {
cancelFunction();
cancelFunction = null;
}
let cartridgeSize = data.byteLength;
config.setMessage(cartridgeSize);
if (cartridgeSize == 0) {
return;
}
currentData = data;
let newURL = window.location.pathname;
if (cartridgeSize <= 1024 && !keepUrl) {
let dataString = '';
for (let byte of U8(data)) {
dataString += String.fromCharCode(byte);
}
newURL += '#' + btoa(dataString);
if (newURL != window.location.pathname + window.location.hash) {
history.pushState(null, null, newURL);
}
}
screen.width = screen.width;
try {
let memSize = { initial: 4 };
if(!devkitMode) {
memSize.maximum = 4;
}
let memory = new WebAssembly.Memory({ initial: 4, maximum: devkitMode ? 16 : 4 });
let memU8 = U8(memory.buffer);
let importObject = {
env: {
memory
},
};
let loader;
let loadModuleData = (data) => {
if (loader && (!devkitMode || U8(data)[0] != 0)) {
memU8.set(U8(data));
let length = loader.exports.load_uw8(data.byteLength);
data = new ArrayBuffer(length);
U8(data).set(memU8.slice(0, length));
}
return data;
}
let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance;
let loadModuleURL = async (url) => instantiate(loadModuleData(await (await fetch(url)).arrayBuffer()));
loader = await loadModuleURL(loaderUrl);
for (let n of ['acos', 'asin', 'atan', 'atan2', 'cos', 'exp', 'log', 'sin', 'tan', 'pow']) {
importObject.env[n] = Math[n];
}
for (let i = 9; i < 64; ++i) {
importObject.env['reserved' + i] = () => { };
}
for (let i = 0; i < 16; ++i) {
importObject.env['g_reserved' + i] = 0;
}
data = loadModuleData(data);
let platform_instance = await loadModuleURL(platformUrl);
for (let name in platform_instance.exports) {
importObject.env[name] = platform_instance.exports[name]
}
let instance = await instantiate(data);
let buffer = U32(imageData.data.buffer);
let startTime = Date.now();
let keepRunning = true;
cancelFunction = () => keepRunning = false;
const timePerFrame = 1000 / 60;
let nextFrame = startTime;
function mainloop() {
if (!keepRunning) {
return;
}
try {
let now = Date.now();
let restart = false;
if (now >= nextFrame) {
let gamepads = navigator.getGamepads();
let gamepad = 0;
for (let i = 0; i < 4; ++i) {
let pad = gamepads[i];
if (!pad) {
continue;
}
for (let j = 0; j < 8; ++j) {
let buttonIdx = (j + 12) % 16;
if (pad.buttons.length > buttonIdx && pad.buttons[buttonIdx].pressed) {
gamepad |= 1 << (i * 8 + j);
}
}
if (pad.axes.length > 1) {
for (let j = 0; j < 4; ++j) {
let v = pad.axes[1 - (j >> 1)];
if (((j & 1) ? v : -v) > 0.5) {
gamepad |= 1 << (i * 8 + j);
}
}
}
if (pad.buttons.length > 9 && pad.buttons[9].pressed) {
restart = true;
}
}
let u32Mem = U32(memory.buffer);
u32Mem[16] = now - startTime;
u32Mem[17] = pad | gamepad;
instance.exports.upd();
platform_instance.exports.endFrame();
let palette = U32(memory.buffer.slice(0x13000, 0x13000 + 1024));
for (let i = 0; i < 320 * 240; ++i) {
buffer[i] = palette[memU8[i + 120]] | 0xff000000;
}
canvasCtx.putImageData(imageData, 0, 0);
nextFrame = Math.max(nextFrame + timePerFrame, now);
}
if (restart) {
runModule(currentData);
} else {
window.requestAnimationFrame(mainloop);
}
} catch (err) {
config.setMessage(cartridgeSize, err.toString());
}
}
mainloop();
} catch (err) {
config.setMessage(cartridgeSize, err.toString());
}
}
function downloadBlob(blob, ext) {
let a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'microw8_' + new Date().toISOString() + ext;
a.click();
URL.revokeObjectURL(a.href);
}
let videoRecorder;
let videoStartTime;
function recordVideo() {
if(videoRecorder) {
videoRecorder.stop();
videoRecorder = null;
return;
}
videoRecorder = new MediaRecorder(screen.captureStream(), {
mimeType: 'video/webm',
videoBitsPerSecond: 25000000
});
let chunks = [];
videoRecorder.ondataavailable = e => {
chunks.push(e.data);
};
let timer = config.timerElement;
if(timer) {
timer.hidden = false;
timer.innerText = "00:00";
}
videoRecorder.onstop = () => {
if(timer) {
timer.hidden = true;
}
downloadBlob(new Blob(chunks, {type: 'video/webm'}), '.webm');
};
videoRecorder.start();
videoStartTime = Date.now();
function updateTimer() {
if(!videoStartTime) {
return;
}
if(timer) {
let duration = Math.floor((Date.now() - videoStartTime) / 1000);
timer.innerText = Math.floor(duration / 60).toString().padStart(2, '0') + ':' + (duration % 60).toString().padStart(2, '0');
}
setTimeout(updateTimer, 1000);
}
setTimeout(updateTimer, 1000);
}
async function runModuleFromURL(url, keepUrl) {
let response = await fetch(url);
let type = response.headers.get('Content-Type');
if(type && type.includes('html')) {
throw false;
}
runModule(await response.arrayBuffer(), keepUrl || devkitMode);
}
return {
runModule,
runModuleFromURL,
setDevkitMode: (m) => devkitMode = m,
};
}

46
web/src/run-web.css Normal file
View File

@@ -0,0 +1,46 @@
html, body, canvas {
padding: 0;
margin: 0;
background-color: #202024;
}
html {
height: 100%;
}
body {
height: 100%;
display: grid;
grid-template-rows: 1fr;
}
#screen {
align-self: center;
justify-self: center;
image-rendering: pixelated;
border: 4px solid #303040;
}
#message {
position: absolute;
width: calc(100% - 16px);
background-color: rgba(0, 0, 0, 0.4);
color: #c64;
padding: 8px;
font: bold 12pt sans-serif;
z-index: 2;
}
@media (min-width: 648px) and (min-height: 488px) {
#screen {
width: 640px;
height: 480px;
}
}
@media (min-width: 968px) and (min-height: 728px) {
#screen {
width: 960px;
height: 720px;
}
}

18
web/src/run-web.html Normal file
View File

@@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<meta charset="utf8" />
<title>uw8-run</title>
<style>
@import "run-web.css";
</style>
</head>
<body>
<canvas id="screen" width="320" height="240" tabindex="1">
</canvas>
<div id="message"></div>
</body>
<script type="module">
import "./run-web.js";
</script>
</html>

19
web/src/run-web.js Normal file
View File

@@ -0,0 +1,19 @@
import MicroW8 from './microw8.js';
let uw8 = MicroW8(document.getElementById('screen'), {
setMessage: (_, err) => {
let elem = document.getElementById('message');
if(err) {
elem.innerText = err;
}
elem.hidden = !err;
}
});
let events = new EventSource('events');
events.onmessage = event => {
console.log(event.data);
if(event.data == 'L') {
uw8.runModuleFromURL('cart', true);
}
};
uw8.runModuleFromURL('cart', true);