mirror of
https://github.com/exoticorn/microw8.git
synced 2026-01-20 19:26:43 +01:00
Compare commits
53 Commits
sound
...
v0.2.1-che
| Author | SHA1 | Date | |
|---|---|---|---|
| a20e569723 | |||
| 8a473cc9b9 | |||
| 4a4beded11 | |||
| 9d629be747 | |||
| daf2a02cd8 | |||
| 8d5374a867 | |||
| 142b6a4c15 | |||
| 877fceb089 | |||
| f0ba0f2b99 | |||
| 760664eb77 | |||
| 465e66ff4b | |||
| e4579d81bc | |||
| 1f5042059c | |||
| 499bb02f2c | |||
| 57a92ba79a | |||
| 7ec1e68a00 | |||
| 539d19e0d7 | |||
| e9a5f702b4 | |||
| ba0b037ec2 | |||
| 0130d1c906 | |||
| 379ece5cbf | |||
| c9c5cb76bd | |||
| a6d6615231 | |||
| fbc86fa78d | |||
| 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 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies
|
- 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'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
1868
Cargo.lock
generated
1868
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -1,22 +1,24 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "uw8"
|
name = "uw8"
|
||||||
version = "0.1.2"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["native", "browser"]
|
default = ["native", "browser"]
|
||||||
native = ["wasmtime"]
|
native = ["wasmtime", "uw8-window", "cpal", "rubato" ]
|
||||||
browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
|
browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wasmtime = { version = "0.35.3", optional = true }
|
wasmtime = { version = "0.37.0", optional = true }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
minifb = { version = "0.22", default-features = false, features = ["x11"] }
|
env_logger = "0.9"
|
||||||
|
log = "0.4"
|
||||||
|
uw8-window = { path = "uw8-window", optional = true }
|
||||||
notify = "4"
|
notify = "4"
|
||||||
pico-args = "0.4"
|
pico-args = "0.4"
|
||||||
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "aac7bbd" }
|
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0e7ea50" }
|
||||||
wat = "1"
|
wat = "1"
|
||||||
uw8-tool = { path = "uw8-tool" }
|
uw8-tool = { path = "uw8-tool" }
|
||||||
same-file = "1"
|
same-file = "1"
|
||||||
@@ -25,4 +27,5 @@ tokio = { version = "1.17.0", features = ["sync", "rt"], optional = true }
|
|||||||
tokio-stream = { version = "0.1.8", features = ["sync"], 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"
|
ansi_term = "0.12.1"
|
||||||
cpal = "0.13.5"
|
cpal = { version = "0.14.1", optional = true }
|
||||||
|
rubato = { version = "0.11.0", optional = true }
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -43,6 +43,27 @@ Options:
|
|||||||
-l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow.
|
-l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow.
|
||||||
-o FILE, --output FILE : Write the loaded and optionally packed cart back to disk.
|
-o FILE, --output FILE : Write the loaded and optionally packed cart back to disk.
|
||||||
|
|
||||||
|
when using the native runtime:
|
||||||
|
|
||||||
|
-m, --no-audio : Disable audio, also reduces cpu load a bit
|
||||||
|
--no-gpu : Force old cpu-only window code
|
||||||
|
--filter FILTER : Select an upscale filter at startup
|
||||||
|
--fullscreen : Start in fullscreen mode
|
||||||
|
|
||||||
|
Note that the cpu-only window does not support fullscreen nor upscale filters.
|
||||||
|
|
||||||
|
Unless --no-gpu is given, uw8 will first try to open a gpu accelerated window, falling back to the old cpu-only window if that fails.
|
||||||
|
Therefore you should rarely need to manually pass --no-gpu. If you prefer the old pixel doubling look to the now default crt filter,
|
||||||
|
you can just pass "--filter nearest" or "--filter 1".
|
||||||
|
|
||||||
|
The upscale filter options are:
|
||||||
|
1, nearest : Anti-aliased nearest filter
|
||||||
|
2, fast_crt : Very simple, cheap crt filter, not very good below a window size of 960x720
|
||||||
|
3, ss_crt : Super sampled crt filter, a little more demanding on the GPU but scales well to smaller window sizes
|
||||||
|
4, chromatic_crt : Variant of fast_crt with a slight offset of the three color dots of a pixel, still pretty cheap
|
||||||
|
5, auto_crt (default) : ss_crt below 960x720, chromatic_crt otherwise
|
||||||
|
|
||||||
|
You can switch the upscale filter at any time using the keys 1-5. You can toggle fullscreen with F.
|
||||||
|
|
||||||
uw8 pack [<options>] <infile> <outfile>
|
uw8 pack [<options>] <infile> <outfile>
|
||||||
|
|
||||||
|
|||||||
55
examples/curlywas/cracklebass.cwa
Normal file
55
examples/curlywas/cracklebass.cwa
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// port of cracklebass by pestis (originally on TIC-80)
|
||||||
|
|
||||||
|
include "../include/microw8-api.cwa"
|
||||||
|
|
||||||
|
const MUSIC_DATA = 0x20000;
|
||||||
|
|
||||||
|
export fn upd() {
|
||||||
|
let inline t = 32!32 * 6 / 100;
|
||||||
|
let inline p = t / 1024;
|
||||||
|
|
||||||
|
let channel:i32;
|
||||||
|
|
||||||
|
loop channels {
|
||||||
|
let inline e = t * channel?MUSIC_DATA / 8;
|
||||||
|
let lazy pattern = (8 * channel + p)?(MUSIC_DATA + 56);
|
||||||
|
let lazy n = !!pattern * (8 * pattern + e / 16 % 8)?MUSIC_DATA;
|
||||||
|
let inline prev_ctrl = (channel * 6)?80;
|
||||||
|
(channel * 6)?80 = if n {
|
||||||
|
let inline base_note = 12 + 12 * channel?(MUSIC_DATA + 4) + n;
|
||||||
|
let inline pitch_drop = e % 16 * channel?(MUSIC_DATA + 94);
|
||||||
|
let inline key_pattern = p?(MUSIC_DATA + 8*4 + 56);
|
||||||
|
let inline key = select(key_pattern, (8 * key_pattern + t / 128 % 8)?MUSIC_DATA, 1);
|
||||||
|
(channel * 6)?83 = base_note - pitch_drop / 4 + key;
|
||||||
|
prev_ctrl & 0xfc | (e / 8 & 2) | 1
|
||||||
|
} else {
|
||||||
|
prev_ctrl & 0xfe
|
||||||
|
};
|
||||||
|
|
||||||
|
branch_if (channel := channel + 1) < 4: channels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data 80 {
|
||||||
|
i8(
|
||||||
|
0x44, 0, 0, 0, 0x50, 0x40,
|
||||||
|
0x4, 0x50, 0, 0, 0x80, 0x80,
|
||||||
|
0x40, 0x80, 0, 0, 0x40, 0x40,
|
||||||
|
0, 0, 0, 0, 0x50, 0x50
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data MUSIC_DATA {
|
||||||
|
i8(
|
||||||
|
16, 2, 8, 8, 1, 2, 2, 3, 1, 0,
|
||||||
|
1,13,16, 0, 1, 8, 1, 0, 1,13,
|
||||||
|
16, 1, 1, 8, 1, 0, 8,13,13, 0,
|
||||||
|
16,13, 1, 0, 1, 0, 1, 0, 1, 1,
|
||||||
|
1, 0, 0, 0, 1, 0,13, 1, 1, 1,
|
||||||
|
6, 8, 1, 1, 6, 8, 1, 1, 2, 1,
|
||||||
|
2, 1, 2, 0, 0, 0, 0, 3, 3, 3,
|
||||||
|
5, 0, 0, 2, 1, 2, 1, 2, 1, 2,
|
||||||
|
0, 4, 4, 0, 4, 4, 4, 4, 0, 0,
|
||||||
|
0, 0, 6, 6, 0, 0, 0, 8
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ global mut f: f32 = 2.0;
|
|||||||
|
|
||||||
export fn upd() {
|
export fn upd() {
|
||||||
let y: i32;
|
let y: i32;
|
||||||
let inline zero = 0.0;
|
let inline zero = 0_f;
|
||||||
|
|
||||||
let lazy control_speed = 0.03125;
|
let lazy control_speed = 0.03125;
|
||||||
s = s + 0.1875 - (f + control_speed) * isButtonPressed(4 <| cls(4)) as f32;
|
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 y == 180 & py > zero {
|
||||||
if x > w | x < zero {
|
if x > w | x < zero {
|
||||||
|
0?80 = 0xc3;
|
||||||
|
3?80 = 32;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
py = zero;
|
py = zero;
|
||||||
@@ -43,6 +45,9 @@ export fn upd() {
|
|||||||
circle(160 as f32, 160 as f32 + py, 22 as f32, -28);
|
circle(160 as f32, 160 as f32 + py, 22 as f32, -28);
|
||||||
circle((160 - 6) as f32, (160 - 6) as f32 + py, 6 as f32, -26);
|
circle((160 - 6) as f32, (160 - 6) as f32 + py, 6 as f32, -26);
|
||||||
|
|
||||||
|
0?86 = py < zero;
|
||||||
|
3?86 = 32 - py as i32;
|
||||||
|
|
||||||
px = px + (isButtonPressed(3) - isButtonPressed(2)) as f32 * control_speed;
|
px = px + (isButtonPressed(3) - isButtonPressed(2)) as f32 * control_speed;
|
||||||
py = py + s;
|
py = py + s;
|
||||||
pz = pz + 1;
|
pz = pz + 1;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ 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.circle_outline" fn circle_outline(f32, f32, f32, i32);
|
||||||
import "env.exp" fn exp(f32) -> f32;
|
import "env.exp" fn exp(f32) -> f32;
|
||||||
import "env.playNote" fn playNote(i32, i32);
|
import "env.playNote" fn playNote(i32, i32);
|
||||||
|
import "env.sndGes" fn sndGes(i32) -> f32;
|
||||||
|
|
||||||
const TIME_MS = 0x40;
|
const TIME_MS = 0x40;
|
||||||
const GAMEPAD = 0x44;
|
const GAMEPAD = 0x44;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
(import "env" "circle_outline" (func $circle_outline (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" "exp" (func $exp (param f32) (result f32)))
|
||||||
(import "env" "playNote" (func $playNote (param i32) (param i32)))
|
(import "env" "playNote" (func $playNote (param i32) (param i32)))
|
||||||
|
(import "env" "sndGes" (func $sndGes (param i32) (result f32)))
|
||||||
|
|
||||||
;; to use defines, include this file with a preprocessor
|
;; to use defines, include this file with a preprocessor
|
||||||
;; like gpp (https://logological.org/gpp).
|
;; like gpp (https://logological.org/gpp).
|
||||||
|
|||||||
74
examples/wat/tunnel_opt.wat
Normal file
74
examples/wat/tunnel_opt.wat
Normal file
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
4
platform/Cargo.lock
generated
4
platform/Cargo.lock
generated
@@ -146,7 +146,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "curlywas"
|
name = "curlywas"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/exoticorn/curlywas.git?rev=aac7bbd#aac7bbd8786a26da0dcbe8320b1afefaf6086464"
|
source = "git+https://github.com/exoticorn/curlywas.git?rev=0e7ea50#0e7ea508cd0e76836283ae68a44c9097df83c8ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ariadne",
|
"ariadne",
|
||||||
@@ -391,7 +391,7 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "upkr"
|
name = "upkr"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cdivsufsort",
|
"cdivsufsort",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="aac7bbd" }
|
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="0e7ea50" }
|
||||||
uw8-tool = { path="../uw8-tool" }
|
uw8-tool = { path="../uw8-tool" }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
lodepng = "3.4"
|
lodepng = "3.4"
|
||||||
Binary file not shown.
Binary file not shown.
@@ -10,7 +10,7 @@ const GesState.Size = GesState.Filter + 8*4;
|
|||||||
const GesStateOffset = 32;
|
const GesStateOffset = 32;
|
||||||
const GesBufferOffset = 32 + GesState.Size;
|
const GesBufferOffset = 32 + GesState.Size;
|
||||||
|
|
||||||
export fn gesSnd(t: i32) -> f32 {
|
export fn sndGes(t: i32) -> f32 {
|
||||||
let baseAddr = 0!0x12c78;
|
let baseAddr = 0!0x12c78;
|
||||||
if !(t & 127) {
|
if !(t & 127) {
|
||||||
let i: i32;
|
let i: i32;
|
||||||
@@ -62,7 +62,6 @@ export fn gesSnd(t: i32) -> f32 {
|
|||||||
let phase = channelState!GesChannelState.Phase;
|
let phase = channelState!GesChannelState.Phase;
|
||||||
|
|
||||||
let inline pulseWidth = channelReg?1;
|
let inline pulseWidth = channelReg?1;
|
||||||
let phaseShift = (pulseWidth - 128) * 255;
|
|
||||||
let invPhaseInc = 1 as f32 / phaseInc as f32;
|
let invPhaseInc = 1 as f32 / phaseInc as f32;
|
||||||
|
|
||||||
i = 0;
|
i = 0;
|
||||||
@@ -131,7 +130,7 @@ export fn gesSnd(t: i32) -> f32 {
|
|||||||
let phaseInc = (freq * (65536.0 / 44100.0)) as i32;
|
let phaseInc = (freq * (65536.0 / 44100.0)) as i32;
|
||||||
|
|
||||||
let phase = channelState!GesChannelState.Phase;
|
let phase = channelState!GesChannelState.Phase;
|
||||||
if modSrc > ch {
|
if modSrc < ch {
|
||||||
phase = phase - (phaseInc << 6);
|
phase = phase - (phaseInc << 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import "env.sin" fn sin(f32) -> f32;
|
|||||||
import "env.cos" fn cos(f32) -> f32;
|
import "env.cos" fn cos(f32) -> f32;
|
||||||
import "env.pow" fn pow(f32, f32) -> f32;
|
import "env.pow" fn pow(f32, f32) -> f32;
|
||||||
import "env.exp" fn exp(f32) -> f32;
|
import "env.exp" fn exp(f32) -> f32;
|
||||||
|
import "env.logChar" fn logChar(i32);
|
||||||
|
|
||||||
export fn time() -> f32 {
|
export fn time() -> f32 {
|
||||||
(0!64) as f32 / 1000 as f32
|
(0!64) as f32 / 1000 as f32
|
||||||
@@ -32,11 +33,9 @@ export fn random() -> i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export fn random64() -> i64 {
|
export fn random64() -> i64 {
|
||||||
let state: i64;
|
let lazy state = randomState ^ (randomState #>> 12i64);
|
||||||
randomState = (state := (
|
let lazy state = state ^ (state << 25i64);
|
||||||
state := randomState ^ (randomState #>> 12i64)
|
randomState = state ^ (state #>> 27i64);
|
||||||
) ^ (state << 25i64)
|
|
||||||
) ^ (state #>> 27i64);
|
|
||||||
randomState * 0x2545f4914f6cdd1di64
|
randomState * 0x2545f4914f6cdd1di64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +61,8 @@ export fn cls(col: i32) {
|
|||||||
let i: i32;
|
let i: i32;
|
||||||
textCursorX = 0;
|
textCursorX = 0;
|
||||||
textCursorY = 0;
|
textCursorY = 0;
|
||||||
graphicsText = 0;
|
outputChannel = 0;
|
||||||
col = (col & 255) * 0x1010101;
|
memory.fill(120, col, 320*240);
|
||||||
loop pixels {
|
|
||||||
i!120 = col;
|
|
||||||
branch_if (i := i + 4) < 320*240: pixels;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn setPixel(x: i32, y: i32, col: i32) {
|
export fn setPixel(x: i32, y: i32, col: i32) {
|
||||||
@@ -91,12 +86,70 @@ fn clamp(v: i32, min: i32, max: i32) -> i32 {
|
|||||||
export fn hline(x1: i32, x2: i32, y: i32, col: i32) {
|
export fn hline(x1: i32, x2: i32, y: i32, col: i32) {
|
||||||
x1 = clamp(x1, 0, 320);
|
x1 = clamp(x1, 0, 320);
|
||||||
x2 = clamp(x2, 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 ptr = y * 320 + x1;
|
||||||
let end = ptr + x2 - 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;
|
ptr?120 = col;
|
||||||
branch_if (ptr := ptr + 1) < end: pixels;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,7 +360,7 @@ global mut textCursorX = 0;
|
|||||||
global mut textCursorY = 0;
|
global mut textCursorY = 0;
|
||||||
global mut textColor = 15;
|
global mut textColor = 15;
|
||||||
global mut bgColor = 0;
|
global mut bgColor = 0;
|
||||||
global mut graphicsText = 0;
|
global mut outputChannel = 0;
|
||||||
|
|
||||||
export fn printChar(char: i32) {
|
export fn printChar(char: i32) {
|
||||||
loop chars {
|
loop chars {
|
||||||
@@ -319,6 +372,11 @@ export fn printChar(char: i32) {
|
|||||||
global mut controlCodeLength = 0;
|
global mut controlCodeLength = 0;
|
||||||
|
|
||||||
fn printSingleChar(char: i32) {
|
fn printSingleChar(char: i32) {
|
||||||
|
if outputChannel >= 2 & (char < 4 | char > 6) {
|
||||||
|
logChar(char);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
controlCodeLength?0x12d20 = char;
|
controlCodeLength?0x12d20 = char;
|
||||||
controlCodeLength = controlCodeLength + 1;
|
controlCodeLength = controlCodeLength + 1;
|
||||||
char = 0x12d20?0;
|
char = 0x12d20?0;
|
||||||
@@ -332,10 +390,12 @@ fn printSingleChar(char: i32) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if char == 4 | char == 5 {
|
if char >= 4 & char <= 6 {
|
||||||
graphicsText = char == 5;
|
outputChannel = char - 4;
|
||||||
textCursorX = 0;
|
if !outputChannel {
|
||||||
textCursorY = 0;
|
textCursorX = 0;
|
||||||
|
textCursorY = 0;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +406,7 @@ fn printSingleChar(char: i32) {
|
|||||||
|
|
||||||
if char == 8 {
|
if char == 8 {
|
||||||
textCursorX = textCursorX - 8;
|
textCursorX = textCursorX - 8;
|
||||||
if !graphicsText & textCursorX < 0 {
|
if !outputChannel & textCursorX < 0 {
|
||||||
textCursorX = 320-8;
|
textCursorX = 320-8;
|
||||||
printSingleChar(11);
|
printSingleChar(11);
|
||||||
}
|
}
|
||||||
@@ -354,7 +414,7 @@ fn printSingleChar(char: i32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if char == 9 {
|
if char == 9 {
|
||||||
if !graphicsText & textCursorX >= 320 {
|
if !outputChannel & textCursorX >= 320 {
|
||||||
printChar(0xd0a);
|
printChar(0xd0a);
|
||||||
}
|
}
|
||||||
textCursorX = textCursorX + 8;
|
textCursorX = textCursorX + 8;
|
||||||
@@ -363,7 +423,7 @@ fn printSingleChar(char: i32) {
|
|||||||
|
|
||||||
if char == 10 {
|
if char == 10 {
|
||||||
textCursorY = textCursorY + 8;
|
textCursorY = textCursorY + 8;
|
||||||
if !graphicsText & textCursorY >= 240 {
|
if !outputChannel & textCursorY >= 240 {
|
||||||
textCursorY = 240 - 8;
|
textCursorY = 240 - 8;
|
||||||
let i: i32;
|
let i: i32;
|
||||||
loop scroll_copy {
|
loop scroll_copy {
|
||||||
@@ -377,7 +437,7 @@ fn printSingleChar(char: i32) {
|
|||||||
|
|
||||||
if char == 11 {
|
if char == 11 {
|
||||||
textCursorY = textCursorY - 8;
|
textCursorY = textCursorY - 8;
|
||||||
if !graphicsText & textCursorY < 0 {
|
if !outputChannel & textCursorY < 0 {
|
||||||
textCursorY = 0;
|
textCursorY = 0;
|
||||||
let i = 320 * (240 - 8);
|
let i = 320 * (240 - 8);
|
||||||
loop scroll_copy {
|
loop scroll_copy {
|
||||||
@@ -417,8 +477,8 @@ fn printSingleChar(char: i32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if char == 31 {
|
if char == 31 {
|
||||||
textCursorX = 0x12d20?1 * (8 - graphicsText * 6);
|
textCursorX = 0x12d20?1 * (8 - outputChannel * 6);
|
||||||
textCursorY = 0x12d20?2 * (8 - graphicsText * 7);
|
textCursorY = 0x12d20?2 * (8 - outputChannel * 7);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +503,7 @@ data(0x12d00) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn drawChar(char: i32) {
|
fn drawChar(char: i32) {
|
||||||
if !graphicsText & textCursorX >= 320 {
|
if !outputChannel & textCursorX >= 320 {
|
||||||
printChar(0xd0a);
|
printChar(0xd0a);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +511,7 @@ fn drawChar(char: i32) {
|
|||||||
loop rows {
|
loop rows {
|
||||||
let bits = (char * 8 + y)?0x13400;
|
let bits = (char * 8 + y)?0x13400;
|
||||||
let x = 0;
|
let x = 0;
|
||||||
if graphicsText {
|
if outputChannel {
|
||||||
loop pixels {
|
loop pixels {
|
||||||
if (bits := bits << 1) & 256 {
|
if (bits := bits << 1) & 256 {
|
||||||
setPixel(textCursorX + x, textCursorY + y, textColor);
|
setPixel(textCursorX + x, textCursorY + y, textColor);
|
||||||
@@ -503,7 +563,7 @@ export fn setBackgroundColor(col: i32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export fn setCursorPosition(x: i32, y: 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;
|
textCursorX = x * scale;
|
||||||
textCursorY = y * scale;
|
textCursorY = y * scale;
|
||||||
}
|
}
|
||||||
@@ -542,13 +602,6 @@ export fn endFrame() {
|
|||||||
68!4 = 68!0;
|
68!4 = 68!0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn memclr(base: i32, size: i32) {
|
|
||||||
loop bytes {
|
|
||||||
(base + (size := size - 1))?0 = 0;
|
|
||||||
branch_if size: bytes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
start fn setup() {
|
start fn setup() {
|
||||||
let i: i32 = 12*16*3-1;
|
let i: i32 = 12*16*3-1;
|
||||||
let avg: f32;
|
let avg: f32;
|
||||||
@@ -581,9 +634,9 @@ start fn setup() {
|
|||||||
branch_if (i := i - 1) >= 0: expand_sweetie;
|
branch_if (i := i - 1) >= 0: expand_sweetie;
|
||||||
}
|
}
|
||||||
|
|
||||||
memclr(0, 64);
|
memory.fill(0, 0, 64);
|
||||||
memclr(112, 8);
|
memory.fill(112, 0, 8);
|
||||||
memclr(0x14000, 0x2c000);
|
memory.fill(0x14000, 0, 0x2c000);
|
||||||
|
|
||||||
|
|
||||||
cls(0);
|
cls(0);
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ The initial motivation behind MicroW8 was to explore whether there was a way to
|
|||||||
* Gamepad input (D-Pad + 4 Buttons)
|
* Gamepad input (D-Pad + 4 Buttons)
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
* [Fireworks](v0.1.2#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022.
|
* [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
|
||||||
* [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
|
* [Fireworks](v0.2.0#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022.
|
||||||
* [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
|
* [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.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
|
* [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.1.2#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ): Just a simple viewer for the default font and palette.
|
* [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:
|
Examplers for older versions:
|
||||||
|
|
||||||
@@ -29,19 +29,36 @@ Examplers for older versions:
|
|||||||
|
|
||||||
## Versions
|
## Versions
|
||||||
|
|
||||||
### v0.1.2
|
### v0.2.1
|
||||||
|
|
||||||
* [Web runtime](v0.1.2)
|
* [Web runtime](v0.2.1)
|
||||||
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-linux.tgz)
|
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-linux.tgz)
|
||||||
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-macos.tgz)
|
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-macos.tgz)
|
||||||
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-windows.zip)
|
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-windows.zip)
|
||||||
|
|
||||||
Changes:
|
Changes:
|
||||||
|
|
||||||
* add option to `uw8 run` to run the cart in the browser using the web runtime
|
* new gpu accelerated renderer with (optional) crt filter
|
||||||
* CurlyWas: implement `include` support
|
* optimized `hline` function, a big speed-up when drawing large filled circles or rectangles
|
||||||
* CurlyWas: implement support for constants
|
* print fractional size of packed `uw8` cart
|
||||||
* fix crash when trying to draw zero sized line
|
|
||||||
|
### 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`)
|
||||||
|
|
||||||
### Older versions
|
### Older versions
|
||||||
|
|
||||||
|
|||||||
@@ -225,30 +225,45 @@ Moving/printing past any border does not cause any special operation, the cursor
|
|||||||
Characters 0-31 are control characters and don't print by default. They take the next 0-2 following characters as parameters.
|
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.
|
Avoid the reserved control chars, they are currently NOPs but their behavior can change in later MicroW8 versions.
|
||||||
|
|
||||||
| Code | Parameters | Operation |
|
| Code | Parameters | Operation |
|
||||||
| ----- | ---------- | ------------------------------------ |
|
| ----- | ---------- | ------------------------------------------ |
|
||||||
| 0 | - | Nop |
|
| 0 | - | Nop |
|
||||||
| 1 | char | Print char (including control chars) |
|
| 1 | char | Print char (including control chars) |
|
||||||
| 2-3 | - | Reserved |
|
| 2-3 | - | Reserved |
|
||||||
| 4 | - | Switch to normal mode |
|
| 4 | - | Switch to normal mode, reset cursor to 0,0 |
|
||||||
| 5 | - | Switch to graphics mode |
|
| 5 | - | Switch to graphics mode |
|
||||||
| 6 | - | Reserved |
|
| 6 | - | Switch output to (debug) console |
|
||||||
| 7 | - | Bell / trigger sound channel 0 |
|
| 7 | - | Bell / trigger sound channel 0 |
|
||||||
| 8 | - | Move cursor left |
|
| 8 | - | Move cursor left |
|
||||||
| 9 | - | Move cursor right |
|
| 9 | - | Move cursor right |
|
||||||
| 10 | - | Move cursor down |
|
| 10 | - | Move cursor down |
|
||||||
| 11 | - | Move cursor up |
|
| 11 | - | Move cursor up |
|
||||||
| 12 | - | do `cls(background_color)` |
|
| 12 | - | do `cls(background_color)` |
|
||||||
| 13 | - | Move cursor to the left border |
|
| 13 | - | Move cursor to the left border |
|
||||||
| 14 | color | Set the background color |
|
| 14 | color | Set the background color |
|
||||||
| 15 | color | Set the text color |
|
| 15 | color | Set the text color |
|
||||||
| 16-23 | - | Reserved |
|
| 16-23 | - | Reserved |
|
||||||
| 24 | - | Swap text/background colors |
|
| 24 | - | Swap text/background colors |
|
||||||
| 25-30 | - | Reserved |
|
| 25-30 | - | Reserved |
|
||||||
| 31 | x, y | Set cursor position (*) |
|
| 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.
|
(*) 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)
|
### 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.
|
Prints the character in the lower 8 bits of `char`. If the upper 24 bits are non-zero, right-shifts `char` by 8 bits and loops back to the beginning.
|
||||||
@@ -286,6 +301,12 @@ As the only means of communication, 32 bytes starting at address 0x00050 are cop
|
|||||||
By default, the `sndGes` function generates sound based on the 32 bytes at 0x00050. This means that in the default configuration those 32 bytes act
|
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.
|
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)
|
### 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
|
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
|
||||||
@@ -415,6 +436,30 @@ and execution of the cart is stopped. Defaults to 30 (0.5s)
|
|||||||
* `-l LEVEL`, `--level LEVEL`: Compression level (0-9). Higher compression levels are really slow.
|
* `-l LEVEL`, `--level LEVEL`: Compression level (0-9). Higher compression levels are really slow.
|
||||||
* `-o FILE`, `--output FILE`: Write the loaded and optionally packed cart back to disk.
|
* `-o FILE`, `--output FILE`: Write the loaded and optionally packed cart back to disk.
|
||||||
|
|
||||||
|
when using the native runtime:
|
||||||
|
|
||||||
|
* `-m`, `--no-audio`: Disable audio, also reduces cpu load a bit
|
||||||
|
* `--no-gpu`: Force old cpu-only window code
|
||||||
|
* `--filter FILTER`: Select an upscale filter at startup
|
||||||
|
* `--fullscreen`: Start in fullscreen mode
|
||||||
|
|
||||||
|
Note that the cpu-only window does not support fullscreen nor upscale filters.
|
||||||
|
|
||||||
|
Unless --no-gpu is given, uw8 will first try to open a gpu accelerated window, falling back to the old cpu-only window if that fails.
|
||||||
|
Therefore you should rarely need to manually pass --no-gpu. If you prefer the old pixel doubling look to the now default crt filter,
|
||||||
|
you can just pass `--filter nearest` or `--filter 1`.
|
||||||
|
|
||||||
|
The upscale filter options are:
|
||||||
|
```
|
||||||
|
1, nearest : Anti-aliased nearest filter
|
||||||
|
2, fast_crt : Very simple, cheap crt filter, not very good below a window size of 960x720
|
||||||
|
3, ss_crt : Super sampled crt filter, a little more demanding on the GPU but scales well to smaller window sizes
|
||||||
|
4, chromatic_crt : Variant of fast_crt with a slight offset of the three color dots of a pixel, still pretty cheap
|
||||||
|
5, auto_crt (default) : ss_crt below 960x720, chromatic_crt otherwise
|
||||||
|
```
|
||||||
|
|
||||||
|
You can switch the upscale filter at any time using the keys 1-5. You can toggle fullscreen with F.
|
||||||
|
|
||||||
## `uw8 pack`
|
## `uw8 pack`
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|||||||
@@ -2,6 +2,69 @@
|
|||||||
description = "Versions"
|
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
|
### v0.1.2
|
||||||
|
|
||||||
* [Web runtime](v0.1.2)
|
* [Web runtime](v0.1.2)
|
||||||
|
|||||||
1
site/static/v0.2.0-rc1/index.html
Normal file
1
site/static/v0.2.0-rc1/index.html
Normal file
File diff suppressed because one or more lines are too long
1
site/static/v0.2.0-rc2/index.html
Normal file
1
site/static/v0.2.0-rc2/index.html
Normal file
File diff suppressed because one or more lines are too long
1
site/static/v0.2.0-rc3/index.html
Normal file
1
site/static/v0.2.0-rc3/index.html
Normal file
File diff suppressed because one or more lines are too long
1
site/static/v0.2.0/index.html
Normal file
1
site/static/v0.2.0/index.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
site/static/v0.2.1/index.html
Normal file
1
site/static/v0.2.1/index.html
Normal file
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<h1 class="text-center heading-text">A WebAssembly based fantasy console</h1>
|
<h1 class="text-center heading-text">A WebAssembly based fantasy console</h1>
|
||||||
</section>
|
</section>
|
||||||
<a href="v0.1.2">
|
<a href="v0.2.1">
|
||||||
<img class="demonstration-gif" style="width:640px;height:480px;image-rendering:pixelated" src="img/technotunnel.png"></img>
|
<img class="demonstration-gif" style="width:640px;height:480px;image-rendering:pixelated" src="img/technotunnel.png"></img>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ use anyhow::Result;
|
|||||||
|
|
||||||
pub trait Runtime {
|
pub trait Runtime {
|
||||||
fn is_open(&self) -> bool;
|
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 load(&mut self, module_data: &[u8]) -> Result<()>;
|
||||||
fn run_frame(&mut self) -> Result<()>;
|
fn run_frame(&mut self) -> Result<()>;
|
||||||
}
|
}
|
||||||
34
src/main.rs
34
src/main.rs
@@ -13,10 +13,11 @@ use uw8::RunWebServer;
|
|||||||
use uw8::Runtime;
|
use uw8::Runtime;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default()).init();
|
||||||
let mut args = Arguments::from_env();
|
let mut args = Arguments::from_env();
|
||||||
|
|
||||||
// try to enable ansi support in win10 cmd shell
|
// try to enable ansi support in win10 cmd shell
|
||||||
#[cfg(target_os="windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let _ = ansi_term::enable_ansi_support();
|
let _ = ansi_term::enable_ansi_support();
|
||||||
|
|
||||||
match args.subcommand()?.as_deref() {
|
match args.subcommand()?.as_deref() {
|
||||||
@@ -52,6 +53,7 @@ fn main() -> Result<()> {
|
|||||||
#[cfg(any(feature = "native", feature = "browser"))]
|
#[cfg(any(feature = "native", feature = "browser"))]
|
||||||
fn run(mut args: Arguments) -> Result<()> {
|
fn run(mut args: Arguments) -> Result<()> {
|
||||||
let watch_mode = args.contains(["-w", "--watch"]);
|
let watch_mode = args.contains(["-w", "--watch"]);
|
||||||
|
#[allow(unused)]
|
||||||
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
|
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
|
||||||
|
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
@@ -79,6 +81,18 @@ fn run(mut args: Arguments) -> Result<()> {
|
|||||||
#[cfg(not(feature = "native"))]
|
#[cfg(not(feature = "native"))]
|
||||||
let run_browser = args.contains(["-b", "--browser"]) || true;
|
let run_browser = args.contains(["-b", "--browser"]) || true;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
let disable_audio = args.contains(["-m", "--no-audio"]);
|
||||||
|
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
let window_config = {
|
||||||
|
let mut config = uw8_window::WindowConfig::default();
|
||||||
|
if !run_browser {
|
||||||
|
config.parse_arguments(&mut args);
|
||||||
|
}
|
||||||
|
config
|
||||||
|
};
|
||||||
|
|
||||||
let filename = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
|
let filename = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
|
||||||
|
|
||||||
let mut watcher = uw8::FileWatcher::new()?;
|
let mut watcher = uw8::FileWatcher::new()?;
|
||||||
@@ -89,7 +103,13 @@ fn run(mut args: Arguments) -> Result<()> {
|
|||||||
#[cfg(not(feature = "native"))]
|
#[cfg(not(feature = "native"))]
|
||||||
unimplemented!();
|
unimplemented!();
|
||||||
#[cfg(feature = "native")]
|
#[cfg(feature = "native")]
|
||||||
Box::new(MicroW8::new()?)
|
{
|
||||||
|
let mut microw8 = MicroW8::new(timeout, window_config)?;
|
||||||
|
if disable_audio {
|
||||||
|
microw8.disable_audio();
|
||||||
|
}
|
||||||
|
Box::new(microw8)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
#[cfg(not(feature = "browser"))]
|
#[cfg(not(feature = "browser"))]
|
||||||
unimplemented!();
|
unimplemented!();
|
||||||
@@ -97,10 +117,6 @@ fn run(mut args: Arguments) -> Result<()> {
|
|||||||
Box::new(RunWebServer::new())
|
Box::new(RunWebServer::new())
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(timeout) = timeout {
|
|
||||||
runtime.set_timeout(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut first_run = true;
|
let mut first_run = true;
|
||||||
|
|
||||||
while runtime.is_open() {
|
while runtime.is_open() {
|
||||||
@@ -159,7 +175,11 @@ fn load_cart(filename: &Path, config: &Config) -> (Result<Vec<u8>>, Vec<PathBuf>
|
|||||||
|
|
||||||
if let Some(ref pack_config) = config.pack {
|
if let Some(ref pack_config) = config.pack {
|
||||||
cart = uw8_tool::pack(&cart, pack_config)?;
|
cart = uw8_tool::pack(&cart, pack_config)?;
|
||||||
println!("packed size: {} bytes", cart.len());
|
println!(
|
||||||
|
"\npacked size: {} bytes ({:.2})",
|
||||||
|
cart.len(),
|
||||||
|
uw8_tool::compressed_size(&cart)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref path) = config.output_path {
|
if let Some(ref path) = config.output_path {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,29 +4,21 @@ use std::{thread, time::Instant};
|
|||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use cpal::traits::*;
|
use cpal::traits::*;
|
||||||
use minifb::{Key, Window, WindowOptions};
|
use rubato::Resampler;
|
||||||
|
use uw8_window::{Window, WindowConfig};
|
||||||
use wasmtime::{
|
use wasmtime::{
|
||||||
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
|
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 {
|
pub struct MicroW8 {
|
||||||
|
window: Window,
|
||||||
|
stream: Option<cpal::Stream>,
|
||||||
engine: Engine,
|
engine: Engine,
|
||||||
loader_module: Module,
|
loader_module: Module,
|
||||||
window: Window,
|
disable_audio: bool,
|
||||||
window_buffer: Vec<u32>,
|
module_data: Option<Vec<u8>>,
|
||||||
instance: Option<UW8Instance>,
|
|
||||||
timeout: u32,
|
timeout: u32,
|
||||||
|
instance: Option<UW8Instance>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UW8Instance {
|
struct UW8Instance {
|
||||||
@@ -35,9 +27,8 @@ struct UW8Instance {
|
|||||||
end_frame: TypedFunc<(), ()>,
|
end_frame: TypedFunc<(), ()>,
|
||||||
update: Option<TypedFunc<(), ()>>,
|
update: Option<TypedFunc<(), ()>>,
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
module: Vec<u8>,
|
|
||||||
watchdog: Arc<Mutex<UW8WatchDog>>,
|
watchdog: Arc<Mutex<UW8WatchDog>>,
|
||||||
sound: Uw8Sound,
|
sound_tx: Option<mpsc::SyncSender<RegisterUpdate>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for UW8Instance {
|
impl Drop for UW8Instance {
|
||||||
@@ -49,63 +40,52 @@ impl Drop for UW8Instance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct UW8WatchDog {
|
struct UW8WatchDog {
|
||||||
interupt: wasmtime::InterruptHandle,
|
engine: Engine,
|
||||||
timeout: u32,
|
|
||||||
stop: bool,
|
stop: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MicroW8 {
|
impl MicroW8 {
|
||||||
pub fn new() -> Result<MicroW8> {
|
pub fn new(timeout: Option<u32>, window_config: WindowConfig) -> Result<MicroW8> {
|
||||||
let engine = wasmtime::Engine::new(wasmtime::Config::new().interruptable(true))?;
|
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 =
|
let loader_module =
|
||||||
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
|
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
|
||||||
|
|
||||||
let options = WindowOptions {
|
let window = Window::new(window_config)?;
|
||||||
scale: minifb::Scale::X2,
|
|
||||||
scale_mode: minifb::ScaleMode::AspectRatioStretch,
|
|
||||||
resize: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let mut window = Window::new("MicroW8", 320, 240, options)?;
|
|
||||||
window.limit_update_rate(Some(std::time::Duration::from_micros(16666)));
|
|
||||||
|
|
||||||
Ok(MicroW8 {
|
Ok(MicroW8 {
|
||||||
|
window,
|
||||||
|
stream: None,
|
||||||
engine,
|
engine,
|
||||||
loader_module,
|
loader_module,
|
||||||
window,
|
disable_audio: false,
|
||||||
window_buffer: vec![0u32; 320 * 240],
|
module_data: None,
|
||||||
|
timeout: timeout.unwrap_or(0),
|
||||||
instance: None,
|
instance: None,
|
||||||
timeout: 30,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(&mut self) {
|
pub fn disable_audio(&mut self) {
|
||||||
self.instance = None;
|
self.disable_audio = true;
|
||||||
for v in &mut self.window_buffer {
|
|
||||||
*v = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Uw8Sound {
|
|
||||||
stream: cpal::Stream,
|
|
||||||
tx: mpsc::SyncSender<[u8; 32]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::Runtime for MicroW8 {
|
impl super::Runtime for MicroW8 {
|
||||||
fn is_open(&self) -> bool {
|
fn is_open(&self) -> bool {
|
||||||
self.window.is_open() && !self.window.is_key_down(Key::Escape)
|
self.window.is_open()
|
||||||
}
|
|
||||||
|
|
||||||
fn set_timeout(&mut self, timeout: u32) {
|
|
||||||
self.timeout = timeout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(&mut self, module_data: &[u8]) -> Result<()> {
|
fn load(&mut self, module_data: &[u8]) -> Result<()> {
|
||||||
self.reset();
|
self.stream = None;
|
||||||
|
self.instance = None;
|
||||||
|
|
||||||
let mut store = wasmtime::Store::new(&self.engine, ());
|
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)))?;
|
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
|
||||||
|
|
||||||
@@ -126,65 +106,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_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])?;
|
let module = wasmtime::Module::new(&self.engine, &memory.data(&store)[..module_length])?;
|
||||||
|
|
||||||
fn add_native_functions(
|
|
||||||
linker: &mut wasmtime::Linker<()>,
|
|
||||||
store: &mut wasmtime::Store<()>,
|
|
||||||
) -> Result<()> {
|
|
||||||
linker.func_wrap("env", "acos", |v: f32| v.acos())?;
|
|
||||||
linker.func_wrap("env", "asin", |v: f32| v.asin())?;
|
|
||||||
linker.func_wrap("env", "atan", |v: f32| v.atan())?;
|
|
||||||
linker.func_wrap("env", "atan2", |x: f32, y: f32| x.atan2(y))?;
|
|
||||||
linker.func_wrap("env", "cos", |v: f32| v.cos())?;
|
|
||||||
linker.func_wrap("env", "exp", |v: f32| v.exp())?;
|
|
||||||
linker.func_wrap("env", "log", |v: f32| v.ln())?;
|
|
||||||
linker.func_wrap("env", "sin", |v: f32| v.sin())?;
|
|
||||||
linker.func_wrap("env", "tan", |v: f32| v.tan())?;
|
|
||||||
linker.func_wrap("env", "pow", |a: f32, b: f32| a.powf(b))?;
|
|
||||||
for i in 10..64 {
|
|
||||||
linker.func_wrap("env", &format!("reserved{}", i), || {})?;
|
|
||||||
}
|
|
||||||
for i in 0..16 {
|
|
||||||
linker.define(
|
|
||||||
"env",
|
|
||||||
&format!("g_reserved{}", i),
|
|
||||||
wasmtime::Global::new(
|
|
||||||
&mut *store,
|
|
||||||
GlobalType::new(ValType::I32, Mutability::Const),
|
|
||||||
0.into(),
|
|
||||||
)?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
add_native_functions(&mut linker, &mut store)?;
|
add_native_functions(&mut linker, &mut store)?;
|
||||||
|
|
||||||
fn instantiate_platform(
|
|
||||||
linker: &mut wasmtime::Linker<()>,
|
|
||||||
store: &mut wasmtime::Store<()>,
|
|
||||||
platform_module: &wasmtime::Module,
|
|
||||||
) -> Result<wasmtime::Instance> {
|
|
||||||
let platform_instance = linker.instantiate(&mut *store, &platform_module)?;
|
|
||||||
|
|
||||||
for export in platform_instance.exports(&mut *store) {
|
|
||||||
linker.define(
|
|
||||||
"env",
|
|
||||||
export.name(),
|
|
||||||
export
|
|
||||||
.into_func()
|
|
||||||
.expect("platform surely only exports functions"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(platform_instance)
|
|
||||||
}
|
|
||||||
|
|
||||||
let platform_instance = instantiate_platform(&mut linker, &mut store, &platform_module)?;
|
let platform_instance = instantiate_platform(&mut linker, &mut store, &platform_module)?;
|
||||||
|
|
||||||
let watchdog = Arc::new(Mutex::new(UW8WatchDog {
|
let watchdog = Arc::new(Mutex::new(UW8WatchDog {
|
||||||
interupt: store.interrupt_handle()?,
|
engine: self.engine.clone(),
|
||||||
timeout: self.timeout,
|
|
||||||
stop: false,
|
stop: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -192,16 +119,11 @@ impl super::Runtime for MicroW8 {
|
|||||||
let watchdog = watchdog.clone();
|
let watchdog = watchdog.clone();
|
||||||
thread::spawn(move || loop {
|
thread::spawn(move || loop {
|
||||||
thread::sleep(Duration::from_millis(17));
|
thread::sleep(Duration::from_millis(17));
|
||||||
if let Ok(mut watchdog) = watchdog.lock() {
|
if let Ok(watchdog) = watchdog.lock() {
|
||||||
if watchdog.stop {
|
if watchdog.stop {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if watchdog.timeout > 0 {
|
watchdog.engine.increment_epoch();
|
||||||
watchdog.timeout -= 1;
|
|
||||||
if watchdog.timeout == 0 {
|
|
||||||
watchdog.interupt.interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -209,135 +131,73 @@ impl super::Runtime for MicroW8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let instance = linker.instantiate(&mut store, &module)?;
|
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 end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?;
|
||||||
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd").ok();
|
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd").ok();
|
||||||
|
|
||||||
let sound = {
|
if let Some(start) = instance.get_typed_func::<(), (), _>(&mut store, "start").ok() {
|
||||||
let mut store = wasmtime::Store::new(&self.engine, ());
|
start.call(&mut store, ())?;
|
||||||
|
}
|
||||||
|
|
||||||
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
|
let (sound_tx, stream) = if self.disable_audio {
|
||||||
|
(None, None)
|
||||||
let mut linker = wasmtime::Linker::new(&self.engine);
|
} else {
|
||||||
linker.define("env", "memory", memory)?;
|
match init_sound(&self.engine, &platform_module, &module) {
|
||||||
add_native_functions(&mut linker, &mut store)?;
|
Ok(sound) => {
|
||||||
|
sound.stream.play()?;
|
||||||
let platform_instance =
|
(Some(sound.tx), Some(sound.stream))
|
||||||
instantiate_platform(&mut linker, &mut store, &platform_module)?;
|
|
||||||
let instance = linker.instantiate(&mut store, &module)?;
|
|
||||||
|
|
||||||
let snd = instance
|
|
||||||
.get_typed_func::<(i32,), f32, _>(&mut store, "snd")
|
|
||||||
.or_else(|_| {
|
|
||||||
platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "gesSnd")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let host = cpal::default_host();
|
|
||||||
let device = host
|
|
||||||
.default_output_device()
|
|
||||||
.ok_or_else(|| anyhow!("No audio output device available"))?;
|
|
||||||
let config = device
|
|
||||||
.supported_output_configs()?
|
|
||||||
.find(|config| {
|
|
||||||
config.min_sample_rate().0 <= 44100
|
|
||||||
&& config.max_sample_rate().0 >= 44100
|
|
||||||
&& config.channels() == 2
|
|
||||||
&& config.sample_format() == cpal::SampleFormat::F32
|
|
||||||
})
|
|
||||||
.ok_or_else(|| anyhow!("Could not find 44.1kHz float config"))?;
|
|
||||||
let config = config.with_sample_rate(cpal::SampleRate(44100));
|
|
||||||
let buffer_size = match *config.buffer_size() {
|
|
||||||
cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default,
|
|
||||||
cpal::SupportedBufferSize::Range { min, max } => {
|
|
||||||
cpal::BufferSize::Fixed(256.max(min).min(max))
|
|
||||||
}
|
}
|
||||||
};
|
Err(err) => {
|
||||||
let config = cpal::StreamConfig {
|
eprintln!("Failed to init sound: {}", err);
|
||||||
buffer_size,
|
(None, None)
|
||||||
..config.config()
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let (tx, rx) = mpsc::sync_channel::<[u8; 32]>(1);
|
|
||||||
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
let mut sample_index = 0;
|
|
||||||
let stream = {
|
|
||||||
device.build_output_stream(
|
|
||||||
&config,
|
|
||||||
move |buffer: &mut [f32], _| {
|
|
||||||
if let Ok(regs) = rx.try_recv() {
|
|
||||||
memory.write(&mut store, 80, ®s).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let time = start_time.elapsed().as_millis() as i32;
|
|
||||||
let mem = memory.data_mut(&mut store);
|
|
||||||
mem[64..68].copy_from_slice(&time.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
for v in buffer {
|
|
||||||
*v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0);
|
|
||||||
sample_index = sample_index.wrapping_add(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
move |err| {
|
|
||||||
dbg!(err);
|
|
||||||
},
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
|
|
||||||
Uw8Sound { stream, tx }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sound.stream.play()?;
|
|
||||||
|
|
||||||
self.instance = Some(UW8Instance {
|
self.instance = Some(UW8Instance {
|
||||||
store,
|
store,
|
||||||
memory,
|
memory,
|
||||||
end_frame,
|
end_frame,
|
||||||
update,
|
update,
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
module: module_data.into(),
|
|
||||||
watchdog,
|
watchdog,
|
||||||
sound,
|
sound_tx,
|
||||||
});
|
});
|
||||||
|
self.stream = stream;
|
||||||
|
self.module_data = Some(module_data.into());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_frame(&mut self) -> Result<()> {
|
fn run_frame(&mut self) -> Result<()> {
|
||||||
|
let input = self.window.begin_frame();
|
||||||
|
|
||||||
|
if input.reset {
|
||||||
|
if let Some(module_data) = self.module_data.take() {
|
||||||
|
self.load(&module_data)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
let mut result = Ok(());
|
let mut result = Ok(());
|
||||||
if let Some(mut instance) = self.instance.take() {
|
if let Some(mut instance) = self.instance.take() {
|
||||||
{
|
let time = (now - instance.start_time).as_millis() as i32;
|
||||||
let time = instance.start_time.elapsed().as_millis() as i32;
|
let next_frame = {
|
||||||
let mut gamepad: u32 = 0;
|
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6;
|
||||||
for key in self.window.get_keys() {
|
let max = now + Duration::from_millis(17);
|
||||||
if let Some(index) = GAMEPAD_KEYS
|
let next_center = now + Duration::from_millis((16 - offset) as u64);
|
||||||
.iter()
|
next_center.min(max)
|
||||||
.enumerate()
|
};
|
||||||
.find(|(_, &k)| k == key)
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
{
|
|
||||||
gamepad |= 1 << index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
{
|
||||||
let mem = instance.memory.data_mut(&mut instance.store);
|
let mem = instance.memory.data_mut(&mut instance.store);
|
||||||
mem[64..68].copy_from_slice(&time.to_le_bytes());
|
mem[64..68].copy_from_slice(&time.to_le_bytes());
|
||||||
mem[68..72].copy_from_slice(&gamepad.to_le_bytes());
|
mem[68..72].copy_from_slice(&input.gamepads);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut watchdog) = instance.watchdog.lock() {
|
instance.store.set_epoch_deadline(self.timeout as u64);
|
||||||
watchdog.timeout = self.timeout;
|
|
||||||
}
|
|
||||||
if let Some(ref update) = instance.update {
|
if let Some(ref update) = instance.update {
|
||||||
result = update.call(&mut instance.store, ());
|
if let Err(err) = update.call(&mut instance.store, ()) {
|
||||||
}
|
result = Err(err);
|
||||||
if let Ok(mut watchdog) = instance.watchdog.lock() {
|
}
|
||||||
watchdog.timeout = 0;
|
|
||||||
}
|
}
|
||||||
instance.end_frame.call(&mut instance.store, ())?;
|
instance.end_frame.call(&mut instance.store, ())?;
|
||||||
|
|
||||||
@@ -345,29 +205,287 @@ impl super::Runtime for MicroW8 {
|
|||||||
|
|
||||||
let mut sound_regs = [0u8; 32];
|
let mut sound_regs = [0u8; 32];
|
||||||
sound_regs.copy_from_slice(&memory[80..112]);
|
sound_regs.copy_from_slice(&memory[80..112]);
|
||||||
instance.sound.tx.send(sound_regs)?;
|
if let Some(ref sound_tx) = instance.sound_tx {
|
||||||
|
let _ = sound_tx.send(RegisterUpdate {
|
||||||
let framebuffer = &memory[120..(120 + 320 * 240)];
|
time,
|
||||||
let palette = &memory[0x13000..];
|
data: sound_regs,
|
||||||
for (i, &color_index) in framebuffer.iter().enumerate() {
|
});
|
||||||
let offset = color_index as usize * 4;
|
|
||||||
self.window_buffer[i] = 0xff000000
|
|
||||||
| ((palette[offset] as u32) << 16)
|
|
||||||
| ((palette[offset + 1] as u32) << 8)
|
|
||||||
| palette[offset + 2] as u32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No) {
|
let framebuffer_mem = &memory[120..(120 + 320 * 240)];
|
||||||
self.load(&instance.module)?;
|
let palette_mem = &memory[0x13000..];
|
||||||
} else if result.is_ok() {
|
self.window
|
||||||
|
.end_frame(framebuffer_mem, palette_mem, next_frame);
|
||||||
|
|
||||||
|
if result.is_ok() {
|
||||||
self.instance = Some(instance);
|
self.instance = Some(instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.window
|
|
||||||
.update_with_buffer(&self.window_buffer, 320, 240)?;
|
|
||||||
|
|
||||||
result?;
|
result?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_native_functions(
|
||||||
|
linker: &mut wasmtime::Linker<()>,
|
||||||
|
store: &mut wasmtime::Store<()>,
|
||||||
|
) -> Result<()> {
|
||||||
|
linker.func_wrap("env", "acos", |v: f32| v.acos())?;
|
||||||
|
linker.func_wrap("env", "asin", |v: f32| v.asin())?;
|
||||||
|
linker.func_wrap("env", "atan", |v: f32| v.atan())?;
|
||||||
|
linker.func_wrap("env", "atan2", |x: f32, y: f32| x.atan2(y))?;
|
||||||
|
linker.func_wrap("env", "cos", |v: f32| v.cos())?;
|
||||||
|
linker.func_wrap("env", "exp", |v: f32| v.exp())?;
|
||||||
|
linker.func_wrap("env", "log", |v: f32| v.ln())?;
|
||||||
|
linker.func_wrap("env", "sin", |v: f32| v.sin())?;
|
||||||
|
linker.func_wrap("env", "tan", |v: f32| v.tan())?;
|
||||||
|
linker.func_wrap("env", "pow", |a: f32, b: f32| a.powf(b))?;
|
||||||
|
for i in 10..64 {
|
||||||
|
linker.func_wrap("env", &format!("reserved{}", i), || {})?;
|
||||||
|
}
|
||||||
|
let log_line = std::sync::Mutex::new(String::new());
|
||||||
|
linker.func_wrap("env", "logChar", move |c: i32| {
|
||||||
|
let mut log_line = log_line.lock().unwrap();
|
||||||
|
if c == 10 {
|
||||||
|
println!("{}", log_line);
|
||||||
|
log_line.clear();
|
||||||
|
} else {
|
||||||
|
log_line.push(c as u8 as char);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
for i in 0..16 {
|
||||||
|
linker.define(
|
||||||
|
"env",
|
||||||
|
&format!("g_reserved{}", i),
|
||||||
|
wasmtime::Global::new(
|
||||||
|
&mut *store,
|
||||||
|
GlobalType::new(ValType::I32, Mutability::Const),
|
||||||
|
0.into(),
|
||||||
|
)?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instantiate_platform(
|
||||||
|
linker: &mut wasmtime::Linker<()>,
|
||||||
|
store: &mut wasmtime::Store<()>,
|
||||||
|
platform_module: &wasmtime::Module,
|
||||||
|
) -> Result<wasmtime::Instance> {
|
||||||
|
let platform_instance = linker.instantiate(&mut *store, &platform_module)?;
|
||||||
|
|
||||||
|
for export in platform_instance.exports(&mut *store) {
|
||||||
|
linker.define(
|
||||||
|
"env",
|
||||||
|
export.name(),
|
||||||
|
export
|
||||||
|
.into_func()
|
||||||
|
.expect("platform surely only exports functions"),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(platform_instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RegisterUpdate {
|
||||||
|
time: i32,
|
||||||
|
data: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Uw8Sound {
|
||||||
|
stream: cpal::Stream,
|
||||||
|
tx: mpsc::SyncSender<RegisterUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_sound(
|
||||||
|
engine: &wasmtime::Engine,
|
||||||
|
platform_module: &wasmtime::Module,
|
||||||
|
module: &wasmtime::Module,
|
||||||
|
) -> Result<Uw8Sound> {
|
||||||
|
let mut store = wasmtime::Store::new(engine, ());
|
||||||
|
store.set_epoch_deadline(60);
|
||||||
|
|
||||||
|
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
|
||||||
|
|
||||||
|
let mut linker = wasmtime::Linker::new(engine);
|
||||||
|
linker.define("env", "memory", memory)?;
|
||||||
|
add_native_functions(&mut linker, &mut store)?;
|
||||||
|
|
||||||
|
let platform_instance = instantiate_platform(&mut linker, &mut store, platform_module)?;
|
||||||
|
let instance = linker.instantiate(&mut store, module)?;
|
||||||
|
|
||||||
|
let snd = instance
|
||||||
|
.get_typed_func::<(i32,), f32, _>(&mut store, "snd")
|
||||||
|
.or_else(|_| platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "sndGes"))?;
|
||||||
|
|
||||||
|
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())
|
||||||
|
.min(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());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_sample(s: f32) -> f32 {
|
||||||
|
if s.is_nan() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
s.max(-1.0).min(1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(clamp_sample(
|
||||||
|
snd.call(&mut store, (sample_index,)).unwrap_or(0.0),
|
||||||
|
));
|
||||||
|
resampler.input_buffers[1].push(clamp_sample(
|
||||||
|
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 = clamp_sample(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 })
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use warp::{http::Response, Filter};
|
|||||||
pub struct RunWebServer {
|
pub struct RunWebServer {
|
||||||
cart: Arc<Mutex<Vec<u8>>>,
|
cart: Arc<Mutex<Vec<u8>>>,
|
||||||
tx: broadcast::Sender<()>,
|
tx: broadcast::Sender<()>,
|
||||||
|
socket_addr: SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunWebServer {
|
impl RunWebServer {
|
||||||
@@ -18,8 +19,13 @@ impl RunWebServer {
|
|||||||
let cart = Arc::new(Mutex::new(Vec::new()));
|
let cart = Arc::new(Mutex::new(Vec::new()));
|
||||||
let (tx, _) = broadcast::channel(1);
|
let (tx, _) = broadcast::channel(1);
|
||||||
|
|
||||||
|
let socket_addr = "127.0.0.1:3030"
|
||||||
|
.parse::<SocketAddr>()
|
||||||
|
.expect("Failed to parse socket address");
|
||||||
|
|
||||||
let server_cart = cart.clone();
|
let server_cart = cart.clone();
|
||||||
let server_tx = tx.clone();
|
let server_tx = tx.clone();
|
||||||
|
let server_addr = socket_addr.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_io()
|
.enable_io()
|
||||||
@@ -47,24 +53,26 @@ impl RunWebServer {
|
|||||||
warp::sse::reply(warp::sse::keep_alive().stream(event_stream(&server_tx)))
|
warp::sse::reply(warp::sse::keep_alive().stream(event_stream(&server_tx)))
|
||||||
});
|
});
|
||||||
|
|
||||||
let socket_addr = "127.0.0.1:3030"
|
let server_future = warp::serve(html.or(cart).or(events)).bind(server_addr);
|
||||||
.parse::<SocketAddr>()
|
|
||||||
.expect("Failed to parse socket address");
|
|
||||||
|
|
||||||
let server_future = warp::serve(html.or(cart).or(events)).bind(socket_addr);
|
|
||||||
println!("Point browser at http://{}", socket_addr);
|
|
||||||
let _ignore_result = webbrowser::open(&format!("http://{}", socket_addr));
|
|
||||||
server_future.await
|
server_future.await
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
RunWebServer { cart, tx }
|
RunWebServer {
|
||||||
|
cart,
|
||||||
|
tx,
|
||||||
|
socket_addr,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::Runtime for RunWebServer {
|
impl super::Runtime for RunWebServer {
|
||||||
fn load(&mut self, module_data: &[u8]) -> Result<()> {
|
fn load(&mut self, module_data: &[u8]) -> Result<()> {
|
||||||
if let Ok(mut lock) = self.cart.lock() {
|
if let Ok(mut lock) = self.cart.lock() {
|
||||||
|
if lock.is_empty() && !module_data.is_empty() {
|
||||||
|
println!("Point browser at http://{}", self.socket_addr);
|
||||||
|
let _ignore_result = webbrowser::open(&format!("http://{}", self.socket_addr));
|
||||||
|
}
|
||||||
lock.clear();
|
lock.clear();
|
||||||
lock.extend_from_slice(module_data);
|
lock.extend_from_slice(module_data);
|
||||||
}
|
}
|
||||||
|
|||||||
62
test/drawing_test.cwa
Normal file
62
test/drawing_test.cwa
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
include "../examples/include/microw8-api.cwa"
|
||||||
|
|
||||||
|
global mut counter = 0;
|
||||||
|
|
||||||
|
export fn upd() {
|
||||||
|
cls(0);
|
||||||
|
|
||||||
|
let col: i32 = 1;
|
||||||
|
|
||||||
|
loop colors {
|
||||||
|
if !testCircle(counter, col) {
|
||||||
|
printInt(counter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
counter += 1;
|
||||||
|
branch_if (col +:= 1) < 256: colors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn testCircle(seed: i32, col: i32) -> i32 {
|
||||||
|
randomSeed(seed);
|
||||||
|
let cx = randomf() * 640_f - 160_f;
|
||||||
|
let cy = randomf() * 480_f - 120_f;
|
||||||
|
let radius = randomf() * 4_f;
|
||||||
|
radius *= radius;
|
||||||
|
radius *= radius;
|
||||||
|
|
||||||
|
circle(cx, cy, radius, col);
|
||||||
|
|
||||||
|
let min_x = max(0_f, floor(cx - radius - 1_f)) as i32;
|
||||||
|
let min_y = max(0_f, floor(cy - radius - 1_f)) as i32;
|
||||||
|
let max_x = min(320_f, ceil(cx + radius + 1_f)) as i32;
|
||||||
|
let max_y = min(240_f, ceil(cy + radius + 1_f)) as i32;
|
||||||
|
|
||||||
|
let x = min_x;
|
||||||
|
loop xloop {
|
||||||
|
if x < max_x {
|
||||||
|
let y = min_y;
|
||||||
|
loop yloop {
|
||||||
|
if y < max_y {
|
||||||
|
let rx = x as f32 + 0.5 - cx;
|
||||||
|
let ry = y as f32 + 0.5 - cy;
|
||||||
|
let d = sqrt(rx*rx + ry*ry) - radius;
|
||||||
|
if abs(d) > 0.001 {
|
||||||
|
let is_inside = d < 0_f;
|
||||||
|
let is_plotted = getPixel(x, y) == col;
|
||||||
|
if is_inside != is_plotted {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
y += 1;
|
||||||
|
branch yloop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x += 1;
|
||||||
|
branch xloop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
1
|
||||||
|
}
|
||||||
20
test/frame_time.cwa
Normal file
20
test/frame_time.cwa
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
include "../examples/include/microw8-api.cwa"
|
||||||
|
|
||||||
|
global mut pos = 0;
|
||||||
|
global mut next = 0;
|
||||||
|
|
||||||
|
export fn upd() {
|
||||||
|
let lazy t = 32!32;
|
||||||
|
let lazy tick = t * 6 / 100;
|
||||||
|
let lazy rel = t - tick * 100 / 6;
|
||||||
|
|
||||||
|
setBackgroundColor(select(tick == next, 0, select(tick < next, 0x35, 0x55)));
|
||||||
|
setCursorPosition(pos % 13 * 3, pos / 13 % 30);
|
||||||
|
if rel < 10 {
|
||||||
|
printChar(32);
|
||||||
|
}
|
||||||
|
printInt(rel);
|
||||||
|
|
||||||
|
pos = pos + 1;
|
||||||
|
next = tick + 1;
|
||||||
|
}
|
||||||
7
test/log.cwa
Normal file
7
test/log.cwa
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
include "../examples/include/microw8-api.cwa"
|
||||||
|
|
||||||
|
export fn upd() {
|
||||||
|
printChar('\06f: ');
|
||||||
|
printInt(32!32 * 6 / 100);
|
||||||
|
printChar('\n\4');
|
||||||
|
}
|
||||||
5
test/start_fn.cwa
Normal file
5
test/start_fn.cwa
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include "../examples/include/microw8-api.cwa"
|
||||||
|
|
||||||
|
export fn start() {
|
||||||
|
printChar('Test');
|
||||||
|
}
|
||||||
13
test/text_modes.cwa
Normal file
13
test/text_modes.cwa
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
include "../examples/include/microw8-api.cwa"
|
||||||
|
|
||||||
|
export fn upd() {
|
||||||
|
printString(USER_MEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
data USER_MEM {
|
||||||
|
i8(12, 31, 5, 6) "Text mode"
|
||||||
|
i8(5, 31, 4, 5) "Graphics mode"
|
||||||
|
i8(6) "Console output\nSecond line\n"
|
||||||
|
i8(4, 31, 4, 12) "Back to text mode"
|
||||||
|
i8(0)
|
||||||
|
}
|
||||||
2
uw8-tool/Cargo.lock
generated
2
uw8-tool/Cargo.lock
generated
@@ -189,7 +189,7 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "upkr"
|
name = "upkr"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cdivsufsort",
|
"cdivsufsort",
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ wasm-encoder = "0.8"
|
|||||||
walrus = "0.19"
|
walrus = "0.19"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
pico-args = "0.4"
|
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"
|
pbr = "1"
|
||||||
@@ -167,6 +167,7 @@ impl BaseModule {
|
|||||||
add_function(&mut functions, &type_map, "exp", &[F32], Some(F32));
|
add_function(&mut functions, &type_map, "exp", &[F32], Some(F32));
|
||||||
|
|
||||||
add_function(&mut functions, &type_map, "playNote", &[I32, I32], None);
|
add_function(&mut functions, &type_map, "playNote", &[I32, I32], None);
|
||||||
|
add_function(&mut functions, &type_map, "sndGes", &[I32], Some(F32));
|
||||||
|
|
||||||
for i in functions.len()..64 {
|
for i in functions.len()..64 {
|
||||||
add_function(
|
add_function(
|
||||||
@@ -286,7 +287,7 @@ impl BaseModule {
|
|||||||
|
|
||||||
pub fn create_binary(path: &Path) -> Result<()> {
|
pub fn create_binary(path: &Path) -> Result<()> {
|
||||||
let base1 = BaseModule::for_format_version(1)?.to_wasm();
|
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)?;
|
File::create(path)?.write_all(&data)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
mod base_module;
|
mod base_module;
|
||||||
mod pack;
|
|
||||||
mod filter_exports;
|
mod filter_exports;
|
||||||
|
mod pack;
|
||||||
|
|
||||||
pub use base_module::BaseModule;
|
pub use base_module::BaseModule;
|
||||||
pub use pack::{pack, pack_file, unpack, unpack_file, PackConfig};
|
|
||||||
pub use filter_exports::filter_exports;
|
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(
|
uw8.extend_from_slice(&upkr::pack(
|
||||||
&result[8..],
|
&result[8..],
|
||||||
level,
|
level,
|
||||||
|
false,
|
||||||
Some(&mut |pos| {
|
Some(&mut |pos| {
|
||||||
pb.set(pos as u64);
|
pb.set(pos as u64);
|
||||||
}),
|
}),
|
||||||
@@ -89,7 +90,7 @@ pub fn unpack(data: Vec<u8>) -> Result<Vec<u8>> {
|
|||||||
let (version, data) = match data[0] {
|
let (version, data) = match data[0] {
|
||||||
0 => return Ok(data),
|
0 => return Ok(data),
|
||||||
1 => (1, data[1..].to_vec()),
|
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),
|
other => bail!("Uknown format version {}", other),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -219,6 +220,11 @@ impl<'a> ParsedModule<'a> {
|
|||||||
validate_table_section(reader)?;
|
validate_table_section(reader)?;
|
||||||
table_section = Some(Section::new(range, ()));
|
table_section = Some(Section::new(range, ()));
|
||||||
}
|
}
|
||||||
|
Payload::MemorySection(reader) => {
|
||||||
|
if reader.get_count() != 0 {
|
||||||
|
bail!("Found non-empty MemorySection. Memory has to be imported!");
|
||||||
|
}
|
||||||
|
}
|
||||||
Payload::ElementSection(mut reader) => {
|
Payload::ElementSection(mut reader) => {
|
||||||
let mut elements = Vec::with_capacity(reader.get_count() as usize);
|
let mut elements = Vec::with_capacity(reader.get_count() as usize);
|
||||||
for _ in 0..reader.get_count() {
|
for _ in 0..reader.get_count() {
|
||||||
@@ -962,6 +968,8 @@ fn remap_function(
|
|||||||
De::I64TruncSatF32U => En::I64TruncSatF32U,
|
De::I64TruncSatF32U => En::I64TruncSatF32U,
|
||||||
De::I64TruncSatF64S => En::I64TruncSatF64S,
|
De::I64TruncSatF64S => En::I64TruncSatF64S,
|
||||||
De::I64TruncSatF64U => En::I64TruncSatF64U,
|
De::I64TruncSatF64U => En::I64TruncSatF64U,
|
||||||
|
De::MemoryCopy { src, dst } => En::MemoryCopy { src, dst },
|
||||||
|
De::MemoryFill { mem } => En::MemoryFill(mem),
|
||||||
other => bail!("Unsupported instruction {:?}", other),
|
other => bail!("Unsupported instruction {:?}", other),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
1
uw8-window/.gitignore
vendored
Normal file
1
uw8-window/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
1704
uw8-window/Cargo.lock
generated
Normal file
1704
uw8-window/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
uw8-window/Cargo.toml
Normal file
18
uw8-window/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[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"
|
||||||
|
pico-args = "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"
|
||||||
83
uw8-window/src/cpu.rs
Normal file
83
uw8-window/src/cpu.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use crate::{Input, WindowImpl};
|
||||||
|
use anyhow::Result;
|
||||||
|
use minifb::{Key, WindowOptions};
|
||||||
|
|
||||||
|
static GAMEPAD_KEYS: &[Key] = &[
|
||||||
|
Key::Up,
|
||||||
|
Key::Down,
|
||||||
|
Key::Left,
|
||||||
|
Key::Right,
|
||||||
|
Key::Z,
|
||||||
|
Key::X,
|
||||||
|
Key::A,
|
||||||
|
Key::S,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct Window {
|
||||||
|
window: minifb::Window,
|
||||||
|
buffer: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
pub fn new() -> Result<Window> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
unsafe {
|
||||||
|
winapi::um::timeapi::timeBeginPeriod(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer: Vec<u32> = vec![0; 320 * 240];
|
||||||
|
|
||||||
|
let options = WindowOptions {
|
||||||
|
scale: minifb::Scale::X2,
|
||||||
|
scale_mode: minifb::ScaleMode::AspectRatioStretch,
|
||||||
|
resize: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let window = minifb::Window::new("MicroW8", 320, 240, options).unwrap();
|
||||||
|
|
||||||
|
Ok(Window { window, buffer })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowImpl for Window {
|
||||||
|
fn begin_frame(&mut self) -> Input {
|
||||||
|
let mut gamepads = [0u8; 4];
|
||||||
|
for key in self.window.get_keys() {
|
||||||
|
if let Some(index) = GAMEPAD_KEYS
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, &k)| k == key)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
{
|
||||||
|
gamepads[0] |= 1 << index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Input {
|
||||||
|
gamepads,
|
||||||
|
reset: self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
self.window
|
||||||
|
.update_with_buffer(&self.buffer, 320, 240)
|
||||||
|
.unwrap();
|
||||||
|
if let Some(sleep) = next_frame.checked_duration_since(Instant::now()) {
|
||||||
|
std::thread::sleep(sleep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_open(&self) -> bool {
|
||||||
|
self.window.is_open() && !self.window.is_key_down(Key::Escape)
|
||||||
|
}
|
||||||
|
}
|
||||||
143
uw8-window/src/gpu/crt.rs
Normal file
143
uw8-window/src/gpu/crt.rs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
use winit::dpi::PhysicalSize;
|
||||||
|
|
||||||
|
use super::Filter;
|
||||||
|
|
||||||
|
pub struct CrtFilter {
|
||||||
|
uniform_buffer: wgpu::Buffer,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CrtFilter {
|
||||||
|
pub fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
screen: &wgpu::TextureView,
|
||||||
|
resolution: PhysicalSize<u32>,
|
||||||
|
surface_format: wgpu::TextureFormat,
|
||||||
|
) -> CrtFilter {
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
texture_scale: texture_scale_from_resolution(resolution),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: uniform_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: 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 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_format,
|
||||||
|
blend: None,
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
primitive: Default::default(),
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: Default::default(),
|
||||||
|
multiview: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
CrtFilter {
|
||||||
|
uniform_buffer,
|
||||||
|
bind_group: crt_bind_group,
|
||||||
|
pipeline: render_pipeline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for CrtFilter {
|
||||||
|
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
texture_scale: texture_scale_from_resolution(new_size),
|
||||||
|
};
|
||||||
|
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
|
||||||
|
render_pass.set_pipeline(&self.pipeline);
|
||||||
|
render_pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
render_pass.draw(0..6, 0..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],
|
||||||
|
}
|
||||||
75
uw8-window/src/gpu/crt.wgsl
Normal file
75
uw8-window/src/gpu/crt.wgsl
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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 i = in_vertex_index / 3u + in_vertex_index % 3u;
|
||||||
|
let x = -1.0 + f32(i % 2u) * 322.0;
|
||||||
|
let y = -1.0 + f32(i / 2u) * 242.0;
|
||||||
|
out.clip_position = vec4<f32>((vec2<f32>(x, y) - vec2<f32>(160.0, 120.0)) / uniforms.texture_scale.xy, 0.0, 1.0);
|
||||||
|
out.tex_coords = vec2<f32>(x, y);
|
||||||
|
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.008) / offset - vec4<f32>(0.0024), 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
162
uw8-window/src/gpu/fast_crt.rs
Normal file
162
uw8-window/src/gpu/fast_crt.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
use winit::dpi::PhysicalSize;
|
||||||
|
|
||||||
|
use super::Filter;
|
||||||
|
|
||||||
|
pub struct FastCrtFilter {
|
||||||
|
uniform_buffer: wgpu::Buffer,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FastCrtFilter {
|
||||||
|
pub fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
screen: &wgpu::TextureView,
|
||||||
|
resolution: PhysicalSize<u32>,
|
||||||
|
surface_format: wgpu::TextureFormat,
|
||||||
|
chromatic: bool,
|
||||||
|
) -> FastCrtFilter {
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
texture_scale: texture_scale_from_resolution(resolution),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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: true },
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
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 sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
layout: &bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&screen),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: uniform_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: None,
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("fast_crt.wgsl").into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let render_pipeline_layout =
|
||||||
|
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: None,
|
||||||
|
bind_group_layouts: &[&bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: None,
|
||||||
|
layout: Some(&render_pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: "vs_main",
|
||||||
|
buffers: &[],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: if chromatic {
|
||||||
|
"fs_main_chromatic"
|
||||||
|
} else {
|
||||||
|
"fs_main"
|
||||||
|
},
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: surface_format,
|
||||||
|
blend: None,
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
primitive: Default::default(),
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: Default::default(),
|
||||||
|
multiview: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
FastCrtFilter {
|
||||||
|
uniform_buffer,
|
||||||
|
bind_group,
|
||||||
|
pipeline: render_pipeline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for FastCrtFilter {
|
||||||
|
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
texture_scale: texture_scale_from_resolution(new_size),
|
||||||
|
};
|
||||||
|
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
|
||||||
|
render_pass.set_pipeline(&self.pipeline);
|
||||||
|
render_pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
render_pass.draw(0..6, 0..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);
|
||||||
|
[
|
||||||
|
scale / res.width as f32,
|
||||||
|
scale / res.height as f32,
|
||||||
|
2.0 / scale,
|
||||||
|
0.0,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
struct Uniforms {
|
||||||
|
texture_scale: [f32; 4],
|
||||||
|
}
|
||||||
66
uw8-window/src/gpu/fast_crt.wgsl
Normal file
66
uw8-window/src/gpu/fast_crt.wgsl
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) tex_coords: vec2<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
texture_scale: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(
|
||||||
|
@builtin(vertex_index) in_vertex_index: u32,
|
||||||
|
) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
let i = in_vertex_index / 3u + in_vertex_index % 3u;
|
||||||
|
let x = 0.0 + f32(i % 2u) * 320.0;
|
||||||
|
let y = 0.0 + f32(i / 2u) * 240.0;
|
||||||
|
out.clip_position = vec4<f32>((vec2<f32>(x, y) - vec2<f32>(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0);
|
||||||
|
out.tex_coords = vec2<f32>(x, y);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var linear_sampler: sampler;
|
||||||
|
|
||||||
|
fn row_factor(offset: f32) -> f32 {
|
||||||
|
return 1.0 / (1.0 + offset * offset * 16.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn col_factor(offset: f32) -> f32 {
|
||||||
|
let offset = max(0.0, abs(offset) - 0.4);
|
||||||
|
return 1.0 / (1.0 + offset * offset * 16.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_screen(tex_coords: vec2<f32>) -> vec4<f32> {
|
||||||
|
let base = round(tex_coords) - vec2<f32>(0.5);
|
||||||
|
let frac = tex_coords - base;
|
||||||
|
|
||||||
|
let top_factor = row_factor(frac.y);
|
||||||
|
let bottom_factor = row_factor(frac.y - 1.0);
|
||||||
|
|
||||||
|
let v = base.y + bottom_factor / (bottom_factor + top_factor);
|
||||||
|
|
||||||
|
let left_factor = col_factor(frac.x);
|
||||||
|
let right_factor = col_factor(frac.x - 1.0);
|
||||||
|
|
||||||
|
let u = base.x + right_factor / (right_factor + left_factor);
|
||||||
|
|
||||||
|
return textureSample(screen_texture, linear_sampler, vec2<f32>(u, v) / vec2<f32>(320.0, 240.0)) * (top_factor + bottom_factor) * (left_factor + right_factor) * 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return sample_screen(in.tex_coords);
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main_chromatic(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let r = sample_screen(in.tex_coords + vec2<f32>(0.2, 0.2)).r;
|
||||||
|
let g = sample_screen(in.tex_coords + vec2<f32>(0.07, -0.27)).g;
|
||||||
|
let b = sample_screen(in.tex_coords + vec2<f32>(-0.27, 0.07)).b;
|
||||||
|
return vec4<f32>(r, g, b, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
545
uw8-window/src/gpu/mod.rs
Normal file
545
uw8-window/src/gpu/mod.rs
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
use crate::{Input, WindowConfig, WindowImpl};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use std::{num::NonZeroU32, time::Instant};
|
||||||
|
|
||||||
|
use winit::{
|
||||||
|
dpi::PhysicalSize,
|
||||||
|
event::{Event, VirtualKeyCode, WindowEvent},
|
||||||
|
event_loop::{ControlFlow, EventLoop},
|
||||||
|
window::{Fullscreen, WindowBuilder},
|
||||||
|
};
|
||||||
|
|
||||||
|
use winit::platform::run_return::EventLoopExtRunReturn;
|
||||||
|
|
||||||
|
mod crt;
|
||||||
|
mod fast_crt;
|
||||||
|
mod square;
|
||||||
|
|
||||||
|
use crt::CrtFilter;
|
||||||
|
use fast_crt::FastCrtFilter;
|
||||||
|
use square::SquareFilter;
|
||||||
|
|
||||||
|
pub struct Window {
|
||||||
|
_instance: wgpu::Instance,
|
||||||
|
surface: wgpu::Surface,
|
||||||
|
_adapter: wgpu::Adapter,
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
palette_screen_mode: PaletteScreenMode,
|
||||||
|
surface_config: wgpu::SurfaceConfiguration,
|
||||||
|
filter: Box<dyn Filter>,
|
||||||
|
event_loop: EventLoop<()>,
|
||||||
|
window: winit::window::Window,
|
||||||
|
gamepads: [u8; 4],
|
||||||
|
next_frame: Instant,
|
||||||
|
is_fullscreen: bool,
|
||||||
|
is_open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
pub fn new(window_config: WindowConfig) -> Result<Window> {
|
||||||
|
async fn create(window_config: WindowConfig) -> Result<Window> {
|
||||||
|
let event_loop = EventLoop::new();
|
||||||
|
let window = WindowBuilder::new()
|
||||||
|
.with_inner_size(PhysicalSize::new(640u32, 480))
|
||||||
|
.with_min_inner_size(PhysicalSize::new(320u32, 240))
|
||||||
|
.with_title("MicroW8")
|
||||||
|
.with_fullscreen(if window_config.fullscreen {
|
||||||
|
Some(Fullscreen::Borderless(None))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.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?;
|
||||||
|
|
||||||
|
let palette_screen_mode = PaletteScreenMode::new(&device);
|
||||||
|
|
||||||
|
let 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 filter: Box<dyn Filter> = create_filter(
|
||||||
|
&device,
|
||||||
|
&palette_screen_mode.screen_view,
|
||||||
|
window.inner_size(),
|
||||||
|
surface_config.format,
|
||||||
|
window_config.filter,
|
||||||
|
);
|
||||||
|
|
||||||
|
surface.configure(&device, &surface_config);
|
||||||
|
|
||||||
|
Ok(Window {
|
||||||
|
event_loop,
|
||||||
|
window,
|
||||||
|
_instance: instance,
|
||||||
|
surface,
|
||||||
|
_adapter: adapter,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
palette_screen_mode,
|
||||||
|
surface_config,
|
||||||
|
filter,
|
||||||
|
gamepads: [0; 4],
|
||||||
|
next_frame: Instant::now(),
|
||||||
|
is_fullscreen: window_config.fullscreen,
|
||||||
|
is_open: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pollster::block_on(create(window_config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowImpl for Window {
|
||||||
|
fn begin_frame(&mut self) -> Input {
|
||||||
|
let mut reset = false;
|
||||||
|
self.event_loop.run_return(|event, _, control_flow| {
|
||||||
|
*control_flow = ControlFlow::WaitUntil(self.next_frame);
|
||||||
|
let mut new_filter = None;
|
||||||
|
match event {
|
||||||
|
Event::WindowEvent { event, .. } => match event {
|
||||||
|
WindowEvent::Resized(new_size) => {
|
||||||
|
self.surface_config.width = new_size.width;
|
||||||
|
self.surface_config.height = new_size.height;
|
||||||
|
self.surface.configure(&self.device, &self.surface_config);
|
||||||
|
self.filter.resize(&self.queue, new_size);
|
||||||
|
}
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
self.is_open = false;
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
WindowEvent::KeyboardInput { input, .. } => {
|
||||||
|
fn gamepad_button(input: &winit::event::KeyboardInput) -> u8 {
|
||||||
|
match input.scancode {
|
||||||
|
44 => 16,
|
||||||
|
45 => 32,
|
||||||
|
30 => 64,
|
||||||
|
31 => 128,
|
||||||
|
_ => match input.virtual_keycode {
|
||||||
|
Some(VirtualKeyCode::Up) => 1,
|
||||||
|
Some(VirtualKeyCode::Down) => 2,
|
||||||
|
Some(VirtualKeyCode::Left) => 4,
|
||||||
|
Some(VirtualKeyCode::Right) => 8,
|
||||||
|
_ => 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input.state == winit::event::ElementState::Pressed {
|
||||||
|
match input.virtual_keycode {
|
||||||
|
Some(VirtualKeyCode::Escape) => {
|
||||||
|
self.is_open = false;
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
Some(VirtualKeyCode::F) => {
|
||||||
|
let fullscreen = if self.window.fullscreen().is_some() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Fullscreen::Borderless(None))
|
||||||
|
};
|
||||||
|
self.is_fullscreen = fullscreen.is_some();
|
||||||
|
self.window.set_fullscreen(fullscreen);
|
||||||
|
}
|
||||||
|
Some(VirtualKeyCode::R) => reset = true,
|
||||||
|
Some(VirtualKeyCode::Key1) => new_filter = Some(1),
|
||||||
|
Some(VirtualKeyCode::Key2) => new_filter = Some(2),
|
||||||
|
Some(VirtualKeyCode::Key3) => new_filter = Some(3),
|
||||||
|
Some(VirtualKeyCode::Key4) => new_filter = Some(4),
|
||||||
|
Some(VirtualKeyCode::Key5) => new_filter = Some(5),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.gamepads[0] |= gamepad_button(&input);
|
||||||
|
} else {
|
||||||
|
self.gamepads[0] &= !gamepad_button(&input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Event::RedrawEventsCleared => {
|
||||||
|
if Instant::now() >= self.next_frame
|
||||||
|
// workaround needed on Wayland until the next winit release
|
||||||
|
&& self.window.fullscreen().is_some() == self.is_fullscreen
|
||||||
|
{
|
||||||
|
*control_flow = ControlFlow::Exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
if let Some(new_filter) = new_filter {
|
||||||
|
self.filter = create_filter(
|
||||||
|
&self.device,
|
||||||
|
&self.palette_screen_mode.screen_view,
|
||||||
|
self.window.inner_size(),
|
||||||
|
self.surface_config.format,
|
||||||
|
new_filter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Input {
|
||||||
|
gamepads: self.gamepads,
|
||||||
|
reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
|
||||||
|
self.next_frame = next_frame;
|
||||||
|
self.palette_screen_mode
|
||||||
|
.write_framebuffer(&self.queue, framebuffer);
|
||||||
|
self.palette_screen_mode.write_palette(&self.queue, palette);
|
||||||
|
|
||||||
|
let output = self.surface.get_current_texture().unwrap();
|
||||||
|
let view = output
|
||||||
|
.texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||||
|
|
||||||
|
self.palette_screen_mode.resolve_screen(&mut encoder);
|
||||||
|
|
||||||
|
{
|
||||||
|
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::Clear(wgpu::Color {
|
||||||
|
r: 0.0,
|
||||||
|
g: 0.0,
|
||||||
|
b: 0.0,
|
||||||
|
a: 1.0,
|
||||||
|
}),
|
||||||
|
store: true,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.filter.render(&mut render_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
output.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_open(&self) -> bool {
|
||||||
|
self.is_open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_filter(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
screen_texture: &wgpu::TextureView,
|
||||||
|
window_size: PhysicalSize<u32>,
|
||||||
|
surface_format: wgpu::TextureFormat,
|
||||||
|
filter: u32,
|
||||||
|
) -> Box<dyn Filter> {
|
||||||
|
match filter {
|
||||||
|
1 => Box::new(SquareFilter::new(
|
||||||
|
device,
|
||||||
|
screen_texture,
|
||||||
|
window_size,
|
||||||
|
surface_format,
|
||||||
|
)),
|
||||||
|
2 => Box::new(FastCrtFilter::new(
|
||||||
|
device,
|
||||||
|
screen_texture,
|
||||||
|
window_size,
|
||||||
|
surface_format,
|
||||||
|
false,
|
||||||
|
)),
|
||||||
|
3 => Box::new(CrtFilter::new(
|
||||||
|
device,
|
||||||
|
screen_texture,
|
||||||
|
window_size,
|
||||||
|
surface_format,
|
||||||
|
)),
|
||||||
|
4 => Box::new(FastCrtFilter::new(
|
||||||
|
device,
|
||||||
|
screen_texture,
|
||||||
|
window_size,
|
||||||
|
surface_format,
|
||||||
|
true,
|
||||||
|
)),
|
||||||
|
_ => Box::new(AutoCrtFilter::new(
|
||||||
|
device,
|
||||||
|
screen_texture,
|
||||||
|
window_size,
|
||||||
|
surface_format,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Filter {
|
||||||
|
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>);
|
||||||
|
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AutoCrtFilter {
|
||||||
|
small: CrtFilter,
|
||||||
|
large: FastCrtFilter,
|
||||||
|
resolution: PhysicalSize<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AutoCrtFilter {
|
||||||
|
fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
screen: &wgpu::TextureView,
|
||||||
|
resolution: PhysicalSize<u32>,
|
||||||
|
surface_format: wgpu::TextureFormat,
|
||||||
|
) -> AutoCrtFilter {
|
||||||
|
let small = CrtFilter::new(device, screen, resolution, surface_format);
|
||||||
|
let large = FastCrtFilter::new(device, screen, resolution, surface_format, true);
|
||||||
|
AutoCrtFilter {
|
||||||
|
small,
|
||||||
|
large,
|
||||||
|
resolution,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for AutoCrtFilter {
|
||||||
|
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
|
||||||
|
self.small.resize(queue, new_size);
|
||||||
|
self.large.resize(queue, new_size);
|
||||||
|
self.resolution = new_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
|
||||||
|
if self.resolution.width < 960 || self.resolution.height < 720 {
|
||||||
|
self.small.render(render_pass);
|
||||||
|
} else {
|
||||||
|
self.large.render(render_pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PaletteScreenMode {
|
||||||
|
framebuffer: wgpu::Texture,
|
||||||
|
palette: wgpu::Texture,
|
||||||
|
screen_view: wgpu::TextureView,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaletteScreenMode {
|
||||||
|
fn new(device: &wgpu::Device) -> PaletteScreenMode {
|
||||||
|
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 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 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
PaletteScreenMode {
|
||||||
|
framebuffer: framebuffer_texture,
|
||||||
|
palette: palette_texture,
|
||||||
|
screen_view: screen_texture_view,
|
||||||
|
bind_group: palette_bind_group,
|
||||||
|
pipeline: palette_pipeline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_framebuffer(&self, queue: &wgpu::Queue, pixels: &[u8]) {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_palette(&self, queue: &wgpu::Queue, palette: &[u8]) {
|
||||||
|
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 resolve_screen(&self, encoder: &mut wgpu::CommandEncoder) {
|
||||||
|
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: None,
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &self.screen_view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Load,
|
||||||
|
store: true,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
render_pass.set_pipeline(&self.pipeline);
|
||||||
|
render_pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
render_pass.draw(0..3, 0..1);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
uw8-window/src/gpu/palette.wgsl
Normal file
24
uw8-window/src/gpu/palette.wgsl
Normal file
@@ -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);
|
||||||
|
}
|
||||||
157
uw8-window/src/gpu/square.rs
Normal file
157
uw8-window/src/gpu/square.rs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
use winit::dpi::PhysicalSize;
|
||||||
|
|
||||||
|
use super::Filter;
|
||||||
|
|
||||||
|
pub struct SquareFilter {
|
||||||
|
uniform_buffer: wgpu::Buffer,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SquareFilter {
|
||||||
|
pub fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
screen: &wgpu::TextureView,
|
||||||
|
resolution: PhysicalSize<u32>,
|
||||||
|
surface_format: wgpu::TextureFormat,
|
||||||
|
) -> SquareFilter {
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
texture_scale: texture_scale_from_resolution(resolution),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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: true },
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
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 sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
layout: &bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&screen),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: uniform_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: None,
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("square.wgsl").into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let render_pipeline_layout =
|
||||||
|
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: None,
|
||||||
|
bind_group_layouts: &[&bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: None,
|
||||||
|
layout: Some(&render_pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: "vs_main",
|
||||||
|
buffers: &[],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: "fs_main",
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: surface_format,
|
||||||
|
blend: None,
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
primitive: Default::default(),
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: Default::default(),
|
||||||
|
multiview: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
SquareFilter {
|
||||||
|
uniform_buffer,
|
||||||
|
bind_group,
|
||||||
|
pipeline: render_pipeline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for SquareFilter {
|
||||||
|
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
texture_scale: texture_scale_from_resolution(new_size),
|
||||||
|
};
|
||||||
|
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
|
||||||
|
render_pass.set_pipeline(&self.pipeline);
|
||||||
|
render_pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
render_pass.draw(0..6, 0..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);
|
||||||
|
[
|
||||||
|
scale / res.width as f32,
|
||||||
|
scale / res.height as f32,
|
||||||
|
2.0 / scale,
|
||||||
|
0.0,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
struct Uniforms {
|
||||||
|
texture_scale: [f32; 4],
|
||||||
|
}
|
||||||
44
uw8-window/src/gpu/square.wgsl
Normal file
44
uw8-window/src/gpu/square.wgsl
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) tex_coords: vec2<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
texture_scale: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(
|
||||||
|
@builtin(vertex_index) in_vertex_index: u32,
|
||||||
|
) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
let i = in_vertex_index / 3u + in_vertex_index % 3u;
|
||||||
|
let x = 0.0 + f32(i % 2u) * 320.0;
|
||||||
|
let y = 0.0 + f32(i / 2u) * 240.0;
|
||||||
|
out.clip_position = vec4<f32>((vec2<f32>(x, y) - vec2<f32>(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0);
|
||||||
|
out.tex_coords = vec2<f32>(x, y);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var linear_sampler: sampler;
|
||||||
|
|
||||||
|
fn aa_tex_coord(c: f32) -> f32 {
|
||||||
|
let low = c - uniforms.texture_scale.z * 0.5;
|
||||||
|
let high = c + uniforms.texture_scale.z * 0.5;
|
||||||
|
let base = floor(low);
|
||||||
|
let center = base + 0.5;
|
||||||
|
let next = base + 1.0;
|
||||||
|
if high > next {
|
||||||
|
return center + (high - next) / (high - low);
|
||||||
|
} else {
|
||||||
|
return center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return textureSample(screen_texture, linear_sampler, vec2<f32>(aa_tex_coord(in.tex_coords.x), aa_tex_coord(in.tex_coords.y)) / vec2<f32>(320.0, 240.0));
|
||||||
|
}
|
||||||
117
uw8-window/src/lib.rs
Normal file
117
uw8-window/src/lib.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
mod cpu;
|
||||||
|
mod gpu;
|
||||||
|
|
||||||
|
pub struct Window {
|
||||||
|
inner: Box<dyn WindowImpl>,
|
||||||
|
fps_counter: Option<FpsCounter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FpsCounter {
|
||||||
|
start: Instant,
|
||||||
|
num_frames: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
pub fn new(config: WindowConfig) -> Result<Window> {
|
||||||
|
let fps_counter = if config.fps_counter {
|
||||||
|
Some(FpsCounter {
|
||||||
|
start: Instant::now(),
|
||||||
|
num_frames: 0,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if config.enable_gpu {
|
||||||
|
match gpu::Window::new(config) {
|
||||||
|
Ok(window) => {
|
||||||
|
return Ok(Window {
|
||||||
|
inner: Box::new(window),
|
||||||
|
fps_counter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(err) => eprintln!(
|
||||||
|
"Failed to create gpu window: {}\nFalling back tp cpu window",
|
||||||
|
err
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cpu::Window::new().map(|window| Window {
|
||||||
|
inner: Box::new(window),
|
||||||
|
fps_counter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_frame(&mut self) -> Input {
|
||||||
|
self.inner.begin_frame()
|
||||||
|
}
|
||||||
|
pub fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
|
||||||
|
self.inner.end_frame(framebuffer, palette, next_frame);
|
||||||
|
if let Some(ref mut fps_counter) = self.fps_counter {
|
||||||
|
fps_counter.num_frames += 1;
|
||||||
|
let elapsed = fps_counter.start.elapsed().as_secs_f32();
|
||||||
|
if elapsed >= 1.0 {
|
||||||
|
println!("fps: {:.1}", fps_counter.num_frames as f32 / elapsed);
|
||||||
|
fps_counter.num_frames = 0;
|
||||||
|
fps_counter.start = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.inner.is_open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct WindowConfig {
|
||||||
|
enable_gpu: bool,
|
||||||
|
filter: u32,
|
||||||
|
fullscreen: bool,
|
||||||
|
fps_counter: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WindowConfig {
|
||||||
|
fn default() -> WindowConfig {
|
||||||
|
WindowConfig {
|
||||||
|
enable_gpu: true,
|
||||||
|
filter: 5,
|
||||||
|
fullscreen: false,
|
||||||
|
fps_counter: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowConfig {
|
||||||
|
pub fn parse_arguments(&mut self, args: &mut pico_args::Arguments) {
|
||||||
|
self.enable_gpu = !args.contains("--no-gpu");
|
||||||
|
if let Some(filter) = args.opt_value_from_str::<_, String>("--filter").unwrap() {
|
||||||
|
self.filter = match filter.as_str() {
|
||||||
|
"1" | "nearest" => 1,
|
||||||
|
"2" | "fast_crt" => 2,
|
||||||
|
"3" | "ss_crt" => 3,
|
||||||
|
"4" | "chromatic" => 4,
|
||||||
|
"5" | "auto_crt" => 5,
|
||||||
|
o => {
|
||||||
|
println!("Unknown --filter '{}'", o);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.fullscreen = args.contains("--fullscreen");
|
||||||
|
self.fps_counter = args.contains("--fps");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Input {
|
||||||
|
pub gamepads: [u8; 4],
|
||||||
|
pub reset: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait WindowImpl {
|
||||||
|
fn begin_frame(&mut self) -> Input;
|
||||||
|
fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant);
|
||||||
|
fn is_open(&self) -> bool;
|
||||||
|
}
|
||||||
59
uw8-window/src/main.rs
Normal file
59
uw8-window/src/main.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use std::time::Instant;
|
||||||
|
use uw8_window::WindowConfig;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
|
|
||||||
|
let mut args = pico_args::Arguments::from_env();
|
||||||
|
|
||||||
|
let mut framebuffer = vec![0u8; 320 * 240];
|
||||||
|
let mut 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 fps_start = Instant::now();
|
||||||
|
let mut fps_counter = 0;
|
||||||
|
|
||||||
|
let mut window_config = WindowConfig::default();
|
||||||
|
window_config.parse_arguments(&mut args);
|
||||||
|
|
||||||
|
let mut window = uw8_window::Window::new(window_config).unwrap();
|
||||||
|
|
||||||
|
while window.is_open() {
|
||||||
|
let input = window.begin_frame();
|
||||||
|
if input.reset {
|
||||||
|
start_time = Instant::now();
|
||||||
|
}
|
||||||
|
draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32());
|
||||||
|
window.end_frame(&framebuffer, bytemuck::cast_slice(&palette), Instant::now());
|
||||||
|
|
||||||
|
fps_counter += 1;
|
||||||
|
let elapsed = fps_start.elapsed().as_secs_f32();
|
||||||
|
if elapsed >= 1.0 {
|
||||||
|
println!("{:.1} fps", fps_counter as f32 / elapsed);
|
||||||
|
fps_start = Instant::now();
|
||||||
|
fps_counter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
web/opus-repro.html
Normal file
57
web/opus-repro.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<html>
|
||||||
|
<button onclick="go()">Go!</button>
|
||||||
|
<canvas id="screen" width="320" height="240"></canvas>
|
||||||
|
<video id="video"></video>
|
||||||
|
<script>
|
||||||
|
function go() {
|
||||||
|
let audioContext = new AudioContext({sampleRate: 44100});
|
||||||
|
|
||||||
|
let oscillator = new OscillatorNode(audioContext);
|
||||||
|
let gain = new GainNode(audioContext, {gain: 1});
|
||||||
|
oscillator.connect(gain);
|
||||||
|
gain.connect(audioContext.destination);
|
||||||
|
for(let i = 0; i < 8; ++i ) {
|
||||||
|
gain.gain.setValueAtTime(1, i / 2);
|
||||||
|
gain.gain.setValueAtTime(0, i / 2 + 0.3);
|
||||||
|
}
|
||||||
|
oscillator.start();
|
||||||
|
oscillator.stop(4);
|
||||||
|
|
||||||
|
let screen = document.getElementById('screen');
|
||||||
|
let context = screen.getContext('2d');
|
||||||
|
let startTime = Date.now();
|
||||||
|
let drawFrame = () => {
|
||||||
|
let time = Date.now() - startTime;
|
||||||
|
context.fillStyle = 'white';
|
||||||
|
context.fillRect(0, 0, 320, 240);
|
||||||
|
if(time < 4000) {
|
||||||
|
if(time % 500 < 300) {
|
||||||
|
context.fillStyle = 'black';
|
||||||
|
context.fillRect(time / 15, 50, 50, 50);
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(drawFrame);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
drawFrame();
|
||||||
|
|
||||||
|
let stream = screen.captureStream();
|
||||||
|
let audioStreamNode = audioContext.createMediaStreamDestination();
|
||||||
|
gain.connect(audioStreamNode);
|
||||||
|
stream.addTrack(audioStreamNode.stream.getAudioTracks()[0]);
|
||||||
|
let recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
|
||||||
|
|
||||||
|
let chunks = [];
|
||||||
|
recorder.ondataavailable = e => chunks.push(e.data);
|
||||||
|
recorder.onstop = () => {
|
||||||
|
let blob = new Blob(chunks, {type: 'video/webm'});
|
||||||
|
let url = URL.createObjectURL(blob);
|
||||||
|
let video = document.getElementById('video');
|
||||||
|
video.src = url;
|
||||||
|
video.play();
|
||||||
|
};
|
||||||
|
|
||||||
|
recorder.start();
|
||||||
|
setTimeout(() => recorder.stop(), 4000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
@@ -3,13 +3,17 @@ class APU extends AudioWorkletProcessor {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.sampleIndex = 0;
|
this.sampleIndex = 0;
|
||||||
|
this.currentTime = 0;
|
||||||
|
this.isFirstMessage = true;
|
||||||
|
this.pendingUpdates = [];
|
||||||
this.port.onmessage = (ev) => {
|
this.port.onmessage = (ev) => {
|
||||||
if(this.memory) {
|
if(this.memory) {
|
||||||
if(isNaN(ev.data)) {
|
if(this.isFirstMessage)
|
||||||
U8(this.memory.buffer, 80, 32).set(U8(ev.data));
|
{
|
||||||
} else {
|
this.currentTime += (ev.data.t - this.currentTime) / 8;
|
||||||
this.startTime = ev.data;
|
this.isFirstMessage = false;
|
||||||
}
|
}
|
||||||
|
this.pendingUpdates.push(ev.data);
|
||||||
} else {
|
} else {
|
||||||
this.load(ev.data[0], ev.data[1]);
|
this.load(ev.data[0], ev.data[1]);
|
||||||
}
|
}
|
||||||
@@ -33,6 +37,16 @@ class APU extends AudioWorkletProcessor {
|
|||||||
importObject.env['reserved' + 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) {
|
for (let i = 0; i < 16; ++i) {
|
||||||
importObject.env['g_reserved' + i] = 0;
|
importObject.env['g_reserved' + i] = 0;
|
||||||
}
|
}
|
||||||
@@ -49,15 +63,19 @@ class APU extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
this.memory = memory;
|
this.memory = memory;
|
||||||
|
|
||||||
this.snd = instance.exports.snd || platform_instance.exports.gesSnd;
|
this.snd = instance.exports.snd || platform_instance.exports.sndGes;
|
||||||
|
|
||||||
this.port.postMessage(2);
|
this.port.postMessage(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
process(inputs, outputs, parameters) {
|
||||||
if(this.snd && this.startTime) {
|
this.isFirstMessage = true;
|
||||||
|
if(this.snd) {
|
||||||
|
while(this.pendingUpdates.length > 0 && this.pendingUpdates[0].t <= this.currentTime) {
|
||||||
|
U8(this.memory.buffer, 80, 32).set(U8(this.pendingUpdates.shift().r));
|
||||||
|
}
|
||||||
let u32Mem = new Uint32Array(this.memory.buffer);
|
let u32Mem = new Uint32Array(this.memory.buffer);
|
||||||
u32Mem[16] = Date.now() - this.startTime;
|
u32Mem[16] = this.currentTime;
|
||||||
let channels = outputs[0];
|
let channels = outputs[0];
|
||||||
let index = this.sampleIndex;
|
let index = this.sampleIndex;
|
||||||
let numSamples = channels[0].length;
|
let numSamples = channels[0].length;
|
||||||
@@ -66,6 +84,7 @@ class APU extends AudioWorkletProcessor {
|
|||||||
channels[1][i] = this.snd(index++);
|
channels[1][i] = this.snd(index++);
|
||||||
}
|
}
|
||||||
this.sampleIndex = index & 0xffffffff;
|
this.sampleIndex = index & 0xffffffff;
|
||||||
|
this.currentTime += numSamples / 44.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="uw8">
|
<div id="uw8">
|
||||||
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.1.2
|
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.2.1
|
||||||
</div>
|
</div>
|
||||||
<div id="centered">
|
<div id="centered">
|
||||||
<canvas class="screen" id="screen" width="320" height="240">
|
<canvas class="screen" id="screen" width="320" height="240">
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function MicroW8(screen, config = {}) {
|
|||||||
audioContext.close();
|
audioContext.close();
|
||||||
keepRunning = false;
|
keepRunning = false;
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
}
|
};
|
||||||
|
|
||||||
let cartridgeSize = data.byteLength;
|
let cartridgeSize = data.byteLength;
|
||||||
|
|
||||||
@@ -206,6 +206,16 @@ export default function MicroW8(screen, config = {}) {
|
|||||||
importObject.env['reserved' + 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) {
|
for (let i = 0; i < 16; ++i) {
|
||||||
importObject.env['g_reserved' + i] = 0;
|
importObject.env['g_reserved' + i] = 0;
|
||||||
}
|
}
|
||||||
@@ -232,7 +242,6 @@ export default function MicroW8(screen, config = {}) {
|
|||||||
let startTime = Date.now();
|
let startTime = Date.now();
|
||||||
|
|
||||||
const timePerFrame = 1000 / 60;
|
const timePerFrame = 1000 / 60;
|
||||||
let nextFrame = startTime;
|
|
||||||
|
|
||||||
audioNode.connect(audioContext.destination);
|
audioNode.connect(audioContext.destination);
|
||||||
|
|
||||||
@@ -244,18 +253,20 @@ export default function MicroW8(screen, config = {}) {
|
|||||||
isPaused = false;
|
isPaused = false;
|
||||||
audioContext.resume();
|
audioContext.resume();
|
||||||
startTime += now - pauseTime;
|
startTime += now - pauseTime;
|
||||||
audioNode.port.postMessage(startTime);
|
|
||||||
} else {
|
} else {
|
||||||
isPaused = true;
|
isPaused = true;
|
||||||
audioContext.suspend();
|
audioContext.suspend();
|
||||||
pauseTime = now;
|
pauseTime = now;
|
||||||
audioNode.port.postMessage(0);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('focus', () => updateVisibility(true), { signal: abortController.signal });
|
window.addEventListener('focus', () => updateVisibility(true), { signal: abortController.signal });
|
||||||
window.addEventListener('blur', () => updateVisibility(false), { signal: abortController.signal });
|
window.addEventListener('blur', () => updateVisibility(false), { signal: abortController.signal });
|
||||||
updateVisibility(document.hasFocus());
|
updateVisibility(document.hasFocus());
|
||||||
|
|
||||||
|
if (instance.exports.start) {
|
||||||
|
instance.exports.start();
|
||||||
|
}
|
||||||
|
|
||||||
function mainloop() {
|
function mainloop() {
|
||||||
if (!keepRunning) {
|
if (!keepRunning) {
|
||||||
return;
|
return;
|
||||||
@@ -263,6 +274,7 @@ export default function MicroW8(screen, config = {}) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let restart = false;
|
let restart = false;
|
||||||
|
let thisFrame;
|
||||||
if (!isPaused) {
|
if (!isPaused) {
|
||||||
let gamepads = navigator.getGamepads();
|
let gamepads = navigator.getGamepads();
|
||||||
let gamepad = 0;
|
let gamepad = 0;
|
||||||
@@ -291,7 +303,8 @@ export default function MicroW8(screen, config = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let u32Mem = U32(memory.buffer);
|
let u32Mem = U32(memory.buffer);
|
||||||
u32Mem[16] = Date.now() - startTime;
|
let time = Date.now() - startTime;
|
||||||
|
u32Mem[16] = time;
|
||||||
u32Mem[17] = pad | gamepad;
|
u32Mem[17] = pad | gamepad;
|
||||||
if(instance.exports.upd) {
|
if(instance.exports.upd) {
|
||||||
instance.exports.upd();
|
instance.exports.upd();
|
||||||
@@ -300,21 +313,26 @@ export default function MicroW8(screen, config = {}) {
|
|||||||
|
|
||||||
let soundRegisters = new ArrayBuffer(32);
|
let soundRegisters = new ArrayBuffer(32);
|
||||||
U8(soundRegisters).set(U8(memory.buffer, 80, 32));
|
U8(soundRegisters).set(U8(memory.buffer, 80, 32));
|
||||||
audioNode.port.postMessage(soundRegisters, [soundRegisters]);
|
audioNode.port.postMessage({t: time, r: soundRegisters}, [soundRegisters]);
|
||||||
|
|
||||||
let palette = U32(memory.buffer, 0x13000, 1024);
|
let palette = U32(memory.buffer, 0x13000, 1024);
|
||||||
for (let i = 0; i < 320 * 240; ++i) {
|
for (let i = 0; i < 320 * 240; ++i) {
|
||||||
buffer[i] = palette[memU8[i + 120]] | 0xff000000;
|
buffer[i] = palette[memU8[i + 120]] | 0xff000000;
|
||||||
}
|
}
|
||||||
canvasCtx.putImageData(imageData, 0, 0);
|
canvasCtx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
let timeOffset = ((time * 6) % 100 - 50) / 6;
|
||||||
|
thisFrame = startTime + time - timeOffset / 8;
|
||||||
|
} else {
|
||||||
|
thisFrame = Date.now();
|
||||||
}
|
}
|
||||||
let now = Date.now();
|
let now = Date.now();
|
||||||
nextFrame = Math.max(nextFrame + timePerFrame, now);
|
let nextFrame = Math.max(thisFrame + timePerFrame, now);
|
||||||
|
|
||||||
if (restart) {
|
if (restart) {
|
||||||
runModule(currentData);
|
runModule(currentData);
|
||||||
} else {
|
} else {
|
||||||
window.setTimeout(mainloop, Math.round(nextFrame - now))
|
window.setTimeout(mainloop, nextFrame - now)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
config.setMessage(cartridgeSize, err.toString());
|
config.setMessage(cartridgeSize, err.toString());
|
||||||
|
|||||||
1158
web/yarn.lock
1158
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user