3 Commits

Author SHA1 Message Date
1a06ebbc95 update zig example 2023-04-08 15:10:09 +02:00
d1b6fa36e8 update frame pacing calculation in native runtime 2023-04-07 18:33:22 +02:00
b3a8512129 update frame pacing calculation in web runtime
fixes carts running too slow in chrome
2023-04-07 17:08:04 +02:00
20 changed files with 2515 additions and 3060 deletions

3200
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,21 +11,21 @@ native = ["wasmtime", "uw8-window", "cpal", "rubato" ]
browser = ["warp", "tokio", "tokio-stream", "webbrowser"] browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
[dependencies] [dependencies]
wasmtime = { version = "19.0.1", optional = true } wasmtime = { version = "5.0.0", optional = true }
anyhow = "1" anyhow = "1"
env_logger = "0.11.3" env_logger = "0.10"
log = "0.4" log = "0.4"
uw8-window = { path = "uw8-window", optional = true } uw8-window = { path = "uw8-window", optional = true }
notify-debouncer-mini = { version = "0.4.1", default-features = false } notify-debouncer-mini = { version = "0.2.1", default-features = false }
pico-args = "0.5" pico-args = "0.5"
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0e7ea50" } 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"
warp = { version = "0.3.6", optional = true } warp = { version = "0.3.3", optional = true }
tokio = { version = "1.37.0", features = ["sync", "rt"], optional = true } tokio = { version = "1.24.0", features = ["sync", "rt"], optional = true }
tokio-stream = { version = "0.1.15", features = ["sync"], optional = true } tokio-stream = { version = "0.1.11", features = ["sync"], optional = true }
webbrowser = { version = "0.8.13", optional = true } webbrowser = { version = "0.8.6", optional = true }
ansi_term = "0.12.1" ansi_term = "0.12.1"
cpal = { version = "0.15.3", optional = true } cpal = { version = "0.14.2", optional = true }
rubato = { version = "0.15.0", optional = true } rubato = { version = "0.12.0", optional = true }

View File

@@ -1,14 +0,0 @@
include "../include/microw8-api.cwa"
export fn upd() {
printString(USER_MEM);
}
data USER_MEM {
// clear screen, switch to graphics text mode, text scale 4
i8(12, 5, 30, 4)
// text color, position, print two lines
i8(15, 86, 31, 8, 80) "Hello," i8(8, 8, 8, 8, 8, 10) "MicroW8!"
// print same two lines with different color and slight offset
i8(15, 47, 31, 10, 82) "Hello," i8(8, 8, 8, 8, 8, 10) "MicroW8!" i8(0)
}

View File

@@ -4,13 +4,13 @@ const SPRITE = 0x20000;
export fn upd() { export fn upd() {
cls(0); cls(0);
let t = time() / 2_f; let t = time();
let i: i32; let i: i32;
loop spriteLoop { loop spriteLoop {
let inline x = sin(t * -1.3 + i as f32 * (3.141 / 30_f)) * 180_f + 160_f; let inline x = sin(t * -1.3 + i as f32 / 8_f) * 180_f + 160_f;
let inline y = sin(t * 1.7 + i as f32 * (3.141 / 40_f)) * 140_f + 120_f; let inline y = sin(t * 1.7 + i as f32 / 9_f) * 140_f + 120_f;
blitSprite(SPRITE, 16, x as i32, y as i32, (i & 3) * 0x200 + 0x100); blitSprite(SPRITE, 16, x as i32, y as i32, 0x100);
branch_if (i +:= 1) < 100: spriteLoop; branch_if (i +:= 1) < 200: spriteLoop;
} }
} }

View File

