diff --git a/Cargo.toml b/Cargo.toml index 260aaa5..df906ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -glfw = "0.60.0" +winit = "0.30" wgpu = "27" pollster = "0.4.0" bytemuck = { version = "1.24.0", features = ["derive"] } diff --git a/README.md b/README.md index c2cb762..4e8120a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # pbio: simple pixel buffer and I/O system -Pixelbuffer-basiertes Fenster mit Palette-Lookup-Renderer (GLFW + wgpu) und Maus/Tastatureingaben. +Pixelbuffer-basiertes Fenster mit Palette-Lookup-Renderer (winit + wgpu) und Maus/Tastatureingaben. -pbio nimmt einen Bytearray, interpretiert diesen als 8-Bit-Pixelbuffer und rendert ihn auf ein Fenster. Optional mit Letterbox für ein festes Seitenverhältnis +pbio nimmt einen Bytearray, interpretiert diesen als 8-Bit-Pixelbuffer und rendert ihn auf ein Fenster. Optional mit Letterbox für ein festes Seitenverhältnis. ## Integration @@ -13,7 +13,7 @@ pbio ist nicht auf crates.io veröffentlicht. Das Crate kann direkt aus dem Git- pbio = { git = "https://codeberg.org/irrlicht/rust-pbio.git" } ``` -pbio benötigt eine funktionierende GLFW-Toolchain (CMake + C-Compiler) zum Bauen sowie aktuelle Grafiktreiber mit Vulkan-, Metal- oder DX12-Unterstützung für wgpu. +pbio braucht keine System-Libraries zum Bauen — winit lädt X11/Wayland zur Laufzeit per dlopen. Zur Laufzeit reichen aktuelle Grafiktreiber mit Vulkan-, Metal- oder DX12-Unterstützung für wgpu. ## Verwendung diff --git a/generate_gimp_palette.py b/generate_gimp_palette.py new file mode 100755 index 0000000..d560b17 --- /dev/null +++ b/generate_gimp_palette.py @@ -0,0 +1,7 @@ +with open("rgb332.gpl", "w") as f: + f.write("GIMP Palette\nName: RGB332\nColumns: 16\n#\n") + for i in range(256): + r = ((i >> 5) * 255) // 7 + g = (((i >> 2) & 0x7) * 255) // 7 + b = ((i & 0x3) * 255) // 3 + f.write(f"{r:3d} {g:3d} {b:3d} Index {i}\n") diff --git a/green.tga b/green.tga new file mode 100644 index 0000000..465af6b Binary files /dev/null and b/green.tga differ diff --git a/src/lib.rs b/src/lib.rs index 9a4d1b3..c1a0cc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,19 @@ //! pbio — Pixelbuffer-basiertes Fenster mit Palette-Lookup-Renderer. use std::collections::VecDeque; +use std::sync::Arc; use std::time::Duration; use wgpu::util::DeviceExt; +use winit::application::ApplicationHandler; +use winit::dpi::LogicalSize; +use winit::event::{ + DeviceEvent, DeviceId, ElementState, KeyEvent, MouseButton as WinitMouseButton, WindowEvent, +}; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::keyboard::{KeyCode, PhysicalKey}; +use winit::platform::pump_events::EventLoopExtPumpEvents; +use winit::window::{CursorGrabMode, Window, WindowAttributes, WindowId}; pub mod palette; @@ -78,7 +88,7 @@ struct Params { // --- Platform -------------------------------------------------------------- pub struct Platform { - // GPU-Ressourcen vor Surface, Surface vor Window/Glfw (Drop-Reihenfolge). + // GPU-Ressourcen vor Surface, Surface vor Window (Drop-Reihenfolge). bind_group: wgpu::BindGroup, pipeline: wgpu::RenderPipeline, index_tex: wgpu::Texture, @@ -93,22 +103,20 @@ pub struct Platform { queue: wgpu::Queue, instance: wgpu::Instance, - window: glfw::PWindow, - events: glfw::GlfwReceiver<(f64, glfw::WindowEvent)>, - glfw: glfw::Glfw, + window: Arc, + event_loop: EventLoop<()>, 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, mouse_visible: bool, mouse_capture: bool, // Letzte rohe (window-space) Cursorposition, um Deltas zu bilden. None nach - // Mode-Wechsel/erstem Frame, damit der GLFW-Recenter keinen Riesensprung erzeugt. + // Mode-Wechsel/erstem Frame, damit ein Cursor-Sprung keinen Riesen-Delta erzeugt. last_cursor_pos: Option<(f64, f64)>, mouse_delta: (f32, f32), } @@ -119,29 +127,27 @@ impl Platform { } async fn new_async(cfg: PlatformConfig) -> Self { - use glfw::fail_on_errors; + let mut event_loop = EventLoop::builder().build().expect("event loop"); - let mut glfw = glfw::init(fail_on_errors!()).unwrap(); - glfw.window_hint(glfw::WindowHint::ClientApi(glfw::ClientApiHint::NoApi)); + let attrs = WindowAttributes::default() + .with_title(cfg.title.clone()) + .with_inner_size(LogicalSize::new(cfg.window_size.0, cfg.window_size.1)); - 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"); + // winit 0.30 erlaubt Fenster-Erstellung nur in ApplicationHandler::resumed. + // Wir pumpen, bis das Bootstrap-Handler das Window angelegt hat. + let mut bootstrap = Bootstrap { attrs, window: None }; + while bootstrap.window.is_none() { + event_loop.pump_app_events(Some(Duration::from_millis(10)), &mut bootstrap); + } + let window = bootstrap.window.unwrap(); - 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 win_size = window.inner_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 surface = instance.create_surface(window.clone()).expect("surface"); let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions { compatible_surface: Some(&surface), @@ -156,15 +162,26 @@ impl Platform { let caps = surface.get_capabilities(&adapter); let format = caps.formats.iter().copied().find(|f| f.is_srgb()).unwrap_or(caps.formats[0]); + // Mailbox/Immediate sind nicht überall in caps.present_modes — Fifo ist + // der einzige garantierte Mode. + let present_mode = if cfg.vsync { + if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) { + wgpu::PresentMode::Mailbox + } else { + wgpu::PresentMode::Fifo + } + } else if caps.present_modes.contains(&wgpu::PresentMode::Immediate) { + wgpu::PresentMode::Immediate + } else { + wgpu::PresentMode::Fifo + }; + let surface_config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format, - width: win_size.0 as u32, - height: win_size.1 as u32, - // TODO: Mailbox ist nicht überall in caps.present_modes — bei - // unsupported Treibern panict configure(). Fifo wäre der sichere - // Vsync-Fallback. Erstmal beobachten. - present_mode: if cfg.vsync { wgpu::PresentMode::Mailbox } else { wgpu::PresentMode::Immediate }, + width: win_size.width, + height: win_size.height, + present_mode, alpha_mode: caps.alpha_modes[0], view_formats: vec![], desired_maximum_frame_latency: 2, @@ -220,12 +237,11 @@ impl Platform { palette: cfg.palette, palette_dirty: false, params_buf, params, surface, config: surface_config, device, queue, instance, - window, events, glfw, + window, event_loop, 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, mouse_visible: cfg.mouse_visible, mouse_capture: cfg.mouse_capture, @@ -249,36 +265,46 @@ impl Platform { } fn apply_cursor_mode(&mut self) { - // capture impliziert hidden — GLFW kennt keinen "locked + visible". - let mode = match (self.mouse_capture, self.mouse_visible) { - (true, _) => glfw::CursorMode::Disabled, - (false, true) => glfw::CursorMode::Normal, - (false, false) => glfw::CursorMode::Hidden, - }; - self.window.set_cursor_mode(mode); + if self.mouse_capture { + // Wayland kann Locked, X11/Windows fallen auf Confined zurück. + let r = self.window.set_cursor_grab(CursorGrabMode::Locked) + .or_else(|_| self.window.set_cursor_grab(CursorGrabMode::Confined)); + if let Err(e) = r { + eprintln!("pbio: cursor grab failed: {:?}", e); + } + self.window.set_cursor_visible(false); + } else { + let _ = self.window.set_cursor_grab(CursorGrabMode::None); + self.window.set_cursor_visible(self.mouse_visible); + } } /// Akkumulierte Mausbewegung seit dem letzten Aufruf, in Framebuffer-Pixeln /// (gleiche Skala wie `Event::MouseMove`). Bei `set_mouse_capture(true)` - /// liefert GLFW unbegrenzte Deltas (FPS-Stil). + /// kommen die Werte aus rohen DeviceEvents (unbegrenzt, FPS-Stil). pub fn mouse_delta(&mut self) -> (f32, f32) { std::mem::replace(&mut self.mouse_delta, (0.0, 0.0)) } - 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 should_close(&self) -> bool { self.close_requested } + pub fn request_close(&mut self) { self.close_requested = 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; + // Felder destructuren, um disjoint &mut-Borrows in den Handler zu reichen. + let Self { + event_loop, event_queue, close_requested, last_cursor_pos, mouse_delta, + surface, device, config, params, params_buf, queue, content_rect, + aspect_ratio, framebuffer_size, mouse_capture, .. + } = self; + + let mut handler = Handler { + event_queue, close_requested, last_cursor_pos, mouse_delta, + surface, device, config, params, params_buf, queue, content_rect, + aspect_ratio: *aspect_ratio, + framebuffer_size: *framebuffer_size, + mouse_capture: *mouse_capture, + }; + event_loop.pump_app_events(timeout, &mut handler); } pub fn drain_events(&mut self) -> impl Iterator + '_ { @@ -328,7 +354,7 @@ impl Platform { 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 = self.instance.create_surface(self.window.clone()).expect("recreate surface"); self.surface.configure(&self.device, &self.config); return; } @@ -363,57 +389,126 @@ impl Platform { self.queue.submit([encoder.finish()]); drawable.present(); } +} - fn handle_glfw_event(&mut self, ev: glfw::WindowEvent) { - use glfw::WindowEvent as W; +// --- Bootstrap-Handler (nur fürs initiale Window-Create) ------------------- + +struct Bootstrap { + attrs: WindowAttributes, + window: Option>, +} + +impl ApplicationHandler for Bootstrap { + fn resumed(&mut self, el: &ActiveEventLoop) { + if self.window.is_none() { + let w = el.create_window(self.attrs.clone()).expect("create_window"); + self.window = Some(Arc::new(w)); + } + } + fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, _: WindowEvent) {} +} + +// --- Laufender Event-Handler (pro pump_events neu aufgebaut) --------------- + +struct Handler<'a> { + event_queue: &'a mut VecDeque, + close_requested: &'a mut bool, + last_cursor_pos: &'a mut Option<(f64, f64)>, + mouse_delta: &'a mut (f32, f32), + surface: &'a wgpu::Surface<'static>, + device: &'a wgpu::Device, + config: &'a mut wgpu::SurfaceConfiguration, + params: &'a mut Params, + params_buf: &'a wgpu::Buffer, + queue: &'a wgpu::Queue, + content_rect: &'a mut (u32, u32, u32, u32), + aspect_ratio: f32, + framebuffer_size: (u32, u32), + mouse_capture: bool, +} + +impl<'a> ApplicationHandler for Handler<'a> { + fn resumed(&mut self, _: &ActiveEventLoop) {} + + fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, ev: WindowEvent) { match ev { - W::FramebufferSize(w, h) => { + WindowEvent::Resized(size) => { + let (w, h) = (size.width, size.height); 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.config.width = w; + self.config.height = h; + self.surface.configure(self.device, self.config); + *self.content_rect = compute_content_rect((w, h), 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.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 }); + self.event_queue.push_back(Event::Resize { width: w, height: h }); } - 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, - }); + WindowEvent::CloseRequested => { + *self.close_requested = true; + self.event_queue.push_back(Event::CloseRequested); } - W::Char(c) => self.event_queue.push_back(Event::Char(c)), - W::CursorPos(x, y) => { - let (cx, cy, cw, ch) = self.content_rect; + WindowEvent::KeyboardInput { event: KeyEvent { physical_key, state, repeat, text, .. }, .. } => { + if let PhysicalKey::Code(code) = physical_key + && let Some(key) = from_winit_keycode(code) + { + self.event_queue.push_back(Event::Key { + key, + pressed: state == ElementState::Pressed, + repeat, + }); + } + if state == ElementState::Pressed + && let Some(t) = text + { + for c in t.chars() { + self.event_queue.push_back(Event::Char(c)); + } + } + } + WindowEvent::CursorMoved { position, .. } => { + let (x, y) = (position.x, position.y); + let (cx, cy, cw, ch) = *self.content_rect; let (fw, fh) = self.framebuffer_size; - let sx = fw as f64 / cw as f64; - let sy = fh as f64 / ch as f64; + let sx = fw as f64 / cw.max(1) as f64; + let sy = fh as f64 / ch.max(1) as f64; - if let Some((lx, ly)) = self.last_cursor_pos { - self.mouse_delta.0 += ((x - lx) * sx) as f32; - self.mouse_delta.1 += ((y - ly) * sy) as f32; + // Im Capture-Mode liefert winit geclampte CursorMoved-Werte; + // den Delta-Pfad nehmen wir dort aus DeviceEvent::MouseMotion. + if !self.mouse_capture { + if let Some((lx, ly)) = *self.last_cursor_pos { + self.mouse_delta.0 += ((x - lx) * sx) as f32; + self.mouse_delta.1 += ((y - ly) * sy) as f32; + } + *self.last_cursor_pos = Some((x, y)); } - self.last_cursor_pos = Some((x, y)); let fx = ((x - cx as f64) * sx) as f32; let fy = ((y - cy as f64) * sy) 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); + WindowEvent::MouseInput { button, state, .. } => if let Some(b) = from_winit_button(button) { + self.event_queue.push_back(Event::MouseBtn { + button: b, + pressed: state == ElementState::Pressed, + }); } _ => {} } } + + fn device_event(&mut self, _: &ActiveEventLoop, _: DeviceId, ev: DeviceEvent) { + if self.mouse_capture && let DeviceEvent::MouseMotion { delta: (dx, dy) } = ev { + let (_, _, cw, ch) = *self.content_rect; + let (fw, fh) = self.framebuffer_size; + let sx = fw as f64 / cw.max(1) as f64; + let sy = fh as f64 / ch.max(1) as f64; + self.mouse_delta.0 += (dx * sx) as f32; + self.mouse_delta.1 += (dy * sy) as f32; + } + } } // --- Helpers --------------------------------------------------------------- @@ -442,30 +537,39 @@ fn make_tex(device: &wgpu::Device, label: &str, w: u32, h: u32, format: wgpu::Te }) } -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_winit_keycode(k: KeyCode) -> Option { + use KeyCode as K; + Some(match k { + K::KeyA => Key::A, K::KeyB => Key::B, K::KeyC => Key::C, K::KeyD => Key::D, + K::KeyE => Key::E, K::KeyF => Key::F, K::KeyG => Key::G, K::KeyH => Key::H, + K::KeyI => Key::I, K::KeyJ => Key::J, K::KeyK => Key::K, K::KeyL => Key::L, + K::KeyM => Key::M, K::KeyN => Key::N, K::KeyO => Key::O, K::KeyP => Key::P, + K::KeyQ => Key::Q, K::KeyR => Key::R, K::KeyS => Key::S, K::KeyT => Key::T, + K::KeyU => Key::U, K::KeyV => Key::V, K::KeyW => Key::W, K::KeyX => Key::X, + K::KeyY => Key::Y, K::KeyZ => Key::Z, + K::Digit0 => Key::Num0, K::Digit1 => Key::Num1, K::Digit2 => Key::Num2, + K::Digit3 => Key::Num3, K::Digit4 => Key::Num4, K::Digit5 => Key::Num5, + K::Digit6 => Key::Num6, K::Digit7 => Key::Num7, K::Digit8 => Key::Num8, + K::Digit9 => Key::Num9, + K::ArrowUp => Key::Up, K::ArrowDown => Key::Down, + K::ArrowLeft => Key::Left, K::ArrowRight => Key::Right, + K::Space => Key::Space, K::Enter => Key::Enter, K::Escape => Key::Escape, + K::Tab => Key::Tab, K::Backspace => Key::Backspace, + K::ShiftLeft => Key::LShift, K::ShiftRight => Key::RShift, + K::ControlLeft => Key::LCtrl, K::ControlRight => Key::RCtrl, + K::AltLeft => Key::LAlt, K::AltRight => Key::RAlt, + K::F1 => Key::F1, K::F2 => Key::F2, K::F3 => Key::F3, K::F4 => Key::F4, + K::F5 => Key::F5, K::F6 => Key::F6, K::F7 => Key::F7, K::F8 => Key::F8, + K::F9 => Key::F9, K::F10 => Key::F10, K::F11 => Key::F11, K::F12 => Key::F12, + _ => return None, + }) } -fn from_glfw_button(b: glfw::MouseButton) -> Option { +fn from_winit_button(b: WinitMouseButton) -> Option { match b { - glfw::MouseButton::Button1 => Some(MouseButton::Left), - glfw::MouseButton::Button2 => Some(MouseButton::Right), - glfw::MouseButton::Button3 => Some(MouseButton::Middle), + WinitMouseButton::Left => Some(MouseButton::Left), + WinitMouseButton::Right => Some(MouseButton::Right), + WinitMouseButton::Middle => Some(MouseButton::Middle), _ => None, } }