add uw8-window crate

This commit is contained in:
2022-07-08 22:11:00 +02:00
parent 7aa70ef39d
commit b0adf7748d
9 changed files with 2395 additions and 0 deletions

1
uw8-window/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

1697
uw8-window/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
uw8-window/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "uw8-window"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
winit = "0.26.1"
env_logger = "0.9"
log = "0.4"
wgpu = "0.13.1"
pollster = "0.2"
bytemuck = { version = "1.4", features = [ "derive" ] }
anyhow = "1"
minifb = { version = "0.23.0", default-features = false, features = ["x11"] }
winapi = "0.3.9"

53
uw8-window/src/cpu.rs Normal file
View File

@@ -0,0 +1,53 @@
use std::time::Instant;
use crate::Framebuffer;
use minifb::{Key, Window, WindowOptions};
pub fn run(mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>) -> ! {
#[cfg(target_os = "windows")]
unsafe {
winapi::um::timeapi::timeBeginPeriod(1);
}
let mut buffer: Vec<u32> = vec![0; 320 * 240];
let options = WindowOptions {
scale: minifb::Scale::X2,
scale_mode: minifb::ScaleMode::AspectRatioStretch,
resize: true,
..Default::default()
};
let mut window = Window::new("MicroW8", 320, 240, options).unwrap();
let mut next_frame = Instant::now();
while window.is_open() && !window.is_key_down(Key::Escape) {
if let Some(sleep) = next_frame.checked_duration_since(Instant::now()) {
std::thread::sleep(sleep);
}
next_frame = update(
&mut CpuFramebuffer {
buffer: &mut buffer,
},
0,
false,
);
window.update_with_buffer(&buffer, 320, 240).unwrap();
}
std::process::exit(0);
}
struct CpuFramebuffer<'a> {
buffer: &'a mut Vec<u32>,
}
impl<'a> Framebuffer for CpuFramebuffer<'a> {
fn update(&mut self, framebuffer: &[u8], palette: &[u8]) {
for (i, &color_index) in framebuffer.iter().enumerate() {
let offset = color_index as usize * 4;
self.buffer[i] = 0xff000000
| ((palette[offset] as u32) << 16)
| ((palette[offset + 1] as u32) << 8)
| palette[offset + 2] as u32;
}
}
}

78
uw8-window/src/crt.wgsl Normal file
View File

