10 Commits

30 changed files with 1535 additions and 927 deletions

2035
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,8 @@ browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
[dependencies] [dependencies]
wasmtime = { version = "0.37.0", optional = true } wasmtime = { version = "0.37.0", optional = true }
anyhow = "1" anyhow = "1"
env_logger = "0.9"
log = "0.4"
uw8-window = { path = "uw8-window", optional = true } uw8-window = { path = "uw8-window", optional = true }
notify = "4" notify = "4"
pico-args = "0.4" pico-args = "0.4"
@@ -25,5 +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 = { version = "0.13.5", optional = true } cpal = { version = "0.14.1", optional = true }
rubato = { version = "0.11.0", optional = true } rubato = { version = "0.11.0", optional = true }

View File

@@ -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>

View File

@@ -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;

View File

@@ -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).

2
platform/Cargo.lock generated
View File

@@ -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",

Binary file not shown.

Binary file not shown.

View File

@@ -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);
} }

View File

@@ -29,6 +29,19 @@ Examplers for older versions:
## Versions ## Versions
### v0.2.1
* [Web runtime](v0.2.1)
* [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.2.1/microw8-0.2.1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-windows.zip)
Changes:
* new gpu accelerated renderer with (optional) crt filter
* optimized `hline` function, a big speed-up when drawing large filled circles or rectangles
* print fractional size of packed `uw8` cart
### v0.2.0 ### v0.2.0
* [Web runtime](v0.2.0) * [Web runtime](v0.2.0)

View File

@@ -436,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:

File diff suppressed because one or more lines are too long

View File

@@ -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.2.0"> <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>

View File

