//! 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, 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, framebuffer_size: (u32, u32), aspect_ratio: f32, // 0.0 = keine Letterbox content_rect: (u32, u32, u32, u32), event_queue: VecDeque, event_scratch: Vec, 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 = 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) { 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 + '_ { 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 { 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 { 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::() 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) }