451 lines
18 KiB
Rust
451 lines
18 KiB
Rust
//! pbio — Pixelbuffer-basiertes Fenster mit Palette-Lookup-Renderer.
|
|
|
|
use std::collections::VecDeque;
|
|
use std::time::Duration;
|
|
|
|
use wgpu::util::DeviceExt;
|
|
|
|
pub mod palette;
|
|
|
|
// --- Konfiguration ---------------------------------------------------------
|
|
|
|
pub struct PlatformConfig {
|
|
pub title: String,
|
|
pub window_size: (u32, u32),
|
|
pub framebuffer_size: (u32, u32),
|
|
pub aspect_ratio: Option<f32>,
|
|
pub palette: [[u8; 3]; 256],
|
|
pub vsync: bool,
|
|
}
|
|
|
|
impl Default for PlatformConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
title: "pbio".into(),
|
|
window_size: (800, 600),
|
|
framebuffer_size: (320, 240),
|
|
aspect_ratio: Some(4.0 / 3.0),
|
|
palette: palette::RGB332,
|
|
vsync: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Events ----------------------------------------------------------------
|
|
|
|
#[non_exhaustive]
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum Key {
|
|
A, B, C, D, E, F, G, H, I, J, K, L, M,
|
|
N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
|
|
Num0, Num1, Num2, Num3, Num4,
|
|
Num5, Num6, Num7, Num8, Num9,
|
|
Up, Down, Left, Right,
|
|
Space, Enter, Escape, Tab, Backspace,
|
|
LShift, RShift, LCtrl, RCtrl, LAlt, RAlt,
|
|
F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
|
|
}
|
|
|
|
#[non_exhaustive]
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum MouseButton { Left, Right, Middle }
|
|
|
|
#[non_exhaustive]
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub enum Event {
|
|
Key { key: Key, pressed: bool, repeat: bool },
|
|
Char (char),
|
|
MouseMove { x: f32, y: f32 },
|
|
MouseBtn { button: MouseButton, pressed: bool },
|
|
Resize { width: u32, height: u32 },
|
|
CloseRequested,
|
|
}
|
|
|
|
// Muss zum Shader-Uniform passen (32 Byte, 16-Byte-aligned).
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
|
struct Params {
|
|
src_size: [f32; 2],
|
|
content_offset: [f32; 2],
|
|
content_size: [f32; 2],
|
|
_pad: [f32; 2],
|
|
}
|
|
|
|
// --- Platform --------------------------------------------------------------
|
|
|
|
pub struct Platform {
|
|
// GPU-Ressourcen vor Surface, Surface vor Window/Glfw (Drop-Reihenfolge).
|
|
bind_group: wgpu::BindGroup,
|
|
pipeline: wgpu::RenderPipeline,
|
|
index_tex: wgpu::Texture,
|
|
#[allow(dead_code)]
|
|
palette_tex: wgpu::Texture,
|
|
params_buf: wgpu::Buffer,
|
|
params: Params,
|
|
surface: wgpu::Surface<'static>,
|
|
config: wgpu::SurfaceConfiguration,
|
|
device: wgpu::Device,
|
|
queue: wgpu::Queue,
|
|
instance: wgpu::Instance,
|
|
|
|
window: glfw::PWindow,
|
|
events: glfw::GlfwReceiver<(f64, glfw::WindowEvent)>,
|
|
glfw: glfw::Glfw,
|
|
|
|
framebuffer: Vec<u8>,
|
|
framebuffer_size: (u32, u32),
|
|
aspect_ratio: f32, // 0.0 = keine Letterbox
|
|
content_rect: (u32, u32, u32, u32),
|
|
event_queue: VecDeque<Event>,
|
|
event_scratch: Vec<glfw::WindowEvent>,
|
|
close_requested: bool,
|
|
}
|
|
|
|
impl Platform {
|
|
pub fn new(config: PlatformConfig) -> Self {
|
|
pollster::block_on(Self::new_async(config))
|
|
}
|
|
|
|
async fn new_async(cfg: PlatformConfig) -> Self {
|
|
use glfw::fail_on_errors;
|
|
|
|
let mut glfw = glfw::init(fail_on_errors!()).unwrap();
|
|
glfw.window_hint(glfw::WindowHint::ClientApi(glfw::ClientApiHint::NoApi));
|
|
|
|
let (mut window, events) = glfw
|
|
.create_window(cfg.window_size.0, cfg.window_size.1, &cfg.title, glfw::WindowMode::Windowed)
|
|
.expect("failed to create GLFW window");
|
|
|
|
window.set_framebuffer_size_polling(true);
|
|
window.set_key_polling(true);
|
|
window.set_char_polling(true);
|
|
window.set_cursor_pos_polling(true);
|
|
window.set_mouse_button_polling(true);
|
|
window.set_close_polling(true);
|
|
|
|
let win_size = window.get_framebuffer_size();
|
|
|
|
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
|
backends: wgpu::Backends::all(),
|
|
..Default::default()
|
|
});
|
|
let surface = instance.create_surface(window.render_context()).expect("surface");
|
|
|
|
let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
|
|
compatible_surface: Some(&surface),
|
|
..Default::default()
|
|
}).await.expect("no suitable wgpu adapter");
|
|
|
|
let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
|
|
label: Some("pbio device"),
|
|
..Default::default()
|
|
}).await.expect("device");
|
|
|
|
let caps = surface.get_capabilities(&adapter);
|
|
let format = caps.formats.iter().copied().find(|f| f.is_srgb()).unwrap_or(caps.formats[0]);
|
|
|
|
let surface_config = wgpu::SurfaceConfiguration {
|
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
|
format,
|
|
width: win_size.0 as u32,
|
|
height: win_size.1 as u32,
|
|
present_mode: if cfg.vsync { wgpu::PresentMode::Mailbox } else { wgpu::PresentMode::Immediate },
|
|
alpha_mode: caps.alpha_modes[0],
|
|
view_formats: vec![],
|
|
desired_maximum_frame_latency: 2,
|
|
};
|
|
surface.configure(&device, &surface_config);
|
|
|
|
// --- Texturen ---
|
|
let (src_w, src_h) = cfg.framebuffer_size;
|
|
let index_tex = make_tex(&device, "pbio index_tex", src_w, src_h, wgpu::TextureFormat::R8Uint);
|
|
let palette_tex = make_tex(&device, "pbio palette_tex", 256, 1, wgpu::TextureFormat::Rgba8Unorm);
|
|
let index_view = index_tex.create_view(&Default::default());
|
|
let palette_view = palette_tex.create_view(&Default::default());
|
|
|
|
let palette_rgba: Vec<u8> = cfg.palette.iter()
|
|
.flat_map(|[r, g, b]| [*r, *g, *b, 255])
|
|
.collect();
|
|
queue.write_texture(
|
|
wgpu::TexelCopyTextureInfoBase { texture: &palette_tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
|
&palette_rgba,
|
|
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(256 * 4), rows_per_image: None },
|
|
wgpu::Extent3d { width: 256, height: 1, depth_or_array_layers: 1 },
|
|
);
|
|
|
|
// --- Uniform ---
|
|
let aspect_ratio = cfg.aspect_ratio.unwrap_or(0.0);
|
|
let content_rect = compute_content_rect((surface_config.width, surface_config.height), aspect_ratio);
|
|
let params = Params {
|
|
src_size: [src_w as f32, src_h as f32],
|
|
content_offset: [content_rect.0 as f32, content_rect.1 as f32],
|
|
content_size: [content_rect.2 as f32, content_rect.3 as f32],
|
|
_pad: [0.0, 0.0],
|
|
};
|
|
let params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("pbio params_buf"),
|
|
contents: bytemuck::bytes_of(¶ms),
|
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
});
|
|
|
|
// --- Pipeline ---
|
|
let (bgl, pipeline) = build_pipeline(&device, surface_config.format);
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("pbio bind_group"),
|
|
layout: &bgl,
|
|
entries: &[
|
|
wgpu::BindGroupEntry { binding: 0, resource: params_buf.as_entire_binding() },
|
|
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&index_view) },
|
|
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&palette_view) },
|
|
],
|
|
});
|
|
|
|
Self {
|
|
bind_group, pipeline, index_tex, palette_tex, params_buf, params,
|
|
surface, config: surface_config, device, queue, instance,
|
|
window, events, glfw,
|
|
framebuffer: vec![0u8; (src_w * src_h) as usize],
|
|
framebuffer_size: (src_w, src_h),
|
|
aspect_ratio, content_rect,
|
|
event_queue: VecDeque::new(),
|
|
event_scratch: Vec::new(),
|
|
close_requested: false,
|
|
}
|
|
}
|
|
|
|
pub fn should_close(&self) -> bool { self.close_requested || self.window.should_close() }
|
|
pub fn request_close(&mut self) { self.close_requested = true; self.window.set_should_close(true); }
|
|
|
|
pub fn poll_events(&mut self, timeout: Option<Duration>) {
|
|
match timeout {
|
|
Some(t) => self.glfw.wait_events_timeout(t.as_secs_f64()),
|
|
None => self.glfw.wait_events(),
|
|
}
|
|
// flush_messages borrowt &events, handle_glfw_event will &mut self → in
|
|
// wiederverwendeten Scratch-Buffer umparken statt pro Frame allokieren.
|
|
let mut scratch = std::mem::take(&mut self.event_scratch);
|
|
scratch.extend(glfw::flush_messages(&self.events).map(|(_, ev)| ev));
|
|
for ev in scratch.drain(..) { self.handle_glfw_event(ev); }
|
|
self.event_scratch = scratch;
|
|
}
|
|
|
|
pub fn drain_events(&mut self) -> impl Iterator<Item = Event> + '_ {
|
|
self.event_queue.drain(..)
|
|
}
|
|
|
|
pub fn framebuffer_mut(&mut self) -> &mut [u8] { &mut self.framebuffer }
|
|
pub fn framebuffer_size(&self) -> (u32, u32) { self.framebuffer_size }
|
|
|
|
pub fn present(&mut self) {
|
|
let (w, h) = self.framebuffer_size;
|
|
self.queue.write_texture(
|
|
wgpu::TexelCopyTextureInfoBase { texture: &self.index_tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
|
&self.framebuffer,
|
|
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w), rows_per_image: None },
|
|
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
|
|
);
|
|
|
|
let drawable = match self.surface.get_current_texture() {
|
|
Ok(d) => d,
|
|
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
|
|
self.surface = self.instance.create_surface(self.window.render_context()).expect("recreate surface");
|
|
self.surface.configure(&self.device, &self.config);
|
|
return;
|
|
}
|
|
Err(e) => { eprintln!("pbio: surface error: {:?}", e); return; }
|
|
};
|
|
|
|
let view = drawable.texture.create_view(&Default::default());
|
|
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("pbio encoder") });
|
|
let (cx, cy, cw, ch) = self.content_rect;
|
|
{
|
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
label: Some("pbio pass"),
|
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
view: &view,
|
|
depth_slice: None,
|
|
resolve_target: None,
|
|
ops: wgpu::Operations {
|
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
|
store: wgpu::StoreOp::Store,
|
|
},
|
|
})],
|
|
depth_stencil_attachment: None,
|
|
occlusion_query_set: None,
|
|
timestamp_writes: None,
|
|
});
|
|
pass.set_viewport(cx as f32, cy as f32, cw as f32, ch as f32, 0.0, 1.0);
|
|
pass.set_scissor_rect(cx, cy, cw, ch);
|
|
pass.set_pipeline(&self.pipeline);
|
|
pass.set_bind_group(0, &self.bind_group, &[]);
|
|
pass.draw(0..3, 0..1);
|
|
}
|
|
self.queue.submit([encoder.finish()]);
|
|
drawable.present();
|
|
}
|
|
|
|
fn handle_glfw_event(&mut self, ev: glfw::WindowEvent) {
|
|
use glfw::WindowEvent as W;
|
|
match ev {
|
|
W::FramebufferSize(w, h) => {
|
|
if w > 0 && h > 0 {
|
|
self.config.width = w as u32;
|
|
self.config.height = h as u32;
|
|
self.surface.configure(&self.device, &self.config);
|
|
self.content_rect = compute_content_rect((self.config.width, self.config.height), self.aspect_ratio);
|
|
let (x, y, cw, ch) = self.content_rect;
|
|
self.params.content_offset = [x as f32, y as f32];
|
|
self.params.content_size = [cw as f32, ch as f32];
|
|
self.queue.write_buffer(&self.params_buf, 0, bytemuck::bytes_of(&self.params));
|
|
}
|
|
self.event_queue.push_back(Event::Resize { width: w.max(0) as u32, height: h.max(0) as u32 });
|
|
}
|
|
W::Key(k, _, action, _) => if let Some(key) = from_glfw_key(k) {
|
|
self.event_queue.push_back(Event::Key {
|
|
key,
|
|
pressed: action != glfw::Action::Release,
|
|
repeat: action == glfw::Action::Repeat,
|
|
});
|
|
}
|
|
W::Char(c) => self.event_queue.push_back(Event::Char(c)),
|
|
W::CursorPos(x, y) => {
|
|
let (cx, cy, cw, ch) = self.content_rect;
|
|
let (fw, fh) = self.framebuffer_size;
|
|
let fx = ((x - cx as f64) / cw as f64 * fw as f64) as f32;
|
|
let fy = ((y - cy as f64) / ch as f64 * fh as f64) as f32;
|
|
self.event_queue.push_back(Event::MouseMove { x: fx, y: fy });
|
|
}
|
|
W::MouseButton(btn, action, _) => if let Some(button) = from_glfw_button(btn) {
|
|
self.event_queue.push_back(Event::MouseBtn { button, pressed: action != glfw::Action::Release });
|
|
}
|
|
W::Close => {
|
|
self.close_requested = true;
|
|
self.event_queue.push_back(Event::CloseRequested);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Helpers ---------------------------------------------------------------
|
|
|
|
fn compute_content_rect((win_w, win_h): (u32, u32), ar: f32) -> (u32, u32, u32, u32) {
|
|
let w = win_w.max(1) as f32;
|
|
let h = win_h.max(1) as f32;
|
|
if ar <= 0.0 { return (0, 0, w as u32, h as u32); }
|
|
let (cw, ch) = if w / h > ar { (h * ar, h) } else { (w, w / ar) };
|
|
let ox = (w - cw) * 0.5;
|
|
let oy = (h - ch) * 0.5;
|
|
(ox.floor().max(0.0) as u32, oy.floor().max(0.0) as u32,
|
|
cw.floor().max(1.0) as u32, ch.floor().max(1.0) as u32)
|
|
}
|
|
|
|
fn make_tex(device: &wgpu::Device, label: &str, w: u32, h: u32, format: wgpu::TextureFormat) -> wgpu::Texture {
|
|
device.create_texture(&wgpu::TextureDescriptor {
|
|
label: Some(label),
|
|
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: wgpu::TextureDimension::D2,
|
|
format,
|
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
|
view_formats: &[],
|
|
})
|
|
}
|
|
|
|
fn from_glfw_key(k: glfw::Key) -> Option<Key> {
|
|
use glfw::Key as G;
|
|
macro_rules! same { ($($n:ident),* $(,)?) => { match k {
|
|
$(G::$n => Some(Key::$n),)*
|
|
G::LeftShift => Some(Key::LShift), G::RightShift => Some(Key::RShift),
|
|
G::LeftControl => Some(Key::LCtrl), G::RightControl => Some(Key::RCtrl),
|
|
G::LeftAlt => Some(Key::LAlt), G::RightAlt => Some(Key::RAlt),
|
|
_ => None,
|
|
}}; }
|
|
same!(
|
|
A, B, C, D, E, F, G, H, I, J, K, L, M,
|
|
N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
|
|
Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9,
|
|
Up, Down, Left, Right,
|
|
Space, Enter, Escape, Tab, Backspace,
|
|
F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
|
|
)
|
|
}
|
|
|
|
fn from_glfw_button(b: glfw::MouseButton) -> Option<MouseButton> {
|
|
match b {
|
|
glfw::MouseButton::Button1 => Some(MouseButton::Left),
|
|
glfw::MouseButton::Button2 => Some(MouseButton::Right),
|
|
glfw::MouseButton::Button3 => Some(MouseButton::Middle),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn build_pipeline(
|
|
device: &wgpu::Device,
|
|
surface_format: wgpu::TextureFormat,
|
|
) -> (wgpu::BindGroupLayout, wgpu::RenderPipeline) {
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("pbio shader"),
|
|
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
|
|
});
|
|
|
|
let tex_entry = |binding, sample_type| wgpu::BindGroupLayoutEntry {
|
|
binding,
|
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
ty: wgpu::BindingType::Texture {
|
|
sample_type, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false,
|
|
},
|
|
count: None,
|
|
};
|
|
|
|
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("pbio BGL"),
|
|
entries: &[
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 0,
|
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
ty: wgpu::BindingType::Buffer {
|
|
ty: wgpu::BufferBindingType::Uniform,
|
|
has_dynamic_offset: false,
|
|
min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<Params>() as u64),
|
|
},
|
|
count: None,
|
|
},
|
|
tex_entry(1, wgpu::TextureSampleType::Uint),
|
|
tex_entry(2, wgpu::TextureSampleType::Float { filterable: false }),
|
|
],
|
|
});
|
|
|
|
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
label: Some("pbio pipeline layout"),
|
|
bind_group_layouts: &[&bgl],
|
|
push_constant_ranges: &[],
|
|
});
|
|
|
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
label: Some("pbio pipeline"),
|
|
layout: Some(&layout),
|
|
vertex: wgpu::VertexState {
|
|
module: &shader, entry_point: Some("vs_main"),
|
|
compilation_options: Default::default(), buffers: &[],
|
|
},
|
|
fragment: Some(wgpu::FragmentState {
|
|
module: &shader, entry_point: Some("fs_main"),
|
|
compilation_options: Default::default(),
|
|
targets: &[Some(wgpu::ColorTargetState {
|
|
format: surface_format,
|
|
blend: Some(wgpu::BlendState::REPLACE),
|
|
write_mask: wgpu::ColorWrites::ALL,
|
|
})],
|
|
}),
|
|
primitive: wgpu::PrimitiveState::default(),
|
|
depth_stencil: None,
|
|
multisample: wgpu::MultisampleState::default(),
|
|
multiview: None,
|
|
cache: None,
|
|
});
|
|
|
|
(bgl, pipeline)
|
|
}
|