12 Commits
30 changed files with 4432 additions and 922 deletions

No files matched your search

Generated
+967 -216
View File
File diff suppressed because it is too large. Load diff
+4 -5
View File
@@ -7,16 +7,16 @@ edition = "2021"
[features]
default = ["native", "browser"]
native = ["wasmtime", "minifb", "cpal", "rubato", "winapi" ]
native = ["wasmtime", "uw8-window", "cpal", "rubato" ]
browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
[dependencies]
wasmtime = { version = "0.35.3", optional = true }
wasmtime = { version = "0.37.0", optional = true }
anyhow = "1"
minifb = { version = "0.22", default-features = false, features = ["x11"], optional = true }
uw8-window = { path = "uw8-window", optional = true }
notify = "4"
pico-args = "0.4"
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0a0d90c" }
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0e7ea50" }
wat = "1"
uw8-tool = { path = "uw8-tool" }
same-file = "1"
@@ -27,4 +27,3 @@ webbrowser = { version = "0.6.0", optional = true }
ansi_term = "0.12.1"
cpal = { version = "0.13.5", optional = true }
rubato = { version = "0.11.0", optional = true }
winapi = { version = "0.3.9", features = ["timeapi"], optional = true }
+74
View 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
)
)
)
+1 -1
View File
@@ -146,7 +146,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "curlywas"
version = "0.1.0"
source = "git+https://github.com/exoticorn/curlywas.git?rev=0a0d90c#0a0d90c8013f1d7856a9b71a19d3d006300f31f9"
source = "git+https://github.com/exoticorn/curlywas.git?rev=0e7ea50#0e7ea508cd0e76836283ae68a44c9097df83c8ac"
dependencies = [
"anyhow",
"ariadne",
+1 -1
View File
@@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="0a0d90c" }
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="0e7ea50" }
uw8-tool = { path="../uw8-tool" }
anyhow = "1"
lodepng = "3.4"
Binary file not shown.
+65 -18
View File
@@ -62,11 +62,7 @@ export fn cls(col: i32) {
textCursorX = 0;
textCursorY = 0;
outputChannel = 0;
col = (col & 255) * 0x1010101;
loop pixels {
i!120 = col;
branch_if (i := i + 4) < 320*240: pixels;
}
memory.fill(120, col, 320*240);
}
export fn setPixel(x: i32, y: i32, col: i32) {
@@ -90,12 +86,70 @@ fn clamp(v: i32, min: i32, max: i32) -> i32 {
export fn hline(x1: i32, x2: i32, y: i32, col: i32) {
x1 = clamp(x1, 0, 320);
x2 = clamp(x2, 0, 320);
if x1 < x2 & y #< 240 {
if y #>= 240 {
return;
}
let word_start = (x1 + 3) & -4;
let word_end = x2 & -4;
if word_end > word_start {
col = (col & 255) * 0x1010101;
let ptr = y * 320 + x1;
let end = ptr + word_start - x1;
if ptr + 2 <= end {
ptr?120 = col;
ptr?121 = col;
ptr += 2;
}
if ptr < end {
ptr?120 = col;
ptr += 1;
}
end += word_end - word_start;
loop words {
if ptr + 16 <= end {
ptr!120 = col;
ptr!124 = col;
ptr!128 = col;
ptr!132 = col;
ptr += 16;
branch words;
}
if ptr + 8 <= end {
ptr!120 = col;
ptr!124 = col;
ptr += 8;
}
if ptr < end {
ptr!120 = col;
ptr += 4;
}
}
end += x2 - word_end;
if ptr + 2 <= end {
ptr?120 = col;
ptr?121 = col;
ptr += 2;
}
if ptr < end {
ptr?120 = col;
}
} else {
let ptr = y * 320 + x1;
let end = ptr + x2 - x1;
loop pixels {
if ptr + 4 <= end {
ptr?120 = col;
ptr?121 = col;
ptr?122 = col;
ptr?123 = col;
ptr += 4;
}
if ptr + 2 <= end {
ptr?120 = col;
ptr?121 = col;
ptr += 2;
}
if ptr < end {
ptr?120 = col;
branch_if (ptr := ptr + 1) < end: pixels;
}
}
}
@@ -548,13 +602,6 @@ export fn endFrame() {
68!4 = 68!0;
}
fn memclr(base: i32, size: i32) {
loop bytes {
(base + (size := size - 1))?0 = 0;
branch_if size: bytes;
}
}
start fn setup() {
let i: i32 = 12*16*3-1;
let avg: f32;
@@ -587,9 +634,9 @@ start fn setup() {
branch_if (i := i - 1) >= 0: expand_sweetie;
}
memclr(0, 64);
memclr(112, 8);
memclr(0x14000, 0x2c000);
memory.fill(0, 0, 64);
memory.fill(112, 0, 8);
memory.fill(0x14000, 0, 0x2c000);
cls(0);
+5 -5
View File
@@ -41,11 +41,11 @@ 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`)
* 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
+9 -9
View File
@@ -14,11 +14,11 @@ 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`)
* 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
@@ -33,10 +33,10 @@ Changes:
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: `+:=`)
* 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
+1 -4
View File
@@ -14,9 +14,6 @@ use anyhow::Result;
pub trait Runtime {
fn is_open(&self) -> bool;
fn set_timeout(&mut self, _timeout: u32) {
eprintln!("Warning: runtime doesn't support timeout");
}
fn load(&mut self, module_data: &[u8]) -> Result<()>;
fn run_frame(&mut self) -> Result<()>;
}
}
+9 -7
View File
@@ -35,7 +35,7 @@ fn main() -> Result<()> {
println!();
println!("Usage:");
#[cfg(any(feature = "native", feature = "browser"))]
println!(" uw8 run [-t/--timeout <frames>] [--b/--browser] [-w/--watch] [-p/--pack] [-u/--uncompressed] [-l/--level] [-o/--output <out-file>] <file>");
println!(" uw8 run [-t/--timeout <frames>] [--no-gpu] [--b/--browser] [-w/--watch] [-p/--pack] [-u/--uncompressed] [-l/--level] [-o/--output <out-file>] <file>");
println!(" uw8 pack [-u/--uncompressed] [-l/--level] <in-file> <out-file>");
println!(" uw8 unpack <in-file> <out-file>");
println!(" uw8 compile [-d/--debug] <in-file> <out-file>");
@@ -52,7 +52,10 @@ fn main() -> Result<()> {
#[cfg(any(feature = "native", feature = "browser"))]
fn run(mut args: Arguments) -> Result<()> {
let watch_mode = args.contains(["-w", "--watch"]);
#[allow(unused)]
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
#[allow(unused)]
let gpu = !args.contains("--no-gpu");
let mut config = Config::default();
if args.contains(["-p", "--pack"]) {
@@ -92,7 +95,7 @@ fn run(mut args: Arguments) -> Result<()> {
unimplemented!();
#[cfg(feature = "native")]
{
let mut microw8 = MicroW8::new()?;
let mut microw8 = MicroW8::new(timeout, gpu)?;
if disable_audio {
microw8.disable_audio();
}
@@ -105,10 +108,6 @@ fn run(mut args: Arguments) -> Result<()> {
Box::new(RunWebServer::new())
};
if let Some(timeout) = timeout {
runtime.set_timeout(timeout);
}
let mut first_run = true;
while runtime.is_open() {
@@ -167,7 +166,10 @@ fn load_cart(filename: &Path, config: &Config) -> (Result<Vec<u8>>, Vec<PathBuf>
if let Some(ref pack_config) = config.pack {
cart = uw8_tool::pack(&cart, pack_config)?;
println!("packed size: {} bytes", cart.len());
println!(
"\npacked size: {:.2} bytes",
uw8_tool::compressed_size(&cart)
);
}
if let Some(ref path) = config.output_path {
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html><head><meta charset="utf8"><title>uw8-run</title><style>body,canvas,html{background-color:#202024;margin:0;padding:0}body,html{height:100%}body{display:grid;grid-template-rows:1fr}#screen{align-self:center;border:4px solid #303040;image-rendering:pixelated;justify-self:center}#message{background-color:rgba(0,0,0,.4);color:#c64;font:700 12pt sans-serif;padding:8px;position:absolute;width:calc(100% - 16px);z-index:2}@media (min-width:648px) and (min-height:488px){#screen{height:480px;width:640px}}@media (min-width:968px) and (min-height:728px){#screen{height:720px;width:960px}}</style></head><body> <canvas id="screen" width="320" height="240" tabindex="1"> </canvas> <div id="message"></div> </body><script type="module">function e(e){return e&&e.__esModule?e.default:e}class t extends AudioWorkletNode{constructor(e){super(e,"apu",{outputChannelCount:[2]})}}let A=(...e)=>new Uint8Array(...e),s=(...e)=>new Uint32Array(...e);let a=function(a,B={}){B.setMessage||(B.setMessage=(e,t)=>{t&&console.log("error: "+t)});let n,r,o,i,l,d,u,g,D=a.getContext("2d"),c=D.createImageData(320,240),F=B.devkitMode,f=0,C=null==B.keyboardElement?a:B.keyboardElement;if(C){let e=e=>{let t,A="keydown"==e.type;switch(e.code){case"ArrowUp":t=1;break;case"ArrowDown":t=2;break;case"ArrowLeft":t=4;break;case"ArrowRight":t=8;break;case"KeyZ":t=16;break;case"KeyX":t=32;break;case"KeyA":t=64;break;case"KeyS":t=128;break;case"KeyR":A&&E(r,!0);break;case"F9":A&&a.toBlob((e=>{Q(e,".png")})),e.preventDefault();break;case"F10":A&&function(){if(l)return l.stop(),l=null,u.disconnect(g),u=null,void(g=null);let e=a.captureStream();g=o.createMediaStreamDestination(),u=i,i.connect(g),e.addTrack(g.stream.getAudioTracks()[0]),l=new MediaRecorder(e,{mimeType:"video/webm",videoBitsPerSecond:25e6});let t=[];l.ondataavailable=e=>{t.push(e.data)};let A=B.timerElement;A&&(A.hidden=!1,A.innerText="00:00");function s(){if(d){if(A){let e=Math.floor((Date.now()-d)/1e3);A.innerText=Math.floor(e/60).toString().padStart(2,"0")+":"+(e%60).toString().padStart(2,"0")}setTimeout(s,1e3)}}l.onstop=()=>{A&&(A.hidden=!0),Q(new Blob(t,{type:"video/webm"}),".webm")},l.start(),d=Date.now(),setTimeout(s,1e3)}(),e.preventDefault()}A?f|=t:f&=~t};C.onkeydown=e,C.onkeyup=e}async function E(l,d){n&&(n(),n=null),o=new AudioContext({sampleRate:44100});let u=!0,g=new AbortController;n=()=>{o.close(),u=!1,g.abort()};let C=l.byteLength;if(B.setMessage(C),0==C)return;await o.audioWorklet.addModule(e("data:application/javascript,let%20e%3D%28...e%29%3D%3Enew%20Uint8Array%28...e%29%3Bclass%20s%20extends%20AudioWorkletProcessor%7Bconstructor%28%29%7Bsuper%28%29%2Cthis.sampleIndex%3D0%2Cthis.currentTime%3D0%2Cthis.isFirstMessage%3D%210%2Cthis.pendingUpdates%3D%5B%5D%2Cthis.port.onmessage%3De%3D%3E%7Bthis.memory%3F%28this.isFirstMessage%26%26%28this.currentTime%2B%3D%28e.data.t-this.currentTime%29%2F8%2Cthis.isFirstMessage%3D%211%29%2Cthis.pendingUpdates.push%28e.data%29%29%3Athis.load%28e.data%5B0%5D%2Ce.data%5B1%5D%29%7D%7Dasync%20load%28e%2Cs%29%7Blet%20t%3Dnew%20WebAssembly.Memory%28%7Binitial%3A4%2Cmaximum%3A4%7D%29%2Ci%3D%7Benv%3A%7Bmemory%3At%7D%7D%3Bfor%28let%20e%20of%5B%22acos%22%2C%22asin%22%2C%22atan%22%2C%22atan2%22%2C%22cos%22%2C%22exp%22%2C%22log%22%2C%22sin%22%2C%22tan%22%2C%22pow%22%5D%29i.env%5Be%5D%3DMath%5Be%5D%3Bfor%28let%20e%3D9%3Be%3C64%3B%2B%2Be%29i.env%5B%22reserved%22%2Be%5D%3D%28%29%3D%3E%7B%7D%3Blet%20r%3D%22%22%3Bi.env.logChar%3De%3D%3E%7B10%3D%3De%3F%28console.log%28r%29%2Cr%3D%22%22%29%3Ar%2B%3DString.fromCharCode%28e%29%7D%3Bfor%28let%20e%3D0%3Be%3C16%3B%2B%2Be%29i.env%5B%22g_reserved%22%2Be%5D%3D0%3Blet%20n%3Dasync%20e%3D%3E%28await%20WebAssembly.instantiate%28e%2Ci%29%29.instance%2Ca%3Dawait%20n%28e%29%3Bfor%28let%20e%20in%20a.exports%29i.env%5Be%5D%3Da.exports%5Be%5D%3Blet%20o%3Dawait%20n%28s%29%3Bthis.memory%3Dt%2Cthis.snd%3Do.exports.snd%7C%7Ca.exports.gesSnd%2Cthis.port.postMessage%282%29%7Dprocess%28s%2Ct%2Ci%29%7Bif%28this.isFirstMessage%3D%210%2Cthis.snd%29%7Bfor%28%3Bthis.pendingUpdates.length%3E0%26%26this.pendingUpdates%5B0%5D.t%3C%3Dthis.currentTime%3B%29e%28this.memory.buffer%2C80%2C32%29.set%28e%28this.pendingUpdates.shift%28%29.r%29%29%3Bnew%20Uint32Array%28this.memory.buffer%29%5B16%5D%3Dthis.currentTime%3Blet%20s%3Dt%5B0%5D%2Ci%3Dthis.sampleIndex%2Cr%3Ds%5B0%5D.length%3Bfor%28let%20e%3D0%3Be%3Cr%3B%2B%2Be%29s%5B0%5D%5Be%5D%3Dthis.snd%28i%2B%2B%29%2Cs%5B1%5D%5Be%5D%3Dthis.snd%28i%2B%2B%29%3Bthis.sampleIndex%3D4294967295%26i%2Cthis.currentTime%2B%3Dr%2F44.1%7Dreturn%210%7D%7DregisterProcessor%28%22apu%22%2Cs%29%3B")),i=new t(o);let Q,m=0,I=new Promise((e=>Q=e)),b=e=>{m|=e,3==m&&Q&&(Q(!0),Q=null)},h=()=>{"suspended"==o.state?(B.startButton&&(B.startButton.style="",a.style="display:none"),(B.startButton||a).onclick=()=>{o.resume()}):(B.startButton&&(B.startButton.style="display:none",a.style=""),b(1))};o.onstatechange=h,h(),r=l;let w=window.location.pathname;if(C<=1024&&!d){let e="";for(let t of A(l))e+=String.fromCharCode(t);w+="#"Line truncated
<!doctype html><html><head><meta charset="utf8"><title>uw8-run</title><style>html,body,canvas{background-color:#202024;margin:0;padding:0}html{height:100%}body{height:100%;grid-template-rows:1fr;display:grid}#screen{image-rendering:pixelated;border:4px solid #303040;place-self:center}#message{width:calc(100% - 16px);color:#c64;z-index:2;background-color:#0006;padding:8px;font:700 12pt sans-serif;position:absolute}@media (min-width:648px) and (min-height:488px){#screen{width:640px;height:480px}}@media (min-width:968px) and (min-height:728px){#screen{width:960px;height:720px}}</style></head><body> <canvas id="screen" width="320" height="240" tabindex="1"> </canvas> <div id="message"></div> </body><script type="module">function e(e){return e&&e.__esModule?e.default:e}class t extends AudioWorkletNode{constructor(e){super(e,"apu",{outputChannelCount:[2]})}}let A=(...e)=>new Uint8Array(...e),a=(...e)=>new Uint32Array(...e);let s=function(s,B={}){B.setMessage||(B.setMessage=(e,t)=>{t&&console.log("error: "+t)});let n,r,o,i,l,F,d,g,C=s.getContext("2d"),u=C.createImageData(320,240),D=B.devkitMode,c=0,f=null==B.keyboardElement?s:B.keyboardElement;if(f){let e=e=>{let t,A="keydown"==e.type;switch(e.code){case"ArrowUp":t=1;break;case"ArrowDown":t=2;break;case"ArrowLeft":t=4;break;case"ArrowRight":t=8;break;case"KeyZ":t=16;break;case"KeyX":t=32;break;case"KeyA":t=64;break;case"KeyS":t=128;break;case"KeyR":A&&w(r,!0);break;case"F9":A&&s.toBlob((e=>{I(e,".png")})),e.preventDefault();break;case"F10":A&&function(){if(l)return l.stop(),l=null,d.disconnect(g),d=null,void(g=null);let e=s.captureStream();g=o.createMediaStreamDestination(),d=i,i.connect(g),e.addTrack(g.stream.getAudioTracks()[0]),l=new MediaRecorder(e,{mimeType:"video/webm",videoBitsPerSecond:25e6});let t=[];l.ondataavailable=e=>{t.push(e.data)};let A=B.timerElement;A&&(A.hidden=!1,A.innerText="00:00");function a(){if(F){if(A){let e=Math.floor((Date.now()-F)/1e3);A.innerText=Math.floor(e/60).toString().padStart(2,"0")+":"+(e%60).toString().padStart(2,"0")}setTimeout(a,1e3)}}l.onstop=()=>{A&&(A.hidden=!0),I(new Blob(t,{type:"video/webm"}),".webm")},l.start(),F=Date.now(),setTimeout(a,1e3)}(),e.preventDefault()}A?c|=t:c&=~t};f.onkeydown=e,f.onkeyup=e}async function w(l,F){n&&(n(),n=null),o=new AudioContext({sampleRate:44100});let d=!0,g=new AbortController;n=()=>{o.close(),d=!1,g.abort()};let f=l.byteLength;if(B.setMessage(f),0==f)return;await o.audioWorklet.addModule(e("data:application/javascript,let%20e%3D%28...e%29%3D%3Enew%20Uint8Array%28...e%29%3Bclass%20s%20extends%20AudioWorkletProcessor%7Bconstructor%28%29%7Bsuper%28%29%2Cthis.sampleIndex%3D0%2Cthis.currentTime%3D0%2Cthis.isFirstMessage%3D%210%2Cthis.pendingUpdates%3D%5B%5D%2Cthis.port.onmessage%3De%3D%3E%7Bthis.memory%3F%28this.isFirstMessage%26%26%28this.currentTime%2B%3D%28e.data.t-this.currentTime%29%2F8%2Cthis.isFirstMessage%3D%211%29%2Cthis.pendingUpdates.push%28e.data%29%29%3Athis.load%28e.data%5B0%5D%2Ce.data%5B1%5D%29%7D%7Dasync%20load%28e%2Cs%29%7Blet%20t%3Dnew%20WebAssembly.Memory%28%7Binitial%3A4%2Cmaximum%3A4%7D%29%2Ci%3D%7Benv%3A%7Bmemory%3At%7D%7D%3Bfor%28let%20e%20of%5B%22acos%22%2C%22asin%22%2C%22atan%22%2C%22atan2%22%2C%22cos%22%2C%22exp%22%2C%22log%22%2C%22sin%22%2C%22tan%22%2C%22pow%22%5D%29i.env%5Be%5D%3DMath%5Be%5D%3Bfor%28let%20e%3D9%3Be%3C64%3B%2B%2Be%29i.env%5B%22reserved%22%2Be%5D%3D%28%29%3D%3E%7B%7D%3Blet%20r%3D%22%22%3Bi.env.logChar%3De%3D%3E%7B10%3D%3De%3F%28console.log%28r%29%2Cr%3D%22%22%29%3Ar%2B%3DString.fromCharCode%28e%29%7D%3Bfor%28let%20e%3D0%3Be%3C16%3B%2B%2Be%29i.env%5B%22g_reserved%22%2Be%5D%3D0%3Blet%20n%3Dasync%20e%3D%3E%28await%20WebAssembly.instantiate%28e%2Ci%29%29.instance%2Ca%3Dawait%20n%28e%29%3Bfor%28let%20e%20in%20a.exports%29i.env%5Be%5D%3Da.exports%5Be%5D%3Blet%20o%3Dawait%20n%28s%29%3Bthis.memory%3Dt%2Cthis.snd%3Do.exports.snd%7C%7Ca.exports.gesSnd%2Cthis.port.postMessage%282%29%7Dprocess%28s%2Ct%2Ci%29%7Bif%28this.isFirstMessage%3D%210%2Cthis.snd%29%7Bfor%28%3Bthis.pendingUpdates.length%3E0%26%26this.pendingUpdates%5B0%5D.t%3C%3Dthis.currentTime%3B%29e%28this.memory.buffer%2C80%2C32%29.set%28e%28this.pendingUpdates.shift%28%29.r%29%29%3Bnew%20Uint32Array%28this.memory.buffer%29%5B16%5D%3Dthis.currentTime%3Blet%20s%3Dt%5B0%5D%2Ci%3Dthis.sampleIndex%2Cr%3Ds%5B0%5D.length%3Bfor%28let%20e%3D0%3Be%3Cr%3B%2B%2Be%29s%5B0%5D%5Be%5D%3Dthis.snd%28i%2B%2B%29%2Cs%5B1%5D%5Be%5D%3Dthis.snd%28i%2B%2B%29%3Bthis.sampleIndex%3D4294967295%26i%2Cthis.currentTime%2B%3Dr%2F44.1%7Dreturn%210%7D%7DregisterProcessor%28%22apu%22%2Cs%29%3B")),i=new t(o);let I,h=0,Q=new Promise((e=>I=e)),m=e=>{h|=e,3==h&&I&&(I(!0),I=null)},M=()=>{"suspended"==o.state?(B.startButton&&(B.startButton.style="",s.style="display:none"),(B.startButton||s).onclick=()=>{o.resume()}):(B.startButton&&(B.startButton.style="display:none",s.style=""),m(1))};o.onstatechange=M,M(),r=l;let E=window.location.pathname;if(f<=1024&&!F){let e="";for(let t of A(l))e+=String.fromCharCode(t);E+="#"+btoa(e),E!=window.locLine truncated
+105 -118
View File
@@ -4,31 +4,24 @@ use std::{thread, time::Instant};
use anyhow::{anyhow, Result};
use cpal::traits::*;
use minifb::{Key, Window, WindowOptions};
use rubato::Resampler;
use wasmtime::{
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
};
static GAMEPAD_KEYS: &[Key] = &[
Key::Up,
Key::Down,
Key::Left,
Key::Right,
Key::Z,
Key::X,
Key::A,
Key::S,
];
pub struct MicroW8 {
tx: mpsc::SyncSender<Option<UW8Instance>>,
rx: mpsc::Receiver<UIEvent>,
stream: Option<cpal::Stream>,
engine: Engine,
loader_module: Module,
window: Window,
window_buffer: Vec<u32>,
instance: Option<UW8Instance>,
timeout: u32,
disable_audio: bool,
module_data: Option<Vec<u8>>,
}
enum UIEvent {
Error(Result<()>),
Reset,
}
struct UW8Instance {
@@ -37,10 +30,8 @@ struct UW8Instance {
end_frame: TypedFunc<(), ()>,
update: Option<TypedFunc<(), ()>>,
start_time: Instant,
next_frame: Instant,
module: Vec<u8>,
watchdog: Arc<Mutex<UW8WatchDog>>,
sound: Option<Uw8Sound>,
sound_tx: Option<mpsc::SyncSender<RegisterUpdate>>,
}
impl Drop for UW8Instance {
@@ -52,47 +43,58 @@ impl Drop for UW8Instance {
}
struct UW8WatchDog {
interupt: wasmtime::InterruptHandle,
timeout: u32,
engine: Engine,
stop: bool,
}
impl MicroW8 {
pub fn new() -> Result<MicroW8> {
#[cfg(target_os = "windows")]
unsafe { winapi::um::timeapi::timeBeginPeriod(1); }
let engine = wasmtime::Engine::new(wasmtime::Config::new().interruptable(true))?;
pub fn new(timeout: Option<u32>, gpu: bool) -> Result<MicroW8> {
let mut config = wasmtime::Config::new();
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
if timeout.is_some() {
config.epoch_interruption(true);
}
let engine = wasmtime::Engine::new(&config)?;
let loader_module =
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
let options = WindowOptions {
scale: minifb::Scale::X2,
scale_mode: minifb::ScaleMode::AspectRatioStretch,
resize: true,
..Default::default()
};
let window = Window::new("MicroW8", 320, 240, options)?;
let (to_ui_tx, to_ui_rx) = mpsc::sync_channel(2);
let (from_ui_tx, from_ui_rx) = mpsc::sync_channel(1);
std::thread::spawn(move || {
let mut state = State {
instance: None,
timeout: timeout.unwrap_or(0),
};
uw8_window::run(gpu, move |framebuffer, gamepad, reset| {
while let Ok(instance) = to_ui_rx.try_recv() {
state.instance = instance;
}
if reset {
from_ui_tx.send(UIEvent::Reset).unwrap();
}
state.run_frame(framebuffer, gamepad).unwrap_or_else(|err| {
from_ui_tx.send(UIEvent::Error(Err(err))).unwrap();
Instant::now()
})
});
});
Ok(MicroW8 {
tx: to_ui_tx,
rx: from_ui_rx,
stream: None,
engine,
loader_module,
window,
window_buffer: vec![0u32; 320 * 240],
instance: None,
timeout: 30,
disable_audio: false,
module_data: None,
})
}
fn reset(&mut self) {
self.instance = None;
for v in &mut self.window_buffer {
*v = 0;
}
}
pub fn disable_audio(&mut self) {
self.disable_audio = true;
}
@@ -100,17 +102,15 @@ impl MicroW8 {
impl super::Runtime for MicroW8 {
fn is_open(&self) -> bool {
self.window.is_open() && !self.window.is_key_down(Key::Escape)
}
fn set_timeout(&mut self, timeout: u32) {
self.timeout = timeout;
true
}
fn load(&mut self, module_data: &[u8]) -> Result<()> {
self.reset();
self.stream = None;
self.tx.send(None)?;
let mut store = wasmtime::Store::new(&self.engine, ());
store.set_epoch_deadline(60);
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
@@ -136,8 +136,7 @@ impl super::Runtime for MicroW8 {
let platform_instance = instantiate_platform(&mut linker, &mut store, &platform_module)?;
let watchdog = Arc::new(Mutex::new(UW8WatchDog {
interupt: store.interrupt_handle()?,
timeout: self.timeout,
engine: self.engine.clone(),
stop: false,
}));
@@ -145,16 +144,11 @@ impl super::Runtime for MicroW8 {
let watchdog = watchdog.clone();
thread::spawn(move || loop {
thread::sleep(Duration::from_millis(17));
if let Ok(mut watchdog) = watchdog.lock() {
if let Ok(watchdog) = watchdog.lock() {
if watchdog.stop {
break;
}
if watchdog.timeout > 0 {
watchdog.timeout -= 1;
if watchdog.timeout == 0 {
watchdog.interupt.interrupt();
}
}
watchdog.engine.increment_epoch();
} else {
break;
}
@@ -162,84 +156,87 @@ impl super::Runtime for MicroW8 {
}
let instance = linker.instantiate(&mut store, &module)?;
if let Ok(mut watchdog) = watchdog.lock() {
watchdog.timeout = 0;
}
let end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?;
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd").ok();
let sound = if self.disable_audio {
None
let (sound_tx, stream) = if self.disable_audio {
(None, None)
} else {
match init_sound(&self.engine, &platform_module, &module) {
Ok(sound) => {
sound.stream.play()?;
Some(sound)
(Some(sound.tx), Some(sound.stream))
}
Err(err) => {
eprintln!("Failed to init sound: {}", err);
None
(None, None)
}
}
};
self.instance = Some(UW8Instance {
self.tx.send(Some(UW8Instance {
store,
memory,
end_frame,
update,
start_time: Instant::now(),
next_frame: Instant::now(),
module: module_data.into(),
watchdog,
sound,
});
sound_tx,
}))?;
self.stream = stream;
self.module_data = Some(module_data.into());
Ok(())
}
fn run_frame(&mut self) -> Result<()> {
let mut result = Ok(());
if let Some(mut instance) = self.instance.take() {
{
if let Some(sleep) = instance.next_frame.checked_duration_since(Instant::now()) {
std::thread::sleep(sleep);
if let Ok(event) = self.rx.try_recv() {
match event {
UIEvent::Error(err) => err,
UIEvent::Reset => {
if let Some(module_data) = self.module_data.take() {
self.load(&module_data)
} else {
Ok(())
}
}
}
} else {
Ok(())
}
}
}
let now = Instant::now();
struct State {
instance: Option<UW8Instance>,
timeout: u32,
}
impl State {
fn run_frame(
&mut self,
framebuffer: &mut dyn uw8_window::Framebuffer,
gamepad: u32,
) -> Result<Instant> {
let now = Instant::now();
let mut result = Ok(now);
if let Some(mut instance) = self.instance.take() {
let time = (now - instance.start_time).as_millis() as i32;
{
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6;
instance.next_frame = now + Duration::from_millis((16 - offset) as u64);
result = Ok(now + Duration::from_millis((16 - offset) as u64));
}
{
let mut gamepad: u32 = 0;
for key in self.window.get_keys() {
if let Some(index) = GAMEPAD_KEYS
.iter()
.enumerate()
.find(|(_, &k)| k == key)
.map(|(i, _)| i)
{
gamepad |= 1 << index;
}
}
let mem = instance.memory.data_mut(&mut instance.store);
mem[64..68].copy_from_slice(&time.to_le_bytes());
mem[68..72].copy_from_slice(&gamepad.to_le_bytes());
}
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = self.timeout;
}
instance.store.set_epoch_deadline(self.timeout as u64);
if let Some(ref update) = instance.update {
result = update.call(&mut instance.store, ());
}
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = 0;
if let Err(err) = update.call(&mut instance.store, ()) {
result = Err(err);
}
}
instance.end_frame.call(&mut instance.store, ())?;
@@ -247,35 +244,23 @@ impl super::Runtime for MicroW8 {
let mut sound_regs = [0u8; 32];
sound_regs.copy_from_slice(&memory[80..112]);
if let Some(ref sound) = instance.sound {
sound.tx.send(RegisterUpdate {
if let Some(ref sound_tx) = instance.sound_tx {
let _ = sound_tx.send(RegisterUpdate {
time,
data: sound_regs,
})?;
});
}
let framebuffer = &memory[120..(120 + 320 * 240)];
let palette = &memory[0x13000..];
for (i, &color_index) in framebuffer.iter().enumerate() {
let offset = color_index as usize * 4;
self.window_buffer[i] = 0xff000000
| ((palette[offset] as u32) << 16)
| ((palette[offset + 1] as u32) << 8)
| palette[offset + 2] as u32;
}
let framebuffer_mem = &memory[120..(120 + 320 * 240)];
let palette_mem = &memory[0x13000..];
framebuffer.update(framebuffer_mem, palette_mem);
if self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No) {
self.load(&instance.module)?;
} else if result.is_ok() {
if result.is_ok() {
self.instance = Some(instance);
}
}
self.window
.update_with_buffer(&self.window_buffer, 320, 240)?;
result?;
Ok(())
Ok(result?)
}
}
@@ -357,6 +342,7 @@ fn init_sound(
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)))?;
@@ -450,6 +436,7 @@ fn init_sound(
}
while !outer_buffer.is_empty() {
store.set_epoch_deadline(30);
while pending_updates
.first()
.into_iter()
+62
View File
@@ -0,0 +1,62 @@
include "../examples/include/microw8-api.cwa"
global mut counter = 0;
export fn upd() {
cls(0);
let col: i32 = 1;
loop colors {
if !testCircle(counter, col) {
printInt(counter);
return;
}
counter += 1;
branch_if (col +:= 1) < 256: colors;
}
}
fn testCircle(seed: i32, col: i32) -> i32 {
randomSeed(seed);
let cx = randomf() * 640_f - 160_f;
let cy = randomf() * 480_f - 120_f;
let radius = randomf() * 4_f;
radius *= radius;
radius *= radius;
circle(cx, cy, radius, col);
let min_x = max(0_f, floor(cx - radius - 1_f)) as i32;
let min_y = max(0_f, floor(cy - radius - 1_f)) as i32;
let max_x = min(320_f, ceil(cx + radius + 1_f)) as i32;
let max_y = min(240_f, ceil(cy + radius + 1_f)) as i32;
let x = min_x;
loop xloop {
if x < max_x {
let y = min_y;
loop yloop {
if y < max_y {
let rx = x as f32 + 0.5 - cx;
let ry = y as f32 + 0.5 - cy;
let d = sqrt(rx*rx + ry*ry) - radius;
if abs(d) > 0.001 {
let is_inside = d < 0_f;
let is_plotted = getPixel(x, y) == col;
if is_inside != is_plotted {
return 0;
}
}
y += 1;
branch yloop;
}
}
x += 1;
branch xloop;
}
}
1
}
+1 -1
View File
@@ -189,7 +189,7 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "upkr"
version = "0.1.0"
source = "git+https://github.com/exoticorn/upkr.git?rev=2e7983fc#2e7983fc650788d98da2eecef2d16f63e849e4a0"
source = "git+https://github.com/exoticorn/upkr.git?rev=d93aec186c9fb91d962c488682a2db125c61306c#d93aec186c9fb91d962c488682a2db125c61306c"
dependencies = [
"anyhow",
"cdivsufsort",
+1 -1
View File
@@ -11,5 +11,5 @@ wasm-encoder = "0.8"
walrus = "0.19"
anyhow = "1"
pico-args = "0.4"
upkr = { git = "https://github.com/exoticorn/upkr.git", rev = "2e7983fc" }
upkr = { git = "https://github.com/exoticorn/upkr.git", rev = "d93aec186c9fb91d962c488682a2db125c61306c" }
pbr = "1"
+1 -1
View File
@@ -286,7 +286,7 @@ impl BaseModule {
pub fn create_binary(path: &Path) -> Result<()> {
let base1 = BaseModule::for_format_version(1)?.to_wasm();
let data = upkr::pack(&base1, 4, None);
let data = upkr::pack(&base1, 4, false, None);
File::create(path)?.write_all(&data)?;
Ok(())
}
+10 -2
View File
@@ -1,7 +1,15 @@
mod base_module;
mod pack;
mod filter_exports;
mod pack;
pub use base_module::BaseModule;
pub use pack::{pack, pack_file, unpack, unpack_file, PackConfig};
pub use filter_exports::filter_exports;
pub use pack::{pack, pack_file, unpack, unpack_file, PackConfig};
pub fn compressed_size(cart: &[u8]) -> f32 {
if cart[0] != 2 {
cart.len() as f32
} else {
upkr::compressed_size(&cart[1..]) + 1.
}
}
+4 -1
View File
@@ -63,6 +63,7 @@ pub fn pack(data: &[u8], config: &PackConfig) -> Result<Vec<u8>> {
uw8.extend_from_slice(&upkr::pack(
&result[8..],
level,
false,
Some(&mut |pos| {
pb.set(pos as u64);
}),
@@ -89,7 +90,7 @@ pub fn unpack(data: Vec<u8>) -> Result<Vec<u8>> {
let (version, data) = match data[0] {
0 => return Ok(data),
1 => (1, data[1..].to_vec()),
2 => (1, upkr::unpack(&data[1..])),
2 => (1, upkr::unpack(&data[1..], false)),
other => bail!("Uknown format version {}", other),
};
@@ -962,6 +963,8 @@ fn remap_function(
De::I64TruncSatF32U => En::I64TruncSatF32U,
De::I64TruncSatF64S => En::I64TruncSatF64S,
De::I64TruncSatF64U => En::I64TruncSatF64U,
De::MemoryCopy { src, dst } => En::MemoryCopy { src, dst },
De::MemoryFill { mem } => En::MemoryFill(mem),
other => bail!("Unsupported instruction {:?}", other),
});
}
+1
View File
@@ -0,0 +1 @@
/target
+1697
View File
File diff suppressed because it is too large. Load diff
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "uw8-window"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
winit = "0.26.1"
env_logger = "0.9"
log = "0.4"
wgpu = "0.13.1"
pollster = "0.2"
bytemuck = { version = "1.4", features = [ "derive" ] }
anyhow = "1"
minifb = { version = "0.23.0", default-features = false, features = ["x11"] }
winapi = "0.3.9"
+77
View File
@@ -0,0 +1,77 @@
use std::time::Instant;
use crate::Framebuffer;
use minifb::{Key, Window, WindowOptions};
static GAMEPAD_KEYS: &[Key] = &[
Key::Up,
Key::Down,
Key::Left,
Key::Right,
Key::Z,
Key::X,
Key::A,
Key::S,
];
pub fn run(mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>) -> ! {
#[cfg(target_os = "windows")]
unsafe {
winapi::um::timeapi::timeBeginPeriod(1);
}
let mut buffer: Vec<u32> = vec![0; 320 * 240];
let options = WindowOptions {
scale: minifb::Scale::X2,
scale_mode: minifb::ScaleMode::AspectRatioStretch,
resize: true,
..Default::default()
};
let mut window = Window::new("MicroW8", 320, 240, options).unwrap();
let mut next_frame = Instant::now();
while window.is_open() && !window.is_key_down(Key::Escape) {
if let Some(sleep) = next_frame.checked_duration_since(Instant::now()) {
std::thread::sleep(sleep);
}
let mut gamepad = 0;
for key in window.get_keys() {
if let Some(index) = GAMEPAD_KEYS
.iter()
.enumerate()
.find(|(_, &k)| k == key)
.map(|(i, _)| i)
{
gamepad |= 1 << index;
}
}
next_frame = update(
&mut CpuFramebuffer {
buffer: &mut buffer,
},
gamepad,
window.is_key_pressed(Key::R, minifb::KeyRepeat::No),
);
window.update_with_buffer(&buffer, 320, 240).unwrap();
}
std::process::exit(0);
}
struct CpuFramebuffer<'a> {
buffer: &'a mut Vec<u32>,
}
impl<'a> Framebuffer for CpuFramebuffer<'a> {
fn update(&mut self, framebuffer: &[u8], palette: &[u8]) {
for (i, &color_index) in framebuffer.iter().enumerate() {
let offset = color_index as usize * 4;
self.buffer[i] = 0xff000000
| ((palette[offset] as u32) << 16)
| ((palette[offset + 1] as u32) << 8)
| palette[offset + 2] as u32;
}
}
}
+78
View File
@@ -0,0 +1,78 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
struct Uniforms {
texture_scale: vec4<f32>,
}
@group(0) @binding(1) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let x = (1.0 - f32(in_vertex_index)) * 3.0;
let y = f32(in_vertex_index & 1u) * 3.0 - 1.0;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y) * uniforms.texture_scale.xy + vec2<f32>(160.0, 120.0);
return out;
}
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
fn sample_pixel(coords: vec2<i32>, offset: vec4<f32>) -> vec3<f32> {
let is_outside = any(vec2<u32>(coords) >= vec2<u32>(320u, 240u));
if(is_outside) {
return vec3<f32>(0.0);
} else {
let f = max(vec4<f32>(0.01) / offset - vec4<f32>(0.003), vec4<f32>(0.0));
return textureLoad(screen_texture, coords, 0).rgb * (f.x + f.y + f.z + f.w);
}
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let pixel = floor(in.tex_coords);
let o = vec2<f32>(0.5) - (in.tex_coords - pixel);
let pixel = vec2<i32>(pixel);
if(pixel.x < -1 || pixel.y < -1 || pixel.x > 320 || pixel.y > 240) {
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
let offset_x = o.xxxx + vec4<f32>(-0.125, 0.375, 0.125, -0.375) * uniforms.texture_scale.z;
let offset_y = o.yyyy + vec4<f32>(-0.375, -0.125, 0.375, 0.125) * uniforms.texture_scale.z;
let offset_x0 = max(abs(offset_x + vec4<f32>(-1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x1 = max(abs(offset_x) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x2 = max(abs(offset_x + vec4<f32>(1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x0 = offset_x0 * offset_x0;
let offset_x1 = offset_x1 * offset_x1;
let offset_x2 = offset_x2 * offset_x2;
let offset_yr = offset_y + vec4<f32>(-1.0);
let offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
var acc = sample_pixel(pixel + vec2<i32>(-1, -1), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(0, -1), offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, -1), offset_x2 + offset_yr);
let offset_yr = vec4<f32>(0.02) + offset_y * offset_y;
acc = acc + sample_pixel(pixel + vec2<i32>(-1, 0), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel, offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, 0), offset_x2 + offset_yr);
let offset_yr = offset_y + vec4<f32>(1.0);
let offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
acc = acc + sample_pixel(pixel + vec2<i32>(-1, 1), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(0, 1), offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, 1), offset_x2 + offset_yr);
return vec4<f32>(acc, 1.0);
}
+469
View File
@@ -0,0 +1,469 @@
use crate::Framebuffer;
use anyhow::{anyhow, Result};
use std::{num::NonZeroU32, time::Instant};
use wgpu::util::DeviceExt;
use winit::{
dpi::PhysicalSize,
event::{Event, VirtualKeyCode, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Fullscreen, WindowBuilder},
};
#[cfg(unix)]
use winit::platform::unix::EventLoopExtUnix;
pub struct Window {
event_loop: EventLoop<()>,
window: winit::window::Window,
instance: wgpu::Instance,
surface: wgpu::Surface,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
}
impl Window {
pub fn new() -> Result<Window> {
async fn create() -> Result<Window> {
let event_loop = EventLoop::new_any_thread();
let window = WindowBuilder::new()
.with_inner_size(PhysicalSize::new(640u32, 480))
.with_min_inner_size(PhysicalSize::new(320u32, 240))
.with_title("MicroW8")
.build(&event_loop)?;
window.set_cursor_visible(false);
let instance = wgpu::Instance::new(wgpu::Backends::all());
let surface = unsafe { instance.create_surface(&window) };
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.ok_or_else(|| anyhow!("Request adapter failed"))?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor::default(), None)
.await?;
Ok(Window {
event_loop,
window,
instance,
surface,
adapter,
device,
queue,
})
}
pollster::block_on(create())
}
pub fn run(
self,
mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>,
) -> ! {
let Window {
event_loop,
window,
instance,
surface,
adapter,
device,
queue,
} = self;
let framebuffer_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Uint,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
label: None,
});
let palette_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 256,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D1,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
label: None,
});
let screen_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
label: None,
});
let framebuffer_texture_view =
framebuffer_texture.create_view(&wgpu::TextureViewDescriptor::default());
let palette_texture_view =
palette_texture.create_view(&wgpu::TextureViewDescriptor::default());
let screen_texture_view =
screen_texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(window.inner_size()),
};
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let palette_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Uint,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D1,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
],
label: None,
});
let palette_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &palette_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&framebuffer_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&palette_texture_view),
},
],
label: None,
});
let crt_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: None,
});
let crt_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &crt_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&screen_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
],
label: None,
});
let palette_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("palette.wgsl").into()),
});
let palette_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&palette_bind_group_layout],
push_constant_ranges: &[],
});
let palette_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&palette_pipeline_layout),
vertex: wgpu::VertexState {
module: &palette_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &palette_shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8UnormSrgb,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
let crt_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("crt.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&crt_bind_group_layout],
push_constant_ranges: &[],
});
let mut surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface.get_supported_formats(&adapter)[0],
width: window.inner_size().width,
height: window.inner_size().height,
present_mode: wgpu::PresentMode::AutoNoVsync,
};
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &crt_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &crt_shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_config.format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
surface.configure(&device, &surface_config);
event_loop.run(move |event, _, control_flow| {
let _ = (&window, &instance, &surface, &adapter, &device);
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(new_size) => {
surface_config.width = new_size.width;
surface_config.height = new_size.height;
surface.configure(&device, &surface_config);
uniforms.texture_scale = texture_scale_from_resolution(new_size);
queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput { input, .. } => {
if input.state == winit::event::ElementState::Pressed {
match input.virtual_keycode {
Some(VirtualKeyCode::Escape) => *control_flow = ControlFlow::Exit,
Some(VirtualKeyCode::F) => {
window.set_fullscreen(if window.fullscreen().is_some() {
None
} else {
Some(Fullscreen::Borderless(None))
});
}
_ => (),
}
}
}
_ => (),
},
Event::MainEventsCleared => {
if let ControlFlow::WaitUntil(t) = *control_flow {
if Instant::now() < t {
return;
}
}
let next_frame = update(
&mut GpuFramebuffer {
queue: &queue,
framebuffer: &framebuffer_texture,
palette: &palette_texture,
},
0,
false,
);
*control_flow = ControlFlow::WaitUntil(next_frame);
let output = surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &screen_texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&palette_pipeline);
render_pass.set_bind_group(0, &palette_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
{
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&render_pipeline);
render_pass.set_bind_group(0, &crt_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
queue.submit(std::iter::once(encoder.finish()));
output.present();
}
_ => (),
}
});
}
}
struct GpuFramebuffer<'a> {
framebuffer: &'a wgpu::Texture,
palette: &'a wgpu::Texture,
queue: &'a wgpu::Queue,
}
impl<'a> Framebuffer for GpuFramebuffer<'a> {
fn update(&mut self, pixels: &[u8], palette: &[u8]) {
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: self.framebuffer,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&bytemuck::cast_slice(pixels),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: NonZeroU32::new(320),
rows_per_image: None,
},
wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
);
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: self.palette,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&bytemuck::cast_slice(palette),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: NonZeroU32::new(256 * 4),
rows_per_image: None,
},
wgpu::Extent3d {
width: 256,
height: 1,
depth_or_array_layers: 1,
},
);
}
}
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
[
res.width as f32 / scale,
res.height as f32 / scale,
2.0 / scale,
0.0,
]
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
texture_scale: [f32; 4],
}
+24
View File
@@ -0,0 +1,24 @@
use std::time::Instant;
mod cpu;
mod gpu;
pub fn run<F: 'static + FnMut(&mut dyn Framebuffer, u32, bool) -> Instant>(
gpu: bool,
update: F,
) -> ! {
if gpu {
match gpu::Window::new() {
Ok(window) => window.run(Box::new(update)),
Err(err) => eprintln!(
"Failed to create gpu window: {}\nFalling back to cpu window",
err
),
}
}
cpu::run(Box::new(update));
}
pub trait Framebuffer {
fn update(&mut self, pixels: &[u8], palette: &[u8]);
}
+40
View File
@@ -0,0 +1,40 @@
use std::time::{Duration, Instant};
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let mut framebuffer = vec![0u8; 320 * 240];
let start_time = Instant::now();
let mut palette = vec![0u32; 256];
for i in 0..256 {
let v = i & 15;
let r = ((i >> 2) & 12) * v;
let g = ((i >> 3) & 12) * v;
let b = ((i >> 4) & 12) * v;
palette[i as usize] = r + (g << 8) + (b << 16);
}
let mut prev_frame = Instant::now();
uw8_window::run(move |gpu_framebuffer, _gamepads, _reset| {
draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32());
gpu_framebuffer.update(&framebuffer, bytemuck::cast_slice(&palette));
prev_frame += Duration::from_secs_f32(1.0 / 60.0);
prev_frame
});
}
fn draw_frame(framebuffer: &mut [u8], time: f32) {
for x in 0..320 {
let xr = x as f32 - 160.0;
for y in 0..240 {
let yr = y as f32 - 120.0;
let f = 8192.0 / (xr * xr + yr * yr);
let u = xr * f + 512.0 + time * 32.0;
let v = yr * f + time * 29.0;
let c = (u.floor() as i32 ^ v.floor() as i32) as u32;
framebuffer[x + y * 320] = c as u8;
}
}
}
+24
View 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);
}
+57
View File
@@ -0,0 +1,57 @@
<html>
<button onclick="go()">Go!</button>
<canvas id="screen" width="320" height="240"></canvas>
<video id="video"></video>
<script>
function go() {
let audioContext = new AudioContext({sampleRate: 44100});
let oscillator = new OscillatorNode(audioContext);
let gain = new GainNode(audioContext, {gain: 1});
oscillator.connect(gain);
gain.connect(audioContext.destination);
for(let i = 0; i < 8; ++i ) {
gain.gain.setValueAtTime(1, i / 2);
gain.gain.setValueAtTime(0, i / 2 + 0.3);
}
oscillator.start();
oscillator.stop(4);
let screen = document.getElementById('screen');
let context = screen.getContext('2d');
let startTime = Date.now();
let drawFrame = () => {
let time = Date.now() - startTime;
context.fillStyle = 'white';
context.fillRect(0, 0, 320, 240);
if(time < 4000) {
if(time % 500 < 300) {
context.fillStyle = 'black';
context.fillRect(time / 15, 50, 50, 50);
}
window.requestAnimationFrame(drawFrame);
}
};
drawFrame();
let stream = screen.captureStream();
let audioStreamNode = audioContext.createMediaStreamDestination();
gain.connect(audioStreamNode);
stream.addTrack(audioStreamNode.stream.getAudioTracks()[0]);
let recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
let chunks = [];
recorder.ondataavailable = e => chunks.push(e.data);
recorder.onstop = () => {
let blob = new Blob(chunks, {type: 'video/webm'});
let url = URL.createObjectURL(blob);
let video = document.getElementById('video');
video.src = url;
video.play();
};
recorder.start();
setTimeout(() => recorder.stop(), 4000);
}
</script>
</html>
+627 -531
View File
File diff suppressed because it is too large. Load diff