mirror of
https://github.com/exoticorn/microw8.git
synced 2026-01-20 19:26:43 +01:00
Compare commits
5 Commits
gpu-window
...
v0.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 760664eb77 | |||
| 465e66ff4b | |||
| e4579d81bc | |||
| 1f5042059c | |||
| 499bb02f2c |
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -3165,12 +3165,14 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uw8"
|
name = "uw8"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term",
|
"ansi_term",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cpal",
|
"cpal",
|
||||||
"curlywas",
|
"curlywas",
|
||||||
|
"env_logger 0.9.0",
|
||||||
|
"log",
|
||||||
"notify",
|
"notify",
|
||||||
"pico-args",
|
"pico-args",
|
||||||
"rubato",
|
"rubato",
|
||||||
@@ -3207,6 +3209,7 @@ dependencies = [
|
|||||||
"env_logger 0.9.0",
|
"env_logger 0.9.0",
|
||||||
"log",
|
"log",
|
||||||
"minifb",
|
"minifb",
|
||||||
|
"pico-args",
|
||||||
"pollster",
|
"pollster",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -43,6 +43,27 @@ Options:
|
|||||||
-l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow.
|
-l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow.
|
||||||
-o FILE, --output FILE : Write the loaded and optionally packed cart back to disk.
|
-o FILE, --output FILE : Write the loaded and optionally packed cart back to disk.
|
||||||
|
|
||||||
|
when using the native runtime:
|
||||||
|
|
||||||
|
-m, --no-audio : Disable audio, also reduces cpu load a bit
|
||||||
|
--no-gpu : Force old cpu-only window code
|
||||||
|
--filter FILTER : Select an upscale filter at startup
|
||||||
|
--fullscreen : Start in fullscreen mode
|
||||||
|
|
||||||
|
Note that the cpu-only window does not support fullscreen nor upscale filters.
|
||||||
|
|
||||||
|
Unless --no-gpu is given, uw8 will first try to open a gpu accelerated window, falling back to the old cpu-only window if that fails.
|
||||||
|
Therefore you should rarely need to manually pass --no-gpu. If you prefer the old pixel doubling look to the now default crt filter,
|
||||||
|
you can just pass "--filter nearest" or "--filter 1".
|
||||||
|
|
||||||
|
The upscale filter options are:
|
||||||
|
1, nearest : Anti-aliased nearest filter
|
||||||
|
2, fast_crt : Very simple, cheap crt filter, not very good below a window size of 960x720
|
||||||
|
3, ss_crt : Super sampled crt filter, a little more demanding on the GPU but scales well to smaller window sizes
|
||||||
|
4, chromatic_crt : Variant of fast_crt with a slight offset of the three color dots of a pixel, still pretty cheap
|
||||||
|
5, auto_crt (default) : ss_crt below 960x720, chromatic_crt otherwise
|
||||||
|
|
||||||
|
You can switch the upscale filter at any time using the keys 1-5. You can toggle fullscreen with F.
|
||||||
|
|
||||||
uw8 pack [<options>] <infile> <outfile>
|
uw8 pack [<options>] <infile> <outfile>
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
23
src/main.rs
23
src/main.rs
@@ -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,16 @@ 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"]);
|
let disable_audio = args.contains(["-m", "--no-audio"]);
|
||||||
|
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
let window_config = {
|
||||||
|
let mut config = 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()))?;
|
||||||
|
|
||||||
@@ -90,12 +98,14 @@ fn run(mut args: Arguments) -> Result<()> {
|
|||||||
|
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
|
use uw8_window::WindowConfig;
|
||||||
|
|
||||||
let mut runtime: Box<dyn Runtime> = if !run_browser {
|
let mut runtime: Box<dyn Runtime> = if !run_browser {
|
||||||
#[cfg(not(feature = "native"))]
|
#[cfg(not(feature = "native"))]
|
||||||
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 +177,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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,20 @@ 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, WindowConfig};
|
||||||
use wasmtime::{
|
use wasmtime::{
|
||||||
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
|
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct MicroW8 {
|
pub struct MicroW8 {
|
||||||
tx: mpsc::SyncSender<Option<UW8Instance>>,
|
window: Window,
|
||||||
rx: mpsc::Receiver<UIEvent>,
|
|
||||||
stream: Option<cpal::Stream>,
|
stream: Option<cpal::Stream>,
|
||||||
engine: Engine,
|
engine: Engine,
|
||||||
loader_module: Module,
|
loader_module: Module,
|
||||||
disable_audio: bool,
|
disable_audio: bool,
|
||||||
module_data: Option<Vec<u8>>,
|
module_data: Option<Vec<u8>>,
|
||||||
}
|
timeout: u32,
|
||||||
|
instance: Option<UW8Instance>,
|
||||||
enum UIEvent {
|
|
||||||
Error(Result<()>),
|
|
||||||
Reset,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UW8Instance {
|
struct UW8Instance {
|
||||||
@@ -48,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() {
|
||||||
@@ -59,39 +56,17 @@ 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 (to_ui_tx, to_ui_rx) = mpsc::sync_channel(2);
|
let window = Window::new(window_config)?;
|
||||||
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 {
|
Ok(MicroW8 {
|
||||||
tx: to_ui_tx,
|
window,
|
||||||
rx: from_ui_rx,
|
|
||||||
stream: None,
|
stream: None,
|
||||||
engine,
|
engine,
|
||||||
loader_module,
|
loader_module,
|
||||||
disable_audio: false,
|
disable_audio: false,
|
||||||
module_data: None,
|
module_data: None,
|
||||||
|
timeout: timeout.unwrap_or(0),
|
||||||
|
instance: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,12 +77,12 @@ impl MicroW8 {
|
|||||||
|
|
||||||
impl super::Runtime for MicroW8 {
|
impl super::Runtime for MicroW8 {
|
||||||
fn is_open(&self) -> bool {
|
fn is_open(&self) -> bool {
|
||||||
true
|
self.window.is_open()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(&mut self, module_data: &[u8]) -> Result<()> {
|
fn load(&mut self, module_data: &[u8]) -> Result<()> {
|
||||||
self.stream = None;
|
self.stream = None;
|
||||||
self.tx.send(None)?;
|
self.instance = None;
|
||||||
|
|
||||||
let mut store = wasmtime::Store::new(&self.engine, ());
|
let mut store = wasmtime::Store::new(&self.engine, ());
|
||||||
store.set_epoch_deadline(60);
|
store.set_epoch_deadline(60);
|
||||||
@@ -174,7 +149,7 @@ impl super::Runtime for MicroW8 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.tx.send(Some(UW8Instance {
|
self.instance = Some(UW8Instance {
|
||||||
store,
|
store,
|
||||||
memory,
|
memory,
|
||||||
end_frame,
|
end_frame,
|
||||||
@@ -182,56 +157,36 @@ impl super::Runtime for MicroW8 {
|
|||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
watchdog,
|
watchdog,
|
||||||
sound_tx,
|
sound_tx,
|
||||||
}))?;
|
});
|
||||||
self.stream = stream;
|
self.stream = stream;
|
||||||
self.module_data = Some(module_data.into());
|
self.module_data = Some(module_data.into());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_frame(&mut self) -> Result<()> {
|
fn run_frame(&mut self) -> Result<()> {
|
||||||
if let Ok(event) = self.rx.try_recv() {
|
let input = self.window.begin_frame();
|
||||||
match event {
|
|
||||||
UIEvent::Error(err) => err,
|
if input.reset {
|
||||||
UIEvent::Reset => {
|
if let Some(module_data) = self.module_data.take() {
|
||||||
if let Some(module_data) = self.module_data.take() {
|
self.load(&module_data)?;
|
||||||
self.load(&module_data)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct State {
|
|
||||||
instance: Option<UW8Instance>,
|
|
||||||
timeout: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
fn run_frame(
|
|
||||||
&mut self,
|
|
||||||
framebuffer: &mut dyn uw8_window::Framebuffer,
|
|
||||||
gamepad: u32,
|
|
||||||
) -> Result<Instant> {
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let mut result = Ok(now);
|
let mut result = Ok(());
|
||||||
if let Some(mut instance) = self.instance.take() {
|
if let Some(mut instance) = self.instance.take() {
|
||||||
let time = (now - instance.start_time).as_millis() as i32;
|
let time = (now - instance.start_time).as_millis() as i32;
|
||||||
{
|
let next_frame = {
|
||||||
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6;
|
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6;
|
||||||
let max = now + Duration::from_millis(17);
|
let max = now + Duration::from_millis(17);
|
||||||
let next_center = now + Duration::from_millis((16 - offset) as u64);
|
let next_center = now + Duration::from_millis((16 - offset) as u64);
|
||||||
result = Ok(next_center.min(max));
|
next_center.min(max)
|
||||||
}
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mem = instance.memory.data_mut(&mut instance.store);
|
let mem = instance.memory.data_mut(&mut instance.store);
|
||||||
mem[64..68].copy_from_slice(&time.to_le_bytes());
|
mem[64..68].copy_from_slice(&time.to_le_bytes());
|
||||||
mem[68..72].copy_from_slice(&gamepad.to_le_bytes());
|
mem[68..72].copy_from_slice(&input.gamepads);
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.store.set_epoch_deadline(self.timeout as u64);
|
instance.store.set_epoch_deadline(self.timeout as u64);
|
||||||
@@ -255,14 +210,16 @@ impl State {
|
|||||||
|
|
||||||
let framebuffer_mem = &memory[120..(120 + 320 * 240)];
|
let framebuffer_mem = &memory[120..(120 + 320 * 240)];
|
||||||
let palette_mem = &memory[0x13000..];
|
let palette_mem = &memory[0x13000..];
|
||||||
framebuffer.update(framebuffer_mem, palette_mem);
|
self.window
|
||||||
|
.end_frame(framebuffer_mem, palette_mem, next_frame);
|
||||||
|
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
self.instance = Some(instance);
|
self.instance = Some(instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result?)
|
result?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
uw8-window/Cargo.lock
generated
7
uw8-window/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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" ] }
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::Framebuffer;
|
use crate::{Input, WindowImpl};
|
||||||
use minifb::{Key, Window, WindowOptions};
|
use anyhow::Result;
|
||||||
|
use minifb::{Key, WindowOptions};
|
||||||
|
|
||||||
static GAMEPAD_KEYS: &[Key] = &[
|
static GAMEPAD_KEYS: &[Key] = &[
|
||||||
Key::Up,
|
Key::Up,
|
||||||
@@ -14,58 +15,53 @@ static GAMEPAD_KEYS: &[Key] = &[
|
|||||||
Key::S,
|
Key::S,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn run(mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>) -> ! {
|
pub struct Window {
|
||||||
#[cfg(target_os = "windows")]
|
window: minifb::Window,
|
||||||
unsafe {
|
buffer: Vec<u32>,
|
||||||
winapi::um::timeapi::timeBeginPeriod(1);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let mut buffer: Vec<u32> = vec![0; 320 * 240];
|
impl Window {
|
||||||
|
pub fn new() -> Result<Window> {
|
||||||
let options = WindowOptions {
|
#[cfg(target_os = "windows")]
|
||||||
scale: minifb::Scale::X2,
|
unsafe {
|
||||||
scale_mode: minifb::ScaleMode::AspectRatioStretch,
|
winapi::um::timeapi::timeBeginPeriod(1);
|
||||||
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;
|
let buffer: Vec<u32> = vec![0; 320 * 240];
|
||||||
for key in window.get_keys() {
|
|
||||||
|
let options = WindowOptions {
|
||||||
|
scale: minifb::Scale::X2,
|
||||||
|
scale_mode: minifb::ScaleMode::AspectRatioStretch,
|
||||||
|
resize: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let window = minifb::Window::new("MicroW8", 320, 240, options).unwrap();
|
||||||
|
|
||||||
|
Ok(Window { window, buffer })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowImpl for Window {
|
||||||
|
fn begin_frame(&mut self) -> Input {
|
||||||
|
let mut gamepads = [0u8; 4];
|
||||||
|
for key in self.window.get_keys() {
|
||||||
if let Some(index) = GAMEPAD_KEYS
|
if let Some(index) = GAMEPAD_KEYS
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_, &k)| k == key)
|
.find(|(_, &k)| k == key)
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
{
|
{
|
||||||
gamepad |= 1 << index;
|
gamepads[0] |= 1 << index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next_frame = update(
|
Input {
|
||||||
&mut CpuFramebuffer {
|
gamepads,
|
||||||
buffer: &mut buffer,
|
reset: self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No),
|
||||||
},
|
}
|
||||||
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> {
|
fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
|
||||||
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() {
|
for (i, &color_index) in framebuffer.iter().enumerate() {
|
||||||
let offset = color_index as usize * 4;
|
let offset = color_index as usize * 4;
|
||||||
self.buffer[i] = 0xff000000
|
self.buffer[i] = 0xff000000
|
||||||
@@ -73,5 +69,15 @@ impl<'a> Framebuffer for CpuFramebuffer<'a> {
|
|||||||
| ((palette[offset + 1] as u32) << 8)
|
| ((palette[offset + 1] as u32) << 8)
|
||||||
| palette[offset + 2] as u32;
|
| palette[offset + 2] as u32;
|
||||||
}
|
}
|
||||||
|
self.window
|
||||||
|
.update_with_buffer(&self.buffer, 320, 240)
|
||||||
|
.unwrap();
|
||||||
|
if let Some(sleep) = next_frame.checked_duration_since(Instant::now()) {
|
||||||
|
std::thread::sleep(sleep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_open(&self) -> bool {
|
||||||
|
self.window.is_open() && !self.window.is_key_down(Key::Escape)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::Framebuffer;
|
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};
|
||||||
|
|
||||||
@@ -9,12 +9,7 @@ use winit::{
|
|||||||
window::{Fullscreen, WindowBuilder},
|
window::{Fullscreen, WindowBuilder},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
use winit::platform::run_return::EventLoopExtRunReturn;
|
||||||
use winit::platform::macos::EventLoopExtMacOS;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use winit::platform::unix::EventLoopExtUnix;
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
use winit::platform::windows::EventLoopExtWindows;
|
|
||||||
|
|
||||||
mod crt;
|
mod crt;
|
||||||
mod fast_crt;
|
mod fast_crt;
|
||||||
@@ -25,23 +20,35 @@ use fast_crt::FastCrtFilter;
|
|||||||
use square::SquareFilter;
|
use square::SquareFilter;
|
||||||
|
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
event_loop: EventLoop<()>,
|
_instance: wgpu::Instance,
|
||||||
window: winit::window::Window,
|
|
||||||
instance: wgpu::Instance,
|
|
||||||
surface: wgpu::Surface,
|
surface: wgpu::Surface,
|
||||||
adapter: wgpu::Adapter,
|
_adapter: wgpu::Adapter,
|
||||||
device: wgpu::Device,
|
device: wgpu::Device,
|
||||||
queue: wgpu::Queue,
|
queue: wgpu::Queue,
|
||||||
|
palette_screen_mode: PaletteScreenMode,
|
||||||
|
surface_config: wgpu::SurfaceConfiguration,
|
||||||
|
filter: Box<dyn Filter>,
|
||||||
|
event_loop: EventLoop<()>,
|
||||||
|
window: winit::window::Window,
|
||||||
|
gamepads: [u8; 4],
|
||||||
|
next_frame: Instant,
|
||||||
|
is_fullscreen: bool,
|
||||||
|
is_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
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_any_thread();
|
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);
|
||||||
@@ -61,70 +68,68 @@ impl Window {
|
|||||||
.request_device(&wgpu::DeviceDescriptor::default(), None)
|
.request_device(&wgpu::DeviceDescriptor::default(), None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let palette_screen_mode = PaletteScreenMode::new(&device);
|
||||||
|
|
||||||
|
let surface_config = wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format: surface.get_supported_formats(&adapter)[0],
|
||||||
|
width: window.inner_size().width,
|
||||||
|
height: window.inner_size().height,
|
||||||
|
present_mode: wgpu::PresentMode::AutoNoVsync,
|
||||||
|
};
|
||||||
|
|
||||||
|
let filter: Box<dyn Filter> = create_filter(
|
||||||
|
&device,
|
||||||
|
&palette_screen_mode.screen_view,
|
||||||
|
window.inner_size(),
|
||||||
|
surface_config.format,
|
||||||
|
window_config.filter,
|
||||||
|
);
|
||||||
|
|
||||||
|
surface.configure(&device, &surface_config);
|
||||||
|
|
||||||
Ok(Window {
|
Ok(Window {
|
||||||
event_loop,
|
event_loop,
|
||||||
window,
|
window,
|
||||||
instance,
|
_instance: instance,
|
||||||
surface,
|
surface,
|
||||||
adapter,
|
_adapter: adapter,
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
|
palette_screen_mode,
|
||||||
|
surface_config,
|
||||||
|
filter,
|
||||||
|
gamepads: [0; 4],
|
||||||
|
next_frame: Instant::now(),
|
||||||
|
is_fullscreen: window_config.fullscreen,
|
||||||
|
is_open: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pollster::block_on(create())
|
pollster::block_on(create(window_config))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run(
|
impl WindowImpl for Window {
|
||||||
self,
|
fn begin_frame(&mut self) -> Input {
|
||||||
mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>,
|
|
||||||
) -> ! {
|
|
||||||
let Window {
|
|
||||||
event_loop,
|
|
||||||
window,
|
|
||||||
instance,
|
|
||||||
surface,
|
|
||||||
adapter,
|
|
||||||
device,
|
|
||||||
queue,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let palette_screen_mode = PaletteScreenMode::new(&device);
|
|
||||||
|
|
||||||
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 mut filter: Box<dyn Filter> = Box::new(CrtFilter::new(
|
|
||||||
&device,
|
|
||||||
&palette_screen_mode.screen_view,
|
|
||||||
window.inner_size(),
|
|
||||||
surface_config.format,
|
|
||||||
));
|
|
||||||
|
|
||||||
surface.configure(&device, &surface_config);
|
|
||||||
|
|
||||||
let mut reset = false;
|
let mut reset = false;
|
||||||
let mut gamepad = 0;
|
self.event_loop.run_return(|event, _, control_flow| {
|
||||||
|
*control_flow = ControlFlow::WaitUntil(self.next_frame);
|
||||||
event_loop.run(move |event, _, control_flow| {
|
let mut new_filter = None;
|
||||||
let _ = (&window, &instance, &surface, &adapter, &device);
|
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::WindowEvent { event, .. } => match event {
|
Event::WindowEvent { event, .. } => match event {
|
||||||
WindowEvent::Resized(new_size) => {
|
WindowEvent::Resized(new_size) => {
|
||||||
surface_config.width = new_size.width;
|
self.surface_config.width = new_size.width;
|
||||||
surface_config.height = new_size.height;
|
self.surface_config.height = new_size.height;
|
||||||
surface.configure(&device, &surface_config);
|
self.surface.configure(&self.device, &self.surface_config);
|
||||||
filter.resize(&queue, new_size);
|
self.filter.resize(&self.queue, new_size);
|
||||||
|
}
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
self.is_open = false;
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
}
|
}
|
||||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
|
||||||
WindowEvent::KeyboardInput { input, .. } => {
|
WindowEvent::KeyboardInput { input, .. } => {
|
||||||
fn gamepad_button(input: &winit::event::KeyboardInput) -> u32 {
|
fn gamepad_button(input: &winit::event::KeyboardInput) -> u8 {
|
||||||
match input.scancode {
|
match input.scancode {
|
||||||
44 => 16,
|
44 => 16,
|
||||||
45 => 32,
|
45 => 32,
|
||||||
@@ -141,124 +146,195 @@ impl Window {
|
|||||||
}
|
}
|
||||||
if input.state == winit::event::ElementState::Pressed {
|
if input.state == winit::event::ElementState::Pressed {
|
||||||
match input.virtual_keycode {
|
match input.virtual_keycode {
|
||||||
Some(VirtualKeyCode::Escape) => *control_flow = ControlFlow::Exit,
|
Some(VirtualKeyCode::Escape) => {
|
||||||
|
self.is_open = false;
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
Some(VirtualKeyCode::F) => {
|
Some(VirtualKeyCode::F) => {
|
||||||
window.set_fullscreen(if window.fullscreen().is_some() {
|
let fullscreen = if self.window.fullscreen().is_some() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(Fullscreen::Borderless(None))
|
Some(Fullscreen::Borderless(None))
|
||||||
});
|
};
|
||||||
|
self.is_fullscreen = fullscreen.is_some();
|
||||||
|
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),
|
||||||
filter = Box::new(SquareFilter::new(
|
Some(VirtualKeyCode::Key2) => new_filter = Some(2),
|
||||||
&device,
|
Some(VirtualKeyCode::Key3) => new_filter = Some(3),
|
||||||
&palette_screen_mode.screen_view,
|
Some(VirtualKeyCode::Key4) => new_filter = Some(4),
|
||||||
window.inner_size(),
|
Some(VirtualKeyCode::Key5) => new_filter = Some(5),
|
||||||
surface_config.format,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Some(VirtualKeyCode::Key2) => {
|
|
||||||
filter = Box::new(FastCrtFilter::new(
|
|
||||||
&device,
|
|
||||||
&palette_screen_mode.screen_view,
|
|
||||||
window.inner_size(),
|
|
||||||
surface_config.format,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Some(VirtualKeyCode::Key3) => {
|
|
||||||
filter = Box::new(CrtFilter::new(
|
|
||||||
&device,
|
|
||||||
&palette_screen_mode.screen_view,
|
|
||||||
window.inner_size(),
|
|
||||||
surface_config.format,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
gamepad |= gamepad_button(&input);
|
self.gamepads[0] |= gamepad_button(&input);
|
||||||
} else {
|
} else {
|
||||||
gamepad &= !gamepad_button(&input);
|
self.gamepads[0] &= !gamepad_button(&input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
Event::MainEventsCleared => {
|
Event::RedrawEventsCleared => {
|
||||||
if let ControlFlow::WaitUntil(t) = *control_flow {
|
if Instant::now() >= self.next_frame
|
||||||
if Instant::now() < t {
|
// workaround needed on Wayland until the next winit release
|
||||||
return;
|
&& self.window.fullscreen().is_some() == self.is_fullscreen
|
||||||
}
|
|
||||||
}
|
|
||||||
let next_frame = update(
|
|
||||||
&mut GpuFramebuffer {
|
|
||||||
queue: &queue,
|
|
||||||
framebuffer: &palette_screen_mode,
|
|
||||||
},
|
|
||||||
gamepad,
|
|
||||||
reset,
|
|
||||||
);
|
|
||||||
reset = 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 });
|
|
||||||
|
|
||||||
palette_screen_mode.resolve_screen(&mut encoder);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut render_pass =
|
*control_flow = ControlFlow::Exit
|
||||||
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
||||||
label: None,
|
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
||||||
view: &view,
|
|
||||||
resolve_target: None,
|
|
||||||
ops: wgpu::Operations {
|
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
|
||||||
r: 0.0,
|
|
||||||
g: 0.0,
|
|
||||||
b: 0.0,
|
|
||||||
a: 1.0,
|
|
||||||
}),
|
|
||||||
store: true,
|
|
||||||
},
|
|
||||||
})],
|
|
||||||
depth_stencil_attachment: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
filter.render(&mut render_pass);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.submit(std::iter::once(encoder.finish()));
|
|
||||||
output.present();
|
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
if let Some(new_filter) = new_filter {
|
||||||
|
self.filter = create_filter(
|
||||||
|
&self.device,
|
||||||
|
&self.palette_screen_mode.screen_view,
|
||||||
|
self.window.inner_size(),
|
||||||
|
self.surface_config.format,
|
||||||
|
new_filter,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
Input {
|
||||||
|
gamepads: self.gamepads,
|
||||||
|
reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
|
||||||
|
self.next_frame = next_frame;
|
||||||
|
self.palette_screen_mode
|
||||||
|
.write_framebuffer(&self.queue, framebuffer);
|
||||||
|
self.palette_screen_mode.write_palette(&self.queue, palette);
|
||||||
|
|
||||||
|
let output = self.surface.get_current_texture().unwrap();
|
||||||
|
let view = output
|
||||||
|
.texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||||
|
|
||||||
|
self.palette_screen_mode.resolve_screen(&mut encoder);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: None,
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
|
r: 0.0,
|
||||||
|
g: 0.0,
|
||||||
|
b: 0.0,
|
||||||
|
a: 1.0,
|
||||||
|
}),
|
||||||
|
store: true,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.filter.render(&mut render_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
output.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_open(&self) -> bool {
|
||||||
|
self.is_open
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GpuFramebuffer<'a> {
|
fn create_filter(
|
||||||
framebuffer: &'a PaletteScreenMode,
|
device: &wgpu::Device,
|
||||||
queue: &'a wgpu::Queue,
|
screen_texture: &wgpu::TextureView,
|
||||||
}
|
window_size: PhysicalSize<u32>,
|
||||||
|
surface_format: wgpu::TextureFormat,
|
||||||
impl<'a> Framebuffer for GpuFramebuffer<'a> {
|
filter: u32,
|
||||||
fn update(&mut self, pixels: &[u8], palette: &[u8]) {
|
) -> Box<dyn Filter> {
|
||||||
self.framebuffer.write_framebuffer(self.queue, pixels);
|
match filter {
|
||||||
self.framebuffer.write_palette(self.queue, palette);
|
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,
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,81 @@
|
|||||||
|
use anyhow::Result;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
mod cpu;
|
mod cpu;
|
||||||
mod gpu;
|
mod gpu;
|
||||||
|
|
||||||
pub fn run<F: 'static + FnMut(&mut dyn Framebuffer, u32, bool) -> Instant>(
|
pub struct Window(Box<dyn WindowImpl>);
|
||||||
gpu: bool,
|
|
||||||
update: F,
|
impl Window {
|
||||||
) -> ! {
|
pub fn new(config: WindowConfig) -> Result<Window> {
|
||||||
if gpu {
|
if config.enable_gpu {
|
||||||
match gpu::Window::new() {
|
match gpu::Window::new(config) {
|
||||||
Ok(window) => window.run(Box::new(update)),
|
Ok(window) => return Ok(Window(Box::new(window))),
|
||||||
Err(err) => eprintln!(
|
Err(err) => eprintln!(
|
||||||
"Failed to create gpu window: {}\nFalling back to cpu window",
|
"Failed to create gpu window: {}\nFalling back tp cpu window",
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
cpu::Window::new().map(|window| Window(Box::new(window)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_frame(&mut self) -> Input {
|
||||||
|
self.0.begin_frame()
|
||||||
|
}
|
||||||
|
pub fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
|
||||||
|
self.0.end_frame(framebuffer, palette, next_frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.0.is_open()
|
||||||
}
|
}
|
||||||
cpu::run(Box::new(update));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Framebuffer {
|
#[derive(Debug)]
|
||||||
fn update(&mut self, pixels: &[u8], palette: &[u8]);
|
pub struct WindowConfig {
|
||||||
|
enable_gpu: bool,
|
||||||
|
filter: u32,
|
||||||
|
fullscreen: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WindowConfig {
|
||||||
|
fn default() -> WindowConfig {
|
||||||
|
WindowConfig {
|
||||||
|
enable_gpu: true,
|
||||||
|
filter: 5,
|
||||||
|
fullscreen: 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Input {
|
||||||
|
pub gamepads: [u8; 4],
|
||||||
|
pub reset: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait WindowImpl {
|
||||||
|
fn begin_frame(&mut self) -> Input;
|
||||||
|
fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant);
|
||||||
|
fn is_open(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
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 start_time = Instant::now();
|
let mut start_time = Instant::now();
|
||||||
|
|
||||||
let mut palette = vec![0u32; 256];
|
let mut palette = vec![0u32; 256];
|
||||||
for i in 0..256 {
|
for i in 0..256 {
|
||||||
@@ -18,11 +21,19 @@ fn main() {
|
|||||||
let mut fps_start = Instant::now();
|
let mut fps_start = Instant::now();
|
||||||
let mut fps_counter = 0;
|
let mut fps_counter = 0;
|
||||||
|
|
||||||
uw8_window::run(true, move |gpu_framebuffer, _gamepads, _reset| {
|
let mut window_config = WindowConfig::default();
|
||||||
for _ in 0..1 {
|
window_config.parse_arguments(&mut args);
|
||||||
draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32());
|
|
||||||
|
let mut window = uw8_window::Window::new(window_config).unwrap();
|
||||||
|
|
||||||
|
while window.is_open() {
|
||||||
|
let input = window.begin_frame();
|
||||||
|
if input.reset {
|
||||||
|
start_time = Instant::now();
|
||||||
}
|
}
|
||||||
gpu_framebuffer.update(&framebuffer, bytemuck::cast_slice(&palette));
|
draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32());
|
||||||
|
window.end_frame(&framebuffer, bytemuck::cast_slice(&palette), Instant::now());
|
||||||
|
|
||||||
fps_counter += 1;
|
fps_counter += 1;
|
||||||
let elapsed = fps_start.elapsed().as_secs_f32();
|
let elapsed = fps_start.elapsed().as_secs_f32();
|
||||||
if elapsed >= 1.0 {
|
if elapsed >= 1.0 {
|
||||||
@@ -30,8 +41,7 @@ fn main() {
|
|||||||
fps_start = Instant::now();
|
fps_start = Instant::now();
|
||||||
fps_counter = 0;
|
fps_counter = 0;
|
||||||
}
|
}
|
||||||
Instant::now()
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_frame(framebuffer: &mut [u8], time: f32) {
|
fn draw_frame(framebuffer: &mut [u8], time: f32) {
|
||||||
|
|||||||
Reference in New Issue
Block a user