Compare commits

...

12 Commits

Author SHA1 Message Date
irrlicht 8c647d3700 CHANGELOG fuer truecolor16, starter-Example auf 620x480
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:12:23 +02:00
irrlicht c5a4d0026b 16-Bit-Truecolor (RGB565) statt 8-Bit-Palette
Framebuffer ist jetzt &mut [u16], jeder Wert ein RGB565-Pixel. Der
Shader dekodiert die Bitfelder direkt aus einer R16Uint-Textur; die
Palette (Textur, API, Presets, Tooling) entfaellt. Neu: pbio::rgb565()
zum Packen von 8-Bit-Kanaelen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:04:05 +02:00
irrlicht 2d81471820 poll_event 2026-05-23 10:49:32 +02:00
irrlicht 2f8d23f6cd Fix Cursor capture bug 2026-05-23 10:06:37 +02:00
irrlicht 26f535d053 Changed backend from glfw to winit 2026-05-22 15:00:08 +02:00
irrlicht 8ce3f40092 hot reload of color palette 2026-05-21 15:26:41 +02:00
irrlicht c77dbb3680 Mouse Delta in FB Pixeln, statt Fenster Px 2026-05-16 13:39:24 +02:00
irrlicht 3698b8696c Camera lock in Readme 2026-05-16 13:32:43 +02:00
irrlicht 8f246a1f3e Implemented mouse lock and delta 2026-05-16 13:14:03 +02:00
irrlicht a40dc779ad Starter Beispielimplementierung 2026-05-16 00:00:33 +02:00
irrlicht aa2a9bd386 Beispiel korrigiert 2026-05-16 00:00:23 +02:00
irrlicht 2033a9699a Locked wgpu version 27 lock to avoid breaking changes in wgpu 2026-05-15 23:04:10 +02:00
8 changed files with 468 additions and 168 deletions
+47
View File
@@ -0,0 +1,47 @@
# Changelog
## Branch `truecolor16` — 16-Bit-Truecolor (RGB565)
Abzweig von `master` (nach `2d81471 poll_event`). Ersetzt den 8-Bit-Paletten-Renderer
durch direktes RGB565-Truecolor. Beide Varianten bleiben unabhängig nutzbar:
`master` = 8 Bit indiziert, `truecolor16` = 16 Bit direkt.
### Geändert
- **Framebuffer**: `framebuffer_mut()` liefert `&mut [u16]` statt `&mut [u8]`.
Jeder Wert ist ein RGB565-Pixel (Bits `RRRRRGGGGGGBBBBB`), keine Palettenindizes mehr.
- **GPU-Pfad**: Die Pixel-Textur ist jetzt `R16Uint` (vorher `R8Uint`); der
Fragment-Shader dekodiert die 5/6/5-Bitfelder direkt, statt in einer
Paletten-Textur nachzuschlagen. Upload pro Frame: `w * h * 2` Byte.
- **Example** `starter.rs`: schreibt RGB565-Werte via `rgb565()`.
### Neu
- `pbio::rgb565(r: u8, g: u8, b: u8) -> u16` (const fn) — packt 8-Bit-Kanäle
in einen RGB565-Pixelwert.
### Entfernt
- `PlatformConfig::palette` sowie `palette()` / `palette_mut()` und das
Dirty-Flag-Handling in `present()`.
- Modul `pbio::palette` inkl. Preset `RGB332`.
- Paletten-Textur und deren Shader-Binding (`@binding(2)`).
- `generate_gimp_palette.py` (Paletten-Tooling, nur auf `master` relevant).
### Migration von `master`
| 8 Bit (`master`) | 16 Bit (`truecolor16`) |
|---|---|
| `fb[i] = index` | `fb[i] = rgb565(r, g, b)` |
| `p.palette_mut()[i] = [r, g, b]` | entfällt — Farbe direkt ins Pixel schreiben |
| `PlatformConfig { palette: …, .. }` | Feld entfernt |
Paletten-Effekte (Color-Cycling, Fades über `palette_mut()`) gibt es hier
nicht — die entsprechenden Farbwerte müssen pro Frame in den Framebuffer
geschrieben werden.
### Unverändert
Events (`poll_event`/`drain_events`), Letterbox/`aspect_ratio`,
Cursor-Modi (`set_mouse_visible`/`set_mouse_capture`), `mouse_delta()`,
VSync-Handling, Fenster-/Surface-Verwaltung.
+2 -2
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
glfw = "0.60.0"
wgpu = "*"
winit = "0.30"
wgpu = "27"
pollster = "0.4.0"
bytemuck = { version = "1.24.0", features = ["derive"] }
+56 -16
View File
@@ -1,8 +1,10 @@
# pbio: simple pixel buffer and I/O system
# pbio: simple pixel buffer and I/O system — 16-Bit-Truecolor-Variante
Pixelbuffer-basiertes Fenster mit Palette-Lookup-Renderer (GLFW + wgpu) und Maus/Tastatureingaben.
Pixelbuffer-basiertes Fenster mit RGB565-Truecolor-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 ein `u16`-Array, interpretiert jeden Wert als RGB565-Pixel (Bits `RRRRRGGGGGGBBBBB`) und rendert ihn auf ein Fenster. Optional mit Letterbox für ein festes Seitenverhältnis.
Dies ist der Branch `truecolor16`. Auf `master` liegt die 8-Bit-Variante mit Palette-Lookup — beide sind unabhängig voneinander nutzbar.
## Integration
@@ -10,10 +12,10 @@ pbio ist nicht auf crates.io veröffentlicht. Das Crate kann direkt aus dem Git-
```toml
[dependencies]
pbio = { git = "https://codeberg.org/irrlicht/rust-pbio.git" }
pbio = { git = "https://codeberg.org/irrlicht/rust-pbio.git", branch = "truecolor16" }
```
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
@@ -31,7 +33,7 @@ fn main() {
while !p.should_close() {
p.poll_events(None);
for ev in p.drain_events() {
while let Some(ev) = p.poll_event() {
if let Event::Key { key: Key::Escape, pressed: true, .. } = ev {
p.request_close();
}
@@ -40,7 +42,8 @@ fn main() {
let (w, _h) = p.framebuffer_size();
let fb = p.framebuffer_mut();
for (i, px) in fb.iter_mut().enumerate() {
*px = ((i as u32 % w) ^ (i as u32 / w)) as u8;
let v = ((i as u32 % w) ^ (i as u32 / w)) as u8;
*px = pbio::rgb565(v, v, v);
}
p.present();
@@ -51,18 +54,55 @@ fn main() {
## API-Überblick
- `PlatformConfig` Titel, Fenster-/Framebuffer-Größe, optionales
`aspect_ratio` (Letterbox), Palette, VSync. Default = 800×600 Fenster,
320×240 Framebuffer, 4:3, `palette::RGB332`.
`aspect_ratio` (Letterbox), VSync, `mouse_visible`, `mouse_capture`.
Default = 800×600 Fenster, 320×240 Framebuffer, 4:3.
- `Platform::new(cfg)` Fenster + GPU-Pipeline aufsetzen.
- `poll_events(timeout)` GLFW-Events einlesen (`None` = blockierend).
- `poll_events(timeout)` winit-Events einlesen (`None` = blockierend).
- `poll_event() -> Option<Event>` ein Event aus der Queue holen. Gibt
`&mut self` nach jedem Call frei, sodass im Loop direkt auf die Platform
reagiert werden kann (`request_close`, `set_mouse_capture` …).
Empfohlen für die normale Event-Schleife.
- `drain_events()` Iterator über `Event` (`Key`, `Char`, `MouseMove`,
`MouseBtn`, `Resize`, `CloseRequested`).
- `framebuffer_mut()` `&mut [u8]` der Länge `w*h`, je Pixel ein Paletten-Index.
`MouseBtn`, `Resize`, `CloseRequested`). Borrowt `&mut self` bis der
Iterator fertig ist — Reaktionen auf die Platform müssen dann nach dem
Loop angewandt werden. Nützlich für reines Loggen/Filtern.
- `framebuffer_mut()` `&mut [u16]` der Länge `w*h`, je Pixel ein RGB565-Wert.
- `rgb565(r, g, b) -> u16` packt 8-Bit-RGB in einen RGB565-Pixelwert (const fn).
- `present()` Frame auf das Fenster bringen.
- `should_close()` / `request_close()`.
- `set_mouse_visible(bool)` Cursor anzeigen oder verstecken (freie Bewegung).
- `set_mouse_capture(bool)` Cursor einsperren + verstecken (winit
`CursorGrabMode::Locked`, Fallback `Confined`), liefert unbegrenzte
Bewegungsdeltas via `mouse_delta()`. Impliziert `mouse_visible = false`.
Im Capture-Mode ist `Event::MouseMove` nicht sinnvoll (eingefrorene oder
am Fensterrand klemmende Werte) — stattdessen `mouse_delta()` benutzen.
- `mouse_delta() -> (f32, f32)` akkumulierte Mausbewegung seit dem letzten Aufruf
in Framebuffer-Pixeln (gleiche Skala wie `MouseMove`), danach genullt. Nützlich
für FPS-Kamerasteuerung.
## Paletten
### Cursor-Modi
`palette::RGB332` ist als Default eingebaut. Eigene Paletten sind ein
`[[u8; 3]; 256]` (RGB pro Index) und werden über `PlatformConfig::palette`
übergeben.
| `mouse_visible` | `mouse_capture` | winit-Modus | Einsatz |
|---|---|---|---|
| `true` | `false` | sichtbar, frei | Standard-UI |
| `false` | `false` | versteckt, frei | Eigener Cursor im Framebuffer |
| `*` | `true` | `Locked` / `Confined`, versteckt | FPS-Kamera, unbegrenzte Deltas |
```rust
// FPS-Kamera
p.set_mouse_capture(true);
let (dx, dy) = p.mouse_delta();
yaw += dx * sensitivity;
pitch += dy * sensitivity;
```
## Pixelformat
RGB565: 5 Bit Rot, 6 Bit Grün, 5 Bit Blau in einem `u16` (Bits
`RRRRRGGGGGGBBBBB`). Der Shader dekodiert die Bitfelder direkt — es gibt
keine Palette. Zum Packen von 8-Bit-Kanälen dient `pbio::rgb565`:
```rust
let orange = pbio::rgb565(255, 160, 0);
p.framebuffer_mut()[0] = orange;
```
+74
View File
@@ -0,0 +1,74 @@
//! pbio starter — Ausgangspunkt für eigene CPU-Render-Projekte.
//!
//! Ausführen: cargo run --example starter --release
//!
//! Zeigt: Game-Loop-Struktur, non-blocking Event-Polling, Tastatur (Esc beendet),
//! Maus, direktes Schreiben auf den RGB565-Framebuffer.
use std::time::Duration;
use pbio::{rgb565, Event, Key, MouseButton, Platform, PlatformConfig};
fn main() {
let mut p = Platform::new(PlatformConfig {
title: "pbio starter".into(),
window_size: (1240, 960),
framebuffer_size: (620, 480),
aspect_ratio: Some(620.0/480.0),
..Default::default()
});
let mut mouse = (0.0_f32, 0.0_f32);
let mut paint = false;
let mut frame = 0u32;
while !p.should_close() {
// 0-Timeout: nicht-blockierend pollen, damit der Loop unabhängig
// von Eingaben pro Frame durchläuft. None würde bis zum nächsten
// Event schlafen — gut für statische UIs, schlecht für Animation.
p.poll_events(Some(Duration::from_millis(0)));
// poll_event() gibt &mut self nach jedem Call wieder frei → Aktionen
// auf p (request_close, set_mouse_capture …) sind direkt im Loop
// erlaubt. drain_events() existiert weiter für den Fall, dass
// nur gelesen wird.
while let Some(ev) = p.poll_event() {
match ev {
Event::Key { key: Key::Escape, pressed: true, .. } => p.request_close(),
Event::MouseMove { x, y } => mouse = (x, y),
Event::MouseBtn { button: MouseButton::Left, pressed } => paint = pressed,
_ => {}
}
}
let (w, h) = p.framebuffer_size();
let fb = p.framebuffer_mut();
// Diagonal wanderndes Farbverlauf-Band, direkt als RGB565-Truecolor.
let t = (frame / 2) as u8;
for y in 0..h {
for x in 0..w {
let r = (x as u8).wrapping_add(t);
let g = (y as u8).wrapping_add(t);
let b = (x as u8).wrapping_add(y as u8);
fb[(y * w + x) as usize] = rgb565(r, g, b);
}
}
// Cursor: 3x3-Quadrat, rot; beim Klick gelb.
let (mx, my) = (mouse.0 as i32, mouse.1 as i32);
let cursor_px = if paint { rgb565(255, 255, 0) } else { rgb565(255, 0, 0) };
for dy in -1..=1 {
for dx in -1..=1 {
let x = mx + dx;
let y = my + dy;
if (0..w as i32).contains(&x) && (0..h as i32).contains(&y) {
fb[(y as u32 * w + x as u32) as usize] = cursor_px;
}
}
}
p.present();
frame = frame.wrapping_add(1);
}
}
BIN
View File
Binary file not shown.
+281 -125
View File
@@ -1,11 +1,19 @@
//! pbio — Pixelbuffer-basiertes Fenster mit Palette-Lookup-Renderer.
//! pbio — Pixelbuffer-basiertes Fenster mit RGB565-Truecolor-Renderer.
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use wgpu::util::DeviceExt;
pub mod palette;
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};
// --- Konfiguration ---------------------------------------------------------
@@ -14,8 +22,9 @@ pub struct PlatformConfig {
pub window_size: (u32, u32),
pub framebuffer_size: (u32, u32),
pub aspect_ratio: Option<f32>,
pub palette: [[u8; 3]; 256],
pub vsync: bool,
pub mouse_visible: bool,
pub mouse_capture: bool,
}
impl Default for PlatformConfig {
@@ -25,12 +34,18 @@ impl Default for PlatformConfig {
window_size: (800, 600),
framebuffer_size: (320, 240),
aspect_ratio: Some(4.0 / 3.0),
palette: palette::RGB332,
vsync: true,
mouse_visible: true,
mouse_capture: false,
}
}
}
/// Packt 8-Bit-RGB in einen RGB565-Pixelwert (Bits `RRRRRGGGGGGBBBBB`).
pub const fn rgb565(r: u8, g: u8, b: u8) -> u16 {
((r as u16 >> 3) << 11) | ((g as u16 >> 2) << 5) | (b as u16 >> 3)
}
// --- Events ----------------------------------------------------------------
#[non_exhaustive]
@@ -74,12 +89,10 @@ 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,
#[allow(dead_code)]
palette_tex: wgpu::Texture,
pixel_tex: wgpu::Texture,
params_buf: wgpu::Buffer,
params: Params,
surface: wgpu::Surface<'static>,
@@ -88,17 +101,22 @@ pub struct Platform {
queue: wgpu::Queue,
instance: wgpu::Instance,
window: glfw::PWindow,
events: glfw::GlfwReceiver<(f64, glfw::WindowEvent)>,
glfw: glfw::Glfw,
window: Arc<Window>,
event_loop: EventLoop<()>,
framebuffer: Vec<u8>,
framebuffer: Vec<u16>,
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,
mouse_visible: bool,
mouse_capture: bool,
// Letzte rohe (window-space) Cursorposition, um Deltas zu bilden. None nach
// Mode-Wechsel/erstem Frame, damit ein Cursor-Sprung keinen Riesen-Delta erzeugt.
last_cursor_pos: Option<(f64, f64)>,
mouse_delta: (f32, f32),
}
impl Platform {
@@ -107,29 +125,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),
@@ -144,15 +160,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,
@@ -160,21 +187,11 @@ impl Platform {
surface.configure(&device, &surface_config);
// --- Texturen ---
// RGB565 hat kein natives wgpu-Format — die rohen u16 landen in einer
// R16Uint-Textur, der Shader dekodiert die Bitfelder.
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 },
);
let pixel_tex = make_tex(&device, "pbio pixel_tex", src_w, src_h, wgpu::TextureFormat::R16Uint);
let pixel_view = pixel_tex.create_view(&Default::default());
// --- Uniform ---
let aspect_ratio = cfg.aspect_ratio.unwrap_or(0.0);
@@ -198,60 +215,111 @@ impl Platform {
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) },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&pixel_view) },
],
});
Self {
bind_group, pipeline, index_tex, palette_tex, params_buf, params,
let mut platform = Self {
bind_group, pipeline, pixel_tex,
params_buf, params,
surface, config: surface_config, device, queue, instance,
window, events, glfw,
framebuffer: vec![0u8; (src_w * src_h) as usize],
window, event_loop,
framebuffer: vec![0u16; (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,
last_cursor_pos: None,
mouse_delta: (0.0, 0.0),
};
platform.apply_cursor_mode();
platform
}
pub fn set_mouse_visible(&mut self, visible: bool) {
self.mouse_visible = visible;
self.apply_cursor_mode();
}
pub fn set_mouse_capture(&mut self, capture: bool) {
self.mouse_capture = capture;
// Recenter nach Mode-Wechsel würde den nächsten Delta springen lassen.
self.last_cursor_pos = None;
self.apply_cursor_mode();
}
fn apply_cursor_mode(&mut self) {
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);
}
}
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); }
/// Akkumulierte Mausbewegung seit dem letzten Aufruf, in Framebuffer-Pixeln
/// (gleiche Skala wie `Event::MouseMove`). Bei `set_mouse_capture(true)`
/// 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 }
pub fn request_close(&mut self) { self.close_requested = 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;
// 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<Item = Event> + '_ {
self.event_queue.drain(..)
}
pub fn framebuffer_mut(&mut self) -> &mut [u8] { &mut self.framebuffer }
/// Holt ein einzelnes Event aus der Queue. Gibt `&mut self` nach jedem Call
/// wieder frei, im Gegensatz zu `drain_events()` — sodass im Loop direkt
/// auf die Platform reagiert werden kann (`request_close`, `set_mouse_capture` …).
pub fn poll_event(&mut self) -> Option<Event> {
self.event_queue.pop_front()
}
pub fn framebuffer_mut(&mut self) -> &mut [u16] { &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::TexelCopyTextureInfoBase { texture: &self.pixel_tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
bytemuck::cast_slice(&self.framebuffer),
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w * 2), 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 = self.instance.create_surface(self.window.clone()).expect("recreate surface");
self.surface.configure(&self.device, &self.config);
return;
}
@@ -286,48 +354,128 @@ 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<Arc<Window>>,
}
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<Event>,
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,
});
}
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;
WindowEvent::CloseRequested => {
*self.close_requested = true;
self.event_queue.push_back(Event::CloseRequested);
}
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.max(1) as f64;
let sy = fh as f64 / ch.max(1) as f64;
// Im Capture-Mode liefert winit geclampte/eingefrorene
// CursorMoved-Werte. Delta kommt dort aus DeviceEvent::MouseMotion,
// MouseMove unterdrücken wir komplett — die Absolutposition wäre
// irreführend (Locked: konstant, Confined: am Rand klemmend).
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));
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 });
}
}
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 ---------------------------------------------------------------
@@ -356,30 +504,39 @@ fn make_tex(device: &wgpu::Device, label: &str, w: u32, h: u32, format: wgpu::Te
})
}
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_winit_keycode(k: KeyCode) -> Option<Key> {
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<MouseButton> {
fn from_winit_button(b: WinitMouseButton) -> Option<MouseButton> {
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,
}
}
@@ -416,7 +573,6 @@ fn build_pipeline(
count: None,
},
tex_entry(1, wgpu::TextureSampleType::Uint),
tex_entry(2, wgpu::TextureSampleType::Float { filterable: false }),
],
});
-19
View File
@@ -1,19 +0,0 @@
//! Palette-Presets.
/// RGB332-Palette: 8 Levels Rot, 8 Levels Grün, 4 Levels Blau.
///
/// Index-Bits: `RRRGGGBB` — `r = idx / 32`, `g = (idx / 4) % 8`, `b = idx % 4`.
pub const RGB332: [[u8; 3]; 256] = {
let mut p = [[0u8; 3]; 256];
let mut i = 0;
while i < 256 {
let r = (i / 32) as u32; // 0..=7
let g = ((i / 4) % 8) as u32; // 0..=7
let b = (i % 4) as u32; // 0..=3
p[i][0] = ((r * 255) / 7) as u8;
p[i][1] = ((g * 255) / 7) as u8;
p[i][2] = ((b * 255) / 3) as u8;
i += 1;
}
p
};
+7 -5
View File
@@ -5,8 +5,7 @@ struct Params {
}
@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var src_indices: texture_2d<u32>;
@group(0) @binding(2) var palette_tex: texture_2d<f32>;
@group(0) @binding(1) var src_pixels: texture_2d<u32>;
@vertex
fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {
@@ -25,8 +24,11 @@ fn fs_main(@builtin(position) frag_xy: vec4<f32>) -> @location(0) vec4<f32> {
let sx = clamp(i32(floor(uv.x * params.src_size.x)), 0, i32(params.src_size.x) - 1);
let sy = clamp(i32(floor(uv.y * params.src_size.y)), 0, i32(params.src_size.y) - 1);
let idx: u32 = textureLoad(src_indices, vec2<i32>(sx, sy), 0).r & 0xFFu;
let rgb = textureLoad(palette_tex, vec2<i32>(i32(idx), 0), 0).rgb;
// RGB565: RRRRRGGGGGGBBBBB
let px: u32 = textureLoad(src_pixels, vec2<i32>(sx, sy), 0).r;
let r = f32((px >> 11u) & 31u) / 31.0;
let g = f32((px >> 5u) & 63u) / 63.0;
let b = f32( px & 31u) / 31.0;
return vec4<f32>(rgb, 1.0);
return vec4<f32>(r, g, b, 1.0);
}