46 Commits

Author SHA1 Message Date
7c5f43f152 Merge branch 'master' into sound 2022-04-11 00:23:08 +02:00
f32b0762b0 update curlywas 2022-04-11 00:18:26 +02:00
9ebb6b6d34 pause module when page doesn't have focus 2022-04-10 23:24:04 +02:00
8a10b99eeb fix non-windows build 2022-04-08 21:22:34 +02:00
6c064a1dd8 enable ansi terminal on windows 10 cmd 2022-04-08 21:11:51 +02:00
37f12f5a2c update web runtime 2022-04-04 09:31:43 +02:00
8ad2885a55 implement ring modulation 2022-04-02 18:19:45 +02:00
1917057b81 fixed one pole filter, wide stereo bit 2022-04-02 00:06:04 +02:00
82c1ddb867 improve attack and noise 2022-04-01 00:40:56 +02:00
8713aa8930 adjust tim_ges to new soundchip version 2022-03-19 22:55:40 +01:00
0f82e6e711 removed aliasing in rect and saw oscilators 2022-03-19 14:53:21 +01:00
0ade24ebf6 add source file to build wasm module with just ges emulation 2022-03-19 11:03:05 +01:00
29186c806f add pulse width support to other wave types 2022-03-10 23:05:07 +01:00
b626d2609a implement proper exponential envelope timings 2022-03-09 23:03:15 +01:00
39ead8220f slight optimization, add pulse width modulation to melody voices 2022-03-09 09:22:27 +01:00
ce18a8a162 add initial pulse width support 2022-03-08 22:52:12 +01:00
a15e796489 first full version of time for ges 2022-03-08 22:20:54 +01:00
f178076b86 all basic wave forms, filters, panning 2022-03-08 09:46:35 +01:00
81adcf0198 implement more of the sound-chip 2022-03-07 23:58:54 +01:00
780caf965a sync sound registers to sound thread 2022-03-07 09:35:11 +01:00
2033f9a172 wait for audio ready before starting cart, add button to unsuspend audio
fixes missing sound when auto-starting cart in chrome
2022-03-06 14:08:44 +01:00
0d514c7dd3 steady on now down to 197 bytes 2022-03-06 10:13:48 +01:00
a8eb3bda27 some clean up and optimization on steady on tim 2022-03-05 23:54:13 +01:00
8b765a5742 Merge branch 'master' into sound 2022-03-05 23:10:14 +01:00
7197c11586 fix microw8.html no-autoload mode 2022-03-05 23:10:04 +01:00
99a423619e fix watch mode not working when initial compile fails 2022-03-05 21:18:55 +01:00
9063e872d3 ported steady on tim as a sound test 2022-03-04 23:38:27 +01:00
85240599e8 first working version with sound 2022-03-04 09:50:10 +01:00
35ec5fdb59 add simple bytebeat example to test first implementation with 2022-03-02 22:42:23 +01:00
a6a82ff5a1 update wasmtime version 2022-03-02 21:50:26 +01:00
973814a629 update hero link to 0.1.2 2022-03-02 08:54:34 +01:00
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
52 changed files with 3137 additions and 893 deletions

1119
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,27 @@
[package]
name = "uw8"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
# 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]
wasmtime = "0.30"
wasmtime = { version = "0.34", optional = true }
anyhow = "1"
minifb = { version = "0.20", default-features = false, features = ["x11"] }
notify = "4"
pico-args = "0.4"
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "196719b" }
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "aac7bbd" }
wat = "1"
uw8-tool = { path = "uw8-tool" }
same-file = "1"
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 }
ansi_term = "0.12.1"

View File

