22 Commits

Author SHA1 Message Date
1f6de62e5d add test program for filled circle drawing 2022-06-12 23:54:56 +02:00
caeaa82787 optimize hline function, update wasmtime 2022-06-12 14:28:01 +02:00
e0450c9039 successfully reprod audio recording in firefox 2022-06-09 23:54:43 +02:00
95d0d92a6f first try for a simple repro of firefox resample fail (failed) 2022-06-09 23:36:23 +02:00
7a6dd0ab6d improve formatting of change log 2022-05-09 01:13:20 +02:00
e7a00dd9c6 prepare for v0.2.0 release 2022-05-09 00:51:51 +02:00
a02243d98c add sound to skipahead 2022-05-08 20:28:06 +02:00
599873890a add docs for debug output 2022-05-08 19:47:07 +02:00
8e9bb002bc improve sleep timer resolution on windows 2022-05-08 18:08:29 +02:00
b2b990333e prepare for v0.2.0-rc3 release 2022-05-08 00:51:11 +02:00
d1556f7be8 add support for writing debug output to the console 2022-05-08 00:41:11 +02:00
9f548cd6f0 update curlywas 2022-05-08 00:19:49 +02:00
7cea4eebd3 improve frame timings some more in both runtimes 2022-05-05 09:53:36 +02:00
3f67e92c5c prepare for 0.2.0-rc2 release 2022-05-04 08:48:16 +02:00
a2714f25e4 fix unstable playback in browser runtime 2022-05-04 00:32:41 +02:00
7e203d93e6 implement scheduled sound updates in native runtime 2022-05-02 08:19:34 +02:00
e44c87d1f6 add port of cracklebass 2022-04-29 00:22:55 +02:00
614b7cf358 add 0.2.0-rc1 download links to site 2022-04-26 23:20:05 +02:00
c42a484adb Update version number to 0.2.0-rc1 2022-04-26 22:38:31 +02:00
3a5f2bf865 add another small doc paragraph for snd function 2022-04-26 19:41:05 +02:00
2dee1b30a4 add support for non 44.1kHz audio configs (resampling) 2022-04-26 00:02:34 +02:00
42f7887ab2 install missing alsa dependency on ci 2022-04-24 23:48:58 +02:00
32 changed files with 1925 additions and 1061 deletions

View File

@@ -27,7 +27,7 @@ jobs:
steps:
- name: Install dependencies
run: sudo apt-get install -y libxkbcommon-dev
run: sudo apt-get install -y libxkbcommon-dev libasound2-dev
if: matrix.os == 'ubuntu-latest'
- name: Checkout
uses: actions/checkout@v2

732
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,22 @@
[package]
name = "uw8"
version = "0.1.2"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["native", "browser"]
native = ["wasmtime"]
native = ["wasmtime", "minifb", "cpal", "rubato", "winapi" ]
browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
[dependencies]
wasmtime = { version = "0.35.3", optional = true }
wasmtime = { version = "0.37.0", optional = true }
anyhow = "1"
minifb = { version = "0.22", default-features = false, features = ["x11"] }
minifb = { version = "0.22", default-features = false, features = ["x11"], optional = true }
notify = "4"
pico-args = "0.4"
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "aac7bbd" }
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0e7ea50" }
wat = "1"
uw8-tool = { path = "uw8-tool" }
same-file = "1"
@@ -25,4 +25,6 @@ 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"
cpal = "0.13.5"
cpal = { version = "0.13.5", optional = true }
rubato = { version = "0.11.0", optional = true }
winapi = { version = "0.3.9", features = ["timeapi"], optional = true }

View File

@@ -0,0 +1,55 @@
// port of cracklebass by pestis (originally on TIC-80)
include "../include/microw8-api.cwa"
const MUSIC_DATA = 0x20000;
export fn upd() {
let inline t = 32!32 * 6 / 100;
let inline p = t / 1024;
let channel:i32;
loop channels {
let inline e = t * channel?MUSIC_DATA / 8;
let lazy pattern = (8 * channel + p)?(MUSIC_DATA + 56);
let lazy n = !!pattern * (8 * pattern + e / 16 % 8)?MUSIC_DATA;
let inline prev_ctrl = (channel * 6)?80;
(channel * 6)?80 = if n {
let inline base_note = 12 + 12 * channel?(MUSIC_DATA + 4) + n;
let inline pitch_drop = e % 16 * channel?(MUSIC_DATA + 94);
let inline key_pattern = p?(MUSIC_DATA + 8*4 + 56);
let inline key = select(key_pattern, (8 * key_pattern + t / 128 % 8)?MUSIC_DATA, 1);
(channel * 6)?83 = base_note - pitch_drop / 4 + key;
prev_ctrl & 0xfc | (e / 8 & 2) | 1
} else {
prev_ctrl & 0xfe
};
branch_if (channel := channel + 1) < 4: channels;
}
}
data 80 {
i8(
0x44, 0, 0, 0, 0x50, 0x40,
0x4, 0x50, 0, 0, 0x80, 0x80,
0x40, 0x80, 0, 0, 0x40, 0x40,
0, 0, 0, 0, 0x50, 0x50
)
}
data MUSIC_DATA {
i8(
16, 2, 8, 8, 1, 2, 2, 3, 1, 0,
1,13,16, 0, 1, 8, 1, 0, 1,13,
16, 1, 1, 8, 1, 0, 8,13,13, 0,
16,13, 1, 0, 1, 0, 1, 0, 1, 1,
1, 0, 0, 0, 1, 0,13, 1, 1, 1,
6, 8, 1, 1, 6, 8, 1, 1, 2, 1,
2, 1, 2, 0, 0, 0, 0, 3, 3, 3,
5, 0, 0, 2, 1, 2, 1, 2, 1, 2,
0, 4, 4, 0, 4, 4, 4, 4, 0, 0,
0, 0, 6, 6, 0, 0, 0, 8
)
}

View File