@@ -5,11 +5,7 @@ pub fn build(b: *std.build.Builder) void {
const lib = b.addSharedLibrary("cart", "main.zig", .unversioned); const lib = b.addSharedLibrary("cart", "main.zig", .unversioned);
lib.setBuildMode(mode); lib.setBuildMode(mode);
lib.setTarget(.{ lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .cpu_features_add = std.Target.wasm.featureSet(&.{.nontrapping_fptoint}) });
.cpu_arch = .wasm32,
.os_tag = .freestanding,
.cpu_features_add = std.Target.wasm.featureSet(&.{ .nontrapping_fptoint })
});
lib.import_memory = true; lib.import_memory = true;
lib.initial_memory = 262144; lib.initial_memory = 262144;
lib.max_memory = 262144; lib.max_memory = 262144;
@@ -18,19 +14,13 @@ pub fn build(b: *std.build.Builder) void {
lib.install(); lib.install();
if (lib.install_step) |install_step| { if (lib.install_step) |install_step| {
const run_filter_exports = b.addSystemCommand(&[_][]const u8{ const run_filter_exports = b.addSystemCommand(&[_][]const u8{ "uw8", "filter-exports", "zig-out/lib/cart.wasm", "zig-out/lib/cart-filtered.wasm" });
"uw8", "filter-exports", "zig-out/lib/cart.wasm", "zig-out/lib/cart-filtered.wasm"
});
run_filter_exports.step.dependOn(&install_step.step); run_filter_exports.step.dependOn(&install_step.step);
const run_wasm_opt = b.addSystemCommand(&[_][]const u8{ const run_wasm_opt = b.addSystemCommand(&[_][]const u8{ "wasm-opt", "--enable-nontrapping-float-to-int", "-Oz", "-o", "zig-out/cart.wasm", "zig-out/lib/cart-filtered.wasm" });
"wasm-opt", "-Oz", "-o", "zig-out/cart.wasm", "zig-out/lib/cart-filtered.wasm"
});
run_wasm_opt.step.dependOn(&run_filter_exports.step); run_wasm_opt.step.dependOn(&run_filter_exports.step);
const run_uw8_pack = b.addSystemCommand(&[_][]const u8{ const run_uw8_pack = b.addSystemCommand(&[_][]const u8{ "uw8", "pack", "-l", "9", "zig-out/cart.wasm", "zig-out/cart.uw8" });
"uw8", "pack", "-l", "9", "zig-out/cart.wasm", "zig-out/cart.uw8"
});
run_uw8_pack.step.dependOn(&run_wasm_opt.step); run_uw8_pack.step.dependOn(&run_wasm_opt.step);
const make_opt = b.step("make_opt", "make size optimized cart"); const make_opt = b.step("make_opt", "make size optimized cart");

View File

@@ -1,11 +1,11 @@
extern fn atan2(x: f32, y: f32) f32; extern fn atan2(x: f32, y: f32) f32;
extern fn time() f32; extern fn time() f32;
pub const FRAMEBUFFER: *[320*240]u8 = @intToPtr(*[320*240]u8, 120); pub const FRAMEBUFFER: *[320 * 240]u8 = @intToPtr(*[320 * 240]u8, 120);
export fn upd() void { export fn upd() void {
var i: u32 = 0; var i: u32 = 0;
while(true) { while (true) {
var t = time() * 63.0; var t = time() * 63.0;
var x = @intToFloat(f32, (@intCast(i32, i % 320) - 160)); var x = @intToFloat(f32, (@intCast(i32, i % 320) - 160));
var y = @intToFloat(f32, (@intCast(i32, i / 320) - 120)); var y = @intToFloat(f32, (@intCast(i32, i / 320) - 120));
@@ -13,8 +13,10 @@ export fn upd() void {
var u = atan2(x, y) * 512.0 / 3.141; var u = atan2(x, y) * 512.0 / 3.141;
var c = @intCast(u8, (@floatToInt(i32, d + t * 2.0) ^ @floatToInt(i32, u + t)) & 255) >> 4; var c = @intCast(u8, (@floatToInt(i32, d + t * 2.0) ^ @floatToInt(i32, u + t)) & 255) >> 4;
FRAMEBUFFER[@as(usize, i)] = c; FRAMEBUFFER[i] = c;
i += 1; i += 1;
if(i >= 320*240) { break; } if (i >= 320 * 240) {
break;
}
} }
} }

Binary file not shown.

View File

@@ -62,6 +62,7 @@ export fn sndGes(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;
@@ -130,7 +131,7 @@ export fn sndGes(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);
} }

View File

@@ -447,7 +447,6 @@ global mut textCursorY = 0;
global mut textColor = 15; global mut textColor = 15;
global mut bgColor = 0; global mut bgColor = 0;
global mut outputChannel = 0; global mut outputChannel = 0;
global mut textScale = 1;
export fn printChar(char: i32) { export fn printChar(char: i32) {
loop chars { loop chars {
@@ -459,8 +458,6 @@ export fn printChar(char: i32) {
global mut controlCodeLength = 0; global mut controlCodeLength = 0;
fn printSingleChar(char: i32) { fn printSingleChar(char: i32) {
let charSize = 8 * textScale;
if outputChannel >= 2 & (char < 4 | char > 6) { if outputChannel >= 2 & (char < 4 | char > 6) {
logChar(char); logChar(char);
return; return;
@@ -494,9 +491,9 @@ fn printSingleChar(char: i32) {
} }
if char == 8 { if char == 8 {
textCursorX = textCursorX - charSize; textCursorX = textCursorX - 8;
if !outputChannel & textCursorX < 0 { if !outputChannel & textCursorX < 0 {
textCursorX = 320-charSize; textCursorX = 320-8;
printSingleChar(11); printSingleChar(11);
} }
return; return;
@@ -506,34 +503,34 @@ fn printSingleChar(char: i32) {
if !outputChannel & textCursorX >= 320 { if !outputChannel & textCursorX >= 320 {
printChar(0xd0a); printChar(0xd0a);
} }
textCursorX = textCursorX + charSize; textCursorX = textCursorX + 8;
return; return;
} }
if char == 10 { if char == 10 {
textCursorY = textCursorY + charSize; textCursorY = textCursorY + 8;
if !outputChannel & textCursorY >= 240 { if !outputChannel & textCursorY >= 240 {
textCursorY = 240 - charSize; textCursorY = 240 - 8;
let i: i32; let i: i32;
loop scroll_copy { loop scroll_copy {
i!120 = (i + 320 * charSize)!120; i!120 = i!(120 + 320 * 8);
branch_if (i := i + 4) < 320 * (240 - charSize): scroll_copy; branch_if (i := i + 4) < 320 * (240 - 8): scroll_copy;
} }
rectangle(0 as f32, (240 - charSize) as f32, 320 as f32, charSize as f32, bgColor); rectangle(0 as f32, (240 - 8) as f32, 320 as f32, 8 as f32, bgColor);
} }
return; return;
} }
if char == 11 { if char == 11 {
textCursorY = textCursorY - charSize; textCursorY = textCursorY - 8;
if !outputChannel & textCursorY < 0 { if !outputChannel & textCursorY < 0 {
textCursorY = 0; textCursorY = 0;
let i = 320 * (240 - charSize); let i = 320 * (240 - 8);
loop scroll_copy { loop scroll_copy {
(i + 320 * charSize)!116 = i!116; i!(116 + 320 * 8) = i!116;
branch_if (i := i - 4): scroll_copy; branch_if (i := i - 4): scroll_copy;
} }
rectangle(0 as f32, 0 as f32, 320 as f32, charSize as f32, bgColor); rectangle(0 as f32, 0 as f32, 320 as f32, 8 as f32, bgColor);
} }
return; return;
} }
@@ -565,12 +562,6 @@ fn printSingleChar(char: i32) {
return; return;
} }
if char == 30 {
let scale = 0x12d20?1;
textScale = select(scale > 0 & scale <= 16, scale, 1);
return;
}
if char == 31 { if char == 31 {
textCursorX = 0x12d20?1 * (8 - outputChannel * 6); textCursorX = 0x12d20?1 * (8 - outputChannel * 6);
textCursorY = 0x12d20?2 * (8 - outputChannel * 7); textCursorY = 0x12d20?2 * (8 - outputChannel * 7);
@@ -593,7 +584,7 @@ data(0x12d00) {
1, 1, 1, 1, // 16-19, 1, 1, 1, 1, // 16-19,
1, 1, 1, 1, // 20-23, 1, 1, 1, 1, // 20-23,
1, 1, 1, 1, // 24-27, 1, 1, 1, 1, // 24-27,
1, 1, 2, 3 // 28-31 1, 1, 1, 3 // 28-31
) )
} }
@@ -602,28 +593,26 @@ fn drawChar(char: i32) {
printChar(0xd0a); printChar(0xd0a);
} }
let charSize = 8 * textScale;
let y: i32; let y: i32;
loop rows { loop rows {
let bits = (char * 8 + y / textScale)?0x13400; let bits = (char * 8 + y)?0x13400;
let x = 0; let x = 0;
if outputChannel { if outputChannel {
loop pixels { loop pixels {
if (bits << (x / textScale)) & 128 { if (bits := bits << 1) & 256 {
setPixel(textCursorX + x, textCursorY + y, textColor); setPixel(textCursorX + x, textCursorY + y, textColor);
} }
branch_if (x := x + 1) < charSize: pixels; branch_if (x := x + 1) < 8: pixels;
} }
} else { } else {
loop pixels { loop pixels {
setPixel(textCursorX + x, textCursorY + y, select((bits << (x / textScale)) & 128, textColor, bgColor)); setPixel(textCursorX + x, textCursorY + y, select((bits := bits << 1) & 256, textColor, bgColor));
branch_if (x := x + 1) < charSize: pixels; branch_if (x := x + 1) < 8: pixels;
} }
} }
branch_if (y := y + 1) < charSize: rows; branch_if (y := y + 1) < 8: rows;
} }
textCursorX = textCursorX + charSize; textCursorX = textCursorX + 8;
} }
export fn printString(ptr: i32) { export fn printString(ptr: i32) {

View File

@@ -5,7 +5,7 @@ description = "Docs"
# Overview # Overview
MicroW8 loads WebAssembly modules with a maximum size of 256kb. Your module needs to export MicroW8 loads WebAssembly modules with a maximum size of 256kb. You module needs to export
a function `fn upd()` which will be called once per frame. a function `fn upd()` which will be called once per frame.
After calling `upd` MicroW8 will display the 320x240 8bpp framebuffer located After calling `upd` MicroW8 will display the 320x240 8bpp framebuffer located
at offset 120 in memory with the 32bpp palette located at 0x13000. at offset 120 in memory with the 32bpp palette located at 0x13000.

View File

@@ -16,14 +16,16 @@ pub struct FileWatcher {
impl FileWatcher { impl FileWatcher {
pub fn new() -> Result<FileWatcher> { pub fn new() -> Result<FileWatcher> {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let debouncer = new_debouncer(Duration::from_millis(100), move |res| match res { let debouncer = new_debouncer(Duration::from_millis(100), None, move |res| match res {
Ok(events) => { Ok(events) => {
for event in events { for event in events {
let _ = tx.send(event); let _ = tx.send(event);
} }
} }
Err(err) => { Err(errs) => {
eprintln!("Error watching for file changes: {err}"); for err in errs {
eprintln!("Error watching for file changes: {err}");
}
} }
})?; })?;
Ok(FileWatcher { Ok(FileWatcher {

View File

@@ -7,7 +7,7 @@ use cpal::traits::*;
use rubato::Resampler; use rubato::Resampler;
use uw8_window::{Window, WindowConfig}; use uw8_window::{Window, WindowConfig};
use wasmtime::{ use wasmtime::{
Engine, Func, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType, Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
}; };
pub struct MicroW8 { pub struct MicroW8 {
@@ -90,7 +90,7 @@ impl super::Runtime for MicroW8 {
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?; let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
let mut linker = wasmtime::Linker::new(&self.engine); let mut linker = wasmtime::Linker::new(&self.engine);
linker.define(&store, "env", "memory", memory)?; linker.define("env", "memory", memory)?;
let loader_instance = linker.instantiate(&mut store, &self.loader_module)?; let loader_instance = linker.instantiate(&mut store, &self.loader_module)?;
let load_uw8 = loader_instance.get_typed_func::<i32, i32>(&mut store, "load_uw8")?; let load_uw8 = loader_instance.get_typed_func::<i32, i32>(&mut store, "load_uw8")?;
@@ -181,10 +181,12 @@ impl super::Runtime for MicroW8 {
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 = (now - instance.start_time).as_millis() as i32;
let next_frame = { let next_frame = {
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6; let frame = (time as u32 as u64 * 6 / 100) as u32;
let max = now + Duration::from_millis(17); let cur_offset = (time as u32).wrapping_sub((frame as u64 * 100 / 6) as u32);
let next_center = now + Duration::from_millis((16 - offset) as u64); let next_time =
next_center.min(max) ((frame as u64 + 1) * 100 / 6 + cur_offset.max(1).min(4) as u64) as u32;
let offset = next_time.wrapping_sub(time as u32);
now + Duration::from_millis(offset as u64)
}; };
{ {
@@ -255,12 +257,15 @@ fn add_native_functions(
} }
})?; })?;
for i in 0..16 { for i in 0..16 {
let global = wasmtime::Global::new( linker.define(
&mut *store, "env",
GlobalType::new(ValType::I32, Mutability::Const), &format!("g_reserved{}", i),
0.into(), wasmtime::Global::new(
&mut *store,
GlobalType::new(ValType::I32, Mutability::Const),
0.into(),
)?,
)?; )?;
linker.define(&store, "env", &format!("g_reserved{}", i), global)?;
} }
Ok(()) Ok(())
@@ -273,18 +278,14 @@ fn instantiate_platform(
) -> Result<wasmtime::Instance> { ) -> Result<wasmtime::Instance> {
let platform_instance = linker.instantiate(&mut *store, &platform_module)?; let platform_instance = linker.instantiate(&mut *store, &platform_module)?;
let exports: Vec<(String, Func)> = platform_instance for export in platform_instance.exports(&mut *store) {
.exports(&mut *store) linker.define(
.map(|e| { "env",
( export.name(),
e.name().to_owned(), export
e.into_func() .into_func()
.expect("platform surely only exports functions"), .expect("platform surely only exports functions"),
) )?;
})
.collect();
for (name, func) in exports {
linker.define(&store, "env", &name, func)?;
} }
Ok(platform_instance) Ok(platform_instance)
@@ -311,7 +312,7 @@ fn init_sound(
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?; let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
let mut linker = wasmtime::Linker::new(engine); let mut linker = wasmtime::Linker::new(engine);
linker.define(&store, "env", "memory", memory)?; linker.define("env", "memory", memory)?;
add_native_functions(&mut linker, &mut store)?; add_native_functions(&mut linker, &mut store)?;
let platform_instance = instantiate_platform(&mut linker, &mut store, platform_module)?; let platform_instance = instantiate_platform(&mut linker, &mut store, platform_module)?;
@@ -328,26 +329,23 @@ fn init_sound(
let mut configs: Vec<_> = device let mut configs: Vec<_> = device
.supported_output_configs()? .supported_output_configs()?
.filter(|config| { .filter(|config| {
config.channels() == 2 config.channels() == 2 && config.sample_format() == cpal::SampleFormat::F32
&& (config.sample_format() == cpal::SampleFormat::F32
|| config.sample_format() == cpal::SampleFormat::I16)
}) })
.collect(); .collect();
configs.sort_by_key(|config| { configs.sort_by_key(|config| {
let rate = 44100 let rate = 44100
.max(config.min_sample_rate().0) .max(config.min_sample_rate().0)
.min(config.max_sample_rate().0); .min(config.max_sample_rate().0);
let prio = if rate >= 44100 { if rate >= 44100 {
rate - 44100 rate - 44100
} else { } else {
(44100 - rate) * 1000 (44100 - rate) * 1000
}; }
prio + (config.sample_format() == cpal::SampleFormat::I16) as u32
}); });
let config = configs let config = configs
.into_iter() .into_iter()
.next() .next()
.ok_or_else(|| anyhow!("Could not find float or 16bit signed output config"))?; .ok_or_else(|| anyhow!("Could not find float output config"))?;
let sample_rate = cpal::SampleRate(44100) let sample_rate = cpal::SampleRate(44100)
.max(config.min_sample_rate()) .max(config.min_sample_rate())
.min(config.max_sample_rate()); .min(config.max_sample_rate());
@@ -358,7 +356,6 @@ fn init_sound(
cpal::BufferSize::Fixed(256.max(min).min(max)) cpal::BufferSize::Fixed(256.max(min).min(max))
} }
}; };
let sample_format = config.sample_format();
let config = cpal::StreamConfig { let config = cpal::StreamConfig {
buffer_size, buffer_size,
..config.config() ..config.config()
@@ -378,8 +375,8 @@ fn init_sound(
None None
} else { } else {
let rs = rubato::FftFixedIn::new(44100, sample_rate, 128, 1, 2)?; let rs = rubato::FftFixedIn::new(44100, sample_rate, 128, 1, 2)?;
let input_buffers = rs.input_buffer_allocate(true); let input_buffers = rs.input_buffer_allocate();
let output_buffers = rs.output_buffer_allocate(true); let output_buffers = rs.output_buffer_allocate();
Some(Resampler { Some(Resampler {
resampler: rs, resampler: rs,
input_buffers, input_buffers,
@@ -391,130 +388,96 @@ fn init_sound(
let mut sample_index = 0; let mut sample_index = 0;
let mut pending_updates: Vec<RegisterUpdate> = Vec::with_capacity(30); let mut pending_updates: Vec<RegisterUpdate> = Vec::with_capacity(30);
let mut current_time = 0; let mut current_time = 0;
let stream = device.build_output_stream(
let mut callback = move |mut outer_buffer: &mut [f32], _: &_| { &config,
let mut first_update = true; move |mut outer_buffer: &mut [f32], _| {
while let Ok(update) = rx.try_recv() { let mut first_update = true;
if first_update { while let Ok(update) = rx.try_recv() {
current_time += update.time.wrapping_sub(current_time) / 8; if first_update {
first_update = false; current_time += update.time.wrapping_sub(current_time) / 8;
} first_update = false;
pending_updates.push(update); }
} 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() { while !outer_buffer.is_empty() {
((update.time.wrapping_sub(current_time) as usize) * sample_rate + 999) / 1000 store.set_epoch_deadline(30);
} else { while pending_updates
outer_buffer.len() .first()
}; .into_iter()
let step_size = (duration.max(64) * 2).min(outer_buffer.len()); .any(|u| u.time.wrapping_sub(current_time) <= 0)
{
let update = pending_updates.remove(0);
memory.write(&mut store, 80, &update.data).unwrap();
}
let mut buffer = &mut outer_buffer[..step_size]; let duration = if let Some(update) = pending_updates.first() {
((update.time.wrapping_sub(current_time) as usize) * sample_rate + 999) / 1000
{
let mem = memory.data_mut(&mut store);
mem[64..68].copy_from_slice(&current_time.to_le_bytes());
}
fn clamp_sample(s: f32) -> f32 {
if s.is_nan() {
0.0
} else { } else {
s.max(-1.0).min(1.0) outer_buffer.len()
};
let step_size = (duration.max(64) * 2).min(outer_buffer.len());
let mut buffer = &mut outer_buffer[..step_size];
{
let mem = memory.data_mut(&mut store);
mem[64..68].copy_from_slice(&current_time.to_le_bytes());
} }
}
if let Some(ref mut resampler) = resampler { if let Some(ref mut resampler) = resampler {
while !buffer.is_empty() { while !buffer.is_empty() {
let copy_size = resampler.output_buffers[0] let copy_size = resampler.output_buffers[0]
.len() .len()
.saturating_sub(resampler.output_index) .saturating_sub(resampler.output_index)
.min(buffer.len() / 2); .min(buffer.len() / 2);
if copy_size == 0 { if copy_size == 0 {
resampler.input_buffers[0].clear(); resampler.input_buffers[0].clear();
resampler.input_buffers[1].clear(); resampler.input_buffers[1].clear();
for _ in 0..resampler.resampler.input_frames_next() { for _ in 0..resampler.resampler.input_frames_next() {
resampler.input_buffers[0].push(clamp_sample( resampler.input_buffers[0]
snd.call(&mut store, (sample_index,)).unwrap_or(0.0), .push(snd.call(&mut store, (sample_index,)).unwrap_or(0.0));
)); resampler.input_buffers[1]
resampler.input_buffers[1].push(clamp_sample( .push(snd.call(&mut store, (sample_index + 1,)).unwrap_or(0.0));
snd.call(&mut store, (sample_index + 1,)).unwrap_or(0.0), sample_index = sample_index.wrapping_add(2);
)); }
sample_index = sample_index.wrapping_add(2);
}
resampler resampler
.resampler .resampler
.process_into_buffer( .process_into_buffer(
&resampler.input_buffers, &resampler.input_buffers,
&mut resampler.output_buffers, &mut resampler.output_buffers,
None, None,
) )
.unwrap(); .unwrap();
resampler.output_index = 0; resampler.output_index = 0;
} else { } else {
for i in 0..copy_size { for i in 0..copy_size {
buffer[i * 2] = resampler.output_buffers[0][resampler.output_index + i]; buffer[i * 2] =
buffer[i * 2 + 1] = resampler.output_buffers[0][resampler.output_index + i];
resampler.output_buffers[1][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..];
} }
resampler.output_index += copy_size; }
buffer = &mut buffer[copy_size * 2..]; } else {
for v in buffer {
*v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0);
sample_index = sample_index.wrapping_add(1);
} }
} }
} else {
for v in buffer { outer_buffer = &mut outer_buffer[step_size..];
*v = clamp_sample(snd.call(&mut store, (sample_index,)).unwrap_or(0.0)); current_time =
sample_index = sample_index.wrapping_add(1); current_time.wrapping_add((step_size * 500 / sample_rate).max(1) as i32);
}
} }
},
outer_buffer = &mut outer_buffer[step_size..]; move |err| {
current_time = current_time.wrapping_add((step_size * 500 / sample_rate).max(1) as i32); dbg!(err);
} },
}; )?;
let stream = if sample_format == cpal::SampleFormat::F32 {
device.build_output_stream(
&config,
callback,
move |err| {
dbg!(err);
},
None,
)?
} else {
device.build_output_stream(
&config,
move |mut buffer: &mut [i16], info| {
let mut float_buffer = [0f32; 256];
while !buffer.is_empty() {
let step_size = buffer.len().min(float_buffer.len());
let step_buffer = &mut float_buffer[..step_size];
callback(step_buffer, info);
for (dest, src) in buffer.iter_mut().take(step_size).zip(step_buffer.iter()) {
*dest = (src.max(-1.0).min(1.0) * 32767.0) as i16;
}
buffer = &mut buffer[step_size..];
}
},
move |err| {
dbg!(err);
},
None,
)?
};
Ok(Uw8Sound { stream, tx }) Ok(Uw8Sound { stream, tx })
} }

View File

@@ -1 +0,0 @@
* add support for 16bit sound (not just float)

View File

@@ -1,17 +1,13 @@
use anyhow::Result;
use std::path::Path; use std::path::Path;
use anyhow::Result;
pub fn filter_exports(in_path: &Path, out_path: &Path) -> Result<()> { pub fn filter_exports(in_path: &Path, out_path: &Path) -> Result<()> {
let mut module = walrus::Module::from_file(in_path)?; let mut module = walrus::Module::from_file(in_path)?;
let exports_to_delete: Vec<_> = module let exports_to_delete: Vec<_> = module.exports.iter().filter_map(|export| match export.name.as_str() {
.exports "upd" => None,
.iter() _ => Some(export.id())
.filter_map(|export| match export.name.as_str() { }).collect();
"start" | "upd" | "snd" => None,
_ => Some(export.id()),
})
.collect();
for id in exports_to_delete { for id in exports_to_delete {
module.exports.delete(id); module.exports.delete(id);

1729
uw8-window/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,13 @@ 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]
winit = "0.29.15" winit = "0.27.5"
env_logger = "0.11.3" env_logger = "0.10"
log = "0.4" log = "0.4"
pico-args = "0.5" pico-args = "0.5"
wgpu = "0.19.3" wgpu = "0.15"
pollster = "0.3.0" pollster = "0.2.5"
bytemuck = { version = "1.15", features = [ "derive" ] } bytemuck = { version = "1.13", features = [ "derive" ] }
anyhow = "1" anyhow = "1"
minifb = { version = "0.25.0", default-features = false, features = ["x11"] } minifb = { version = "0.23.0", default-features = false, features = ["x11"] }
winapi = { version = "0.3.9", features = [ "timeapi" ] } winapi = { version = "0.3.9", features = [ "timeapi" ] }

View File

@@ -30,8 +30,8 @@ fn row_factor(offset: f32) -> f32 {
} }
fn col_factor(offset: f32) -> f32 { fn col_factor(offset: f32) -> f32 {
let o = max(0.0, abs(offset) - 0.4); let offset = max(0.0, abs(offset) - 0.4);
return 1.0 / (1.0 + o * o * 16.0); return 1.0 / (1.0 + offset * offset * 16.0);
} }
fn sample_screen(tex_coords: vec2<f32>) -> vec4<f32> { fn sample_screen(tex_coords: vec2<f32>) -> vec4<f32> {

View File

@@ -1,16 +1,16 @@
use crate::{Input, WindowConfig, WindowImpl}; use crate::{Input, WindowConfig, WindowImpl};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::{sync::Arc, time::Instant}; use std::{num::NonZeroU32, time::Instant};
use winit::{ use winit::{
dpi::PhysicalSize, dpi::PhysicalSize,
event::{Event, WindowEvent}, event::{Event, VirtualKeyCode, WindowEvent},
event_loop::{ControlFlow, EventLoop}, event_loop::{ControlFlow, EventLoop},
keyboard::{Key, KeyCode, NamedKey, PhysicalKey},
platform::pump_events::{EventLoopExtPumpEvents, PumpStatus},
window::{Fullscreen, WindowBuilder}, window::{Fullscreen, WindowBuilder},
}; };
use winit::platform::run_return::EventLoopExtRunReturn;
mod crt; mod crt;
mod fast_crt; mod fast_crt;
mod square; mod square;
@@ -21,7 +21,7 @@ use square::SquareFilter;
pub struct Window { pub struct Window {
_instance: wgpu::Instance, _instance: wgpu::Instance,
surface: wgpu::Surface<'static>, surface: wgpu::Surface,
_adapter: wgpu::Adapter, _adapter: wgpu::Adapter,
device: wgpu::Device, device: wgpu::Device,
queue: wgpu::Queue, queue: wgpu::Queue,
@@ -29,7 +29,7 @@ pub struct Window {
surface_config: wgpu::SurfaceConfiguration, surface_config: wgpu::SurfaceConfiguration,
filter: Box<dyn Filter>, filter: Box<dyn Filter>,
event_loop: EventLoop<()>, event_loop: EventLoop<()>,
window: Arc<winit::window::Window>, window: winit::window::Window,
gamepads: [u8; 4], gamepads: [u8; 4],
next_frame: Instant, next_frame: Instant,
is_fullscreen: bool, is_fullscreen: bool,
@@ -39,12 +39,9 @@ pub struct Window {
impl Window { impl Window {
pub fn new(window_config: WindowConfig) -> Result<Window> { pub fn new(window_config: WindowConfig) -> Result<Window> {
async fn create(window_config: WindowConfig) -> Result<Window> { async fn create(window_config: WindowConfig) -> Result<Window> {
let event_loop = EventLoop::new()?; let event_loop = EventLoop::new();
let window = WindowBuilder::new() let window = WindowBuilder::new()
.with_inner_size(PhysicalSize::new( .with_inner_size(PhysicalSize::new(640u32, 480))
(320. * window_config.scale).round() as u32,
(240. * window_config.scale).round() as u32,
))
.with_min_inner_size(PhysicalSize::new(320u32, 240)) .with_min_inner_size(PhysicalSize::new(320u32, 240))
.with_title("MicroW8") .with_title("MicroW8")
.with_fullscreen(if window_config.fullscreen { .with_fullscreen(if window_config.fullscreen {
@@ -56,10 +53,8 @@ impl Window {
window.set_cursor_visible(false); window.set_cursor_visible(false);
let window = Arc::new(window);
let instance = wgpu::Instance::new(Default::default()); let instance = wgpu::Instance::new(Default::default());
let surface = instance.create_surface(window.clone())?; let surface = unsafe { instance.create_surface(&window) }?;
let adapter = instance let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions { .request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower, power_preference: wgpu::PowerPreference::LowPower,
@@ -77,13 +72,7 @@ impl Window {
let surface_config = wgpu::SurfaceConfiguration { let surface_config = wgpu::SurfaceConfiguration {
present_mode: wgpu::PresentMode::AutoNoVsync, present_mode: wgpu::PresentMode::AutoNoVsync,
..surface ..surface.get_default_config(&adapter, window.inner_size().width, window.inner_size().height).expect("Surface incompatible with adapter")
.get_default_config(
&adapter,
window.inner_size().width,
window.inner_size().height,
)
.expect("Surface incompatible with adapter")
}; };
let filter: Box<dyn Filter> = create_filter( let filter: Box<dyn Filter> = create_filter(
@@ -121,93 +110,88 @@ impl Window {
impl WindowImpl for Window { impl WindowImpl for Window {
fn begin_frame(&mut self) -> Input { fn begin_frame(&mut self) -> Input {
let mut reset = false; let mut reset = false;
self.event_loop self.event_loop.run_return(|event, _, control_flow| {
.set_control_flow(ControlFlow::WaitUntil(self.next_frame)); *control_flow = ControlFlow::WaitUntil(self.next_frame);
while self.is_open { let mut new_filter = None;
let timeout = self.next_frame.saturating_duration_since(Instant::now()); match event {
let status = self.event_loop.pump_events(Some(timeout), |event, elwt| { Event::WindowEvent { event, .. } => match event {
let mut new_filter = None; WindowEvent::Resized(new_size) => {
match event { self.surface_config.width = new_size.width;
Event::WindowEvent { event, .. } => match event { self.surface_config.height = new_size.height;
WindowEvent::Resized(new_size) => { self.surface.configure(&self.device, &self.surface_config);
self.surface_config.width = new_size.width; self.filter.resize(&self.queue, new_size);
self.surface_config.height = new_size.height; }
self.surface.configure(&self.device, &self.surface_config); WindowEvent::CloseRequested => {
self.filter.resize(&self.queue, new_size); self.is_open = false;
} *control_flow = ControlFlow::Exit;
WindowEvent::CloseRequested => { }
elwt.exit(); WindowEvent::KeyboardInput { input, .. } => {
} fn gamepad_button(input: &winit::event::KeyboardInput) -> u8 {
WindowEvent::KeyboardInput { event, .. } => { match input.scancode {
fn gamepad_button(input: &winit::event::KeyEvent) -> u8 { 44 => 16,
match input.physical_key { 45 => 32,
PhysicalKey::Code(KeyCode::KeyZ) => 16, 30 => 64,
PhysicalKey::Code(KeyCode::KeyX) => 32, 31 => 128,
PhysicalKey::Code(KeyCode::KeyA) => 64, _ => match input.virtual_keycode {
PhysicalKey::Code(KeyCode::KeyS) => 128, Some(VirtualKeyCode::Up) => 1,
_ => match input.logical_key { Some(VirtualKeyCode::Down) => 2,
Key::Named(NamedKey::ArrowUp) => 1, Some(VirtualKeyCode::Left) => 4,
Key::Named(NamedKey::ArrowDown) => 2, Some(VirtualKeyCode::Right) => 8,
Key::Named(NamedKey::ArrowLeft) => 4, _ => 0,
Key::Named(NamedKey::ArrowRight) => 8, },
_ => 0,
},
}
} }
if event.state == winit::event::ElementState::Pressed { }
match event.logical_key { if input.state == winit::event::ElementState::Pressed {
Key::Named(NamedKey::Escape) => { match input.virtual_keycode {
elwt.exit(); Some(VirtualKeyCode::Escape) => {
} self.is_open = false;
Key::Character(ref c) => match c.as_str() { *control_flow = ControlFlow::Exit;
"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);
}
"r" => reset = true,
"1" => new_filter = Some(1),
"2" => new_filter = Some(2),
"3" => new_filter = Some(3),
"4" => new_filter = Some(4),
"5" => new_filter = Some(5),
_ => (),
},
_ => (),
} }
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(&event); self.gamepads[0] |= gamepad_button(&input);
} else { } else {
self.gamepads[0] &= !gamepad_button(&event); 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,
);
}
});
match status {
PumpStatus::Exit(_) => self.is_open = false,
_ => (), _ => (),
} }
if let Some(new_filter) = new_filter {
if Instant::now() >= self.next_frame { self.filter = create_filter(
break; &self.device,
&self.palette_screen_mode.screen_view,
self.window.inner_size(),
self.surface_config.format,
new_filter,
);
} }
} });
Input { Input {
gamepads: self.gamepads, gamepads: self.gamepads,
reset, reset,
@@ -243,12 +227,10 @@ impl WindowImpl for Window {
b: 0.0, b: 0.0,
a: 1.0, a: 1.0,
}), }),
store: wgpu::StoreOp::Store, store: true,
}, },
})], })],
depth_stencil_attachment: None, depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
}); });
self.filter.render(&mut render_pass); self.filter.render(&mut render_pass);
@@ -372,7 +354,7 @@ impl PaletteScreenMode {
format: wgpu::TextureFormat::R8Uint, format: wgpu::TextureFormat::R8Uint,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
label: None, label: None,
view_formats: &[], view_formats: &[]
}); });
let palette_texture = device.create_texture(&wgpu::TextureDescriptor { let palette_texture = device.create_texture(&wgpu::TextureDescriptor {
@@ -387,7 +369,7 @@ impl PaletteScreenMode {
format: wgpu::TextureFormat::Rgba8UnormSrgb, format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
label: None, label: None,
view_formats: &[], view_formats: &[]
}); });
let screen_texture = device.create_texture(&wgpu::TextureDescriptor { let screen_texture = device.create_texture(&wgpu::TextureDescriptor {
@@ -402,7 +384,7 @@ impl PaletteScreenMode {
format: wgpu::TextureFormat::Rgba8UnormSrgb, format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
label: None, label: None,
view_formats: &[], view_formats: &[]
}); });
let framebuffer_texture_view = let framebuffer_texture_view =
@@ -509,7 +491,7 @@ impl PaletteScreenMode {
&bytemuck::cast_slice(pixels), &bytemuck::cast_slice(pixels),
wgpu::ImageDataLayout { wgpu::ImageDataLayout {
offset: 0, offset: 0,
bytes_per_row: Some(320), bytes_per_row: NonZeroU32::new(320),
rows_per_image: None, rows_per_image: None,
}, },
wgpu::Extent3d { wgpu::Extent3d {
@@ -531,7 +513,7 @@ impl PaletteScreenMode {
&bytemuck::cast_slice(palette), &bytemuck::cast_slice(palette),
wgpu::ImageDataLayout { wgpu::ImageDataLayout {
offset: 0, offset: 0,
bytes_per_row: Some(256 * 4), bytes_per_row: NonZeroU32::new(256 * 4),
rows_per_image: None, rows_per_image: None,
}, },
wgpu::Extent3d { wgpu::Extent3d {
@@ -550,12 +532,10 @@ impl PaletteScreenMode {
resolve_target: None, resolve_target: None,
ops: wgpu::Operations { ops: wgpu::Operations {
load: wgpu::LoadOp::Load, load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store, store: true,
}, },
})], })],
depth_stencil_attachment: None, depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
}); });
render_pass.set_pipeline(&self.pipeline); render_pass.set_pipeline(&self.pipeline);

View File

@@ -15,7 +15,7 @@ struct FpsCounter {
} }
impl Window { impl Window {
pub fn new(mut config: WindowConfig) -> Result<Window> { pub fn new(config: WindowConfig) -> Result<Window> {
let fps_counter = if config.fps_counter { let fps_counter = if config.fps_counter {
Some(FpsCounter { Some(FpsCounter {
start: Instant::now(), start: Instant::now(),
@@ -24,7 +24,6 @@ impl Window {
} else { } else {
None None
}; };
config.scale = config.scale.max(1.).min(20.);
if config.enable_gpu { if config.enable_gpu {
match gpu::Window::new(config) { match gpu::Window::new(config) {
Ok(window) => { Ok(window) => {
@@ -72,7 +71,6 @@ pub struct WindowConfig {
filter: u32, filter: u32,
fullscreen: bool, fullscreen: bool,
fps_counter: bool, fps_counter: bool,
scale: f32,
} }
impl Default for WindowConfig { impl Default for WindowConfig {
@@ -82,7 +80,6 @@ impl Default for WindowConfig {
filter: 5, filter: 5,
fullscreen: false, fullscreen: false,
fps_counter: false, fps_counter: false,
scale: 2.,
} }
} }
} }
@@ -105,10 +102,6 @@ impl WindowConfig {
} }
self.fullscreen = args.contains("--fullscreen"); self.fullscreen = args.contains("--fullscreen");
self.fps_counter = args.contains("--fps"); self.fps_counter = args.contains("--fps");
self.scale = args
.opt_value_from_str("--scale")
.unwrap()
.unwrap_or(self.scale);
} }
} }

View File

@@ -274,7 +274,7 @@ export default function MicroW8(screen, config = {}) {
try { try {
let restart = false; let restart = false;
let thisFrame; let nextFrame = 0;
if (!isPaused) { if (!isPaused) {
let gamepads = navigator.getGamepads(); let gamepads = navigator.getGamepads();
let gamepad = 0; let gamepad = 0;
@@ -321,13 +321,12 @@ export default function MicroW8(screen, config = {}) {
} }
canvasCtx.putImageData(imageData, 0, 0); canvasCtx.putImageData(imageData, 0, 0);
let timeOffset = ((time * 6) % 100 - 50) / 6; let thisFrame = Math.floor(time * 6 / 100);
thisFrame = startTime + time - timeOffset / 8; let timeOffset = time - thisFrame * 100 / 6;
} else { nextFrame = Math.ceil(startTime + (thisFrame + 1) * 100 / 6 + Math.min(4, timeOffset));
thisFrame = Date.now();
} }
let now = Date.now(); let now = Date.now();
let nextFrame = Math.max(thisFrame + timePerFrame, now); nextFrame = Math.max(nextFrame, now);
if (restart) { if (restart) {
runModule(currentData); runModule(currentData);