12 Commits

15 changed files with 917 additions and 312 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "uw8"
version = "0.2.0"
version = "0.2.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -35,7 +35,7 @@ fn main() -> Result<()> {
println!();
println!("Usage:");
#[cfg(any(feature = "native", feature = "browser"))]
println!(" uw8 run [-t/--timeout <frames>] [--b/--browser] [-w/--watch] [-p/--pack] [-u/--uncompressed] [-l/--level] [-o/--output <out-file>] <file>");
println!(" uw8 run [-t/--timeout <frames>] [--no-gpu] [--b/--browser] [-w/--watch] [-p/--pack] [-u/--uncompressed] [-l/--level] [-o/--output <out-file>] <file>");
println!(" uw8 pack [-u/--uncompressed] [-l/--level] <in-file> <out-file>");
println!(" uw8 unpack <in-file> <out-file>");
println!(" uw8 compile [-d/--debug] <in-file> <out-file>");
@@ -54,6 +54,8 @@ fn run(mut args: Arguments) -> Result<()> {
let watch_mode = args.contains(["-w", "--watch"]);
#[allow(unused)]
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
#[allow(unused)]
let gpu = !args.contains("--no-gpu");
let mut config = Config::default();
if args.contains(["-p", "--pack"]) {
@@ -93,7 +95,7 @@ fn run(mut args: Arguments) -> Result<()> {
unimplemented!();
#[cfg(feature = "native")]
{
let mut microw8 = MicroW8::new(timeout)?;
let mut microw8 = MicroW8::new(timeout, gpu)?;
if disable_audio {
microw8.disable_audio();
}

View File

@@ -10,7 +10,18 @@ use wasmtime::{
};
pub struct MicroW8 {
tx: mpsc::SyncSender<Vec<u8>>,
tx: mpsc::SyncSender<Option<UW8Instance>>,
rx: mpsc::Receiver<UIEvent>,
stream: Option<cpal::Stream>,
engine: Engine,
loader_module: Module,
disable_audio: bool,
module_data: Option<Vec<u8>>,
}
enum UIEvent {
Error(Result<()>),
Reset,
}
struct UW8Instance {
@@ -19,9 +30,8 @@ struct UW8Instance {
end_frame: TypedFunc<(), ()>,
update: Option<TypedFunc<(), ()>>,
start_time: Instant,
module: Vec<u8>,
watchdog: Arc<Mutex<UW8WatchDog>>,
sound: Option<Uw8Sound>,
sound_tx: Option<mpsc::SyncSender<RegisterUpdate>>,
}
impl Drop for UW8Instance {
@@ -38,7 +48,7 @@ struct UW8WatchDog {
}
impl MicroW8 {
pub fn new(timeout: Option<u32>) -> Result<MicroW8> {
pub fn new(timeout: Option<u32>, gpu: bool) -> Result<MicroW8> {
let mut config = wasmtime::Config::new();
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
if timeout.is_some() {
@@ -49,37 +59,45 @@ impl MicroW8 {
let loader_module =
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(1);
let (to_ui_tx, to_ui_rx) = mpsc::sync_channel(2);
let (from_ui_tx, from_ui_rx) = mpsc::sync_channel(1);
std::thread::spawn(move || {
let mut state = State {
engine,
loader_module,
disable_audio: false,
instance: None,
timeout: timeout.unwrap_or(0),
};
uw8_window::run(move |framebuffer, gamepad, reset| {
if let Ok(module_data) = rx.try_recv() {
if let Err(err) = state.load(&module_data) {
eprintln!("Failed to load module: {}", err);
}
uw8_window::run(gpu, move |framebuffer, gamepad, reset| {
while let Ok(instance) = to_ui_rx.try_recv() {
state.instance = instance;
}
state
.run_frame(framebuffer, gamepad, reset)
.unwrap_or_else(|err| {
eprintln!("Runtime error: {}", err);
Instant::now()
})
if reset {
from_ui_tx.send(UIEvent::Reset).unwrap();
}
state.run_frame(framebuffer, gamepad).unwrap_or_else(|err| {
from_ui_tx.send(UIEvent::Error(Err(err))).unwrap();
Instant::now()
})
});
});
Ok(MicroW8 { tx })
Ok(MicroW8 {
tx: to_ui_tx,
rx: from_ui_rx,
stream: None,
engine,
loader_module,
disable_audio: false,
module_data: None,
})
}
pub fn disable_audio(&mut self) {}
pub fn disable_audio(&mut self) {
self.disable_audio = true;
}
}
impl super::Runtime for MicroW8 {
@@ -88,26 +106,8 @@ impl super::Runtime for MicroW8 {
}
fn load(&mut self, module_data: &[u8]) -> Result<()> {
self.tx.send(module_data.into())?;
Ok(())
}
fn run_frame(&mut self) -> Result<()> {
Ok(())
}
}
struct State {
engine: Engine,
loader_module: Module,
disable_audio: bool,
instance: Option<UW8Instance>,
timeout: u32,
}
impl State {
fn load(&mut self, module_data: &[u8]) -> Result<()> {
self.instance = None;
self.stream = None;
self.tx.send(None)?;
let mut store = wasmtime::Store::new(&self.engine, ());
store.set_epoch_deadline(60);
@@ -159,39 +159,63 @@ impl State {
let end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?;
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd").ok();
let sound = if self.disable_audio {
None
let (sound_tx, stream) = if self.disable_audio {
(None, None)
} else {
match init_sound(&self.engine, &platform_module, &module) {
Ok(sound) => {
sound.stream.play()?;
Some(sound)
(Some(sound.tx), Some(sound.stream))
}
Err(err) => {
eprintln!("Failed to init sound: {}", err);
None
(None, None)
}
}
};
self.instance = Some(UW8Instance {
self.tx.send(Some(UW8Instance {
store,
memory,
end_frame,
update,
start_time: Instant::now(),
module: module_data.into(),
watchdog,
sound,
});
sound_tx,
}))?;
self.stream = stream;
self.module_data = Some(module_data.into());
Ok(())
}
fn run_frame(&mut self) -> Result<()> {
if let Ok(event) = self.rx.try_recv() {
match event {
UIEvent::Error(err) => err,
UIEvent::Reset => {
if let Some(module_data) = self.module_data.take() {
self.load(&module_data)
} else {
Ok(())
}
}
}
} else {
Ok(())
}
}
}
struct State {
instance: Option<UW8Instance>,
timeout: u32,
}
impl State {
fn run_frame(
&mut self,
framebuffer: &mut dyn uw8_window::Framebuffer,
gamepad: u32,
reset: bool,
) -> Result<Instant> {
let now = Instant::now();
let mut result = Ok(now);
@@ -199,7 +223,9 @@ impl State {
let time = (now - instance.start_time).as_millis() as i32;
{
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6;
result = Ok(now + Duration::from_millis((16 - offset) as u64));
let max = now + Duration::from_millis(17);
let next_center = now + Duration::from_millis((16 - offset) as u64);
result = Ok(next_center.min(max));
}
{
@@ -220,20 +246,18 @@ impl State {
let mut sound_regs = [0u8; 32];
sound_regs.copy_from_slice(&memory[80..112]);
if let Some(ref sound) = instance.sound {
sound.tx.send(RegisterUpdate {
if let Some(ref sound_tx) = instance.sound_tx {
let _ = sound_tx.send(RegisterUpdate {
time,
data: sound_regs,
})?;
});
}
let framebuffer_mem = &memory[120..(120 + 320 * 240)];
let palette_mem = &memory[0x13000..];
framebuffer.update(framebuffer_mem, palette_mem);
if reset {
self.load(&instance.module)?;
} else if result.is_ok() {
if result.is_ok() {
self.instance = Some(instance);
}
}

View File

@@ -35,12 +35,25 @@ pub fn run(mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant
if let Some(sleep) = next_frame.checked_duration_since(Instant::now()) {
std::thread::sleep(sleep);
}
let mut gamepad = 0;
for key in window.get_keys() {
if let Some(index) = GAMEPAD_KEYS
.iter()
.enumerate()
.find(|(_, &k)| k == key)
.map(|(i, _)| i)
{
gamepad |= 1 << index;
}
}
next_frame = update(
&mut CpuFramebuffer {
buffer: &mut buffer,
},
0,
false,
gamepad,
window.is_key_pressed(Key::R, minifb::KeyRepeat::No),
);
window.update_with_buffer(&buffer, 320, 240).unwrap();
}

143
uw8-window/src/gpu/crt.rs Normal file
View File

@@ -0,0 +1,143 @@
use wgpu::util::DeviceExt;
use winit::dpi::PhysicalSize;
use super::Filter;
pub struct CrtFilter {
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
}
impl CrtFilter {
pub fn new(
device: &wgpu::Device,
screen: &wgpu::TextureView,
resolution: PhysicalSize<u32>,
surface_format: wgpu::TextureFormat,
) -> CrtFilter {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(resolution),
};
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let crt_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: None,
});
let crt_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &crt_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&screen),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
],
label: None,
});
let crt_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("crt.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&crt_bind_group_layout],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &crt_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &crt_shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
CrtFilter {
uniform_buffer,
bind_group: crt_bind_group,
pipeline: render_pipeline,
}
}
}
impl Filter for CrtFilter {
fn resize(&self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(new_size),
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
}
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
[
res.width as f32 / scale,
res.height as f32 / scale,
2.0 / scale,
0.0,
]
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
texture_scale: [f32; 4],
}

View File

@@ -14,10 +14,11 @@ fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let x = (1.0 - f32(in_vertex_index)) * 3.0;
let y = f32(in_vertex_index & 1u) * 3.0 - 1.0;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y) * uniforms.texture_scale.xy + vec2<f32>(160.0, 120.0);
let i = in_vertex_index / 3u + in_vertex_index % 3u;
let x = -1.0 + f32(i % 2u) * 322.0;
let y = -1.0 + f32(i / 2u) * 242.0;
out.clip_position = vec4<f32>((vec2<f32>(x, y) - vec2<f32>(160.0, 120.0)) / uniforms.texture_scale.xy, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y);
return out;
}
@@ -28,7 +29,7 @@ fn sample_pixel(coords: vec2<i32>, offset: vec4<f32>) -> vec3<f32> {
if(is_outside) {
return vec3<f32>(0.0);
} else {
let f = max(vec4<f32>(0.01) / offset - vec4<f32>(0.003), vec4<f32>(0.0));
let f = max(vec4<f32>(0.008) / offset - vec4<f32>(0.0024), vec4<f32>(0.0));
return textureLoad(screen_texture, coords, 0).rgb * (f.x + f.y + f.z + f.w);
}
}
@@ -39,10 +40,6 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let o = vec2<f32>(0.5) - (in.tex_coords - pixel);
let pixel = vec2<i32>(pixel);
if(pixel.x < -1 || pixel.y < -1 || pixel.x > 320 || pixel.y > 240) {
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
let offset_x = o.xxxx + vec4<f32>(-0.125, 0.375, 0.125, -0.375) * uniforms.texture_scale.z;
let offset_y = o.yyyy + vec4<f32>(-0.375, -0.125, 0.375, 0.125) * uniforms.texture_scale.z;

View File

@@ -0,0 +1,157 @@
use wgpu::util::DeviceExt;
use winit::dpi::PhysicalSize;
use super::Filter;
pub struct FastCrtFilter {
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
}
impl FastCrtFilter {
pub fn new(
device: &wgpu::Device,
screen: &wgpu::TextureView,
resolution: PhysicalSize<u32>,
surface_format: wgpu::TextureFormat,
) -> FastCrtFilter {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(resolution),
};
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&screen),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: uniform_buffer.as_entire_binding(),
},
],
label: None,
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("fast_crt.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
FastCrtFilter {
uniform_buffer,
bind_group,
pipeline: render_pipeline,
}
}
}
impl Filter for FastCrtFilter {
fn resize(&self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(new_size),
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
}
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
[
scale / res.width as f32,
scale / res.height as f32,
2.0 / scale,
0.0,
]
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
texture_scale: [f32; 4],
}

View File

@@ -0,0 +1,54 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
struct Uniforms {
texture_scale: vec4<f32>,
}
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let i = in_vertex_index / 3u + in_vertex_index % 3u;
let x = 0.0 + f32(i % 2u) * 320.0;
let y = 0.0 + f32(i / 2u) * 240.0;
out.clip_position = vec4<f32>((vec2<f32>(x, y) - vec2<f32>(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y);
return out;
}
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
@group(0) @binding(1) var linear_sampler: sampler;
fn row_factor(offset: f32) -> f32 {
return 1.0 / (1.0 + offset * offset * 16.0);
}
fn col_factor(offset: f32) -> f32 {
let offset = max(0.0, abs(offset) - 0.4);
return 1.0 / (1.0 + offset * offset * 16.0);
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let base = round(in.tex_coords) - vec2<f32>(0.5);
let frac = in.tex_coords - base;
let top_factor = row_factor(frac.y);
let bottom_factor = row_factor(frac.y - 1.0);
let v = base.y + bottom_factor / (bottom_factor + top_factor);
let left_factor = col_factor(frac.x);
let right_factor = col_factor(frac.x - 1.0);
let u = base.x + right_factor / (right_factor + left_factor);
return textureSample(screen_texture, linear_sampler, vec2<f32>(u, v) / vec2<f32>(320.0, 240.0)) * (top_factor + bottom_factor) * (left_factor + right_factor) * 1.1;
}

View File

@@ -2,7 +2,6 @@ use crate::Framebuffer;
use anyhow::{anyhow, Result};
use std::{num::NonZeroU32, time::Instant};
use wgpu::util::DeviceExt;
use winit::{
dpi::PhysicalSize,
event::{Event, VirtualKeyCode, WindowEvent},
@@ -10,8 +9,20 @@ use winit::{
window::{Fullscreen, WindowBuilder},
};
#[cfg(unix)]
#[cfg(target_os = "macos")]
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 fast_crt;
mod square;
use crt::CrtFilter;
use fast_crt::FastCrtFilter;
use square::SquareFilter;
pub struct Window {
event_loop: EventLoop<()>,
@@ -77,6 +88,187 @@ impl Window {
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 gamepad = 0;
event_loop.run(move |event, _, control_flow| {
let _ = (&window, &instance, &surface, &adapter, &device);
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(new_size) => {
surface_config.width = new_size.width;
surface_config.height = new_size.height;
surface.configure(&device, &surface_config);
filter.resize(&queue, new_size);
}
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput { input, .. } => {
fn gamepad_button(input: &winit::event::KeyboardInput) -> u32 {
match input.scancode {
44 => 16,
45 => 32,
30 => 64,
31 => 128,
_ => match input.virtual_keycode {
Some(VirtualKeyCode::Up) => 1,
Some(VirtualKeyCode::Down) => 2,
Some(VirtualKeyCode::Left) => 4,
Some(VirtualKeyCode::Right) => 8,
_ => 0,
},
}
}
if input.state == winit::event::ElementState::Pressed {
match input.virtual_keycode {
Some(VirtualKeyCode::Escape) => *control_flow = ControlFlow::Exit,
Some(VirtualKeyCode::F) => {
window.set_fullscreen(if window.fullscreen().is_some() {
None
} else {
Some(Fullscreen::Borderless(None))
});
}
Some(VirtualKeyCode::R) => reset = true,
Some(VirtualKeyCode::Key1) => {
filter = Box::new(SquareFilter::new(
&device,
&palette_screen_mode.screen_view,
window.inner_size(),
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);
} else {
gamepad &= !gamepad_button(&input);
}
}
_ => (),
},
Event::MainEventsCleared => {
if let ControlFlow::WaitUntil(t) = *control_flow {
if Instant::now() < t {
return;
}
}
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 =
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();
}
_ => (),
}
});
}
}
struct GpuFramebuffer<'a> {
framebuffer: &'a PaletteScreenMode,
queue: &'a wgpu::Queue,
}
impl<'a> Framebuffer for GpuFramebuffer<'a> {
fn update(&mut self, pixels: &[u8], palette: &[u8]) {
self.framebuffer.write_framebuffer(self.queue, pixels);
self.framebuffer.write_palette(self.queue, palette);
}
}
trait Filter {
fn resize(&self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>);
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>);
}
struct PaletteScreenMode {
framebuffer: wgpu::Texture,
palette: wgpu::Texture,
screen_view: wgpu::TextureView,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
}
impl PaletteScreenMode {
fn new(device: &wgpu::Device) -> PaletteScreenMode {
let framebuffer_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 320,
@@ -126,16 +318,6 @@ impl Window {
let screen_texture_view =
screen_texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(window.inner_size()),
};
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let palette_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
@@ -178,48 +360,6 @@ impl Window {
label: None,
});
let crt_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: None,
});
let crt_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &crt_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&screen_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
],
label: None,
});
let palette_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("palette.wgsl").into()),
@@ -255,165 +395,19 @@ impl Window {
multiview: None,
});
let crt_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("crt.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&crt_bind_group_layout],
push_constant_ranges: &[],
});
let mut surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface.get_supported_formats(&adapter)[0],
width: window.inner_size().width,
height: window.inner_size().height,
present_mode: wgpu::PresentMode::AutoNoVsync,
};
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &crt_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &crt_shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_config.format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
surface.configure(&device, &surface_config);
event_loop.run(move |event, _, control_flow| {
let _ = (&window, &instance, &surface, &adapter, &device);
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(new_size) => {
surface_config.width = new_size.width;
surface_config.height = new_size.height;
surface.configure(&device, &surface_config);
uniforms.texture_scale = texture_scale_from_resolution(new_size);
queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput { input, .. } => {
if input.state == winit::event::ElementState::Pressed {
match input.virtual_keycode {
Some(VirtualKeyCode::Escape) => *control_flow = ControlFlow::Exit,
Some(VirtualKeyCode::F) => {
window.set_fullscreen(if window.fullscreen().is_some() {
None
} else {
Some(Fullscreen::Borderless(None))
});
}
_ => (),
}
}
}
_ => (),
},
Event::MainEventsCleared => {
if let ControlFlow::WaitUntil(t) = *control_flow {
if Instant::now() < t {
return;
}
}
let next_frame = update(
&mut GpuFramebuffer {
queue: &queue,
framebuffer: &framebuffer_texture,
palette: &palette_texture,
},
0,
false,
);
*control_flow = ControlFlow::WaitUntil(next_frame);
let output = surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &screen_texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&palette_pipeline);
render_pass.set_bind_group(0, &palette_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
{
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&render_pipeline);
render_pass.set_bind_group(0, &crt_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
queue.submit(std::iter::once(encoder.finish()));
output.present();
}
_ => (),
}
});
PaletteScreenMode {
framebuffer: framebuffer_texture,
palette: palette_texture,
screen_view: screen_texture_view,
bind_group: palette_bind_group,
pipeline: palette_pipeline,
}
}
}
struct GpuFramebuffer<'a> {
framebuffer: &'a wgpu::Texture,
palette: &'a wgpu::Texture,
queue: &'a wgpu::Queue,
}
impl<'a> Framebuffer for GpuFramebuffer<'a> {
fn update(&mut self, pixels: &[u8], palette: &[u8]) {
self.queue.write_texture(
fn write_framebuffer(&self, queue: &wgpu::Queue, pixels: &[u8]) {
queue.write_texture(
wgpu::ImageCopyTexture {
texture: self.framebuffer,
texture: &self.framebuffer,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
@@ -430,9 +424,12 @@ impl<'a> Framebuffer for GpuFramebuffer<'a> {
depth_or_array_layers: 1,
},
);
self.queue.write_texture(
}
fn write_palette(&self, queue: &wgpu::Queue, palette: &[u8]) {
queue.write_texture(
wgpu::ImageCopyTexture {
texture: self.palette,
texture: &self.palette,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
@@ -450,20 +447,23 @@ impl<'a> Framebuffer for GpuFramebuffer<'a> {
},
);
}
}
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
[
res.width as f32 / scale,
res.height as f32 / scale,
2.0 / scale,
0.0,
]
}
fn resolve_screen(&self, encoder: &mut wgpu::CommandEncoder) {
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.screen_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
texture_scale: [f32; 4],
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
}

View File

@@ -0,0 +1,157 @@
use wgpu::util::DeviceExt;
use winit::dpi::PhysicalSize;
use super::Filter;
pub struct SquareFilter {
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
}
impl SquareFilter {
pub fn new(
device: &wgpu::Device,
screen: &wgpu::TextureView,
resolution: PhysicalSize<u32>,
surface_format: wgpu::TextureFormat,
) -> SquareFilter {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(resolution),
};
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&screen),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: uniform_buffer.as_entire_binding(),
},
],
label: None,
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("square.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
SquareFilter {
uniform_buffer,
bind_group,
pipeline: render_pipeline,
}
}
}
impl Filter for SquareFilter {
fn resize(&self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(new_size),
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
}
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
[
scale / res.width as f32,
scale / res.height as f32,
2.0 / scale,
0.0,
]
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
texture_scale: [f32; 4],
}

View File

@@ -0,0 +1,44 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
struct Uniforms {
texture_scale: vec4<f32>,
}
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let i = in_vertex_index / 3u + in_vertex_index % 3u;
let x = 0.0 + f32(i % 2u) * 320.0;
let y = 0.0 + f32(i / 2u) * 240.0;
out.clip_position = vec4<f32>((vec2<f32>(x, y) - vec2<f32>(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y);
return out;
}
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
@group(0) @binding(1) var linear_sampler: sampler;
fn aa_tex_coord(c: f32) -> f32 {
let low = c - uniforms.texture_scale.z * 0.5;
let high = c + uniforms.texture_scale.z * 0.5;
let base = floor(low);
let center = base + 0.5;
let next = base + 1.0;
if high > next {
return center + (high - next) / (high - low);
} else {
return center;
}
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(screen_texture, linear_sampler, vec2<f32>(aa_tex_coord(in.tex_coords.x), aa_tex_coord(in.tex_coords.y)) / vec2<f32>(320.0, 240.0));
}

View File

@@ -3,13 +3,18 @@ use std::time::Instant;
mod cpu;
mod gpu;
pub fn run<F: 'static + FnMut(&mut dyn Framebuffer, u32, bool) -> Instant>(update: F) -> ! {
match gpu::Window::new() {
Ok(window) => window.run(Box::new(update)),
Err(err) => eprintln!(
"Failed to create gpu window: {}\nFalling back to cpu window",
err
),
pub fn run<F: 'static + FnMut(&mut dyn Framebuffer, u32, bool) -> Instant>(
gpu: bool,
update: F,
) -> ! {
if gpu {
match gpu::Window::new() {
Ok(window) => window.run(Box::new(update)),
Err(err) => eprintln!(
"Failed to create gpu window: {}\nFalling back to cpu window",
err
),
}
}
cpu::run(Box::new(update));
}

View File

@@ -1,4 +1,4 @@
use std::time::{Duration, Instant};
use std::time::Instant;
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
@@ -15,13 +15,22 @@ fn main() {
palette[i as usize] = r + (g << 8) + (b << 16);
}
let mut prev_frame = Instant::now();
let mut fps_start = Instant::now();
let mut fps_counter = 0;
uw8_window::run(move |gpu_framebuffer, _gamepads, _reset| {
draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32());
uw8_window::run(true, move |gpu_framebuffer, _gamepads, _reset| {
for _ in 0..1 {
draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32());
}
gpu_framebuffer.update(&framebuffer, bytemuck::cast_slice(&palette));
prev_frame += Duration::from_secs_f32(1.0 / 60.0);
prev_frame
fps_counter += 1;
let elapsed = fps_start.elapsed().as_secs_f32();
if elapsed >= 1.0 {
println!("{:.1} fps", fps_counter as f32 / elapsed);
fps_start = Instant::now();
fps_counter = 0;
}
Instant::now()
});
}

View File

@@ -10,7 +10,7 @@
</head>
<body>
<div id="uw8">
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.2.0
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.2.1
</div>
<div id="centered">
<canvas class="screen" id="screen" width="320" height="240">