@@ -13,6 +13,7 @@ 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
@@ -35,7 +36,7 @@ fn main() -> Result<()> {
println!(); println!();
println!("Usage:"); println!("Usage:");
#[cfg(any(feature = "native", feature = "browser"))] #[cfg(any(feature = "native", feature = "browser"))]
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 run [-t/--timeout <frames>] [--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 pack [-u/--uncompressed] [-l/--level] <in-file> <out-file>");
println!(" uw8 unpack <in-file> <out-file>"); println!(" uw8 unpack <in-file> <out-file>");
println!(" uw8 compile [-d/--debug] <in-file> <out-file>"); println!(" uw8 compile [-d/--debug] <in-file> <out-file>");
@@ -54,8 +55,6 @@ fn run(mut args: Arguments) -> Result<()> {
let watch_mode = args.contains(["-w", "--watch"]); let watch_mode = args.contains(["-w", "--watch"]);
#[allow(unused)] #[allow(unused)]
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?; let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
#[allow(unused)]
let gpu = !args.contains("--no-gpu");
let mut config = Config::default(); let mut config = Config::default();
if args.contains(["-p", "--pack"]) { if args.contains(["-p", "--pack"]) {
@@ -82,7 +81,17 @@ 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;
let disable_audio = args.contains(["-m", "--disable-audio"]); #[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()))?;
@@ -95,7 +104,7 @@ fn run(mut args: Arguments) -> Result<()> {
unimplemented!(); unimplemented!();
#[cfg(feature = "native")] #[cfg(feature = "native")]
{ {
let mut microw8 = MicroW8::new(timeout, gpu)?; let mut microw8 = MicroW8::new(timeout, window_config)?;
if disable_audio { if disable_audio {
microw8.disable_audio(); microw8.disable_audio();
} }
@@ -167,7 +176,8 @@ 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!( println!(
"\npacked size: {:.2} bytes", "\npacked size: {} bytes ({:.2})",
cart.len(),
uw8_tool::compressed_size(&cart) uw8_tool::compressed_size(&cart)
); );
} }

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@ use std::{thread, time::Instant};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use cpal::traits::*; use cpal::traits::*;
use rubato::Resampler; use rubato::Resampler;
use uw8_window::Window; 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,
}; };
@@ -45,7 +45,7 @@ struct UW8WatchDog {
} }
impl MicroW8 { impl MicroW8 {
pub fn new(timeout: Option<u32>, gpu: bool) -> Result<MicroW8> { pub fn new(timeout: Option<u32>, window_config: WindowConfig) -> Result<MicroW8> {
let mut config = wasmtime::Config::new(); let mut config = wasmtime::Config::new();
config.cranelift_opt_level(wasmtime::OptLevel::Speed); config.cranelift_opt_level(wasmtime::OptLevel::Speed);
if timeout.is_some() { if timeout.is_some() {
@@ -56,7 +56,7 @@ impl MicroW8 {
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 window = Window::new(gpu)?; let window = Window::new(window_config)?;
Ok(MicroW8 { Ok(MicroW8 {
window, window,
@@ -314,7 +314,7 @@ fn init_sound(
let snd = instance let snd = instance
.get_typed_func::<(i32,), f32, _>(&mut store, "snd") .get_typed_func::<(i32,), f32, _>(&mut store, "snd")
.or_else(|_| platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "gesSnd"))?; .or_else(|_| platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "sndGes"))?;
let host = cpal::default_host(); let host = cpal::default_host();
let device = host let device = host
@@ -342,7 +342,7 @@ fn init_sound(
.ok_or_else(|| anyhow!("Could not find float 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())
.max(config.max_sample_rate()); .min(config.max_sample_rate());
let config = config.with_sample_rate(sample_rate); let config = config.with_sample_rate(sample_rate);
let buffer_size = match *config.buffer_size() { let buffer_size = match *config.buffer_size() {
cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default, cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default,
@@ -419,6 +419,14 @@ fn init_sound(
mem[64..68].copy_from_slice(&current_time.to_le_bytes()); mem[64..68].copy_from_slice(&current_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 { 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]
@@ -429,10 +437,12 @@ fn init_sound(
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] resampler.input_buffers[0].push(clamp_sample(
.push(snd.call(&mut store, (sample_index,)).unwrap_or(0.0)); snd.call(&mut store, (sample_index,)).unwrap_or(0.0),
resampler.input_buffers[1] ));
.push(snd.call(&mut store, (sample_index + 1,)).unwrap_or(0.0)); 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); sample_index = sample_index.wrapping_add(2);
} }
@@ -458,7 +468,7 @@ fn init_sound(
} }
} else { } else {
for v in buffer { for v in buffer {
*v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0); *v = clamp_sample(snd.call(&mut store, (sample_index,)).unwrap_or(0.0));
sample_index = sample_index.wrapping_add(1); sample_index = sample_index.wrapping_add(1);
} }
} }

1
todo.txt Normal file
View File

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

View File

@@ -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(

View File

@@ -220,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() {

7
uw8-window/Cargo.lock generated
View File

@@ -986,6 +986,12 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pico-args"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.9" version = "0.2.9"
@@ -1296,6 +1302,7 @@ dependencies = [
"env_logger", "env_logger",
"log", "log",
"minifb", "minifb",
"pico-args",
"pollster", "pollster",
"wgpu", "wgpu",
"winapi", "winapi",

View File

@@ -9,9 +9,10 @@ edition = "2021"
winit = "0.26.1" winit = "0.26.1"
env_logger = "0.9" env_logger = "0.9"
log = "0.4" log = "0.4"
pico-args = "0.4"
wgpu = "0.13.1" wgpu = "0.13.1"
pollster = "0.2" pollster = "0.2"
bytemuck = { version = "1.4", features = [ "derive" ] } bytemuck = { version = "1.4", features = [ "derive" ] }
anyhow = "1" anyhow = "1"
minifb = { version = "0.23.0", default-features = false, features = ["x11"] } minifb = { version = "0.23.0", default-features = false, features = ["x11"] }
winapi = "0.3.9" winapi = { version = "0.3.9", features = ["std"] }

View File

@@ -78,6 +78,6 @@ impl WindowImpl for Window {
} }
fn is_open(&self) -> bool { fn is_open(&self) -> bool {
self.window.is_open() self.window.is_open() && !self.window.is_key_down(Key::Escape)
} }
} }

View File

@@ -112,7 +112,7 @@ impl CrtFilter {
} }
impl Filter for CrtFilter { impl Filter for CrtFilter {
fn resize(&self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) { fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
let uniforms = Uniforms { let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(new_size), texture_scale: texture_scale_from_resolution(new_size),
}; };

View File

@@ -15,6 +15,7 @@ impl FastCrtFilter {
screen: &wgpu::TextureView, screen: &wgpu::TextureView,
resolution: PhysicalSize<u32>, resolution: PhysicalSize<u32>,
surface_format: wgpu::TextureFormat, surface_format: wgpu::TextureFormat,
chromatic: bool,
) -> FastCrtFilter { ) -> FastCrtFilter {
let uniforms = Uniforms { let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(resolution), texture_scale: texture_scale_from_resolution(resolution),
@@ -104,7 +105,11 @@ impl FastCrtFilter {
}, },
fragment: Some(wgpu::FragmentState { fragment: Some(wgpu::FragmentState {
module: &shader, module: &shader,
entry_point: "fs_main", entry_point: if chromatic {
"fs_main_chromatic"
} else {
"fs_main"
},
targets: &[Some(wgpu::ColorTargetState { targets: &[Some(wgpu::ColorTargetState {
format: surface_format, format: surface_format,
blend: None, blend: None,
@@ -126,7 +131,7 @@ impl FastCrtFilter {
} }
impl Filter for FastCrtFilter { impl Filter for FastCrtFilter {
fn resize(&self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) { fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
let uniforms = Uniforms { let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(new_size), texture_scale: texture_scale_from_resolution(new_size),
}; };

View File

@@ -34,10 +34,9 @@ fn col_factor(offset: f32) -> f32 {
return 1.0 / (1.0 + offset * offset * 16.0); return 1.0 / (1.0 + offset * offset * 16.0);
} }
@fragment fn sample_screen(tex_coords: vec2<f32>) -> vec4<f32> {
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { let base = round(tex_coords) - vec2<f32>(0.5);
let base = round(in.tex_coords) - vec2<f32>(0.5); let frac = tex_coords - base;
let frac = in.tex_coords - base;
let top_factor = row_factor(frac.y); let top_factor = row_factor(frac.y);
let bottom_factor = row_factor(frac.y - 1.0); let bottom_factor = row_factor(frac.y - 1.0);
@@ -52,3 +51,16 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
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; 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);
}

View File

@@ -1,4 +1,4 @@
use crate::{Input, WindowImpl}; use crate::{Input, WindowConfig, WindowImpl};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::{num::NonZeroU32, time::Instant}; use std::{num::NonZeroU32, time::Instant};
@@ -37,13 +37,18 @@ pub struct Window {
} }
impl Window { impl Window {
pub fn new() -> Result<Window> { pub fn new(window_config: WindowConfig) -> Result<Window> {
async fn create() -> 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(640u32, 480)) .with_inner_size(PhysicalSize::new(640u32, 480))
.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 {
Some(Fullscreen::Borderless(None))
} else {
None
})
.build(&event_loop)?; .build(&event_loop)?;
window.set_cursor_visible(false); window.set_cursor_visible(false);
@@ -73,12 +78,13 @@ impl Window {
present_mode: wgpu::PresentMode::AutoNoVsync, present_mode: wgpu::PresentMode::AutoNoVsync,
}; };
let filter: Box<dyn Filter> = Box::new(CrtFilter::new( let filter: Box<dyn Filter> = create_filter(
&device, &device,
&palette_screen_mode.screen_view, &palette_screen_mode.screen_view,
window.inner_size(), window.inner_size(),
surface_config.format, surface_config.format,
)); window_config.filter,
);
surface.configure(&device, &surface_config); surface.configure(&device, &surface_config);
@@ -95,12 +101,12 @@ impl Window {
filter, filter,
gamepads: [0; 4], gamepads: [0; 4],
next_frame: Instant::now(), next_frame: Instant::now(),
is_fullscreen: false, is_fullscreen: window_config.fullscreen,
is_open: true, is_open: true,
}) })
} }
pollster::block_on(create()) pollster::block_on(create(window_config))
} }
} }
@@ -109,6 +115,7 @@ impl WindowImpl for Window {
let mut reset = false; let mut reset = false;
self.event_loop.run_return(|event, _, control_flow| { self.event_loop.run_return(|event, _, control_flow| {
*control_flow = ControlFlow::WaitUntil(self.next_frame); *control_flow = ControlFlow::WaitUntil(self.next_frame);
let mut new_filter = None;
match event { match event {
Event::WindowEvent { event, .. } => match event { Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(new_size) => { WindowEvent::Resized(new_size) => {
@@ -153,42 +160,24 @@ impl WindowImpl for Window {
self.window.set_fullscreen(fullscreen); self.window.set_fullscreen(fullscreen);
} }
Some(VirtualKeyCode::R) => reset = true, Some(VirtualKeyCode::R) => reset = true,
Some(VirtualKeyCode::Key1) => { Some(VirtualKeyCode::Key1) => new_filter = Some(1),
self.filter = Box::new(SquareFilter::new( Some(VirtualKeyCode::Key2) => new_filter = Some(2),
&self.device, Some(VirtualKeyCode::Key3) => new_filter = Some(3),
&self.palette_screen_mode.screen_view, Some(VirtualKeyCode::Key4) => new_filter = Some(4),
self.window.inner_size(), Some(VirtualKeyCode::Key5) => new_filter = Some(5),
self.surface_config.format,
))
}
Some(VirtualKeyCode::Key2) => {
self.filter = Box::new(FastCrtFilter::new(
&self.device,
&self.palette_screen_mode.screen_view,
self.window.inner_size(),
self.surface_config.format,
))
}
Some(VirtualKeyCode::Key3) => {
self.filter = Box::new(CrtFilter::new(
&self.device,
&self.palette_screen_mode.screen_view,
self.window.inner_size(),
self.surface_config.format,
))
}
_ => (), _ => (),
} }
self.gamepads[0] |= gamepad_button(&input); self.gamepads[0] |= gamepad_button(&input);
} else { } else {
self.gamepads[1] &= !gamepad_button(&input); self.gamepads[0] &= !gamepad_button(&input);
} }
} }
_ => (), _ => (),
}, },
Event::RedrawEventsCleared => { Event::RedrawEventsCleared => {
if Instant::now() >= self.next_frame if Instant::now() >= self.next_frame
// workaround needed on Wayland until the next winit release
&& self.window.fullscreen().is_some() == self.is_fullscreen && self.window.fullscreen().is_some() == self.is_fullscreen
{ {
*control_flow = ControlFlow::Exit *control_flow = ControlFlow::Exit
@@ -196,6 +185,15 @@ impl WindowImpl for Window {
} }
_ => (), _ => (),
} }
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 { Input {
gamepads: self.gamepads, gamepads: self.gamepads,
@@ -250,11 +248,93 @@ impl WindowImpl for Window {
} }
} }
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 { trait Filter {
fn resize(&self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>); fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>);
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>); 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 { struct PaletteScreenMode {
framebuffer: wgpu::Texture, framebuffer: wgpu::Texture,
palette: wgpu::Texture, palette: wgpu::Texture,

View File

@@ -126,7 +126,7 @@ impl SquareFilter {
} }
impl Filter for SquareFilter { impl Filter for SquareFilter {
fn resize(&self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) { fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
let uniforms = Uniforms { let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(new_size), texture_scale: texture_scale_from_resolution(new_size),
}; };

View File

@@ -4,31 +4,104 @@ use std::time::Instant;
mod cpu; mod cpu;
mod gpu; mod gpu;
pub struct Window(Box<dyn WindowImpl>); pub struct Window {
inner: Box<dyn WindowImpl>,
fps_counter: Option<FpsCounter>,
}
struct FpsCounter {
start: Instant,
num_frames: u32,
}
impl Window { impl Window {
pub fn new(gpu: bool) -> Result<Window> { pub fn new(config: WindowConfig) -> Result<Window> {
if gpu { let fps_counter = if config.fps_counter {
match gpu::Window::new() { Some(FpsCounter {
Ok(window) => return Ok(Window(Box::new(window))), 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!( Err(err) => eprintln!(
"Failed to create gpu window: {}\nFalling back tp cpu window", "Failed to create gpu window: {}\nFalling back tp cpu window",
err err
), ),
} }
} }
cpu::Window::new().map(|window| Window(Box::new(window))) cpu::Window::new().map(|window| Window {
inner: Box::new(window),
fps_counter,
})
} }
pub fn begin_frame(&mut self) -> Input { pub fn begin_frame(&mut self) -> Input {
self.0.begin_frame() self.inner.begin_frame()
} }
pub fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) { pub fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
self.0.end_frame(framebuffer, palette, next_frame) 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 { pub fn is_open(&self) -> bool {
self.0.is_open() 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");
} }
} }

View File

@@ -1,8 +1,11 @@
use std::time::Instant; use std::time::Instant;
use uw8_window::WindowConfig;
fn main() { fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 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 framebuffer = vec![0u8; 320 * 240];
let mut start_time = Instant::now(); let mut start_time = Instant::now();
@@ -18,7 +21,10 @@ fn main() {
let mut fps_start = Instant::now(); let mut fps_start = Instant::now();
let mut fps_counter = 0; let mut fps_counter = 0;
let mut window = uw8_window::Window::new(true).unwrap(); 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() { while window.is_open() {
let input = window.begin_frame(); let input = window.begin_frame();

View File

@@ -63,7 +63,7 @@ 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);
} }