@@ -15,9 +15,9 @@ See [here](https://exoticorn.github.io/microw8/) for more information and docs.
## Downloads
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-windows.zip)
* [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)
The download includes
@@ -35,6 +35,7 @@ Runs <file> which can be a binary WebAssembly module, an `.uw8` cart, a wat (Web
Options:
-b, --browser : Run in browser instead of using native runtime
-t, --timeout FRAMES : Sets the timeout in frames (1/60s)
-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.

View File

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

View File

@@ -1,11 +1,4 @@
import "env.time" fn time() -> f32;
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;
include "../include/microw8-api.cwa"
export fn upd() {
cls(0);
@@ -15,10 +8,10 @@ export fn upd() {
let inline rocket = i #>> 9;
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;
seed(rocket);
randomSeed(rocket);
let inline x = randomf() * 645 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 dy = cos(angle);
let lazy dist = local_time * (randomf() * 44 as f32);

View File

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

View File

@@ -1,10 +1,4 @@
import "env.memory" memory(4);
import "env.cls" fn cls(i32);
import "env.time" fn time() -> f32;
import "env.line" fn line(f32, f32, f32, f32, i32);
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
include "../include/microw8-api.cwa"
export fn upd() {
cls(0);

View File

@@ -1,12 +1,4 @@
import "env.memory" memory(4);
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);
include "../include/microw8-api.cwa"
global mut pz: i32 = 4;
global mut px: f32 = 2.0;
@@ -19,7 +11,7 @@ export fn upd() {
let inline zero = 0.0;
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;
printInt(pz);
@@ -33,8 +25,8 @@ export fn upd() {
let inline c = (z & 1) * -2;
let inline yf = y as f32;
rect(rx, yf, rw, yf / 6 as f32, c + 1);
rect(rx, yf, rw, 1 as f32, c - 4);
rectangle(rx, yf, rw, yf / 6 as f32, c + 1);
rectangle(rx, yf, rw, 1 as f32, c - 4);
if y == 180 & py > 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 - 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;
pz = pz + 1;
}

View File

@@ -0,0 +1,38 @@
// Steady On Tim, It's Only A Budget Game
// by Gasman / Hooy-Program
// ported to MicroW8 by exoticorn/icebird
include "../include/microw8-api.cwa"
fn melody(t: i32, T: i32) -> i32 {
let inline riff_pos = abs(((T&31) - 16) as f32) as i32;
let lazy shift = ((1-((T>>5)&3))%2-1) as f32 / 6 as f32;
let inline note_count = 5 - (T >= 512);
let inline octave = (riff_pos/5) as f32;
let inline riff_note = 5514 >> (riff_pos % note_count * 4) & 15;
let inline melody_freq = pow(2 as f32, shift + octave - (riff_note as f32 / 12 as f32));
let inline melody = (t as f32 * melody_freq) as i32 & 128;
let inline arp_note = ((0x85>>((t>>12)%3*4)) & 15) - 1;
let inline arp_freq = pow(2 as f32, shift + (arp_note as f32 / 12 as f32));
let inline arp_vol = (T >= 256) * (12-T%12);
let inline arpeggio = ((t as f32 * arp_freq) as i32 & 128) * arp_vol / 12;
melody + arpeggio
}
export fn snd(t: i32) -> f32 {
let lazy T = t/10000;
let inline mel_arp = melody(t, T)/3 + melody(t, T-3)/5;
let inline bass_vol = (T >= 128) & (197 >> (T % 8));
let inline bass_freq = pow(2 as f32, (((T & 4) * ((T & 7) - 1)) as f32 / 24 as f32 - 5 as f32));
let inline bass = ((t as f32 * bass_freq) as i32 & 63) * bass_vol;
let inline snare_ish = (random() & 31) * (8 - (T + 4) % 8) / 8;
let inline sample = mel_arp + bass + snare_ish;
sample as f32 / 255 as f32
}

View File

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

@@ -0,0 +1,59 @@
import "env.memory" memory(4);
fn melody(ch: i32, t: i32, T: i32) {
let lazy riff_pos = abs(((T&31) - 16) as f32) as i32;
let lazy shift = ((1-((T>>5)&3))%2-1) * 2;
let inline note_count = 5 - (T >= 512);
let inline octave = (riff_pos/5) * 12;
let inline riff_note = 5514 >> (riff_pos % note_count * 4) & 15;
let inline melody_note = shift + octave - riff_note;
ch?1 = 248 - riff_pos * 15;
ch?3 = melody_note + 64;
let inline arp_note = shift + ((0x85>>((t/2)%3*4)) & 15) - 1;
80?3 = arp_note + 64;
}
export fn upd() {
let lazy t = 32!32 / (1000/60);
let lazy T = t / 7;
melody(98, t, T - 3);
melody(92, t, T);
80?0 = ((T >= 256) & (T/12+(T-3)/12)) * 2 | 0x44; // arp trigger
if T >= 128 {
let inline bass_step = T % 8;
86?3 = if bass_step / 2 == 2 {
86?0 = 0xc2;
80
} else {
86?0 = ((197 >> bass_step) & 1) | 0x48;
((T & 4) * ((T & 7) - 1)) / 2 + 28
};
}
}
data 80 {
i8(
0, 0x81, 0, 0, 0, 0x90,
0, 0x4c, 0, 0, 0, 0x5c,
5, 0, 0, 0, 0, 0x4c,
5, 0, 0, 0, 0, 0x4c,
0xfa, 0x85,
0x81, 0x81, 0, 110, 0, 90
)
}
/*
include "../../platform/src/ges.cwa"
import "env.pow" fn pow(f32, f32) -> f32;
import "env.sin" fn sin(f32) -> f32;
export fn snd(t: i32) -> f32 {
gesSnd(t)
}
*/

View File

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

View File

@@ -1,7 +1,4 @@
import "env.memory" memory(4);
import "env.atan2" fn atan2(f32, f32) -> f32;
import "env.time" fn time() -> f32;
include "../include/microw8-api.cwa"
export fn upd() {
let i: i32;
@@ -12,7 +9,7 @@ export fn upd() {
let inline d = 40000 as f32 / sqrt(x * x + y * y);
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;
i?120 = c;
i?FRAMEBUFFER = c;
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;

127
platform/Cargo.lock generated
View File

@@ -79,9 +79,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chumsky"
version = "0.5.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d3efff85e8572b1c3fa0127706af58c4fff8458f8d9436d54b1e97573c7a3f"
checksum = "8d02796e4586c6c41aeb68eae9bfb4558a522c35f1430c14b40136c3706e09e4"
dependencies = [
"ahash 0.3.8",
]
@@ -146,14 +146,14 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "curlywas"
version = "0.1.0"
source = "git+https://github.com/exoticorn/curlywas.git?rev=196719b#196719b35ef377cb7e001554b27ac5de013dcf2b"
source = "git+https://github.com/exoticorn/curlywas.git?rev=aac7bbd#aac7bbd8786a26da0dcbe8320b1afefaf6086464"
dependencies = [
"anyhow",
"ariadne",
"chumsky",
"pico-args",
"wasm-encoder",
"wasmparser",
"wasm-encoder 0.10.0",
"wasmparser 0.83.0",
]
[[package]]
@@ -197,6 +197,21 @@ dependencies = [
"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]]
name = "lazy_static"
version = "1.4.0"
@@ -227,6 +242,15 @@ dependencies = [
"rgb",
]
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "miniz_oxide"
version = "0.4.4"
@@ -286,6 +310,24 @@ version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "rgb"
version = "0.8.31"
@@ -304,6 +346,17 @@ dependencies = [
"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]]
name = "time"
version = "0.1.43"
@@ -323,6 +376,18 @@ dependencies = [
"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]]
name = "upkr"
version = "0.1.0"
@@ -342,8 +407,9 @@ dependencies = [
"pbr",
"pico-args",
"upkr",
"wasm-encoder",
"wasmparser",
"walrus",
"wasm-encoder 0.8.0",
"wasmparser 0.81.0",
]
[[package]]
@@ -352,6 +418,32 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
@@ -367,12 +459,33 @@ dependencies = [
"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]]
name = "wasmparser"
version = "0.81.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98930446519f63d00a836efdc22f67766ceae8dbcc1571379f2bcabc6b2b9abc"
[[package]]
name = "wasmparser"
version = "0.83.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "718ed7c55c2add6548cca3ddd6383d738cd73b892df400e96b9aa876f0141d7a"
[[package]]
name = "winapi"
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
[dependencies]
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="196719b" }
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="aac7bbd" }
uw8-tool = { path="../uw8-tool" }
anyhow = "1"
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.

221
platform/src/ges.cwa Normal file
View File

@@ -0,0 +1,221 @@
const GesChannelState.Trigger = 0;
const GesChannelState.EnvState = 1;
const GesChannelState.EnvVol = 2;
const GesChannelState.Phase = 4;
const GesChannelState.Size = 8;
const GesState.Filter = GesChannelState.Size * 4;
const GesState.Size = GesState.Filter + 8*4;
const GesStateOffset = 112;
const GesBufferOffset = 112 + GesState.Size;
export fn gesSnd(t: i32) -> f32 {
if !(t & 127) {
let i: i32;
loop clearLoop {
i!GesBufferOffset = 0;
branch_if (i := i + 4) < 128*8: clearLoop;
}
let ch: i32;
loop channelLoop {
let lazy channelState = GesStateOffset + ch * GesChannelState.Size;
let lazy channelReg = 80 + ch * 6;
let envState = channelState?GesChannelState.EnvState;
let envVol = i32.load16_u(channelState, GesChannelState.EnvVol);
let lazy oldTrigger = channelState?GesChannelState.Trigger;
let lazy ctrl = channelReg?0;
if (oldTrigger ^ ctrl) & (ctrl | 2) & 3 {
envState = 1;
envVol = 0;
}
channelState?GesChannelState.Trigger = ctrl;
if envState {
let lazy attack = channelReg?4 & 15;
envVol = envVol + 12 * pow(1.675, (15 - attack) as f32) as i32;
if envVol >= 65535 {
envVol = 65535;
envState = 0;
}
} else {
let inline decay = (channelReg - (ctrl & 1))?5 >> 4;
let inline dec = 8 * pow(1.5625, (15 - decay) as f32) as i32;
envVol = envVol - ((dec * (envVol + 8192)) >> 16);
let inline sustain = (channelReg?5 & 15) << 12;
let lazy targetVol = (ctrl & 1) * sustain;
if envVol < targetVol {
envVol = targetVol;
}
}
channelState?GesChannelState.EnvState = envState;
i32.store16(envVol, channelState, GesChannelState.EnvVol);
let inline note = i32.load16_u(channelReg, 2);
let lazy freq = 440 as f32 * pow(2.0, (note - 69*256) as f32 / (12*256) as f32);
let phaseInc = (freq * (65536.0 / 44100.0)) as i32;
let phase = channelState!GesChannelState.Phase;
let inline pulseWidth = channelReg?1;
let phaseShift = (pulseWidth - 128) * 255;
let invPhaseInc = 1 as f32 / phaseInc as f32;
i = 0;
let wave = ctrl >> 6;
if wave < 2 {
if wave {
let pulsePhase1 = pulseWidth << 23;
let pulsePhase2 = (511 - pulseWidth) << 23;
loop sawLoop {
let p = (phase ^ 32768) << 16;
let saw = (p >> 16) - polyBlep(phase, invPhaseInc, -32767);
let saw2 = select(p #>= pulsePhase1 & p #< pulsePhase2, -saw, saw);
let saw2 = saw2 -
polyBlep((p - pulsePhase1) >> 16, invPhaseInc, -saw) -
polyBlep((p - pulsePhase2) >> 16, invPhaseInc, saw);
i!(GesBufferOffset + 128*4) = saw2;
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: sawLoop;
}
}
else
{
let pulsePhase = 32768 + pulseWidth * 128;
loop rectLoop {
i!(GesBufferOffset + 128*4) = select((phase & 65535) < pulsePhase, -32768, 32767) -
polyBlep(phase, invPhaseInc, -32767) -
polyBlep(phase - pulsePhase, invPhaseInc, 32767);
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: rectLoop;
}
}
} else {
if wave == 2 {
let scale = pulseWidth + 256;
loop triLoop {
let s = phase << 16;
s = (s ^ (s >> 31));
s = (s >> 8) * scale;
s = (s ^ (s >> 31));
i!(GesBufferOffset + 128*4) = (s >> 15) - 32768;
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: triLoop;
}
} else {
loop noiseLoop {
let s = phase >> 12;
let inline pulse = ((phase >> 8) & 255) >= pulseWidth;
s = s * 0x6746ba73;
s = s ^ (s >> 15) * pulse;
i!(GesBufferOffset + 128*4) = (s * 0x835776c7) >> 16;
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: noiseLoop;
}
}
}
channelState!GesChannelState.Phase = phase;
if ctrl & 32 {
let lazy modSrc = (ch - 1) & 3;
let inline channelState = GesStateOffset + modSrc * GesChannelState.Size;
let inline channelReg = 80 + modSrc * 6;
let inline note = i32.load16_u(channelReg, 2);
let inline freq = 440 as f32 * pow(2.0, (note - 69*256) as f32 / (12*256) as f32);
let phaseInc = (freq * (65536.0 / 44100.0)) as i32;
let phase = channelState!GesChannelState.Phase;
if modSrc > ch {
phase = phase - (phaseInc << 6);
}
i = 0;
loop ringLoop {
let s = phase << 16;
s = (s ^ (s >> 31));
i!(GesBufferOffset + 128*4) = (i!(GesBufferOffset + 128*4) * ((s >> 15) - 32768)) >> 15;
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: ringLoop;
}
}
let channelVol = ((ch >> 1)?0x68 >> ((ch & 1) * 4)) & 15;
envVol = envVol * channelVol / 15;
let leftVol = (select(ctrl & 16, 0x3d5b, 0x6a79) >> (ch * 4)) & 15;
let rightVol = 16 - leftVol;
let lazy filter = (ctrl >> 2) & 3;
i = 0;
if filter < 2 {
if filter {
let f = (4096 as f32 - min(4096 as f32, 4096 as f32 * exp(freq * (-8.0 * 3.141 / 44100.0)))) as i32;
let low = (ch * 8)!(GesStateOffset + GesState.Filter);
loop filterLoop {
let in = (i!(GesBufferOffset + 128*4) * envVol) >> 18;
low = low + (((in - low) * f) >> 12);
(i * 2)!GesBufferOffset = (i * 2)!GesBufferOffset + ((low * leftVol) >> 4);
(i * 2)!(GesBufferOffset + 4) = (i * 2)!(GesBufferOffset + 4) + ((low * rightVol) >> 4);
branch_if (i := i + 4) < 64*4: filterLoop;
(ch * 8)!(GesStateOffset + GesState.Filter) = low;
(ch * 8)!(GesStateOffset + GesState.Filter + 4) = 0;
}
} else {
loop mixLoop {
let sample = (i!(GesBufferOffset + 128*4) * envVol) >> 18;
(i * 2)!GesBufferOffset = (i * 2)!GesBufferOffset + ((sample * leftVol) >> 4);
(i * 2)!(GesBufferOffset + 4) = (i * 2)!(GesBufferOffset + 4) + ((sample * rightVol) >> 4);
branch_if (i := i + 4) < 64*4: mixLoop;
(ch * 8)!(GesStateOffset + GesState.Filter) = sample;
(ch * 8)!(GesStateOffset + GesState.Filter + 4) = 0;
}
}
} else {
filter = filter - 2;
let ctrl = filter?0x6a;
let note = i32.load16_u(filter * 2, 0x6c);
let inline freq = 440 as f32 * pow(2.0, (note - 69*256) as f32 / (12*256) as f32);
let F = (8192 as f32 * sin(min(0.25, freq / 44100 as f32) * 3.1415)) as i32;
let Q = 8192 - (ctrl >> 4) * (7000/15);
let Qlimit = (8192*4096/F - F/2) * 3 / 4;
if Q > Qlimit {
Q = Qlimit;
}
let low_out = ctrl & 1;
let high_out = (ctrl >> 1) & 1;
let band_out = (ctrl >> 2) & 1;
let low = (ch * 8)!(GesStateOffset + GesState.Filter);
let band = (ch * 8)!(GesStateOffset + GesState.Filter + 4);
loop filterLoop {
let in = (i!(GesBufferOffset + 128*4) * envVol) >> 18;
let high = in - low - ((band * Q) >> 12);
band = band + ((F * high) >> 12);
low = low + ((F * band) >> 12);
let sample = low * low_out + high * high_out + band * band_out;
(i * 2)!GesBufferOffset = (i * 2)!GesBufferOffset + ((sample * leftVol) >> 4);
(i * 2)!(GesBufferOffset + 4) = (i * 2)!(GesBufferOffset + 4) + ((sample * rightVol) >> 4);
branch_if (i := i + 4) < 64*4: filterLoop;
(ch * 8)!(GesStateOffset + GesState.Filter) = low;
(ch * 8)!(GesStateOffset + GesState.Filter + 4) = band;
}
}
branch_if (ch := ch + 1) < 4: channelLoop;
}
}
(((t & 127) * 4)!GesBufferOffset) as f32 / 32768 as f32
}
fn polyBlep(transientPhase: i32, invPhaseInc: f32, magnitude: i32) -> i32 {
let lazy t = ((transientPhase << 16) >> 16) as f32 * invPhaseInc;
let lazy x = max(0 as f32, 1 as f32 - abs(t));
(f32.copysign(x * x, t) * magnitude as f32) as i32
}

View File

@@ -0,0 +1,6 @@
import "env.memory" memory(1);
import "env.sin" fn sin(f32) -> f32;
import "env.pow" fn pow(f32, f32) -> f32;
import "env.exp" fn exp(f32) -> f32;
include "ges.cwa"

View File

@@ -11,13 +11,13 @@ fn main() -> Result<()> {
convert_font()?;
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()).0?;
File::create("bin/loader.wasm")?.write_all(&loader)?;
println!("Loader (including base module): {} bytes", loader.len());
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()).0?;
println!("Compressing platform module");
let platform = uw8_tool::pack(
&platform,

View File

@@ -1,6 +1,9 @@
import "env.memory" memory(4);
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
import "env.pow" fn pow(f32, f32) -> f32;
import "env.exp" fn exp(f32) -> f32;
export fn time() -> f32 {
(0!64) as f32 / 1000 as f32
@@ -256,6 +259,11 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
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;
p = floor(p) + 0.5 - p;
if max_axis < 0 as f32 {
@@ -267,7 +275,7 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
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);
setPixel(i32.trunc_sat_f32_s(x1 + f * dx), i32.trunc_sat_f32_s(y1 + f * dy), col);
if !steps {
return;
@@ -280,7 +288,7 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
loop pixels {
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;
y1 = y1 + dy;
branch pixels;
@@ -288,7 +296,7 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
}
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);
}
//////////
@@ -495,6 +503,12 @@ export fn setCursorPosition(x: i32, y: i32) {
textCursorY = y * scale;
}
///////////
// SOUND //
///////////
include "ges.cwa"
///////////
// SETUP //
///////////
@@ -503,6 +517,13 @@ export fn endFrame() {
68!4 = 68!0;
}
fn memclr(base: i32, size: i32) {
loop bytes {
(base + (size := size - 1))?0 = 0;
branch_if size: bytes;
}
}
start fn setup() {
let i: i32 = 12*16*3-1;
let avg: f32;
@@ -535,10 +556,26 @@ start fn setup() {
branch_if (i := i - 1) >= 0: expand_sweetie;
}
memclr(0, 64);
memclr(112, 8);
memclr(0x14000, 0x2c000);
cls(0);
randomSeed(random());
}
data 80 {
i8(
0, 128, 0, 69, 0x8, 0xc8,
0, 128, 0, 69, 0x8, 0xc8,
0, 128, 0, 69, 0x8, 0xc8,
0, 128, 0, 69, 0x8, 0xc8,
0xff, 0xff,
1, 1, 0, 100, 0, 100
)
}
data 0x13000+192*4 {
i32(
0x2c1c1a,

View File

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

View File

@@ -15,11 +15,11 @@ The initial motivation behind MicroW8 was to explore whether there was a way to
* Gamepad input (D-Pad + 4 Buttons)
## Examples
* [Fireworks](v0.1.1#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022.
* [Skip Ahead](v0.1.1#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.1.1#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.1.1#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.1.1#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ): Just a simple viewer for the default font and palette.
* [Fireworks](v0.1.2#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022.
* [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.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.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.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:
@@ -29,6 +29,38 @@ Examplers for older 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
* [Web runtime](v0.1.0)

View File

@@ -18,7 +18,9 @@ The memory has to be imported as `env` `memory` and has a maximum size of 256kb
00000-00040: user memory
00040-00044: time since module start in ms
00044-0004c: gamepad state
0004c-00078: reserved
0004c-00050: reserved
00050-00070: sound registers
00070-00078: reserved
00078-12c78: frame buffer
12c78-13000: reserved
13000-13400: palette
@@ -47,7 +49,7 @@ Returns the arccosine 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.
@@ -269,6 +271,38 @@ Sets the background color.
Sets the cursor position. In normal mode `x` and `y` are multiplied by 8 to get the pixel position, in graphics mode they are used as is.
## Sound
```
Per channel:
00 : CTRL - wave form, ring, sync, filter send, trigger
bit 0: note on flag
bit 1: note trigger
bit 2,3: filter 0,1 send
bit 6,7: wave form (rect, saw, tri, noise)
01 : PULS - pulse width
02 : FINE - fine tuning
03 : NOTE - note
04 : ENVA - attack, decay
05 : ENVR - sustain, release
50-56: channel 0
56-5b: channel 1
5c-61: channel 2
62-67: channel 3
68: VO01 - volumes channel 0&1
69: VO23 - volumes channel 2&3
6a : FCTR 0 - type, resonance
6b : FCTR 1 - type, resonance
6c : FFIN 0 - cutoff fine tuning
6d : FNOT 0 - cutoff note
6e : FFIN 1 - cutoff fine tuning
6f : FNOT 1 - cutoff note
```
# The `uw8` tool
The `uw8` tool included in the MicroW8 download includes a number of useful tools for developing MicroW8 carts. For small productions written in
@@ -284,7 +318,8 @@ Runs `<file>` which can be a binary WebAssembly module, an `.uw8` cart, a wat (W
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)
* `-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.

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,23 +1,38 @@
use anyhow::{bail, Result};
use notify::{DebouncedEvent, Watcher, RecommendedWatcher};
use std::{
collections::BTreeSet,
path::{Path, PathBuf},
sync::mpsc,
time::Duration,
};
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,
watcher: RecommendedWatcher,
watched_files: BTreeSet<PathBuf>,
directories: BTreeSet<PathBuf>,
rx: mpsc::Receiver<DebouncedEvent>,
}
pub struct FileWatcherBuilder(BTreeSet<PathBuf>);
impl FileWatcher {
pub fn builder() -> FileWatcherBuilder {
FileWatcherBuilder(BTreeSet::new())
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>> {
@@ -38,33 +53,3 @@ impl FileWatcher {
Ok(None)
}
}
impl FileWatcherBuilder {
pub fn add_file<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
self.0.insert(path.into());
self
}
pub fn build(self) -> Result<FileWatcher> {
let mut directories: BTreeSet<&Path> = BTreeSet::new();
for file in &self.0 {
if let Some(directory) = file.parent() {
directories.insert(directory);
}
}
let (tx, rx) = mpsc::channel();
let mut watcher = notify::watcher(tx, Duration::from_millis(100))?;
for directory in directories {
watcher.watch(directory, notify::RecursiveMode::NonRecursive)?;
}
Ok(FileWatcher {
_watcher: watcher,
watched_files: self.0,
rx,
})
}
}

View File

@@ -1,270 +1,22 @@
use std::io::prelude::*;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{fs::File, thread, time::Instant};
use anyhow::Result;
use minifb::{Key, Window, WindowOptions};
use wasmtime::{
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
};
mod filewatcher;
#[cfg(feature = "native")]
mod run_native;
#[cfg(feature = "browser")]
mod run_web;
pub use filewatcher::FileWatcher;
#[cfg(feature = "native")]
pub use run_native::MicroW8;
#[cfg(feature = "browser")]
pub use run_web::RunWebServer;
static GAMEPAD_KEYS: &'static [Key] = &[
Key::Up,
Key::Down,
Key::Left,
Key::Right,
Key::Z,
Key::X,
Key::A,
Key::S,
];
use anyhow::Result;
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;
}
pub trait Runtime {
fn is_open(&self) -> bool;
fn set_timeout(&mut self, _timeout: u32) {
eprintln!("Warning: runtime doesn't support timeout");
}
}
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(())
}
}
fn load(&mut self, module_data: &[u8]) -> Result<()>;
fn run_frame(&mut self) -> Result<()>;
}

View File

@@ -1,23 +1,30 @@
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::process;
use std::{
path::{Path, PathBuf},
process::exit,
};
use anyhow::Result;
use pico_args::Arguments;
use uw8::{FileWatcher, MicroW8};
#[cfg(feature = "native")]
use uw8::MicroW8;
#[cfg(feature = "browser")]
use uw8::RunWebServer;
#[cfg(any(feature = "native", feature = "browser"))]
use uw8::Runtime;
fn main() -> Result<()> {
let mut args = Arguments::from_env();
match args.subcommand()?.as_ref().map(|s| s.as_str()) {
// try to enable ansi support in win10 cmd shell
#[cfg(target_os="windows")]
let _ = ansi_term::enable_ansi_support();
match args.subcommand()?.as_deref() {
Some("version") => {
println!("{}", env!("CARGO_PKG_VERSION"));
Ok(())
}
#[cfg(any(feature = "native", feature = "browser"))]
Some("run") => run(args),
Some("pack") => pack(args),
Some("unpack") => unpack(args),
@@ -27,7 +34,8 @@ fn main() -> Result<()> {
println!("uw8 {}", env!("CARGO_PKG_VERSION"));
println!();
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 unpack <in-file> <out-file>");
println!(" uw8 compile [-d/--debug] <in-file> <out-file>");
@@ -41,6 +49,7 @@ fn main() -> Result<()> {
}
}
#[cfg(any(feature = "native", feature = "browser"))]
fn run(mut args: Arguments) -> Result<()> {
let watch_mode = args.contains(["-w", "--watch"]);
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
@@ -65,37 +74,53 @@ fn run(mut args: Arguments) -> Result<()> {
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 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 {
uw8.set_timeout(timeout);
runtime.set_timeout(timeout);
}
let mut watcher = FileWatcher::builder();
let mut first_run = true;
if watch_mode {
watcher.add_file(&filename);
}
let watcher = watcher.build()?;
if let Err(err) = start_cart(&filename, &mut uw8, &config) {
eprintln!("Load error: {}", err);
if !watch_mode {
exit(1);
}
}
while uw8.is_open() {
if watcher.poll_changed_file()?.is_some() {
if let Err(err) = start_cart(&filename, &mut uw8, &config) {
eprintln!("Load error: {}", err);
while runtime.is_open() {
if first_run || watcher.poll_changed_file()?.is_some() {
let (result, dependencies) = start_cart(&filename, &mut *runtime, &config);
if watch_mode {
for dep in dependencies {
watcher.add_file(dep)?;
}
}
if let Err(err) = result {
eprintln!("Load error: {}", err);
if !watch_mode {
exit(1);
}
}
first_run = false;
}
if let Err(err) = uw8.run_frame() {
if let Err(err) = runtime.run_frame() {
eprintln!("Runtime error: {}", err);
if !watch_mode {
exit(1);
@@ -112,39 +137,99 @@ struct Config {
output_path: Option<PathBuf>,
}
fn load_cart(filename: &Path, pack: &Option<uw8_tool::PackConfig>) -> Result<Vec<u8>> {
let mut cart = vec![];
File::open(filename)?.read_to_end(&mut cart)?;
if cart[0] >= 10 {
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())?
fn load_cart(filename: &Path, config: &Config) -> (Result<Vec<u8>>, Vec<PathBuf>) {
let mut dependencies = Vec::new();
fn inner(filename: &Path, config: &Config, dependencies: &mut Vec<PathBuf>) -> Result<Vec<u8>> {
let mut cart = match SourceType::of_file(filename)? {
SourceType::Binary => {
let mut cart = vec![];
File::open(filename)?.read_to_end(&mut cart)?;
cart
}
SourceType::Wat => {
let cart = wat::parse_file(filename)?;
cart
}
SourceType::CurlyWas => {
let (module, deps) = curlywas::compile_file(filename, curlywas::Options::default());
*dependencies = deps;
module?
}
};
if let Some(ref pack_config) = config.pack {
cart = uw8_tool::pack(&cart, pack_config)?;
println!("packed size: {} bytes", cart.len());
}
if let Some(ref path) = config.output_path {
File::create(path)?.write_all(&cart)?;
}
Ok(cart)
}
if let Some(pack_config) = pack {
cart = uw8_tool::pack(&cart, pack_config)?;
println!("packed size: {} bytes", cart.len());
let result = inner(filename, config, &mut dependencies);
if dependencies.is_empty() {
dependencies.push(filename.to_path_buf());
}
Ok(cart)
(result, dependencies)
}
fn start_cart(filename: &Path, uw8: &mut MicroW8, config: &Config) -> Result<()> {
let cart = load_cart(filename, &config.pack)?;
enum SourceType {
Binary,
Wat,
CurlyWas,
}
if let Some(ref path) = config.output_path {
File::create(path)?.write_all(&cart)?;
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)
}
}
if let Err(err) = uw8.load_from_memory(&cart) {
#[cfg(any(feature = "native", feature = "browser"))]
fn start_cart(
filename: &Path,
runtime: &mut dyn Runtime,
config: &Config,
) -> (Result<()>, Vec<PathBuf>) {
let (cart, dependencies) = load_cart(filename, config);
let cart = match cart {
Ok(cart) => cart,
Err(err) => return (Err(err), dependencies),
};
if let Err(err) = runtime.load(&cart) {
eprintln!("Load error: {}", err);
Err(err)
(Err(err), dependencies)
} else {
Ok(())
(Ok(()), dependencies)
}
}
@@ -163,7 +248,14 @@ fn pack(mut args: Arguments) -> Result<()> {
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,
},
)
.0?;
File::create(out_file)?.write_all(&cart)?;
@@ -173,8 +265,8 @@ fn pack(mut args: Arguments) -> Result<()> {
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).into()
uw8_tool::unpack_file(&in_file, &out_file)
}
fn compile(mut args: Arguments) -> Result<()> {
@@ -186,7 +278,7 @@ fn compile(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()))?;
let module = curlywas::compile_file(in_file, options)?;
let module = curlywas::compile_file(in_file, options).0?;
File::create(out_file)?.write_all(&module)?;
Ok(())

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.

30
test/ges_test.cwa Normal file
View File

@@ -0,0 +1,30 @@
import "env.memory" memory(4);
import "env.pow" fn pow(f32, f32) -> f32;
import "env.sin" fn sin(f32) -> f32;
import "env.cls" fn cls(i32);
import "env.rectangle" fn rectangle(f32, f32, f32, f32, i32);
include "../platform/src/ges.cwa"
export fn snd(t: i32) -> f32 {
gesSnd(t)
}
export fn upd() {
80?0 = 32!32 / 200 & 2 | 0x41;
80?3 = (32!32 / 400)%7*12/7 + 40;
let pulse = (32!32 * 256 / 2000) & 511;
if pulse >= 256 {
pulse = 511 - pulse;
}
80?1 = pulse;
cls(0);
rectangle(0.0, 100.0, (pulse * 320 / 256) as f32, 16.0, 15);
}
data 80 {
i8(
0x41, 0, 0, 80, 0x70, 0
)
}

59
tests/plot_ges.cwa Normal file
View File

@@ -0,0 +1,59 @@
include "../examples/include/microw8-api.cwa"
export fn upd() {
80?0 = (32!32 >> 11 << 6) | 5;
80?1 = (sin(time() * 6 as f32) * 95 as f32) as i32 + 128;
plotGes();
}
data 80 { i8 (
1, 128, 0, 69, 0, 15,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0xff, 0xff,
0xc1, 0, 0, 110, 0, 0
) }
//import "env.gesSnd" fn gesSnd(i32) -> f32;
include "../platform/src/ges.cwa"
export fn snd(t: i32) -> f32 {
gesSnd(t)
}
global mut samplePos: i32 = 0;
const SoundBuffer = 0x30000;
fn plotGes() {
rectangle(0 as f32, 10 as f32, 320 as f32, 320 as f32, 0);
let count = (time() * 44100 as f32) as i32 * 2 - samplePos;
let i: i32;
loop genLoop {
(i*4)$SoundBuffer = gesSnd(samplePos + i);
branch_if (i := i + 1) < count: genLoop;
}
samplePos = samplePos + count;
let ch: i32;
loop channelLoop {
let offset = 159;
i = 0;
loop searchLoop {
offset = offset + 1;
branch_if (offset * 8 + ch - 8)$SoundBuffer < 0 as f32 | (offset * 8 + ch)$SoundBuffer >= 0 as f32 & offset + 160 < count: searchLoop;
}
offset = ch + (offset - 160) * 8;
i = 0;
loop plotLoop {
setPixel(i, floor((i * 8 + offset)$SoundBuffer * 127 as f32) as i32 + 60 + ch * (120/8), 15);
branch_if (i := i + 1) < 320: plotLoop;
}
branch_if (ch := ch + 8) < 16: channelLoop;
}
}

View File

@@ -71,26 +71,100 @@ impl BaseModule {
add_function(&mut functions, &type_map, "randomSeed", &[I32], None);
add_function(&mut functions, &type_map, "cls", &[I32], None);
add_function(&mut functions, &type_map, "setPixel", &[I32, I32, I32], 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,
"setPixel",
&[I32, I32, I32],
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, "isButtonPressed", &[I32], Some(I32));
add_function(&mut functions, &type_map, "isButtonTriggered", &[I32], Some(I32));
add_function(
&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, "printString", &[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, "setBackgroundColor", &[I32], None);
add_function(&mut functions, &type_map, "setCursorPosition", &[I32, I32], None);
add_function(
&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(&mut functions, &type_map, "circle_outline", &[F32, F32, F32, I32], None);
add_function(
&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 {
add_function(
@@ -214,6 +288,68 @@ impl BaseModule {
File::create(path)?.write_all(&data)?;
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(
@@ -241,3 +377,30 @@ fn lookup_type(
};
*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 anyhow::Result;
use uw8_tool::BaseModule;
use pico_args::Arguments;
use uw8_tool::BaseModule;
fn main() -> Result<()> {
let mut args = Arguments::from_env();
@@ -32,6 +32,14 @@ fn main() -> Result<()> {
let dest: PathBuf = args.free_from_str()?;
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);
print_help();

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/

2
web/run Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
rm -rf .parcel-cache && yarn parcel src/index.html

69
web/src/audiolet.js Normal file
View File

@@ -0,0 +1,69 @@
let U8 = (...a) => new Uint8Array(...a);
class APU extends AudioWorkletProcessor {
constructor() {
super();
this.sampleIndex = 0;
this.port.onmessage = (ev) => {
if(this.memory) {
U8(this.memory.buffer, 80, 32).set(U8(ev.data));
} else {
this.load(ev.data[0], ev.data[1]);
}
};
}
async load(platform_data, data) {
let memory = new WebAssembly.Memory({ initial: 4, maximum: 4 });
let importObject = {
env: {
memory
},
};
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;
}
let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance;
let platform_instance = await instantiate(platform_data);
for (let name in platform_instance.exports) {
importObject.env[name] = platform_instance.exports[name]
}
let instance = await instantiate(data);
this.memory = memory;
this.snd = instance.exports.snd || platform_instance.exports.gesSnd;
this.port.postMessage(2);
}
process(inputs, outputs, parameters) {
if(this.snd) {
let channels = outputs[0];
let index = this.sampleIndex;
let numSamples = channels[0].length;
for(let i = 0; i < numSamples; ++i) {
channels[0][i] = this.snd(index++);
channels[1][i] = this.snd(index++);
}
this.sampleIndex = index & 0xffffffff;
}
return true;
}
}
registerProcessor('apu', APU);

View File

@@ -10,13 +10,14 @@
</head>
<body>
<div id="uw8">
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.1.1
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.1.2
</div>
<div id="centered">
<canvas id="screen" width="320" height="240">
<canvas class="screen" id="screen" width="320" height="240">
</canvas>
<div id="timer" hidden="true"></div>
<div id="message"></div>
<button class="screen" id="start" style="display:none">Click to start</button>
<div id="timer" hidden="true"></div>
<div id="message"></div>
<button id="cartButton" style="visibility:hidden">Load cart...</button>
</div>
<div id="footer">

View File

@@ -1,5 +1,4 @@
import loaderUrl from "data-url:../../platform/bin/loader.wasm";
import platformUrl from "data-url:../../platform/bin/platform.uw8";
import MicroW8 from './microw8.js';
function setMessage(size, error) {
let html = size ? `${size} bytes` : 'Insert cart';
@@ -9,314 +8,28 @@ function setMessage(size, error) {
document.getElementById('message').innerHTML = html;
}
let screen = document.getElementById('screen');
let canvasCtx = screen.getContext('2d');
let imageData = canvasCtx.createImageData(320, 240);
let devkitMode;
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, 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;
}
};
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 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) {
setMessage(cartridgeSize, err.toString());
}
}
mainloop();
} catch (err) {
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 = document.getElementById("timer");
timer.hidden = false;
timer.innerText = "00:00";
videoRecorder.onstop = () => {
timer.hidden = true;
downloadBlob(new Blob(chunks, {type: 'video/webm'}), '.webm');
};
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 || devkitMode);
}
let uw8 = MicroW8(document.getElementById('screen'), {
setMessage,
keyboardElement: window,
timerElement: document.getElementById("timer"),
startButton: document.getElementById("start")
});
function runModuleFromHash() {
let hash = window.location.hash.slice(1);
if(hash == 'devkit') {
devkitMode = true;
uw8.setDevkitMode(true);
return;
}
devkitMode = false;
uw8.setDevkitMode(false);
if (hash.length > 0) {
if (hash.startsWith("url=")) {
runModuleFromURL(hash.slice(4), true);
uw8.runModuleFromURL(hash.slice(4), true);
} else {
runModuleFromURL('data:;base64,' + hash);
uw8.runModuleFromURL('data:;base64,' + hash);
}
} else {
runModule(new ArrayBuffer(0));
uw8.runModule(new ArrayBuffer(0));
}
}
@@ -331,7 +44,7 @@ let setupLoad = () => {
fileInput.accept = '.wasm,.uw8,application/wasm';
fileInput.onchange = () => {
if (fileInput.files.length > 0) {
runModuleFromURL(URL.createObjectURL(fileInput.files[0]));
uw8.runModuleFromURL(URL.createObjectURL(fileInput.files[0]));
}
};
fileInput.click();
@@ -345,7 +58,7 @@ let setupLoad = () => {
let files = e.dataTransfer && e.dataTransfer.files;
if(files && files.length == 1) {
e.preventDefault();
runModuleFromURL(URL.createObjectURL(e.dataTransfer.files[0]));
uw8.runModuleFromURL(URL.createObjectURL(e.dataTransfer.files[0]));
}
}
@@ -367,7 +80,9 @@ if(location.hash.length != 0) {
url += 'cart.uw8';
}
try {
await runModuleFromURL(url, true);
if(!await uw8.runModuleFromURL(url, true)) {
setupLoad();
}
} catch(e) {
setupLoad();
}

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

@@ -0,0 +1,398 @@
import loaderUrl from "data-url:../../platform/bin/loader.wasm";
import platformUrl from "data-url:../../platform/bin/platform.uw8";
import audioWorkletUrl from "data-url:./audiolet.js";
class AudioNode extends AudioWorkletNode {
constructor(context) {
super(context, 'apu', {outputChannelCount: [2]});
}
}
let U8 = (...a) => new Uint8Array(...a);
let U32 = (...a) => new Uint32Array(...a);
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 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 audioContext = new AudioContext({sampleRate: 44100});
let keepRunning = true;
let abortController = new AbortController();
cancelFunction = () => {
audioContext.close();
keepRunning = false;
abortController.abort();
}
let cartridgeSize = data.byteLength;
config.setMessage(cartridgeSize);
if (cartridgeSize == 0) {
return;
}
await audioContext.audioWorklet.addModule(audioWorkletUrl);
let audioNode = new AudioNode(audioContext);
let audioReadyFlags = 0;
let audioReadyResolve;
let audioReadyPromise = new Promise(resolve => audioReadyResolve = resolve);
let updateAudioReady = (f) => {
audioReadyFlags |= f;
if(audioReadyFlags == 3 && audioReadyResolve) {
audioReadyResolve(true);
audioReadyResolve = null;
}
};
audioNode.port.onmessage = (e) => updateAudioReady(e.data);
let audioStateChange = () => {
if(audioContext.state == 'suspended') {
if(config.startButton) {
config.startButton.style = '';
screen.style = 'display:none';
}
(config.startButton || screen).onclick = () => {
audioContext.resume();
};
} else {
if(config.startButton) {
config.startButton.style = 'display:none';
screen.style = '';
}
updateAudioReady(1);
}
};
audioContext.onstatechange = audioStateChange;
audioStateChange();
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(memSize);
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) => loadModuleData(await (await fetch(url)).arrayBuffer());
loader = await instantiate(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_data = await loadModuleURL(platformUrl);
audioNode.port.postMessage([platform_data, data]);
let platform_instance = await instantiate(platform_data);
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);
await audioReadyPromise;
let startTime = Date.now();
const timePerFrame = 1000 / 60;
let nextFrame = startTime;
audioNode.connect(audioContext.destination);
let isPaused = false;
let pauseTime = startTime;
let updateVisibility = isVisible => {
let now = Date.now();
if(isVisible) {
isPaused = false;
audioContext.resume();
startTime += now - pauseTime;
} else {
isPaused = true;
audioContext.suspend();
pauseTime = now;
}
};
window.addEventListener('focus', () => updateVisibility(true), { signal: abortController.signal });
window.addEventListener('blur', () => updateVisibility(false), { signal: abortController.signal });
updateVisibility(document.hasFocus());
function mainloop() {
if (!keepRunning) {
return;
}
try {
let now = Date.now();
let restart = false;
if (now >= nextFrame && !isPaused) {
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;
if(instance.exports.upd) {
instance.exports.upd();
}
platform_instance.exports.endFrame();
let soundRegisters = new ArrayBuffer(32);
U8(soundRegisters).set(U8(memory.buffer, 80, 32));
audioNode.port.postMessage(soundRegisters, [soundRegisters]);
let palette = U32(memory.buffer, 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')) || response.status != 200) {
return false;
}
runModule(await response.arrayBuffer(), keepUrl || devkitMode);
return true;
}
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);

View File

@@ -37,7 +37,7 @@ a:hover {
color: #405040;
}
#screen {
.screen {
width: 320px;
height: 240px;
image-rendering: pixelated;
@@ -45,9 +45,16 @@ a:hover {
margin-bottom: 8px;
border: 4px solid #303040;
box-shadow: 5px 5px 20px black;
}
#screen {
cursor: none;
}
#start {
font-size: 150%;
}
#timer::before {
content: '';
display: inline-block;
@@ -84,21 +91,21 @@ button:active {
}
@media (min-width: 680px) and (min-height: 620px) {
#screen {
.screen {
width: 640px;
height: 480px;
}
}
@media (min-width: 1000px) and (min-height: 800px) {
#screen {
.screen {
width: 960px;
height: 720px;
}
}
@media (width:640px) and (height:480px) {
#screen {
.screen {
width: 640px;
height: 480px;
border: 0;