mirror of
https://github.com/exoticorn/microw8.git
synced 2026-06-28 13:49:40 +02:00
Compare commits
73 Commits
v0.1.2
..
eb724e8785
| Author | SHA1 | Date | |
|---|---|---|---|
| eb724e8785 | |||
| f559c5b7d4 | |||
| 9dabf75732 | |||
| b0adf7748d | |||
| 7aa70ef39d | |||
| 2ce91ef49c | |||
| 7caad08b7c | |||
| 1f6de62e5d | |||
| caeaa82787 | |||
| e0450c9039 | |||
| 95d0d92a6f | |||
| 7a6dd0ab6d | |||
| e7a00dd9c6 | |||
| a02243d98c | |||
| 599873890a | |||
| 8e9bb002bc | |||
| b2b990333e | |||
| d1556f7be8 | |||
| 9f548cd6f0 | |||
| 7cea4eebd3 | |||
| 3f67e92c5c | |||
| a2714f25e4 | |||
| 7e203d93e6 | |||
| e44c87d1f6 | |||
| 614b7cf358 | |||
| c42a484adb | |||
| 3a5f2bf865 | |||
| 2dee1b30a4 | |||
| 42f7887ab2 | |||
| 4c82f4ad02 | |||
| 4dd8c3b029 | |||
| 2bf8938183 | |||
| 491bf88ade | |||
| e05701300c | |||
| df0c169d54 | |||
| 61941bceeb | |||
| 8fa64519e4 | |||
| 7a52ce4e4c | |||
| 6f20d303c8 | |||
| a5edeb21d8 | |||
| 2839fe5be4 | |||
| 893158e136 | |||
| 7c5f43f152 | |||
| f32b0762b0 | |||
| 9ebb6b6d34 | |||
| 8a10b99eeb | |||
| 6c064a1dd8 | |||
| 37f12f5a2c | |||
| 8ad2885a55 | |||
| 1917057b81 | |||
| 82c1ddb867 | |||
| 8713aa8930 | |||
| 0f82e6e711 | |||
| 0ade24ebf6 | |||
| 29186c806f | |||
| b626d2609a | |||
| 39ead8220f | |||
| ce18a8a162 | |||
| a15e796489 | |||
| f178076b86 | |||
| 81adcf0198 | |||
| 780caf965a | |||
| 2033f9a172 | |||
| 0d514c7dd3 | |||
| a8eb3bda27 | |||
| 8b765a5742 | |||
| 7197c11586 | |||
| 99a423619e | |||
| 9063e872d3 | |||
| 85240599e8 | |||
| 35ec5fdb59 | |||
| a6a82ff5a1 | |||
| 973814a629 |
@@ -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
|
||||
|
||||
Generated
+1304
-216
File diff suppressed because it is too large
Load Diff
+9
-6
@@ -1,26 +1,29 @@
|
||||
[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", "uw8-window", "cpal", "rubato" ]
|
||||
browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
|
||||
|
||||
[dependencies]
|
||||
wasmtime = { version = "0.30", optional = true }
|
||||
wasmtime = { version = "0.37.0", optional = true }
|
||||
anyhow = "1"
|
||||
minifb = { version = "0.20", default-features = false, features = ["x11"] }
|
||||
uw8-window = { path = "uw8-window", optional = true }
|
||||
notify = "4"
|
||||
pico-args = "0.4"
|
||||
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "89638565" }
|
||||
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0e7ea50" }
|
||||
wat = "1"
|
||||
uw8-tool = { path = "uw8-tool" }
|
||||
same-file = "1"
|
||||
warp = { version = "0.3.2", optional = true }
|
||||
tokio = { version = "1.17.0", features = ["sync", "rt"], optional = true }
|
||||
tokio-stream = { version = "0.1.8", features = ["sync"], optional = true }
|
||||
webbrowser = { version = "0.6.0", optional = true }
|
||||
webbrowser = { version = "0.6.0", optional = true }
|
||||
ansi_term = "0.12.1"
|
||||
cpal = { version = "0.13.5", optional = true }
|
||||
rubato = { version = "0.11.0", optional = true }
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
include "../include/microw8-api.cwa"
|
||||
|
||||
global mut frame = 0;
|
||||
|
||||
export fn upd() {
|
||||
if frame % 16 == 0 {
|
||||
let ch: i32;
|
||||
loop channels {
|
||||
playNote(ch, (ch * 32 + (frame / 16) % 32)?0x20000);
|
||||
branch_if ch := (ch + 1) % 4: channels;
|
||||
}
|
||||
}
|
||||
frame = frame + 1;
|
||||
}
|
||||
|
||||
data 0x20000 {
|
||||
i8(
|
||||
0x4e, 0x0, 0x0, 0x4c, 0x49, 0x0, 0x45, 0x47,
|
||||
0x49, 0x47, 0x45, 0x44, 0x42, 0x0, 0x3d, 0x41,
|
||||
0x44, 0x0, 0x0, 0x47, 0x49, 0x47, 0x45, 0x41,
|
||||
0x44, 0x0, 0x0, 0x0, 0x42, 0x0, 0x0, 0x0,
|
||||
|
||||
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
|
||||
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
|
||||
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
|
||||
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
|
||||
|
||||
0x2a, 0x0, 0x0, 0x0, 0x2d, 0x0, 0x0, 0x0,
|
||||
0x2c, 0x0, 0x28, 0x0, 0x2a, 0x0, 0x0, 0x0,
|
||||
0x25, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0,
|
||||
0x2c, 0x0, 0x2d, 0x0, 0x2a, 0x0, 0x25, 0x0,
|
||||
|
||||
0x0, 0x0, 0x31, 0x0, 0x34, 0x0, 0x0, 0x36,
|
||||
0x38, 0x39, 0x38, 0x34, 0x36, 0x0, 0x0, 0x0,
|
||||
0x0, 0x3d, 0x3b, 0x39, 0x38, 0x0, 0x0, 0x0,
|
||||
0x0, 0x39, 0x38, 0x39, 0x38, 0x0, 0x36, 0x0
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Steady On Tim, It's Only A Budget Game
|
||||
// original bytebeat by Gasman / Hooy-Program
|
||||
// ported to MicroW8/GES by exoticorn/icebird
|
||||
|
||||
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 = 230 - riff_pos * 14;
|
||||
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 | 0x48; // arp trigger
|
||||
|
||||
if T >= 128 {
|
||||
let inline bass_step = T % 8;
|
||||
86?3 = if bass_step / 2 == 2 {
|
||||
86?0 = 0xd6;
|
||||
81
|
||||
} else {
|
||||
86?0 = ((197 >> bass_step) & 1) | 0x48;
|
||||
((T & 4) * ((T & 7) - 1)) / 2 + 28
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
data 80 {
|
||||
i8(
|
||||
0, 0x90, 0, 0, 0, 0x90,
|
||||
0, 0x4c, 0, 0, 0, 0x4c,
|
||||
0x19, 0, 0, 0, 0, 0x4c,
|
||||
0x19, 0, 0, 0, 0, 0x4c,
|
||||
0xfa, 0x84,
|
||||
0xc1, 0xc1, 0, 107, 0, 0x4c
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
include "../../platform/src/ges.cwa"
|
||||
|
||||
import "env.pow" fn pow(f32, f32) -> f32;
|
||||
import "env.exp" fn exp(f32) -> f32;
|
||||
import "env.sin" fn sin(f32) -> f32;
|
||||
|
||||
export fn snd(t: i32) -> f32 {
|
||||
gesSnd(t)
|
||||
}
|
||||
*/
|
||||
@@ -33,6 +33,7 @@ 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;
|
||||
import "env.playNote" fn playNote(i32, i32);
|
||||
|
||||
const TIME_MS = 0x40;
|
||||
const GAMEPAD = 0x44;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
(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)))
|
||||
(import "env" "playNote" (func $playNote (param i32) (param i32)))
|
||||
|
||||
;; to use defines, include this file with a preprocessor
|
||||
;; like gpp (https://logological.org/gpp).
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
(module
|
||||
(import "env" "atan2" (func $atan2 (param f32 f32) (result f32)))
|
||||
(import "env" "time" (func $time (result f32)))
|
||||
(import "env" "memory" (memory 4))
|
||||
(func (export "upd")
|
||||
(local $y i32)
|
||||
(local $i i32)
|
||||
(local $x i32)
|
||||
|
||||
(loop $pixels
|
||||
i32.const 1
|
||||
local.get $i
|
||||
|
||||
local.get $i
|
||||
|
||||
i32.const 36928
|
||||
f32.convert_i32_s
|
||||
local.get $i
|
||||
i32.const 320
|
||||
i32.rem_s
|
||||
i32.const 160
|
||||
i32.sub
|
||||
local.tee $x
|
||||
local.get $x
|
||||
i32.mul
|
||||
local.get $i
|
||||
i32.const 320
|
||||
i32.div_s
|
||||
i32.const 120
|
||||
i32.sub
|
||||
local.tee $y
|
||||
local.get $y
|
||||
i32.mul
|
||||
i32.add
|
||||
f32.convert_i32_s
|
||||
f32.sqrt
|
||||
f32.div
|
||||
call $time
|
||||
i32.const 163
|
||||
f32.convert_i32_s
|
||||
f32.mul
|
||||
f32.add
|
||||
i32.trunc_sat_f32_s
|
||||
|
||||
local.get $x
|
||||
f32.convert_i32_s
|
||||
local.get $y
|
||||
f32.convert_i32_s
|
||||
call $atan2
|
||||
i32.const 163
|
||||
f32.convert_i32_s
|
||||
f32.mul
|
||||
call $time
|
||||
i32.const 64
|
||||
f32.convert_i32_s
|
||||
f32.mul
|
||||
f32.add
|
||||
i32.trunc_f32_s
|
||||
|
||||
i32.xor
|
||||
i32.const 4
|
||||
i32.shr_s
|
||||
i32.const 15
|
||||
i32.and
|
||||
i32.store8 offset=120
|
||||
|
||||
i32.add
|
||||
local.tee $i
|
||||
i32.const 76800
|
||||
i32.rem_s
|
||||
br_if $pixels
|
||||
)
|
||||
)
|
||||
)
|
||||
Generated
+1
-1
@@ -146,7 +146,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
[[package]]
|
||||
name = "curlywas"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/exoticorn/curlywas.git?rev=89638565#896385654ab2c089200920b6dea4abec641c88d6"
|
||||
source = "git+https://github.com/exoticorn/curlywas.git?rev=0e7ea50#0e7ea508cd0e76836283ae68a44c9097df83c8ac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ariadne",
|
||||
|
||||
+1
-1
@@ -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="89638565" }
|
||||
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="0e7ea50" }
|
||||
uw8-tool = { path="../uw8-tool" }
|
||||
anyhow = "1"
|
||||
lodepng = "3.4"
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,222 @@
|
||||
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 = 32;
|
||||
const GesBufferOffset = 32 + GesState.Size;
|
||||
|
||||
export fn gesSnd(t: i32) -> f32 {
|
||||
let baseAddr = 0!0x12c78;
|
||||
if !(t & 127) {
|
||||
let i: i32;
|
||||
loop clearLoop {
|
||||
(baseAddr + i)!GesBufferOffset = 0;
|
||||
branch_if (i := i + 4) < 128*4: clearLoop;
|
||||
}
|
||||
|
||||
let ch: i32;
|
||||
loop channelLoop {
|
||||
let lazy channelState = baseAddr + GesStateOffset + ch * GesChannelState.Size;
|
||||
let lazy channelReg = baseAddr + 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);
|
||||
(baseAddr + i)!(GesBufferOffset + 128*4) = saw2;
|
||||
phase = phase + phaseInc;
|
||||
branch_if (i := i + 4) < 64*4: sawLoop;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let pulsePhase = 32768 + pulseWidth * 128;
|
||||
loop rectLoop {
|
||||
(baseAddr + 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));
|
||||
(baseAddr + 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;
|
||||
(baseAddr + 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 = baseAddr + GesStateOffset + modSrc * GesChannelState.Size;
|
||||
let inline channelReg = baseAddr + 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));
|
||||
(baseAddr + i)!(GesBufferOffset + 128*4) = ((baseAddr + i)!(GesBufferOffset + 128*4) * ((s >> 15) - 32768)) >> 15;
|
||||
phase = phase + phaseInc;
|
||||
branch_if (i := i + 4) < 64*4: ringLoop;
|
||||
}
|
||||
}
|
||||
|
||||
let channelVol = ((baseAddr + (ch >> 1))?24 >> ((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 = (baseAddr + ch * 8)!(GesStateOffset + GesState.Filter);
|
||||
loop filterLoop {
|
||||
let in = ((baseAddr + i)!(GesBufferOffset + 128*4) * envVol) >> 18;
|
||||
low = low + (((in - low) * f) >> 12);
|
||||
(baseAddr + i * 2)!GesBufferOffset = (baseAddr + i * 2)!GesBufferOffset + ((low * leftVol) >> 4);
|
||||
(baseAddr + i * 2)!(GesBufferOffset + 4) = (baseAddr + i * 2)!(GesBufferOffset + 4) + ((low * rightVol) >> 4);
|
||||
branch_if (i := i + 4) < 64*4: filterLoop;
|
||||
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter) = low;
|
||||
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter + 4) = 0;
|
||||
}
|
||||
} else {
|
||||
loop mixLoop {
|
||||
let sample = ((baseAddr + i)!(GesBufferOffset + 128*4) * envVol) >> 18;
|
||||
(baseAddr + i * 2)!GesBufferOffset = (baseAddr + i * 2)!GesBufferOffset + ((sample * leftVol) >> 4);
|
||||
(baseAddr + i * 2)!(GesBufferOffset + 4) = (baseAddr + i * 2)!(GesBufferOffset + 4) + ((sample * rightVol) >> 4);
|
||||
branch_if (i := i + 4) < 64*4: mixLoop;
|
||||
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter) = sample;
|
||||
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter + 4) = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filter = filter - 2;
|
||||
let ctrl = (baseAddr + filter)?26;
|
||||
let note = i32.load16_u(baseAddr + filter * 2, 28);
|
||||
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 = (baseAddr + ch * 8)!(GesStateOffset + GesState.Filter);
|
||||
let band = (baseAddr + ch * 8)!(GesStateOffset + GesState.Filter + 4);
|
||||
loop filterLoop {
|
||||
let in = ((baseAddr + 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;
|
||||
(baseAddr + i * 2)!GesBufferOffset = (baseAddr + i * 2)!GesBufferOffset + ((sample * leftVol) >> 4);
|
||||
(baseAddr + i * 2)!(GesBufferOffset + 4) = (baseAddr + i * 2)!(GesBufferOffset + 4) + ((sample * rightVol) >> 4);
|
||||
branch_if (i := i + 4) < 64*4: filterLoop;
|
||||
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter) = low;
|
||||
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter + 4) = band;
|
||||
}
|
||||
}
|
||||
|
||||
branch_if (ch := ch + 1) < 4: channelLoop;
|
||||
}
|
||||
}
|
||||
((baseAddr + (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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -11,16 +11,16 @@ fn main() -> Result<()> {
|
||||
convert_font()?;
|
||||
|
||||
println!("Compiling loader module");
|
||||
let loader = curlywas::compile_file("src/loader.cwa", curlywas::Options::default())?;
|
||||
File::create("bin/loader.wasm")?.write_all(&loader.wasm)?;
|
||||
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.wasm.len());
|
||||
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.wasm,
|
||||
&platform,
|
||||
&uw8_tool::PackConfig::default().with_compression_level(4),
|
||||
)?;
|
||||
File::create("bin/platform.uw8")?.write_all(&platform)?;
|
||||
|
||||
+131
-28
@@ -1,6 +1,10 @@
|
||||
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;
|
||||
import "env.logChar" fn logChar(i32);
|
||||
|
||||
export fn time() -> f32 {
|
||||
(0!64) as f32 / 1000 as f32
|
||||
@@ -29,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
|
||||
}
|
||||
|
||||
@@ -59,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) {
|
||||
@@ -88,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,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 {
|
||||
@@ -316,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;
|
||||
@@ -329,16 +399,14 @@ fn printSingleChar(char: i32) {
|
||||
return;
|
||||
}
|
||||
|
||||
if char == 4 | char == 5 {
|
||||
graphicsText = char == 5;
|
||||
textCursorX = 0;
|
||||
textCursorY = 0;
|
||||
if char == 7 {
|
||||
80?0 = 80?0 ^ 2;
|
||||
return;
|
||||
}
|
||||
|
||||
if char == 8 {
|
||||
textCursorX = textCursorX - 8;
|
||||
if !graphicsText & textCursorX < 0 {
|
||||
if !outputChannel & textCursorX < 0 {
|
||||
textCursorX = 320-8;
|
||||
printSingleChar(11);
|
||||
}
|
||||
@@ -346,7 +414,7 @@ fn printSingleChar(char: i32) {
|
||||
}
|
||||
|
||||
if char == 9 {
|
||||
if !graphicsText & textCursorX >= 320 {
|
||||
if !outputChannel & textCursorX >= 320 {
|
||||
printChar(0xd0a);
|
||||
}
|
||||
textCursorX = textCursorX + 8;
|
||||
@@ -355,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 {
|
||||
@@ -369,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 {
|
||||
@@ -409,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;
|
||||
}
|
||||
|
||||
@@ -435,7 +503,7 @@ data(0x12d00) {
|
||||
}
|
||||
|
||||
fn drawChar(char: i32) {
|
||||
if !graphicsText & textCursorX >= 320 {
|
||||
if !outputChannel & textCursorX >= 320 {
|
||||
printChar(0xd0a);
|
||||
}
|
||||
|
||||
@@ -443,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);
|
||||
@@ -495,11 +563,37 @@ 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;
|
||||
}
|
||||
|
||||
///////////
|
||||
// SOUND //
|
||||
///////////
|
||||
|
||||
include "ges.cwa"
|
||||
|
||||
export fn playNote(channel: i32, note: i32) {
|
||||
(channel * 6)?80 = (channel * 6)?80 & 0xfe ^ if note {
|
||||
(channel * 6)?83 = note & 127;
|
||||
2 | !(note >> 7)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
}
|
||||
|
||||
data 80 {
|
||||
i8(
|
||||
0x80, 0xc0, 0, 81, 0xa0, 0x50,
|
||||
0xc4, 0, 0, 69, 0x60, 0x40,
|
||||
0x44, 0xb0, 0, 69, 0x90, 0x43,
|
||||
0x4, 0xf0, 0, 69, 0xa4, 0x44,
|
||||
0xff, 0xff,
|
||||
1, 1, 0, 100, 0, 100
|
||||
)
|
||||
}
|
||||
|
||||
///////////
|
||||
// SETUP //
|
||||
///////////
|
||||
@@ -540,10 +634,19 @@ start fn setup() {
|
||||
branch_if (i := i - 1) >= 0: expand_sweetie;
|
||||
}
|
||||
|
||||
memory.fill(0, 0, 64);
|
||||
memory.fill(112, 0, 8);
|
||||
memory.fill(0x14000, 0, 0x2c000);
|
||||
|
||||
|
||||
cls(0);
|
||||
randomSeed(random());
|
||||
}
|
||||
|
||||
data 0x12c78 {
|
||||
i32(80)
|
||||
}
|
||||
|
||||
data 0x13000+192*4 {
|
||||
i32(
|
||||
0x2c1c1a,
|
||||
|
||||
+20
-37
@@ -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,41 +29,24 @@ 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`)
|
||||
|
||||
### v0.1.1
|
||||
### Older versions
|
||||
|
||||
* [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)
|
||||
* [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)
|
||||
[Find older versions here.](versions)
|
||||
+165
-22
@@ -18,9 +18,12 @@ 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 data (synced to sound thread)
|
||||
00070-00078: reserved
|
||||
00078-12c78: frame buffer
|
||||
12c78-13000: reserved
|
||||
12c78-12c7c: sound registers/work area base address (for sndGes function)
|
||||
12c7c-13000: reserved
|
||||
13000-13400: palette
|
||||
13400-13c00: font
|
||||
13c00-14000: reserved
|
||||
@@ -222,29 +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-7 | - | Reserved |
|
||||
| 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.
|
||||
@@ -269,6 +288,130 @@ 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
|
||||
|
||||
### Low level operation
|
||||
|
||||
MicroW8 actually runs two instances of your module. On the first instance, it calls `upd` and displays the framebuffer found in its memory. On the
|
||||
second instance, it calls `snd` instead with an incrementing sample index and expects that function to return sound samples for the left and right
|
||||
channel at 44100 Hz. If your module does not export a `snd` function, it calls the api function `sndGes` instead.
|
||||
|
||||
As the only means of communication, 32 bytes starting at address 0x00050 are copied from main to sound memory after `upd` returns.
|
||||
|
||||
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
|
||||
sound playing on that channel. A note value 128-255 will trigger note-128 and immediately stop it (playing attack+release parts of envelope).
|
||||
|
||||
This function assumes the default setup, with the `sndGes` registers located at 0x00050.
|
||||
|
||||
### fn sndGes(sampleIndex: i32) -> f32
|
||||
|
||||
This implements a sound chip, generating sound based on 32 bytes of sound registers.
|
||||
|
||||
The spec of this sound chip are:
|
||||
|
||||
- 4 channels with individual volume control (0-15)
|
||||
- rect, saw, tri, noise wave forms selectable per channel
|
||||
- each wave form supports some kind of pulse width modulation
|
||||
- each channel has an optional automatic low pass filter, or can be sent to one of two manually controllable filters
|
||||
- each channel can select between a narrow and a wide stereo positioning. The two stereo positions of each channel are fixed.
|
||||
- optional ring modulation
|
||||
|
||||
This function requires 1024 bytes of working memory, the first 32 bytes of which are interpreted as the sound registers.
|
||||
The base address of its working memory can be configured by writing the address to 0x12c78. It defaults to 0x00050.
|
||||
|
||||
Here is a short description of the 32 sound registers.
|
||||
|
||||
```
|
||||
00 - CTRL0
|
||||
06 - CTRL1
|
||||
0c - CTRL2
|
||||
12 - CTRL3
|
||||
| 7 6 | 5 | 4 | 3 2 | 1 | 0 |
|
||||
| wave | ring | wide | filter | trigger | note on |
|
||||
|
||||
note on: stay in decay/sustain part of envelope
|
||||
trigger: the attack part of the envlope is triggered when either this changes
|
||||
or note on is changed from 0 to 1.
|
||||
filter : 0 - no filter
|
||||
1 - fixed 6db 1-pole filter with cutoff two octaves above note
|
||||
2 - programmable filter 0
|
||||
3 - programmable filter 1
|
||||
wide : use wide stereo panning
|
||||
ring : ring modulate with triangle wave at frequency of previous channel
|
||||
wave : 0 - rectangle
|
||||
1 - saw
|
||||
2 - triangle
|
||||
3 - noise
|
||||
|
||||
01 - PULS0
|
||||
07 - PULS1
|
||||
0d - PULS2
|
||||
13 - PULS3
|
||||
Pulse width 0-255, with 0 being the plain version of each wave form.
|
||||
rectangle - 50%-100% pulse width
|
||||
saw - inverts 0%-100% of the saw wave form around the center
|
||||
triangle - morphs into an octave up triangle wave
|
||||
noise - blends into a decimated saw wave (just try it out)
|
||||
|
||||
02 - FINE0
|
||||
08 - FINE1
|
||||
0e - FINE2
|
||||
14 - FINE3
|
||||
Fractional note
|
||||
|
||||
03 - NOTE0
|
||||
09 - NOTE1
|
||||
0f - NOTE2
|
||||
15 - NOTE3
|
||||
Note, 69 = A4
|
||||
|
||||
04 - ENVA0
|
||||
0a - ENVA1
|
||||
10 - ENVA2
|
||||
16 - ENVA3
|
||||
| 7 6 5 4 | 3 2 1 0 |
|
||||
| decay | attack |
|
||||
|
||||
05 - ENVB0
|
||||
0b - ENVB1
|
||||
11 - ENVB2
|
||||
17 - ENVB3
|
||||
| 7 6 5 4 | 3 2 1 0 |
|
||||
| release | sustain |
|
||||
|
||||
18 - VO01
|
||||
| 7 6 5 4 | 3 2 1 0 |
|
||||
| volume 1 | volume 0 |
|
||||
|
||||
19 - VO23
|
||||
| 7 6 5 4 | 3 2 1 0 |
|
||||
| volume 3 | volume 2 |
|
||||
|
||||
1a - FCTR0
|
||||
1b - FCTR1
|
||||
| 7 6 5 4 | 3 | 2 | 1 | 0 |
|
||||
| resonance | 0 | band | high | low |
|
||||
|
||||
1c - FFIN0
|
||||
1e - FFIN1
|
||||
cutoff frequency - fractional note
|
||||
|
||||
1d - FNOT0
|
||||
1f - FNOT1
|
||||
cutoff frequency - 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
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
+++
|
||||
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)
|
||||
* [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)
|
||||
* [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)
|
||||
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
@@ -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.2.0">
|
||||
<img class="demonstration-gif" style="width:640px;height:480px;image-rendering:pixelated" src="img/technotunnel.png"></img>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
+1
-4
@@ -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<()>;
|
||||
}
|
||||
}
|
||||
|
||||
+80
-52
@@ -15,6 +15,10 @@ use uw8::Runtime;
|
||||
fn main() -> Result<()> {
|
||||
let mut args = Arguments::from_env();
|
||||
|
||||
// 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"));
|
||||
@@ -31,7 +35,7 @@ fn main() -> Result<()> {
|
||||
println!();
|
||||
println!("Usage:");
|
||||
#[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 run [-t/--timeout <frames>] [--no-gpu] [--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>");
|
||||
@@ -48,7 +52,10 @@ 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"])?;
|
||||
#[allow(unused)]
|
||||
let gpu = !args.contains("--no-gpu");
|
||||
|
||||
let mut config = Config::default();
|
||||
if args.contains(["-p", "--pack"]) {
|
||||
@@ -75,6 +82,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()?;
|
||||
@@ -85,7 +94,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, gpu)?;
|
||||
if disable_audio {
|
||||
microw8.disable_audio();
|
||||
}
|
||||
Box::new(microw8)
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(feature = "browser"))]
|
||||
unimplemented!();
|
||||
@@ -93,27 +108,20 @@ 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() {
|
||||
if first_run || watcher.poll_changed_file()?.is_some() {
|
||||
match start_cart(&filename, &mut *runtime, &config) {
|
||||
Ok(dependencies) => {
|
||||
if watch_mode {
|
||||
for dep in dependencies {
|
||||
watcher.add_file(dep)?;
|
||||
}
|
||||
}
|
||||
let (result, dependencies) = start_cart(&filename, &mut *runtime, &config);
|
||||
if watch_mode {
|
||||
for dep in dependencies {
|
||||
watcher.add_file(dep)?;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Load error: {}", err);
|
||||
if !watch_mode {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if let Err(err) = result {
|
||||
eprintln!("Load error: {}", err);
|
||||
if !watch_mode {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
first_run = false;
|
||||
@@ -136,37 +144,48 @@ struct Config {
|
||||
output_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn load_cart(filename: &Path, config: &Config) -> Result<(Vec<u8>, Vec<PathBuf>)> {
|
||||
fn load_cart(filename: &Path, config: &Config) -> (Result<Vec<u8>>, Vec<PathBuf>) {
|
||||
let mut dependencies = Vec::new();
|
||||
let mut cart = match SourceType::of_file(filename)? {
|
||||
SourceType::Binary => {
|
||||
let mut cart = vec![];
|
||||
File::open(filename)?.read_to_end(&mut cart)?;
|
||||
dependencies.push(filename.to_path_buf());
|
||||
cart
|
||||
}
|
||||
SourceType::Wat => {
|
||||
let cart = wat::parse_file(filename)?;
|
||||
dependencies.push(filename.to_path_buf());
|
||||
cart
|
||||
}
|
||||
SourceType::CurlyWas => {
|
||||
let module = curlywas::compile_file(filename, curlywas::Options::default())?;
|
||||
dependencies = module.dependencies;
|
||||
module.wasm
|
||||
}
|
||||
};
|
||||
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 pack_config) = config.pack {
|
||||
cart = uw8_tool::pack(&cart, pack_config)?;
|
||||
println!(
|
||||
"\npacked size: {:.2} bytes",
|
||||
uw8_tool::compressed_size(&cart)
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref path) = config.output_path {
|
||||
File::create(path)?.write_all(&cart)?;
|
||||
}
|
||||
|
||||
Ok(cart)
|
||||
}
|
||||
|
||||
if let Some(ref path) = config.output_path {
|
||||
File::create(path)?.write_all(&cart)?;
|
||||
let result = inner(filename, config, &mut dependencies);
|
||||
|
||||
if dependencies.is_empty() {
|
||||
dependencies.push(filename.to_path_buf());
|
||||
}
|
||||
|
||||
Ok((cart, dependencies))
|
||||
(result, dependencies)
|
||||
}
|
||||
|
||||
enum SourceType {
|
||||
@@ -205,14 +224,22 @@ impl SourceType {
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native", feature = "browser"))]
|
||||
fn start_cart(filename: &Path, runtime: &mut dyn Runtime, config: &Config) -> Result<Vec<PathBuf>> {
|
||||
let cart = load_cart(filename, config)?;
|
||||
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.0) {
|
||||
if let Err(err) = runtime.load(&cart) {
|
||||
eprintln!("Load error: {}", err);
|
||||
Err(err)
|
||||
(Err(err), dependencies)
|
||||
} else {
|
||||
Ok(cart.1)
|
||||
(Ok(()), dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,13 +258,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(
|
||||
let cart = load_cart(
|
||||
&in_file,
|
||||
&Config {
|
||||
pack: Some(pack_config),
|
||||
output_path: None,
|
||||
},
|
||||
)?;
|
||||
)
|
||||
.0?;
|
||||
|
||||
File::create(out_file)?.write_all(&cart)?;
|
||||
|
||||
@@ -260,8 +288,8 @@ 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)?;
|
||||
File::create(out_file)?.write_all(&module.wasm)?;
|
||||
let module = curlywas::compile_file(in_file, options).0?;
|
||||
File::create(out_file)?.write_all(&module)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+392
-134
@@ -1,41 +1,37 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::{thread, time::Instant};
|
||||
|
||||
use anyhow::Result;
|
||||
use minifb::{Key, Window, WindowOptions};
|
||||
use anyhow::{anyhow, Result};
|
||||
use cpal::traits::*;
|
||||
use rubato::Resampler;
|
||||
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 {
|
||||
tx: mpsc::SyncSender<Option<UW8Instance>>,
|
||||
rx: mpsc::Receiver<UIEvent>,
|
||||
stream: Option<cpal::Stream>,
|
||||
engine: Engine,
|
||||
loader_module: Module,
|
||||
window: Window,
|
||||
window_buffer: Vec<u32>,
|
||||
instance: Option<UW8Instance>,
|
||||
timeout: u32,
|
||||
disable_audio: bool,
|
||||
module_data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
enum UIEvent {
|
||||
Error(Result<()>),
|
||||
Reset,
|
||||
}
|
||||
|
||||
struct UW8Instance {
|
||||
store: Store<()>,
|
||||
memory: Memory,
|
||||
end_frame: TypedFunc<(), ()>,
|
||||
update: TypedFunc<(), ()>,
|
||||
update: Option<TypedFunc<(), ()>>,
|
||||
start_time: Instant,
|
||||
module: Vec<u8>,
|
||||
watchdog: Arc<Mutex<UW8WatchDog>>,
|
||||
sound_tx: Option<mpsc::SyncSender<RegisterUpdate>>,
|
||||
}
|
||||
|
||||
impl Drop for UW8Instance {
|
||||
@@ -47,58 +43,74 @@ 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>, gpu: bool) -> Result<MicroW8> {
|
||||
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"))?;
|
||||
|
||||
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)));
|
||||
let (to_ui_tx, to_ui_rx) = mpsc::sync_channel(2);
|
||||
let (from_ui_tx, from_ui_rx) = mpsc::sync_channel(1);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut state = State {
|
||||
instance: None,
|
||||
timeout: timeout.unwrap_or(0),
|
||||
};
|
||||
|
||||
uw8_window::run(gpu, move |framebuffer, gamepad, reset| {
|
||||
while let Ok(instance) = to_ui_rx.try_recv() {
|
||||
state.instance = instance;
|
||||
}
|
||||
|
||||
if reset {
|
||||
from_ui_tx.send(UIEvent::Reset).unwrap();
|
||||
}
|
||||
|
||||
state.run_frame(framebuffer, gamepad).unwrap_or_else(|err| {
|
||||
from_ui_tx.send(UIEvent::Error(Err(err))).unwrap();
|
||||
Instant::now()
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
Ok(MicroW8 {
|
||||
tx: to_ui_tx,
|
||||
rx: from_ui_rx,
|
||||
stream: None,
|
||||
engine,
|
||||
loader_module,
|
||||
window,
|
||||
window_buffer: vec![0u32; 320 * 240],
|
||||
instance: None,
|
||||
timeout: 30,
|
||||
disable_audio: false,
|
||||
module_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.instance = None;
|
||||
for v in &mut self.window_buffer {
|
||||
*v = 0;
|
||||
}
|
||||
pub fn disable_audio(&mut self) {
|
||||
self.disable_audio = true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
true
|
||||
}
|
||||
|
||||
fn load(&mut self, module_data: &[u8]) -> Result<()> {
|
||||
self.reset();
|
||||
self.stream = None;
|
||||
self.tx.send(None)?;
|
||||
|
||||
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)))?;
|
||||
|
||||
@@ -119,46 +131,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])?;
|
||||
|
||||
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(),
|
||||
)?,
|
||||
)?;
|
||||
}
|
||||
add_native_functions(&mut linker, &mut store)?;
|
||||
|
||||
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 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,
|
||||
}));
|
||||
|
||||
@@ -166,16 +144,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;
|
||||
}
|
||||
@@ -183,78 +156,363 @@ 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")?;
|
||||
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd").ok();
|
||||
|
||||
self.instance = Some(UW8Instance {
|
||||
let (sound_tx, stream) = if self.disable_audio {
|
||||
(None, None)
|
||||
} else {
|
||||
match init_sound(&self.engine, &platform_module, &module) {
|
||||
Ok(sound) => {
|
||||
sound.stream.play()?;
|
||||
(Some(sound.tx), Some(sound.stream))
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to init sound: {}", err);
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.tx.send(Some(UW8Instance {
|
||||
store,
|
||||
memory,
|
||||
end_frame,
|
||||
update,
|
||||
start_time: Instant::now(),
|
||||
module: module_data.into(),
|
||||
watchdog,
|
||||
});
|
||||
|
||||
sound_tx,
|
||||
}))?;
|
||||
self.stream = stream;
|
||||
self.module_data = Some(module_data.into());
|
||||
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;
|
||||
if let Ok(event) = self.rx.try_recv() {
|
||||
match event {
|
||||
UIEvent::Error(err) => err,
|
||||
UIEvent::Reset => {
|
||||
if let Some(module_data) = self.module_data.take() {
|
||||
self.load(&module_data)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
instance: Option<UW8Instance>,
|
||||
timeout: u32,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn run_frame(
|
||||
&mut self,
|
||||
framebuffer: &mut dyn uw8_window::Framebuffer,
|
||||
gamepad: u32,
|
||||
) -> Result<Instant> {
|
||||
let now = Instant::now();
|
||||
let mut result = Ok(now);
|
||||
if let Some(mut instance) = self.instance.take() {
|
||||
let time = (now - instance.start_time).as_millis() as i32;
|
||||
{
|
||||
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6;
|
||||
result = Ok(now + Duration::from_millis((16 - offset) as u64));
|
||||
}
|
||||
|
||||
{
|
||||
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.store.set_epoch_deadline(self.timeout as u64);
|
||||
if let Some(ref update) = instance.update {
|
||||
if let Err(err) = update.call(&mut instance.store, ()) {
|
||||
result = Err(err);
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
let mut sound_regs = [0u8; 32];
|
||||
sound_regs.copy_from_slice(&memory[80..112]);
|
||||
if let Some(ref sound_tx) = instance.sound_tx {
|
||||
let _ = sound_tx.send(RegisterUpdate {
|
||||
time,
|
||||
data: sound_regs,
|
||||
});
|
||||
}
|
||||
|
||||
if self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No) {
|
||||
self.load(&instance.module)?;
|
||||
} else if result.is_ok() {
|
||||
let framebuffer_mem = &memory[120..(120 + 320 * 240)];
|
||||
let palette_mem = &memory[0x13000..];
|
||||
framebuffer.update(framebuffer_mem, palette_mem);
|
||||
|
||||
if result.is_ok() {
|
||||
self.instance = Some(instance);
|
||||
}
|
||||
}
|
||||
|
||||
self.window
|
||||
.update_with_buffer(&self.window_buffer, 320, 240)?;
|
||||
|
||||
result?;
|
||||
Ok(())
|
||||
Ok(result?)
|
||||
}
|
||||
}
|
||||
|
||||
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(¤t_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 })
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
include "../examples/include/microw8-api.cwa"
|
||||
|
||||
export fn upd() {
|
||||
printChar('\06f: ');
|
||||
printInt(32!32 * 6 / 100);
|
||||
printChar('\n\4');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Generated
+1
-1
@@ -189,7 +189,7 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
[[package]]
|
||||
name = "upkr"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/exoticorn/upkr.git?rev=2e7983fc#2e7983fc650788d98da2eecef2d16f63e849e4a0"
|
||||
source = "git+https://github.com/exoticorn/upkr.git?rev=d93aec186c9fb91d962c488682a2db125c61306c#d93aec186c9fb91d962c488682a2db125c61306c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cdivsufsort",
|
||||
|
||||
+1
-1
@@ -11,5 +11,5 @@ wasm-encoder = "0.8"
|
||||
walrus = "0.19"
|
||||
anyhow = "1"
|
||||
pico-args = "0.4"
|
||||
upkr = { git = "https://github.com/exoticorn/upkr.git", rev = "2e7983fc" }
|
||||
upkr = { git = "https://github.com/exoticorn/upkr.git", rev = "d93aec186c9fb91d962c488682a2db125c61306c" }
|
||||
pbr = "1"
|
||||
@@ -166,6 +166,8 @@ impl BaseModule {
|
||||
|
||||
add_function(&mut functions, &type_map, "exp", &[F32], Some(F32));
|
||||
|
||||
add_function(&mut functions, &type_map, "playNote", &[I32, I32], None);
|
||||
|
||||
for i in functions.len()..64 {
|
||||
add_function(
|
||||
&mut functions,
|
||||
@@ -284,14 +286,17 @@ impl BaseModule {
|
||||
|
||||
pub fn create_binary(path: &Path) -> Result<()> {
|
||||
let base1 = BaseModule::for_format_version(1)?.to_wasm();
|
||||
let data = upkr::pack(&base1, 4, None);
|
||||
let data = upkr::pack(&base1, 4, false, None);
|
||||
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,
|
||||
"// 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 {
|
||||
@@ -402,5 +407,5 @@ const CONSTANTS: &[(&str, u32)] = &[
|
||||
("BUTTON_A", 4),
|
||||
("BUTTON_B", 5),
|
||||
("BUTTON_X", 6),
|
||||
("BUTTON_Y", 7)
|
||||
("BUTTON_Y", 7),
|
||||
];
|
||||
|
||||
+10
-2
@@ -1,7 +1,15 @@
|
||||
mod base_module;
|
||||
mod pack;
|
||||
mod filter_exports;
|
||||
mod pack;
|
||||
|
||||
pub use base_module::BaseModule;
|
||||
pub use pack::{pack, pack_file, unpack, unpack_file, PackConfig};
|
||||
pub use filter_exports::filter_exports;
|
||||
pub use pack::{pack, pack_file, unpack, unpack_file, PackConfig};
|
||||
|
||||
pub fn compressed_size(cart: &[u8]) -> f32 {
|
||||
if cart[0] != 2 {
|
||||
cart.len() as f32
|
||||
} else {
|
||||
upkr::compressed_size(&cart[1..]) + 1.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ pub fn pack(data: &[u8], config: &PackConfig) -> Result<Vec<u8>> {
|
||||
uw8.extend_from_slice(&upkr::pack(
|
||||
&result[8..],
|
||||
level,
|
||||
false,
|
||||
Some(&mut |pos| {
|
||||
pb.set(pos as u64);
|
||||
}),
|
||||
@@ -89,7 +90,7 @@ pub fn unpack(data: Vec<u8>) -> Result<Vec<u8>> {
|
||||
let (version, data) = match data[0] {
|
||||
0 => return Ok(data),
|
||||
1 => (1, data[1..].to_vec()),
|
||||
2 => (1, upkr::unpack(&data[1..])),
|
||||
2 => (1, upkr::unpack(&data[1..], false)),
|
||||
other => bail!("Uknown format version {}", other),
|
||||
};
|
||||
|
||||
@@ -962,6 +963,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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+1697
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "uw8-window"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
winit = "0.26.1"
|
||||
env_logger = "0.9"
|
||||
log = "0.4"
|
||||
wgpu = "0.13.1"
|
||||
pollster = "0.2"
|
||||
bytemuck = { version = "1.4", features = [ "derive" ] }
|
||||
anyhow = "1"
|
||||
minifb = { version = "0.23.0", default-features = false, features = ["x11"] }
|
||||
winapi = "0.3.9"
|
||||
@@ -0,0 +1,77 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::Framebuffer;
|
||||
use minifb::{Key, Window, WindowOptions};
|
||||
|
||||
static GAMEPAD_KEYS: &[Key] = &[
|
||||
Key::Up,
|
||||
Key::Down,
|
||||
Key::Left,
|
||||
Key::Right,
|
||||
Key::Z,
|
||||
Key::X,
|
||||
Key::A,
|
||||
Key::S,
|
||||
];
|
||||
|
||||
pub fn run(mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>) -> ! {
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
winapi::um::timeapi::timeBeginPeriod(1);
|
||||
}
|
||||
|
||||
let mut buffer: Vec<u32> = vec![0; 320 * 240];
|
||||
|
||||
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).unwrap();
|
||||
|
||||
let mut next_frame = Instant::now();
|
||||
while window.is_open() && !window.is_key_down(Key::Escape) {
|
||||
if let Some(sleep) = next_frame.checked_duration_since(Instant::now()) {
|
||||
std::thread::sleep(sleep);
|
||||
}
|
||||
|
||||
let mut gamepad = 0;
|
||||
for key in window.get_keys() {
|
||||
if let Some(index) = GAMEPAD_KEYS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, &k)| k == key)
|
||||
.map(|(i, _)| i)
|
||||
{
|
||||
gamepad |= 1 << index;
|
||||
}
|
||||
}
|
||||
|
||||
next_frame = update(
|
||||
&mut CpuFramebuffer {
|
||||
buffer: &mut buffer,
|
||||
},
|
||||
gamepad,
|
||||
window.is_key_pressed(Key::R, minifb::KeyRepeat::No),
|
||||
);
|
||||
window.update_with_buffer(&buffer, 320, 240).unwrap();
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
struct CpuFramebuffer<'a> {
|
||||
buffer: &'a mut Vec<u32>,
|
||||
}
|
||||
|
||||
impl<'a> Framebuffer for CpuFramebuffer<'a> {
|
||||
fn update(&mut self, framebuffer: &[u8], palette: &[u8]) {
|
||||
for (i, &color_index) in framebuffer.iter().enumerate() {
|
||||
let offset = color_index as usize * 4;
|
||||
self.buffer[i] = 0xff000000
|
||||
| ((palette[offset] as u32) << 16)
|
||||
| ((palette[offset + 1] as u32) << 8)
|
||||
| palette[offset + 2] as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) tex_coords: vec2<f32>,
|
||||
}
|
||||
|
||||
struct Uniforms {
|
||||
texture_scale: vec4<f32>,
|
||||
}
|
||||
|
||||
@group(0) @binding(1) var<uniform> uniforms: Uniforms;
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@builtin(vertex_index) in_vertex_index: u32,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let x = (1.0 - f32(in_vertex_index)) * 3.0;
|
||||
let y = f32(in_vertex_index & 1u) * 3.0 - 1.0;
|
||||
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||
out.tex_coords = vec2<f32>(x, y) * uniforms.texture_scale.xy + vec2<f32>(160.0, 120.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
|
||||
|
||||
fn sample_pixel(coords: vec2<i32>, offset: vec4<f32>) -> vec3<f32> {
|
||||
let is_outside = any(vec2<u32>(coords) >= vec2<u32>(320u, 240u));
|
||||
if(is_outside) {
|
||||
return vec3<f32>(0.0);
|
||||
} else {
|
||||
let f = max(vec4<f32>(0.01) / offset - vec4<f32>(0.003), vec4<f32>(0.0));
|
||||
return textureLoad(screen_texture, coords, 0).rgb * (f.x + f.y + f.z + f.w);
|
||||
}
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let pixel = floor(in.tex_coords);
|
||||
let o = vec2<f32>(0.5) - (in.tex_coords - pixel);
|
||||
let pixel = vec2<i32>(pixel);
|
||||
|
||||
if(pixel.x < -1 || pixel.y < -1 || pixel.x > 320 || pixel.y > 240) {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
let offset_x = o.xxxx + vec4<f32>(-0.125, 0.375, 0.125, -0.375) * uniforms.texture_scale.z;
|
||||
let offset_y = o.yyyy + vec4<f32>(-0.375, -0.125, 0.375, 0.125) * uniforms.texture_scale.z;
|
||||
|
||||
let offset_x0 = max(abs(offset_x + vec4<f32>(-1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
|
||||
let offset_x1 = max(abs(offset_x) - vec4<f32>(0.5), vec4<f32>(0.0));
|
||||
let offset_x2 = max(abs(offset_x + vec4<f32>(1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
|
||||
|
||||
let offset_x0 = offset_x0 * offset_x0;
|
||||
let offset_x1 = offset_x1 * offset_x1;
|
||||
let offset_x2 = offset_x2 * offset_x2;
|
||||
|
||||
let offset_yr = offset_y + vec4<f32>(-1.0);
|
||||
let offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
|
||||
|
||||
var acc = sample_pixel(pixel + vec2<i32>(-1, -1), offset_x0 + offset_yr);
|
||||
acc = acc + sample_pixel(pixel + vec2<i32>(0, -1), offset_x1 + offset_yr);
|
||||
acc = acc + sample_pixel(pixel + vec2<i32>(1, -1), offset_x2 + offset_yr);
|
||||
|
||||
let offset_yr = vec4<f32>(0.02) + offset_y * offset_y;
|
||||
|
||||
acc = acc + sample_pixel(pixel + vec2<i32>(-1, 0), offset_x0 + offset_yr);
|
||||
acc = acc + sample_pixel(pixel, offset_x1 + offset_yr);
|
||||
acc = acc + sample_pixel(pixel + vec2<i32>(1, 0), offset_x2 + offset_yr);
|
||||
|
||||
let offset_yr = offset_y + vec4<f32>(1.0);
|
||||
let offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
|
||||
|
||||
acc = acc + sample_pixel(pixel + vec2<i32>(-1, 1), offset_x0 + offset_yr);
|
||||
acc = acc + sample_pixel(pixel + vec2<i32>(0, 1), offset_x1 + offset_yr);
|
||||
acc = acc + sample_pixel(pixel + vec2<i32>(1, 1), offset_x2 + offset_yr);
|
||||
|
||||
return vec4<f32>(acc, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
use crate::Framebuffer;
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{num::NonZeroU32, time::Instant};
|
||||
|
||||
use wgpu::util::DeviceExt;
|
||||
use winit::{
|
||||
dpi::PhysicalSize,
|
||||
event::{Event, VirtualKeyCode, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
window::{Fullscreen, WindowBuilder},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use winit::platform::unix::EventLoopExtUnix;
|
||||
|
||||
pub struct Window {
|
||||
event_loop: EventLoop<()>,
|
||||
window: winit::window::Window,
|
||||
instance: wgpu::Instance,
|
||||
surface: wgpu::Surface,
|
||||
adapter: wgpu::Adapter,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new() -> Result<Window> {
|
||||
async fn create() -> Result<Window> {
|
||||
let event_loop = EventLoop::new_any_thread();
|
||||
let window = WindowBuilder::new()
|
||||
.with_inner_size(PhysicalSize::new(640u32, 480))
|
||||
.with_min_inner_size(PhysicalSize::new(320u32, 240))
|
||||
.with_title("MicroW8")
|
||||
.build(&event_loop)?;
|
||||
|
||||
window.set_cursor_visible(false);
|
||||
|
||||
let instance = wgpu::Instance::new(wgpu::Backends::all());
|
||||
let surface = unsafe { instance.create_surface(&window) };
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::LowPower,
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("Request adapter failed"))?;
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor::default(), None)
|
||||
.await?;
|
||||
|
||||
Ok(Window {
|
||||
event_loop,
|
||||
window,
|
||||
instance,
|
||||
surface,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
})
|
||||
}
|
||||
|
||||
pollster::block_on(create())
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
self,
|
||||
mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>,
|
||||
) -> ! {
|
||||
let Window {
|
||||
event_loop,
|
||||
window,
|
||||
instance,
|
||||
surface,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
} = self;
|
||||
let framebuffer_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
size: wgpu::Extent3d {
|
||||
width: 320,
|
||||
height: 240,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R8Uint,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
label: None,
|
||||
});
|
||||
|
||||
let palette_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
size: wgpu::Extent3d {
|
||||
width: 256,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D1,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
label: None,
|
||||
});
|
||||
|
||||
let screen_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
size: wgpu::Extent3d {
|
||||
width: 320,
|
||||
height: 240,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
label: None,
|
||||
});
|
||||
|
||||
let framebuffer_texture_view =
|
||||
framebuffer_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let palette_texture_view =
|
||||
palette_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let screen_texture_view =
|
||||
screen_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut uniforms = Uniforms {
|
||||
texture_scale: texture_scale_from_resolution(window.inner_size()),
|
||||
};
|
||||
|
||||
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(&[uniforms]),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let palette_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
sample_type: wgpu::TextureSampleType::Uint,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: wgpu::TextureViewDimension::D1,
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: None,
|
||||
});
|
||||
|
||||
let palette_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout: &palette_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&framebuffer_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&palette_texture_view),
|
||||
},
|
||||
],
|
||||
label: None,
|
||||
});
|
||||
|
||||
let crt_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: None,
|
||||
});
|
||||
|
||||
let crt_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout: &crt_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&screen_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: None,
|
||||
});
|
||||
|
||||
let palette_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("palette.wgsl").into()),
|
||||
});
|
||||
|
||||
let palette_pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[&palette_bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let palette_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&palette_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &palette_shader,
|
||||
entry_point: "vs_main",
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &palette_shader,
|
||||
entry_point: "fs_main",
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
multiview: None,
|
||||
});
|
||||
|
||||
let crt_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("crt.wgsl").into()),
|
||||
});
|
||||
|
||||
let render_pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[&crt_bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let mut surface_config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface.get_supported_formats(&adapter)[0],
|
||||
width: window.inner_size().width,
|
||||
height: window.inner_size().height,
|
||||
present_mode: wgpu::PresentMode::AutoNoVsync,
|
||||
};
|
||||
|
||||
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&render_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &crt_shader,
|
||||
entry_point: "vs_main",
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &crt_shader,
|
||||
entry_point: "fs_main",
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: surface_config.format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
multiview: None,
|
||||
});
|
||||
|
||||
surface.configure(&device, &surface_config);
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
let _ = (&window, &instance, &surface, &adapter, &device);
|
||||
|
||||
match event {
|
||||
Event::WindowEvent { event, .. } => match event {
|
||||
WindowEvent::Resized(new_size) => {
|
||||
surface_config.width = new_size.width;
|
||||
surface_config.height = new_size.height;
|
||||
surface.configure(&device, &surface_config);
|
||||
uniforms.texture_scale = texture_scale_from_resolution(new_size);
|
||||
queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
|
||||
}
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::KeyboardInput { input, .. } => {
|
||||
if input.state == winit::event::ElementState::Pressed {
|
||||
match input.virtual_keycode {
|
||||
Some(VirtualKeyCode::Escape) => *control_flow = ControlFlow::Exit,
|
||||
Some(VirtualKeyCode::F) => {
|
||||
window.set_fullscreen(if window.fullscreen().is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(Fullscreen::Borderless(None))
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Event::MainEventsCleared => {
|
||||
if let ControlFlow::WaitUntil(t) = *control_flow {
|
||||
if Instant::now() < t {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let next_frame = update(
|
||||
&mut GpuFramebuffer {
|
||||
queue: &queue,
|
||||
framebuffer: &framebuffer_texture,
|
||||
palette: &palette_texture,
|
||||
},
|
||||
0,
|
||||
false,
|
||||
);
|
||||
*control_flow = ControlFlow::WaitUntil(next_frame);
|
||||
|
||||
let output = surface.get_current_texture().unwrap();
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut encoder = device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
|
||||
{
|
||||
let mut render_pass =
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &screen_texture_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
});
|
||||
|
||||
render_pass.set_pipeline(&palette_pipeline);
|
||||
render_pass.set_bind_group(0, &palette_bind_group, &[]);
|
||||
render_pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
{
|
||||
let mut render_pass =
|
||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
});
|
||||
|
||||
render_pass.set_pipeline(&render_pipeline);
|
||||
render_pass.set_bind_group(0, &crt_bind_group, &[]);
|
||||
render_pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
queue.submit(std::iter::once(encoder.finish()));
|
||||
output.present();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct GpuFramebuffer<'a> {
|
||||
framebuffer: &'a wgpu::Texture,
|
||||
palette: &'a wgpu::Texture,
|
||||
queue: &'a wgpu::Queue,
|
||||
}
|
||||
|
||||
impl<'a> Framebuffer for GpuFramebuffer<'a> {
|
||||
fn update(&mut self, pixels: &[u8], palette: &[u8]) {
|
||||
self.queue.write_texture(
|
||||
wgpu::ImageCopyTexture {
|
||||
texture: self.framebuffer,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&bytemuck::cast_slice(pixels),
|
||||
wgpu::ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: NonZeroU32::new(320),
|
||||
rows_per_image: None,
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: 320,
|
||||
height: 240,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
self.queue.write_texture(
|
||||
wgpu::ImageCopyTexture {
|
||||
texture: self.palette,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&bytemuck::cast_slice(palette),
|
||||
wgpu::ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: NonZeroU32::new(256 * 4),
|
||||
rows_per_image: None,
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: 256,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
|
||||
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
|
||||
[
|
||||
res.width as f32 / scale,
|
||||
res.height as f32 / scale,
|
||||
2.0 / scale,
|
||||
0.0,
|
||||
]
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Uniforms {
|
||||
texture_scale: [f32; 4],
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use std::time::Instant;
|
||||
|
||||
mod cpu;
|
||||
mod gpu;
|
||||
|
||||
pub fn run<F: 'static + FnMut(&mut dyn Framebuffer, u32, bool) -> Instant>(
|
||||
gpu: bool,
|
||||
update: F,
|
||||
) -> ! {
|
||||
if gpu {
|
||||
match gpu::Window::new() {
|
||||
Ok(window) => window.run(Box::new(update)),
|
||||
Err(err) => eprintln!(
|
||||
"Failed to create gpu window: {}\nFalling back to cpu window",
|
||||
err
|
||||
),
|
||||
}
|
||||
}
|
||||
cpu::run(Box::new(update));
|
||||
}
|
||||
|
||||
pub trait Framebuffer {
|
||||
fn update(&mut self, pixels: &[u8], palette: &[u8]);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn main() {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
let mut framebuffer = vec![0u8; 320 * 240];
|
||||
let start_time = Instant::now();
|
||||
|
||||
let mut palette = vec![0u32; 256];
|
||||
for i in 0..256 {
|
||||
let v = i & 15;
|
||||
let r = ((i >> 2) & 12) * v;
|
||||
let g = ((i >> 3) & 12) * v;
|
||||
let b = ((i >> 4) & 12) * v;
|
||||
palette[i as usize] = r + (g << 8) + (b << 16);
|
||||
}
|
||||
|
||||
let mut prev_frame = Instant::now();
|
||||
|
||||
uw8_window::run(move |gpu_framebuffer, _gamepads, _reset| {
|
||||
draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32());
|
||||
gpu_framebuffer.update(&framebuffer, bytemuck::cast_slice(&palette));
|
||||
prev_frame += Duration::from_secs_f32(1.0 / 60.0);
|
||||
prev_frame
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_frame(framebuffer: &mut [u8], time: f32) {
|
||||
for x in 0..320 {
|
||||
let xr = x as f32 - 160.0;
|
||||
for y in 0..240 {
|
||||
let yr = y as f32 - 120.0;
|
||||
let f = 8192.0 / (xr * xr + yr * yr);
|
||||
let u = xr * f + 512.0 + time * 32.0;
|
||||
let v = yr * f + time * 29.0;
|
||||
let c = (u.floor() as i32 ^ v.floor() as i32) as u32;
|
||||
framebuffer[x + y * 320] = c as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) tex_coords: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let x = (1.0 - f32(vertex_index)) * 3.0;
|
||||
let y = f32(vertex_index & 1u) * 3.0 - 1.0;
|
||||
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||
out.tex_coords = vec2<f32>((x + 1.0) * 160.0, (y + 1.0) * 120.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var framebuffer_texture: texture_2d<u32>;
|
||||
@group(0) @binding(1) var palette_texture: texture_1d<f32>;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let texel = vec2<i32>(floor(in.tex_coords));
|
||||
let index = textureLoad(framebuffer_texture, texel, 0).r;
|
||||
return textureLoad(palette_texture, i32(index), 0);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
rm -rf .parcel-cache && yarn parcel src/index.html
|
||||
@@ -0,0 +1,94 @@
|
||||
let U8 = (...a) => new Uint8Array(...a);
|
||||
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(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]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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] = () => { };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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] = this.currentTime;
|
||||
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;
|
||||
this.currentTime += numSamples / 44.1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('apu', APU);
|
||||
+5
-4
@@ -10,13 +10,14 @@
|
||||
</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 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">
|
||||
|
||||
+4
-1
@@ -12,6 +12,7 @@ let uw8 = MicroW8(document.getElementById('screen'), {
|
||||
setMessage,
|
||||
keyboardElement: window,
|
||||
timerElement: document.getElementById("timer"),
|
||||
startButton: document.getElementById("start")
|
||||
});
|
||||
|
||||
function runModuleFromHash() {
|
||||
@@ -79,7 +80,9 @@ if(location.hash.length != 0) {
|
||||
url += 'cart.uw8';
|
||||
}
|
||||
try {
|
||||
await uw8.runModuleFromURL(url, true);
|
||||
if(!await uw8.runModuleFromURL(url, true)) {
|
||||
setupLoad();
|
||||
}
|
||||
} catch(e) {
|
||||
setupLoad();
|
||||
}
|
||||
|
||||
+132
-22
@@ -1,5 +1,15 @@
|
||||
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) {
|
||||
@@ -18,9 +28,6 @@ export default function MicroW8(screen, config = {}) {
|
||||
|
||||
let currentData;
|
||||
|
||||
let U8 = (d) => new Uint8Array(d);
|
||||
let U32 = (d) => new Uint32Array(d);
|
||||
|
||||
let pad = 0;
|
||||
let keyboardElement = config.keyboardElement == undefined ? screen : config.keyboardElement;
|
||||
if(keyboardElement) {
|
||||
@@ -83,6 +90,9 @@ export default function MicroW8(screen, config = {}) {
|
||||
keyboardElement.onkeydown = keyHandler;
|
||||
keyboardElement.onkeyup = keyHandler;
|
||||
}
|
||||
|
||||
let audioContext;
|
||||
let audioNode;
|
||||
|
||||
async function runModule(data, keepUrl) {
|
||||
if (cancelFunction) {
|
||||
@@ -90,6 +100,15 @@ export default function MicroW8(screen, config = {}) {
|
||||
cancelFunction = null;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -97,6 +116,39 @@ export default function MicroW8(screen, config = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
await audioContext.audioWorklet.addModule(audioWorkletUrl);
|
||||
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;
|
||||
}
|
||||
};
|
||||
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;
|
||||
@@ -119,7 +171,7 @@ export default function MicroW8(screen, config = {}) {
|
||||
if(!devkitMode) {
|
||||
memSize.maximum = 4;
|
||||
}
|
||||
let memory = new WebAssembly.Memory({ initial: 4, maximum: devkitMode ? 16 : 4 });
|
||||
let memory = new WebAssembly.Memory(memSize);
|
||||
let memU8 = U8(memory.buffer);
|
||||
|
||||
let importObject = {
|
||||
@@ -142,9 +194,9 @@ export default function MicroW8(screen, config = {}) {
|
||||
|
||||
let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance;
|
||||
|
||||
let loadModuleURL = async (url) => instantiate(loadModuleData(await (await fetch(url)).arrayBuffer()));
|
||||
let loadModuleURL = async (url) => loadModuleData(await (await fetch(url)).arrayBuffer());
|
||||
|
||||
loader = await loadModuleURL(loaderUrl);
|
||||
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];
|
||||
@@ -154,13 +206,28 @@ 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;
|
||||
}
|
||||
|
||||
data = loadModuleData(data);
|
||||
|
||||
let platform_instance = await loadModuleURL(platformUrl);
|
||||
let platform_data = await loadModuleURL(platformUrl);
|
||||
|
||||
audioNode.port.onmessage = (e) => updateAudioReady(e.data);
|
||||
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]
|
||||
@@ -169,24 +236,42 @@ export default function MicroW8(screen, config = {}) {
|
||||
let instance = await instantiate(data);
|
||||
|
||||
let buffer = U32(imageData.data.buffer);
|
||||
|
||||
await audioReadyPromise;
|
||||
|
||||
let startTime = Date.now();
|
||||
|
||||
let keepRunning = true;
|
||||
cancelFunction = () => keepRunning = false;
|
||||
|
||||
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) {
|
||||
let thisFrame;
|
||||
if (!isPaused) {
|
||||
let gamepads = navigator.getGamepads();
|
||||
let gamepad = 0;
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
@@ -214,23 +299,36 @@ export default function MicroW8(screen, config = {}) {
|
||||
}
|
||||
|
||||
let u32Mem = U32(memory.buffer);
|
||||
u32Mem[16] = now - startTime;
|
||||
let time = Date.now() - startTime;
|
||||
u32Mem[16] = time;
|
||||
u32Mem[17] = pad | gamepad;
|
||||
instance.exports.upd();
|
||||
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({t: time, r: soundRegisters}, [soundRegisters]);
|
||||
|
||||
let palette = U32(memory.buffer.slice(0x13000, 0x13000 + 1024));
|
||||
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);
|
||||
|
||||
let timeOffset = ((time * 6) % 100 - 50) / 6;
|
||||
thisFrame = startTime + time - timeOffset / 8;
|
||||
} else {
|
||||
thisFrame = Date.now();
|
||||
}
|
||||
let now = Date.now();
|
||||
let nextFrame = Math.max(thisFrame + timePerFrame, now);
|
||||
|
||||
if (restart) {
|
||||
runModule(currentData);
|
||||
} else {
|
||||
window.requestAnimationFrame(mainloop);
|
||||
window.setTimeout(mainloop, nextFrame - now)
|
||||
}
|
||||
} catch (err) {
|
||||
config.setMessage(cartridgeSize, err.toString());
|
||||
@@ -253,14 +351,25 @@ export default function MicroW8(screen, config = {}) {
|
||||
|
||||
let videoRecorder;
|
||||
let videoStartTime;
|
||||
let videoAudioSourceNode;
|
||||
let videoAudioStreamNode;
|
||||
function recordVideo() {
|
||||
if(videoRecorder) {
|
||||
videoRecorder.stop();
|
||||
videoRecorder = null;
|
||||
videoAudioSourceNode.disconnect(videoAudioStreamNode);
|
||||
videoAudioSourceNode = null;
|
||||
videoAudioStreamNode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = screen.captureStream();
|
||||
videoAudioStreamNode = audioContext.createMediaStreamDestination();
|
||||
videoAudioSourceNode = audioNode;
|
||||
audioNode.connect(videoAudioStreamNode);
|
||||
stream.addTrack(videoAudioStreamNode.stream.getAudioTracks()[0]);
|
||||
|
||||
videoRecorder = new MediaRecorder(screen.captureStream(), {
|
||||
videoRecorder = new MediaRecorder(stream, {
|
||||
mimeType: 'video/webm',
|
||||
videoBitsPerSecond: 25000000
|
||||
});
|
||||
@@ -305,10 +414,11 @@ export default function MicroW8(screen, config = {}) {
|
||||
async function runModuleFromURL(url, keepUrl) {
|
||||
let response = await fetch(url);
|
||||
let type = response.headers.get('Content-Type');
|
||||
if(type && type.includes('html')) {
|
||||
throw false;
|
||||
if((type && type.includes('html')) || response.status != 200) {
|
||||
return false;
|
||||
}
|
||||
runModule(await response.arrayBuffer(), keepUrl || devkitMode);
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+11
-4
@@ -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;
|
||||
|
||||
+778
-3909
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user