@@ -0,0 +1,78 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
struct Uniforms {
texture_scale: vec4<f32>,
}
@group(0) @binding(1) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let x = (1.0 - f32(in_vertex_index)) * 3.0;
let y = f32(in_vertex_index & 1u) * 3.0 - 1.0;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y) * uniforms.texture_scale.xy + vec2<f32>(160.0, 120.0);
return out;
}
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
fn sample_pixel(coords: vec2<i32>, offset: vec4<f32>) -> vec3<f32> {
let is_outside = any(vec2<u32>(coords) >= vec2<u32>(320u, 240u));
if(is_outside) {
return vec3<f32>(0.0);
} else {
let f = max(vec4<f32>(0.01) / offset - vec4<f32>(0.003), vec4<f32>(0.0));
return textureLoad(screen_texture, coords, 0).rgb * (f.x + f.y + f.z + f.w);
}
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let pixel = floor(in.tex_coords);
let o = vec2<f32>(0.5) - (in.tex_coords - pixel);
let pixel = vec2<i32>(pixel);
if(pixel.x < -1 || pixel.y < -1 || pixel.x > 320 || pixel.y > 240) {
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
let offset_x = o.xxxx + vec4<f32>(-0.125, 0.375, 0.125, -0.375) * uniforms.texture_scale.z;
let offset_y = o.yyyy + vec4<f32>(-0.375, -0.125, 0.375, 0.125) * uniforms.texture_scale.z;
let offset_x0 = max(abs(offset_x + vec4<f32>(-1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x1 = max(abs(offset_x) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x2 = max(abs(offset_x + vec4<f32>(1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x0 = offset_x0 * offset_x0;
let offset_x1 = offset_x1 * offset_x1;
let offset_x2 = offset_x2 * offset_x2;
let offset_yr = offset_y + vec4<f32>(-1.0);
let offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
var acc = sample_pixel(pixel + vec2<i32>(-1, -1), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(0, -1), offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, -1), offset_x2 + offset_yr);
let offset_yr = vec4<f32>(0.02) + offset_y * offset_y;
acc = acc + sample_pixel(pixel + vec2<i32>(-1, 0), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel, offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, 0), offset_x2 + offset_yr);
let offset_yr = offset_y + vec4<f32>(1.0);
let offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
acc = acc + sample_pixel(pixel + vec2<i32>(-1, 1), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(0, 1), offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, 1), offset_x2 + offset_yr);
return vec4<f32>(acc, 1.0);
}

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

@@ -0,0 +1,466 @@
use crate::Framebuffer;
use anyhow::{anyhow, Result};
use std::{num::NonZeroU32, time::Instant};
use wgpu::util::DeviceExt;
use winit::{
dpi::PhysicalSize,
event::{Event, VirtualKeyCode, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Fullscreen, WindowBuilder},
};
pub struct Window {
event_loop: EventLoop<()>,
window: winit::window::Window,
instance: wgpu::Instance,
surface: wgpu::Surface,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
}
impl Window {
pub fn new() -> Result<Window> {
async fn create() -> Result<Window> {
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_inner_size(PhysicalSize::new(640u32, 480))
.with_min_inner_size(PhysicalSize::new(320u32, 240))
.with_title("MicroW8")
.build(&event_loop)?;
window.set_cursor_visible(false);
let instance = wgpu::Instance::new(wgpu::Backends::all());
let surface = unsafe { instance.create_surface(&window) };
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.ok_or_else(|| anyhow!("Request adapter failed"))?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor::default(), None)
.await?;
Ok(Window {
event_loop,
window,
instance,
surface,
adapter,
device,
queue,
})
}
pollster::block_on(create())
}
pub fn run(
self,
mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>,
) -> ! {
let Window {
event_loop,
window,
instance,
surface,
adapter,
device,
queue,
} = self;
let framebuffer_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Uint,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
label: None,
});
let palette_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 256,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D1,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
label: None,
});
let screen_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
label: None,
});
let framebuffer_texture_view =
framebuffer_texture.create_view(&wgpu::TextureViewDescriptor::default());
let palette_texture_view =
palette_texture.create_view(&wgpu::TextureViewDescriptor::default());
let screen_texture_view =
screen_texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(window.inner_size()),
};
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let palette_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Uint,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D1,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
],
label: None,
});
let palette_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &palette_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&framebuffer_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&palette_texture_view),
},
],
label: None,
});
let crt_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: None,
});
let crt_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &crt_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&screen_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
],
label: None,
});
let palette_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("palette.wgsl").into()),
});
let palette_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&palette_bind_group_layout],
push_constant_ranges: &[],
});
let palette_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&palette_pipeline_layout),
vertex: wgpu::VertexState {
module: &palette_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &palette_shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8UnormSrgb,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
let crt_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("crt.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&crt_bind_group_layout],
push_constant_ranges: &[],
});
let mut surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface.get_supported_formats(&adapter)[0],
width: window.inner_size().width,
height: window.inner_size().height,
present_mode: wgpu::PresentMode::AutoNoVsync,
};
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &crt_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &crt_shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_config.format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
surface.configure(&device, &surface_config);
event_loop.run(move |event, _, control_flow| {
let _ = (&window, &instance, &surface, &adapter, &device);
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(new_size) => {
surface_config.width = new_size.width;
surface_config.height = new_size.height;
surface.configure(&device, &surface_config);
uniforms.texture_scale = texture_scale_from_resolution(new_size);
queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput { input, .. } => {
if input.state == winit::event::ElementState::Pressed {
match input.virtual_keycode {
Some(VirtualKeyCode::Escape) => *control_flow = ControlFlow::Exit,
Some(VirtualKeyCode::F) => {
window.set_fullscreen(if window.fullscreen().is_some() {
None
} else {
Some(Fullscreen::Borderless(None))
});
}
_ => (),
}
}
}
_ => (),
},
Event::MainEventsCleared => {
if let ControlFlow::WaitUntil(t) = *control_flow {
if Instant::now() < t {
return;
}
}
let next_frame = update(
&mut GpuFramebuffer {
queue: &queue,
framebuffer: &framebuffer_texture,
palette: &palette_texture,
},
0,
false,
);
*control_flow = ControlFlow::WaitUntil(next_frame);
let output = surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &screen_texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&palette_pipeline);
render_pass.set_bind_group(0, &palette_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
{
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&render_pipeline);
render_pass.set_bind_group(0, &crt_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
queue.submit(std::iter::once(encoder.finish()));
output.present();
}
_ => (),
}
});
}
}
struct GpuFramebuffer<'a> {
framebuffer: &'a wgpu::Texture,
palette: &'a wgpu::Texture,
queue: &'a wgpu::Queue,
}
impl<'a> Framebuffer for GpuFramebuffer<'a> {
fn update(&mut self, pixels: &[u8], palette: &[u8]) {
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: self.framebuffer,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&bytemuck::cast_slice(pixels),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: NonZeroU32::new(320),
rows_per_image: None,
},
wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
);
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: self.palette,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&bytemuck::cast_slice(palette),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: NonZeroU32::new(256 * 4),
rows_per_image: None,
},
wgpu::Extent3d {
width: 256,
height: 1,
depth_or_array_layers: 1,
},
);
}
}
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
[
res.width as f32 / scale,
res.height as f32 / scale,
2.0 / scale,
0.0,
]
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
texture_scale: [f32; 4],
}