@@ -8,7 +8,7 @@ global mut f: f32 = 2.0;
export fn upd() {
let y: i32;
let inline zero = 0.0;
let inline zero = 0_f;
let lazy control_speed = 0.03125;
s = s + 0.1875 - (f + control_speed) * isButtonPressed(4 <| cls(4)) as f32;
@@ -30,6 +30,8 @@ export fn upd() {
if y == 180 & py > zero {
if x > w | x < zero {
0?80 = 0xc3;
3?80 = 32;
return;
}
py = zero;
@@ -43,6 +45,9 @@ 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);
0?86 = py < zero;
3?86 = 32 - py as i32;
px = px + (isButtonPressed(3) - isButtonPressed(2)) as f32 * control_speed;
py = py + s;
pz = pz + 1;

2
platform/Cargo.lock generated
View File

@@ -146,7 +146,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "curlywas"
version = "0.1.0"
source = "git+https://github.com/exoticorn/curlywas.git?rev=aac7bbd#aac7bbd8786a26da0dcbe8320b1afefaf6086464"
source = "git+https://github.com/exoticorn/curlywas.git?rev=0e7ea50#0e7ea508cd0e76836283ae68a44c9097df83c8ac"
dependencies = [
"anyhow",
"ariadne",

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="aac7bbd" }
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="0e7ea50" }
uw8-tool = { path="../uw8-tool" }
anyhow = "1"
lodepng = "3.4"

Binary file not shown.

View File

@@ -4,6 +4,7 @@ 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;
import "env.logChar" fn logChar(i32);
export fn time() -> f32 {
(0!64) as f32 / 1000 as f32
@@ -32,11 +33,9 @@ export fn random() -> i32 {
}
export fn random64() -> i64 {
let state: i64;
randomState = (state := (
state := randomState ^ (randomState #>> 12i64)
) ^ (state << 25i64)
) ^ (state #>> 27i64);
let lazy state = randomState ^ (randomState #>> 12i64);
let lazy state = state ^ (state << 25i64);
randomState = state ^ (state #>> 27i64);
randomState * 0x2545f4914f6cdd1di64
}
@@ -62,12 +61,8 @@ export fn cls(col: i32) {
let i: i32;
textCursorX = 0;
textCursorY = 0;
graphicsText = 0;
col = (col & 255) * 0x1010101;
loop pixels {
i!120 = col;
branch_if (i := i + 4) < 320*240: pixels;
}
outputChannel = 0;
memory.fill(120, col, 320*240);
}
export fn setPixel(x: i32, y: i32, col: i32) {
@@ -91,12 +86,70 @@ fn clamp(v: i32, min: i32, max: i32) -> i32 {
export fn hline(x1: i32, x2: i32, y: i32, col: i32) {
x1 = clamp(x1, 0, 320);
x2 = clamp(x2, 0, 320);
if x1 < x2 & y #< 240 {
if y #>= 240 {
return;
}
let word_start = (x1 + 3) & -4;
let word_end = x2 & -4;
if word_end > word_start {
col = (col & 255) * 0x1010101;
let ptr = y * 320 + x1;
let end = ptr + word_start - x1;
if ptr + 2 <= end {
ptr?120 = col;
ptr?121 = col;
ptr += 2;
}
if ptr < end {
ptr?120 = col;
ptr += 1;
}
end += word_end - word_start;
loop words {
if ptr + 16 <= end {
ptr!120 = col;
ptr!124 = col;
ptr!128 = col;
ptr!132 = col;
ptr += 16;
branch words;
}
if ptr + 8 <= end {
ptr!120 = col;
ptr!124 = col;
ptr += 8;
}
if ptr < end {
ptr!120 = col;
ptr += 4;
}
}
end += x2 - word_end;
if ptr + 2 <= end {
ptr?120 = col;
ptr?121 = col;
ptr += 2;
}
if ptr < end {
ptr?120 = col;
}
} else {
let ptr = y * 320 + x1;
let end = ptr + x2 - x1;
loop pixels {
if ptr + 4 <= end {
ptr?120 = col;
ptr?121 = col;
ptr?122 = col;
ptr?123 = col;
ptr += 4;
}
if ptr + 2 <= end {
ptr?120 = col;
ptr?121 = col;
ptr += 2;
}
if ptr < end {
ptr?120 = col;
branch_if (ptr := ptr + 1) < end: pixels;
}
}
}
@@ -307,7 +360,7 @@ global mut textCursorX = 0;
global mut textCursorY = 0;
global mut textColor = 15;
global mut bgColor = 0;
global mut graphicsText = 0;
global mut outputChannel = 0;
export fn printChar(char: i32) {
loop chars {
@@ -319,6 +372,20 @@ export fn printChar(char: i32) {
global mut controlCodeLength = 0;
fn printSingleChar(char: i32) {
if char >= 4 & char <= 6 {
outputChannel = char - 4;
if !outputChannel {
textCursorX = 0;
textCursorY = 0;
}
return;
}
if outputChannel >= 2 {
logChar(char);
return;
}
controlCodeLength?0x12d20 = char;
controlCodeLength = controlCodeLength + 1;
char = 0x12d20?0;
@@ -332,13 +399,6 @@ fn printSingleChar(char: i32) {
return;
}
if char == 4 | char == 5 {
graphicsText = char == 5;
textCursorX = 0;
textCursorY = 0;
return;
}
if char == 7 {
80?0 = 80?0 ^ 2;
return;
@@ -346,7 +406,7 @@ fn printSingleChar(char: i32) {
if char == 8 {
textCursorX = textCursorX - 8;
if !graphicsText & textCursorX < 0 {
if !outputChannel & textCursorX < 0 {
textCursorX = 320-8;
printSingleChar(11);
}
@@ -354,7 +414,7 @@ fn printSingleChar(char: i32) {
}
if char == 9 {
if !graphicsText & textCursorX >= 320 {
if !outputChannel & textCursorX >= 320 {
printChar(0xd0a);
}
textCursorX = textCursorX + 8;
@@ -363,7 +423,7 @@ fn printSingleChar(char: i32) {
if char == 10 {
textCursorY = textCursorY + 8;
if !graphicsText & textCursorY >= 240 {
if !outputChannel & textCursorY >= 240 {
textCursorY = 240 - 8;
let i: i32;
loop scroll_copy {
@@ -377,7 +437,7 @@ fn printSingleChar(char: i32) {
if char == 11 {
textCursorY = textCursorY - 8;
if !graphicsText & textCursorY < 0 {
if !outputChannel & textCursorY < 0 {
textCursorY = 0;
let i = 320 * (240 - 8);
loop scroll_copy {
@@ -417,8 +477,8 @@ fn printSingleChar(char: i32) {
}
if char == 31 {
textCursorX = 0x12d20?1 * (8 - graphicsText * 6);
textCursorY = 0x12d20?2 * (8 - graphicsText * 7);
textCursorX = 0x12d20?1 * (8 - outputChannel * 6);
textCursorY = 0x12d20?2 * (8 - outputChannel * 7);
return;
}
@@ -443,7 +503,7 @@ data(0x12d00) {
}
fn drawChar(char: i32) {
if !graphicsText & textCursorX >= 320 {
if !outputChannel & textCursorX >= 320 {
printChar(0xd0a);
}
@@ -451,7 +511,7 @@ fn drawChar(char: i32) {
loop rows {
let bits = (char * 8 + y)?0x13400;
let x = 0;
if graphicsText {
if outputChannel {
loop pixels {
if (bits := bits << 1) & 256 {
setPixel(textCursorX + x, textCursorY + y, textColor);
@@ -503,7 +563,7 @@ export fn setBackgroundColor(col: i32) {
}
export fn setCursorPosition(x: i32, y: i32) {
let lazy scale = select(graphicsText, 1, 8);
let lazy scale = select(outputChannel, 1, 8);
textCursorX = x * scale;
textCursorY = y * scale;
}
@@ -542,13 +602,6 @@ 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;
@@ -581,9 +634,9 @@ start fn setup() {
branch_if (i := i - 1) >= 0: expand_sweetie;
}
memclr(0, 64);
memclr(112, 8);
memclr(0x14000, 0x2c000);
memory.fill(0, 0, 64);
memory.fill(112, 0, 8);
memory.fill(0x14000, 0, 0x2c000);
cls(0);

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.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.
* [Skip Ahead](v0.2.0#AgVfq24KI2Ok2o8qVtPYj27fSuGnfeSKgbOkIOsaEQMov8TDYQ6UjdjwkZrYcM1i9alo4/+Bhm1PRFEa0YHJlJAk/PGoc2K41rejv9ZSqJqIHNjr7cappqhOR2jT+jk+0b0+U6hO+geRCTP2aufWs7L+f/Z27NFY8LKlqPSv+C6Rd6+ohoKi6sYl5Kcrlf1cyTinV7jTTnmbcXWVDBA5rRKxAGMUTDS8rHxqSztRITOaQVP1pSdYgi/BDdOJOxSOIkeaId84S+Ycls5na7EgwSfVIpgqF+tcfkUecb8t2mQrXA7pyKrh/wzHn5N6Oe5aOgmzY2YpTIct) (249 bytes): A port of my [TIC-80 256byte game](http://tic80.com/play?cart=1735) from LoveByte'21, now with sound
* [Fireworks](v0.2.0#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022.
* [OhNoAnotherTunnel](v0.2.0#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.2.0#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.2.0#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ): Just a simple viewer for the default font and palette.
Examplers for older versions:
@@ -29,19 +29,23 @@ Examplers for older versions:
## Versions
### v0.1.2
### v0.2.0
* [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)
* [Web runtime](v0.2.0)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-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
* [add sound support!](docs#sound)
* add support to redirect text output to the console for debugging using control code 6
* update curlywas:
* add support for `else if`
* add support for escape sequences in strings
* add support for char literals
* add support for binop-assignment, eg. `+=`, `^=`, `<<=` etc. (also support for the tee operator: `+:=`)
* "integer constant cast to float" literal syntax in CurlyWas (ex. `1_f` is equivalent to `1 as f32`)
### Older versions

View File

@@ -225,30 +225,45 @@ Moving/printing past any border does not cause any special operation, the cursor
Characters 0-31 are control characters and don't print by default. They take the next 0-2 following characters as parameters.
Avoid the reserved control chars, they are currently NOPs but their behavior can change in later MicroW8 versions.
| Code | Parameters | Operation |
| ----- | ---------- | ------------------------------------ |
| 0 | - | Nop |
| 1 | char | Print char (including control chars) |
| 2-3 | - | Reserved |
| 4 | - | Switch to normal mode |
| 5 | - | Switch to graphics mode |
| 6 | - | Reserved |
| 7 | - | Bell / trigger sound channel 0 |
| 8 | - | Move cursor left |
| 9 | - | Move cursor right |
| 10 | - | Move cursor down |
| 11 | - | Move cursor up |
| 12 | - | do `cls(background_color)` |
| 13 | - | Move cursor to the left border |
| 14 | color | Set the background color |
| 15 | color | Set the text color |
| 16-23 | - | Reserved |
| 24 | - | Swap text/background colors |
| 25-30 | - | Reserved |
| 31 | x, y | Set cursor position (*) |
| Code | Parameters | Operation |
| ----- | ---------- | ------------------------------------------ |
| 0 | - | Nop |
| 1 | char | Print char (including control chars) |
| 2-3 | - | Reserved |
| 4 | - | Switch to normal mode, reset cursor to 0,0 |
| 5 | - | Switch to graphics mode |
| 6 | - | Switch output to (debug) console |
| 7 | - | Bell / trigger sound channel 0 |
| 8 | - | Move cursor left |
| 9 | - | Move cursor right |
| 10 | - | Move cursor down |
| 11 | - | Move cursor up |
| 12 | - | do `cls(background_color)` |
| 13 | - | Move cursor to the left border |
| 14 | color | Set the background color |
| 15 | color | Set the text color |
| 16-23 | - | Reserved |
| 24 | - | Swap text/background colors |
| 25-30 | - | Reserved |
| 31 | x, y | Set cursor position (*) |
(*) In graphics mode, the x coordinate is doubled when using control char 31 to be able to cover the whole screen with one byte.
#### Debug output
Control code 6 switches all text output (except codes 4 and 5 to switch output back to the screen) to the console. Where exactly this ends
up (if at all) is an implementation detail of the runtimes. The native dev-runtime writes the debug output to `stdout`, the web runtime to
the debug console using `console.log`. Both implementation buffer the output until they encounter a newline character (10) in the output stream.
There may be future runtimes that ignore the debug output completely.
In CurlyWas, a simple way to log some value might look like this:
```
printChar('\06V: '); // switch to console out, print some prefix
printInt(some_value);
printChar('\n\4'); // newline and switch back to screen
```
### fn printChar(char: i32)
Prints the character in the lower 8 bits of `char`. If the upper 24 bits are non-zero, right-shifts `char` by 8 bits and loops back to the beginning.
@@ -286,6 +301,12 @@ As the only means of communication, 32 bytes starting at address 0x00050 are cop
By default, the `sndGes` function generates sound based on the 32 bytes at 0x00050. This means that in the default configuration those 32 bytes act
as sound registers. See the `sndGes` function for the meaning of those registers.
### export fn snd(sampleIndex: i32) -> f32
If the module exports a `snd` function, it is called 88200 times per second to provide PCM sample data for playback (44.1kHz stereo).
The `sampleIndex` will start at 0 and increments by 1 for each call. On even indices the function is expected to return a sample value for
the left channel, on odd indices for the right channel.
### fn playNote(channel: i32, note: i32)
Triggers a note (1-127) on the given channel (0-3). Notes are semitones with 69 being A4 (same as MIDI). A note value of 0 stops the

View File

@@ -2,6 +2,69 @@
description = "Versions"
+++
### v0.2.0
* [Web runtime](v0.2.0)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-windows.zip)
Changes:
* [add sound support!](docs#sound)
* add support to redirect text output to the console for debugging using control code 6
* update curlywas:
* add support for `else if`
* add support for escape sequences in strings
* add support for char literals
* add support for binop-assignment, eg. `+=`, `^=`, `<<=` etc. (also support for the tee operator: `+:=`)
* "integer constant cast to float" literal syntax in CurlyWas (ex. `1_f` is equivalent to `1 as f32`)
### v0.2.0-rc3
* [Web runtime](v0.2.0-rc3)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc3/microw8-0.2.0-rc3-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc3/microw8-0.2.0-rc3-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc3/microw8-0.2.0-rc3-windows.zip)
Changes:
* improve timing stability some more. essentially now guaranteeing that "frame = time_ms * 6 / 100" returns
consecutive frame numbers, provided the module can be run at 60 fps
* add support to redirect text output to the console for debugging using control code 6
* update curlywas:
* add support for `else if`
* add support for escape sequences in strings
* add support for char literals
* add support for binop-assignment, eg. `+=`, `^=`, `<<=` etc. (also support for the tee operator: `+:=`)
### v0.2.0-rc2
* [Web runtime](v0.2.0-rc2)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-windows.zip)
Changes:
* fix timing issues of sound playback, especially on systems with large sound buffers
### v0.2.0-rc1
* [Web runtime](v0.2.0-rc1)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-windows.zip)
Changes:
* [add sound support](docs#sound)
* "integer constant cast to float" literal syntax in CurlyWas (ex. `1_f` is equivalent to `1 as f32`)
Known issues:
* timing accuracy/update frequency of sound support currently depends on sound buffer size
### v0.1.2
* [Web runtime](v0.1.2)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -14,9 +14,6 @@ use anyhow::Result;
pub trait Runtime {
fn is_open(&self) -> bool;
fn set_timeout(&mut self, _timeout: u32) {
eprintln!("Warning: runtime doesn't support timeout");
}
fn load(&mut self, module_data: &[u8]) -> Result<()>;
fn run_frame(&mut self) -> Result<()>;
}
}

View File

@@ -16,7 +16,7 @@ fn main() -> Result<()> {
let mut args = Arguments::from_env();
// try to enable ansi support in win10 cmd shell
#[cfg(target_os="windows")]
#[cfg(target_os = "windows")]
let _ = ansi_term::enable_ansi_support();
match args.subcommand()?.as_deref() {
@@ -52,6 +52,7 @@ fn main() -> Result<()> {
#[cfg(any(feature = "native", feature = "browser"))]
fn run(mut args: Arguments) -> Result<()> {
let watch_mode = args.contains(["-w", "--watch"]);
#[allow(unused)]
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
let mut config = Config::default();
@@ -79,6 +80,8 @@ fn run(mut args: Arguments) -> Result<()> {
#[cfg(not(feature = "native"))]
let run_browser = args.contains(["-b", "--browser"]) || true;
let disable_audio = args.contains(["-m", "--disable-audio"]);
let filename = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let mut watcher = uw8::FileWatcher::new()?;
@@ -89,7 +92,13 @@ fn run(mut args: Arguments) -> Result<()> {
#[cfg(not(feature = "native"))]
unimplemented!();
#[cfg(feature = "native")]
Box::new(MicroW8::new()?)
{
let mut microw8 = MicroW8::new(timeout)?;
if disable_audio {
microw8.disable_audio();
}
Box::new(microw8)
}
} else {
#[cfg(not(feature = "browser"))]
unimplemented!();
@@ -97,10 +106,6 @@ fn run(mut args: Arguments) -> Result<()> {
Box::new(RunWebServer::new())
};
if let Some(timeout) = timeout {
runtime.set_timeout(timeout);
}
let mut first_run = true;
while runtime.is_open() {

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@ use std::{thread, time::Instant};
use anyhow::{anyhow, Result};
use cpal::traits::*;
use minifb::{Key, Window, WindowOptions};
use rubato::Resampler;
use wasmtime::{
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
};
@@ -27,6 +28,7 @@ pub struct MicroW8 {
window_buffer: Vec<u32>,
instance: Option<UW8Instance>,
timeout: u32,
disable_audio: bool,
}
struct UW8Instance {
@@ -35,9 +37,10 @@ struct UW8Instance {
end_frame: TypedFunc<(), ()>,
update: Option<TypedFunc<(), ()>>,
start_time: Instant,
next_frame: Instant,
module: Vec<u8>,
watchdog: Arc<Mutex<UW8WatchDog>>,
sound: Uw8Sound,
sound: Option<Uw8Sound>,
}
impl Drop for UW8Instance {
@@ -49,14 +52,23 @@ impl Drop for UW8Instance {
}
struct UW8WatchDog {
interupt: wasmtime::InterruptHandle,
timeout: u32,
engine: Engine,
stop: bool,
}
impl MicroW8 {
pub fn new() -> Result<MicroW8> {
let engine = wasmtime::Engine::new(wasmtime::Config::new().interruptable(true))?;
pub fn new(timeout: Option<u32>) -> Result<MicroW8> {
#[cfg(target_os = "windows")]
unsafe {
winapi::um::timeapi::timeBeginPeriod(1);
}
let mut config = wasmtime::Config::new();
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
if timeout.is_some() {
config.epoch_interruption(true);
}
let engine = wasmtime::Engine::new(&config)?;
let loader_module =
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
@@ -67,8 +79,7 @@ impl MicroW8 {
resize: true,
..Default::default()
};
let mut window = Window::new("MicroW8", 320, 240, options)?;
window.limit_update_rate(Some(std::time::Duration::from_micros(16666)));
let window = Window::new("MicroW8", 320, 240, options)?;
Ok(MicroW8 {
engine,
@@ -76,7 +87,8 @@ impl MicroW8 {
window,
window_buffer: vec![0u32; 320 * 240],
instance: None,
timeout: 30,
timeout: timeout.unwrap_or(0),
disable_audio: false,
})
}
@@ -86,11 +98,10 @@ impl MicroW8 {
*v = 0;
}
}
}
struct Uw8Sound {
stream: cpal::Stream,
tx: mpsc::SyncSender<[u8; 32]>,
pub fn disable_audio(&mut self) {
self.disable_audio = true;
}
}
impl super::Runtime for MicroW8 {
@@ -98,14 +109,11 @@ impl super::Runtime for MicroW8 {
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, ());
store.set_epoch_deadline(60);
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
@@ -126,65 +134,12 @@ impl super::Runtime for MicroW8 {
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])?;
fn add_native_functions(
linker: &mut wasmtime::Linker<()>,
store: &mut wasmtime::Store<()>,
) -> Result<()> {
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 10..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(),
)?,
)?;
}
Ok(())
}
add_native_functions(&mut linker, &mut store)?;
fn instantiate_platform(
linker: &mut wasmtime::Linker<()>,
store: &mut wasmtime::Store<()>,
platform_module: &wasmtime::Module,
) -> Result<wasmtime::Instance> {
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"),
)?;
}
Ok(platform_instance)
}
let platform_instance = instantiate_platform(&mut linker, &mut store, &platform_module)?;
let watchdog = Arc::new(Mutex::new(UW8WatchDog {
interupt: store.interrupt_handle()?,
timeout: self.timeout,
engine: self.engine.clone(),
stop: false,
}));
@@ -192,16 +147,11 @@ impl super::Runtime for MicroW8 {
let watchdog = watchdog.clone();
thread::spawn(move || loop {
thread::sleep(Duration::from_millis(17));
if let Ok(mut watchdog) = watchdog.lock() {
if let Ok(watchdog) = watchdog.lock() {
if watchdog.stop {
break;
}
if watchdog.timeout > 0 {
watchdog.timeout -= 1;
if watchdog.timeout == 0 {
watchdog.interupt.interrupt();
}
}
watchdog.engine.increment_epoch();
} else {
break;
}
@@ -209,97 +159,31 @@ impl super::Runtime for MicroW8 {
}
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").ok();
let sound = {
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)?;
add_native_functions(&mut linker, &mut store)?;
let platform_instance =
instantiate_platform(&mut linker, &mut store, &platform_module)?;
let instance = linker.instantiate(&mut store, &module)?;
let snd = instance
.get_typed_func::<(i32,), f32, _>(&mut store, "snd")
.or_else(|_| {
platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "gesSnd")
})?;
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| anyhow!("No audio output device available"))?;
let config = device
.supported_output_configs()?
.find(|config| {
config.min_sample_rate().0 <= 44100
&& config.max_sample_rate().0 >= 44100
&& config.channels() == 2
&& config.sample_format() == cpal::SampleFormat::F32
})
.ok_or_else(|| anyhow!("Could not find 44.1kHz float config"))?;
let config = config.with_sample_rate(cpal::SampleRate(44100));
let buffer_size = match *config.buffer_size() {
cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default,
cpal::SupportedBufferSize::Range { min, max } => {
cpal::BufferSize::Fixed(256.max(min).min(max))
let sound = if self.disable_audio {
None
} else {
match init_sound(&self.engine, &platform_module, &module) {
Ok(sound) => {
sound.stream.play()?;
Some(sound)
}
};
let config = cpal::StreamConfig {
buffer_size,
..config.config()
};
let (tx, rx) = mpsc::sync_channel::<[u8; 32]>(1);
let start_time = Instant::now();
let mut sample_index = 0;
let stream = {
device.build_output_stream(
&config,
move |buffer: &mut [f32], _| {
if let Ok(regs) = rx.try_recv() {
memory.write(&mut store, 80, &regs).unwrap();
}
{
let time = start_time.elapsed().as_millis() as i32;
let mem = memory.data_mut(&mut store);
mem[64..68].copy_from_slice(&time.to_le_bytes());
}
for v in buffer {
*v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0);
sample_index = sample_index.wrapping_add(1);
}
},
move |err| {
dbg!(err);
},
)?
};
Uw8Sound { stream, tx }
Err(err) => {
eprintln!("Failed to init sound: {}", err);
None
}
}
};
sound.stream.play()?;
self.instance = Some(UW8Instance {
store,
memory,
end_frame,
update,
start_time: Instant::now(),
next_frame: Instant::now(),
module: module_data.into(),
watchdog,
sound,
@@ -312,7 +196,19 @@ impl super::Runtime for MicroW8 {
let mut result = Ok(());
if let Some(mut instance) = self.instance.take() {
{
let time = instance.start_time.elapsed().as_millis() as i32;
if let Some(sleep) = instance.next_frame.checked_duration_since(Instant::now()) {
std::thread::sleep(sleep);
}
}
let now = Instant::now();
let time = (now - instance.start_time).as_millis() as i32;
{
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6;
instance.next_frame = now + Duration::from_millis((16 - offset) as u64);
}
{
let mut gamepad: u32 = 0;
for key in self.window.get_keys() {
if let Some(index) = GAMEPAD_KEYS
@@ -330,22 +226,22 @@ impl super::Runtime for MicroW8 {
mem[68..72].copy_from_slice(&gamepad.to_le_bytes());
}
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = self.timeout;
}
instance.store.set_epoch_deadline(self.timeout as u64);
if let Some(ref update) = instance.update {
result = 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 mut sound_regs = [0u8; 32];
sound_regs.copy_from_slice(&memory[80..112]);
instance.sound.tx.send(sound_regs)?;
if let Some(ref sound) = instance.sound {
sound.tx.send(RegisterUpdate {
time,
data: sound_regs,
})?;
}
let framebuffer = &memory[120..(120 + 320 * 240)];
let palette = &memory[0x13000..];
@@ -371,3 +267,256 @@ impl super::Runtime for MicroW8 {
Ok(())
}
}
fn add_native_functions(
linker: &mut wasmtime::Linker<()>,
store: &mut wasmtime::Store<()>,
) -> Result<()> {
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 10..64 {
linker.func_wrap("env", &format!("reserved{}", i), || {})?;
}
let log_line = std::sync::Mutex::new(String::new());
linker.func_wrap("env", "logChar", move |c: i32| {
let mut log_line = log_line.lock().unwrap();
if c == 10 {
println!("{}", log_line);
log_line.clear();
} else {
log_line.push(c as u8 as char);
}
})?;
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(),
)?,
)?;
}
Ok(())
}
fn instantiate_platform(
linker: &mut wasmtime::Linker<()>,
store: &mut wasmtime::Store<()>,
platform_module: &wasmtime::Module,
) -> Result<wasmtime::Instance> {
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"),
)?;
}
Ok(platform_instance)
}
struct RegisterUpdate {
time: i32,
data: [u8; 32],
}
struct Uw8Sound {
stream: cpal::Stream,
tx: mpsc::SyncSender<RegisterUpdate>,
}
fn init_sound(
engine: &wasmtime::Engine,
platform_module: &wasmtime::Module,
module: &wasmtime::Module,
) -> Result<Uw8Sound> {
let mut store = wasmtime::Store::new(engine, ());
store.set_epoch_deadline(60);
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
let mut linker = wasmtime::Linker::new(engine);
linker.define("env", "memory", memory)?;
add_native_functions(&mut linker, &mut store)?;
let platform_instance = instantiate_platform(&mut linker, &mut store, platform_module)?;
let instance = linker.instantiate(&mut store, module)?;
let snd = instance
.get_typed_func::<(i32,), f32, _>(&mut store, "snd")
.or_else(|_| platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "gesSnd"))?;
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| anyhow!("No audio output device available"))?;
let mut configs: Vec<_> = device
.supported_output_configs()?
.filter(|config| {
config.channels() == 2 && config.sample_format() == cpal::SampleFormat::F32
})
.collect();
configs.sort_by_key(|config| {
let rate = 44100
.max(config.min_sample_rate().0)
.min(config.max_sample_rate().0);
if rate >= 44100 {
rate - 44100
} else {
(44100 - rate) * 1000
}
});
let config = configs
.into_iter()
.next()
.ok_or_else(|| anyhow!("Could not find float output config"))?;
let sample_rate = cpal::SampleRate(44100)
.max(config.min_sample_rate())
.max(config.max_sample_rate());
let config = config.with_sample_rate(sample_rate);
let buffer_size = match *config.buffer_size() {
cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default,
cpal::SupportedBufferSize::Range { min, max } => {
cpal::BufferSize::Fixed(256.max(min).min(max))
}
};
let config = cpal::StreamConfig {
buffer_size,
..config.config()
};
let sample_rate = config.sample_rate.0 as usize;
let (tx, rx) = mpsc::sync_channel::<RegisterUpdate>(30);
struct Resampler {
resampler: rubato::FftFixedIn<f32>,
input_buffers: Vec<Vec<f32>>,
output_buffers: Vec<Vec<f32>>,
output_index: usize,
}
let mut resampler: Option<Resampler> = if sample_rate == 44100 {
None
} else {
let rs = rubato::FftFixedIn::new(44100, sample_rate, 128, 1, 2)?;
let input_buffers = rs.input_buffer_allocate();
let output_buffers = rs.output_buffer_allocate();
Some(Resampler {
resampler: rs,
input_buffers,
output_buffers,
output_index: usize::MAX,
})
};
let mut sample_index = 0;
let mut pending_updates: Vec<RegisterUpdate> = Vec::with_capacity(30);
let mut current_time = 0;
let stream = device.build_output_stream(
&config,
move |mut outer_buffer: &mut [f32], _| {
let mut first_update = true;
while let Ok(update) = rx.try_recv() {
if first_update {
current_time += update.time.wrapping_sub(current_time) / 8;
first_update = false;
}
pending_updates.push(update);
}
while !outer_buffer.is_empty() {
store.set_epoch_deadline(30);
while pending_updates
.first()
.into_iter()
.any(|u| u.time.wrapping_sub(current_time) <= 0)
{
let update = pending_updates.remove(0);
memory.write(&mut store, 80, &update.data).unwrap();
}
let duration = if let Some(update) = pending_updates.first() {
((update.time.wrapping_sub(current_time) as usize) * sample_rate + 999) / 1000
} else {
outer_buffer.len()
};
let step_size = (duration.max(64) * 2).min(outer_buffer.len());
let mut buffer = &mut outer_buffer[..step_size];
{
let mem = memory.data_mut(&mut store);
mem[64..68].copy_from_slice(&current_time.to_le_bytes());
}
if let Some(ref mut resampler) = resampler {
while !buffer.is_empty() {
let copy_size = resampler.output_buffers[0]
.len()
.saturating_sub(resampler.output_index)
.min(buffer.len() / 2);
if copy_size == 0 {
resampler.input_buffers[0].clear();
resampler.input_buffers[1].clear();
for _ in 0..resampler.resampler.input_frames_next() {
resampler.input_buffers[0]
.push(snd.call(&mut store, (sample_index,)).unwrap_or(0.0));
resampler.input_buffers[1]
.push(snd.call(&mut store, (sample_index + 1,)).unwrap_or(0.0));
sample_index = sample_index.wrapping_add(2);
}
resampler
.resampler
.process_into_buffer(
&resampler.input_buffers,
&mut resampler.output_buffers,
None,
)
.unwrap();
resampler.output_index = 0;
} else {
for i in 0..copy_size {
buffer[i * 2] =
resampler.output_buffers[0][resampler.output_index + i];
buffer[i * 2 + 1] =
resampler.output_buffers[1][resampler.output_index + i];
}
resampler.output_index += copy_size;
buffer = &mut buffer[copy_size * 2..];
}
}
} else {
for v in buffer {
*v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0);
sample_index = sample_index.wrapping_add(1);
}
}
outer_buffer = &mut outer_buffer[step_size..];
current_time =
current_time.wrapping_add((step_size * 500 / sample_rate).max(1) as i32);
}
},
move |err| {
dbg!(err);
},
)?;
Ok(Uw8Sound { stream, tx })
}

62
test/drawing_test.cwa Normal file
View File

@@ -0,0 +1,62 @@
include "../examples/include/microw8-api.cwa"
global mut counter = 0;
export fn upd() {
cls(0);
let col: i32 = 1;
loop colors {
if !testCircle(counter, col) {
printInt(counter);
return;
}
counter += 1;
branch_if (col +:= 1) < 256: colors;
}
}
fn testCircle(seed: i32, col: i32) -> i32 {
randomSeed(seed);
let cx = randomf() * 640_f - 160_f;
let cy = randomf() * 480_f - 120_f;
let radius = randomf() * 4_f;
radius *= radius;
radius *= radius;
circle(cx, cy, radius, col);
let min_x = max(0_f, floor(cx - radius - 1_f)) as i32;
let min_y = max(0_f, floor(cy - radius - 1_f)) as i32;
let max_x = min(320_f, ceil(cx + radius + 1_f)) as i32;
let max_y = min(240_f, ceil(cy + radius + 1_f)) as i32;
let x = min_x;
loop xloop {
if x < max_x {
let y = min_y;
loop yloop {
if y < max_y {
let rx = x as f32 + 0.5 - cx;
let ry = y as f32 + 0.5 - cy;
let d = sqrt(rx*rx + ry*ry) - radius;
if abs(d) > 0.001 {
let is_inside = d < 0_f;
let is_plotted = getPixel(x, y) == col;
if is_inside != is_plotted {
return 0;
}
}
y += 1;
branch yloop;
}
}
x += 1;
branch xloop;
}
}
1
}

20
test/frame_time.cwa Normal file
View File

@@ -0,0 +1,20 @@
include "../examples/include/microw8-api.cwa"
global mut pos = 0;
global mut next = 0;
export fn upd() {
let lazy t = 32!32;
let lazy tick = t * 6 / 100;
let lazy rel = t - tick * 100 / 6;
setBackgroundColor(select(tick == next, 0, select(tick < next, 0x35, 0x55)));
setCursorPosition(pos % 13 * 3, pos / 13 % 30);
if rel < 10 {
printChar(32);
}
printInt(rel);
pos = pos + 1;
next = tick + 1;
}

7
test/log.cwa Normal file
View File

@@ -0,0 +1,7 @@
include "../examples/include/microw8-api.cwa"
export fn upd() {
printChar('\06f: ');
printInt(32!32 * 6 / 100);
printChar('\n\4');
}

View File

@@ -962,6 +962,8 @@ fn remap_function(
De::I64TruncSatF32U => En::I64TruncSatF32U,
De::I64TruncSatF64S => En::I64TruncSatF64S,
De::I64TruncSatF64U => En::I64TruncSatF64U,
De::MemoryCopy { src, dst } => En::MemoryCopy { src, dst },
De::MemoryFill { mem } => En::MemoryFill(mem),
other => bail!("Unsupported instruction {:?}", other),
});
}

57
web/opus-repro.html Normal file
View File

@@ -0,0 +1,57 @@
<html>
<button onclick="go()">Go!</button>
<canvas id="screen" width="320" height="240"></canvas>
<video id="video"></video>
<script>
function go() {
let audioContext = new AudioContext({sampleRate: 44100});
let oscillator = new OscillatorNode(audioContext);
let gain = new GainNode(audioContext, {gain: 1});
oscillator.connect(gain);
gain.connect(audioContext.destination);
for(let i = 0; i < 8; ++i ) {
gain.gain.setValueAtTime(1, i / 2);
gain.gain.setValueAtTime(0, i / 2 + 0.3);
}
oscillator.start();
oscillator.stop(4);
let screen = document.getElementById('screen');
let context = screen.getContext('2d');
let startTime = Date.now();
let drawFrame = () => {
let time = Date.now() - startTime;
context.fillStyle = 'white';
context.fillRect(0, 0, 320, 240);
if(time < 4000) {
if(time % 500 < 300) {
context.fillStyle = 'black';
context.fillRect(time / 15, 50, 50, 50);
}
window.requestAnimationFrame(drawFrame);
}
};
drawFrame();
let stream = screen.captureStream();
let audioStreamNode = audioContext.createMediaStreamDestination();
gain.connect(audioStreamNode);
stream.addTrack(audioStreamNode.stream.getAudioTracks()[0]);
let recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
let chunks = [];
recorder.ondataavailable = e => chunks.push(e.data);
recorder.onstop = () => {
let blob = new Blob(chunks, {type: 'video/webm'});
let url = URL.createObjectURL(blob);
let video = document.getElementById('video');
video.src = url;
video.play();
};
recorder.start();
setTimeout(() => recorder.stop(), 4000);
}
</script>
</html>

View File

@@ -3,13 +3,17 @@ class APU extends AudioWorkletProcessor {
constructor() {
super();
this.sampleIndex = 0;
this.currentTime = 0;
this.isFirstMessage = true;
this.pendingUpdates = [];
this.port.onmessage = (ev) => {
if(this.memory) {
if(isNaN(ev.data)) {
U8(this.memory.buffer, 80, 32).set(U8(ev.data));
} else {
this.startTime = ev.data;
if(this.isFirstMessage)
{
this.currentTime += (ev.data.t - this.currentTime) / 8;
this.isFirstMessage = false;
}
this.pendingUpdates.push(ev.data);
} else {
this.load(ev.data[0], ev.data[1]);
}
@@ -33,6 +37,16 @@ class APU extends AudioWorkletProcessor {
importObject.env['reserved' + i] = () => { };
}
let logLine = '';
importObject.env['logChar'] = (c) => {
if(c == 10) {
console.log(logLine);
logLine = '';
} else {
logLine += String.fromCharCode(c);
}
};
for (let i = 0; i < 16; ++i) {
importObject.env['g_reserved' + i] = 0;
}
@@ -55,9 +69,13 @@ class APU extends AudioWorkletProcessor {
}
process(inputs, outputs, parameters) {
if(this.snd && this.startTime) {
this.isFirstMessage = true;
if(this.snd) {
while(this.pendingUpdates.length > 0 && this.pendingUpdates[0].t <= this.currentTime) {
U8(this.memory.buffer, 80, 32).set(U8(this.pendingUpdates.shift().r));
}
let u32Mem = new Uint32Array(this.memory.buffer);
u32Mem[16] = Date.now() - this.startTime;
u32Mem[16] = this.currentTime;
let channels = outputs[0];
let index = this.sampleIndex;
let numSamples = channels[0].length;
@@ -66,6 +84,7 @@ class APU extends AudioWorkletProcessor {
channels[1][i] = this.snd(index++);
}
this.sampleIndex = index & 0xffffffff;
this.currentTime += numSamples / 44.1;
}
return true;

View File

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

View File

@@ -107,7 +107,7 @@ export default function MicroW8(screen, config = {}) {
audioContext.close();
keepRunning = false;
abortController.abort();
}
};
let cartridgeSize = data.byteLength;
@@ -206,6 +206,16 @@ export default function MicroW8(screen, config = {}) {
importObject.env['reserved' + i] = () => { };
}
let logLine = '';
importObject.env['logChar'] = (c) => {
if(c == 10) {
console.log(logLine);
logLine = '';
} else {
logLine += String.fromCharCode(c);
}
};
for (let i = 0; i < 16; ++i) {
importObject.env['g_reserved' + i] = 0;
}
@@ -232,7 +242,6 @@ export default function MicroW8(screen, config = {}) {
let startTime = Date.now();
const timePerFrame = 1000 / 60;
let nextFrame = startTime;
audioNode.connect(audioContext.destination);
@@ -244,18 +253,16 @@ export default function MicroW8(screen, config = {}) {
isPaused = false;
audioContext.resume();
startTime += now - pauseTime;
audioNode.port.postMessage(startTime);
} else {
isPaused = true;
audioContext.suspend();
pauseTime = now;
audioNode.port.postMessage(0);
}
};
window.addEventListener('focus', () => updateVisibility(true), { signal: abortController.signal });
window.addEventListener('blur', () => updateVisibility(false), { signal: abortController.signal });
updateVisibility(document.hasFocus());
function mainloop() {
if (!keepRunning) {
return;
@@ -263,6 +270,7 @@ export default function MicroW8(screen, config = {}) {
try {
let restart = false;
let thisFrame;
if (!isPaused) {
let gamepads = navigator.getGamepads();
let gamepad = 0;
@@ -291,7 +299,8 @@ export default function MicroW8(screen, config = {}) {
}
let u32Mem = U32(memory.buffer);
u32Mem[16] = Date.now() - startTime;
let time = Date.now() - startTime;
u32Mem[16] = time;
u32Mem[17] = pad | gamepad;
if(instance.exports.upd) {
instance.exports.upd();
@@ -300,21 +309,26 @@ export default function MicroW8(screen, config = {}) {
let soundRegisters = new ArrayBuffer(32);
U8(soundRegisters).set(U8(memory.buffer, 80, 32));
audioNode.port.postMessage(soundRegisters, [soundRegisters]);
audioNode.port.postMessage({t: time, r: 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);
let timeOffset = ((time * 6) % 100 - 50) / 6;
thisFrame = startTime + time - timeOffset / 8;
} else {
thisFrame = Date.now();
}
let now = Date.now();
nextFrame = Math.max(nextFrame + timePerFrame, now);
let nextFrame = Math.max(thisFrame + timePerFrame, now);
if (restart) {
runModule(currentData);
} else {
window.setTimeout(mainloop, Math.round(nextFrame - now))
window.setTimeout(mainloop, nextFrame - now)
}
} catch (err) {
config.setMessage(cartridgeSize, err.toString());

File diff suppressed because it is too large Load Diff