19
uw8-window/src/lib.rs Normal file
View File

@@ -0,0 +1,19 @@
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
),
}
cpu::run(Box::new(update));
}
pub trait Framebuffer {
fn update(&mut self, pixels: &[u8], palette: &[u8]);
}

40
uw8-window/src/main.rs Normal file
View File

@@ -0,0 +1,40 @@
use std::time::{Duration, Instant};
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let mut framebuffer = vec![0u8; 320 * 240];
let start_time = Instant::now();
let mut palette = vec![0u32; 256];
for i in 0..256 {
let v = i & 15;
let r = ((i >> 2) & 12) * v;
let g = ((i >> 3) & 12) * v;
let b = ((i >> 4) & 12) * v;
palette[i as usize] = r + (g << 8) + (b << 16);
}
let mut prev_frame = Instant::now();
uw8_window::run(move |gpu_framebuffer, _gamepads, _reset| {
draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32());
gpu_framebuffer.update(&framebuffer, bytemuck::cast_slice(&palette));
prev_frame += Duration::from_secs_f32(1.0 / 60.0);
prev_frame
});
}
fn draw_frame(framebuffer: &mut [u8], time: f32) {
for x in 0..320 {
let xr = x as f32 - 160.0;
for y in 0..240 {
let yr = y as f32 - 120.0;
let f = 8192.0 / (xr * xr + yr * yr);
let u = xr * f + 512.0 + time * 32.0;
let v = yr * f + time * 29.0;
let c = (u.floor() as i32 ^ v.floor() as i32) as u32;
framebuffer[x + y * 320] = c as u8;
}
}
}

View File

@@ -0,0 +1,24 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let x = (1.0 - f32(vertex_index)) * 3.0;
let y = f32(vertex_index & 1u) * 3.0 - 1.0;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.tex_coords = vec2<f32>((x + 1.0) * 160.0, (y + 1.0) * 120.0);
return out;
}
@group(0) @binding(0) var framebuffer_texture: texture_2d<u32>;
@group(0) @binding(1) var palette_texture: texture_1d<f32>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let texel = vec2<i32>(floor(in.tex_coords));
let index = textureLoad(framebuffer_texture, texel, 0).r;
return textureLoad(palette_texture, i32(index), 0);
}