From c5a4d0026bca5a740305bc91acb48aa8c7652109 Mon Sep 17 00:00:00 2001 From: irrlicht Date: Sun, 5 Jul 2026 15:04:05 +0200 Subject: [PATCH] 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 --- README.md | 40 +++++++++---------- examples/starter.rs | 24 ++++++----- generate_gimp_palette.py | 7 ---- src/lib.rs | 86 ++++++++++------------------------------ src/palette.rs | 19 --------- src/shader.wgsl | 12 +++--- 6 files changed, 60 insertions(+), 128 deletions(-) delete mode 100755 generate_gimp_palette.py delete mode 100644 src/palette.rs diff --git a/README.md b/README.md index a30c8d4..6faf01e 100644 --- a/README.md +++ b/README.md @@ -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 (winit + 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,7 +12,7 @@ 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 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. @@ -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,21 +54,20 @@ fn main() { ## API-Überblick - `PlatformConfig` – Titel, Fenster-/Framebuffer-Größe, optionales - `aspect_ratio` (Letterbox), Palette, VSync, `mouse_visible`, `mouse_capture`. - 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)` – winit-Events einlesen (`None` = blockierend). - `poll_event() -> Option` – 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`, `palette_mut` …). + 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`). 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 [u8]` der Länge `w*h`, je Pixel ein Paletten-Index. -- `palette_mut()` / `palette()` – `&mut [[u8; 3]; 256]` bzw. `&[...]`; Schreibzugriff - markiert die Palette dirty, der nächste `present()` lädt sie hoch. +- `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). @@ -94,17 +96,13 @@ yaw += dx * sensitivity; pitch += dy * sensitivity; ``` -## Paletten +## Pixelformat -`palette::RGB332` ist als Default eingebaut. Eigene Paletten sind ein -`[[u8; 3]; 256]` (RGB pro Index) und werden über `PlatformConfig::palette` -übergeben. - -Zur Laufzeit kann die Palette über `palette_mut()` modifiziert werden — einzelne -Einträge oder die komplette Tabelle. Der Upload passiert automatisch im nächsten -`present()`: +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 -p.palette_mut()[42] = [255, 0, 0]; // Slot 42 → rot -p.present(); +let orange = pbio::rgb565(255, 160, 0); +p.framebuffer_mut()[0] = orange; ``` diff --git a/examples/starter.rs b/examples/starter.rs index a2ca8fa..b89cb1c 100644 --- a/examples/starter.rs +++ b/examples/starter.rs @@ -3,11 +3,11 @@ //! Ausführen: cargo run --example starter --release //! //! Zeigt: Game-Loop-Struktur, non-blocking Event-Polling, Tastatur (Esc beendet), -//! Maus, direktes Schreiben auf den indizierten Framebuffer. +//! Maus, direktes Schreiben auf den RGB565-Framebuffer. use std::time::Duration; -use pbio::{Event, Key, MouseButton, Platform, PlatformConfig}; +use pbio::{rgb565, Event, Key, MouseButton, Platform, PlatformConfig}; fn main() { let mut p = Platform::new(PlatformConfig { @@ -29,8 +29,8 @@ fn main() { 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, palette_mut …) sind direkt - // im Loop erlaubt. drain_events() existiert weiter für den Fall, dass + // 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 { @@ -44,24 +44,26 @@ fn main() { let (w, h) = p.framebuffer_size(); let fb = p.framebuffer_mut(); - // Diagonal wanderndes Gradientband. Jeder Pixel ist ein Palettenindex - // 0..=255 — Default-Palette ist RGB332, daher die sichtbaren Bänder. - let t = (frame / 256) as u8; + // Diagonal wanderndes Farbverlauf-Band, direkt als RGB565-Truecolor. + let t = (frame / 2) as u8; for y in 0..h { for x in 0..w { - fb[(y * w + x) as usize] = (x as u8).wrapping_add(y as u8).wrapping_add(t); + 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 in Palettenindex 0xE0 (rot), beim Klick 0xFC (gelb). + // Cursor: 3x3-Quadrat, rot; beim Klick gelb. let (mx, my) = (mouse.0 as i32, mouse.1 as i32); - let cursor_ix = if paint { 0xFC } else { 0xE0 }; + 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_ix; + fb[(y as u32 * w + x as u32) as usize] = cursor_px; } } } diff --git a/generate_gimp_palette.py b/generate_gimp_palette.py deleted file mode 100755 index d560b17..0000000 --- a/generate_gimp_palette.py +++ /dev/null @@ -1,7 +0,0 @@ -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/src/lib.rs b/src/lib.rs index 48375a6..ae7330e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! pbio — Pixelbuffer-basiertes Fenster mit Palette-Lookup-Renderer. +//! pbio — Pixelbuffer-basiertes Fenster mit RGB565-Truecolor-Renderer. use std::collections::VecDeque; use std::sync::Arc; @@ -15,8 +15,6 @@ use winit::keyboard::{KeyCode, PhysicalKey}; use winit::platform::pump_events::EventLoopExtPumpEvents; use winit::window::{CursorGrabMode, Window, WindowAttributes, WindowId}; -pub mod palette; - // --- Konfiguration --------------------------------------------------------- pub struct PlatformConfig { @@ -24,7 +22,6 @@ pub struct PlatformConfig { pub window_size: (u32, u32), pub framebuffer_size: (u32, u32), pub aspect_ratio: Option, - pub palette: [[u8; 3]; 256], pub vsync: bool, pub mouse_visible: bool, pub mouse_capture: bool, @@ -37,7 +34,6 @@ 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, @@ -45,6 +41,11 @@ impl Default for PlatformConfig { } } +/// 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] @@ -91,10 +92,7 @@ pub struct Platform { // GPU-Ressourcen vor Surface, Surface vor Window (Drop-Reihenfolge). bind_group: wgpu::BindGroup, pipeline: wgpu::RenderPipeline, - index_tex: wgpu::Texture, - palette_tex: wgpu::Texture, - palette: [[u8; 3]; 256], - palette_dirty: bool, + pixel_tex: wgpu::Texture, params_buf: wgpu::Buffer, params: Params, surface: wgpu::Surface<'static>, @@ -106,7 +104,7 @@ pub struct Platform { window: Arc, event_loop: EventLoop<()>, - framebuffer: Vec, + framebuffer: Vec, framebuffer_size: (u32, u32), aspect_ratio: f32, // 0.0 = keine Letterbox content_rect: (u32, u32, u32, u32), @@ -189,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 = 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); @@ -227,18 +215,16 @@ 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) }, ], }); let mut platform = Self { - bind_group, pipeline, index_tex, palette_tex, - palette: cfg.palette, palette_dirty: false, + bind_group, pipeline, pixel_tex, params_buf, params, surface, config: surface_config, device, queue, instance, window, event_loop, - framebuffer: vec![0u8; (src_w * src_h) as usize], + framebuffer: vec![0u16; (src_w * src_h) as usize], framebuffer_size: (src_w, src_h), aspect_ratio, content_rect, event_queue: VecDeque::new(), @@ -313,49 +299,20 @@ impl Platform { /// 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`, - /// `palette_mut` …). + /// auf die Platform reagiert werden kann (`request_close`, `set_mouse_capture` …). pub fn poll_event(&mut self) -> Option { self.event_queue.pop_front() } - pub fn framebuffer_mut(&mut self) -> &mut [u8] { &mut self.framebuffer } - pub fn framebuffer_size(&self) -> (u32, u32) { self.framebuffer_size } - - /// Liefert &mut auf die 256-Einträge-Palette. Setzt pessimistisch ein - /// Dirty-Flag — der nächste `present()` lädt die Palette neu hoch. - pub fn palette_mut(&mut self) -> &mut [[u8; 3]; 256] { - self.palette_dirty = true; - &mut self.palette - } - pub fn palette(&self) -> &[[u8; 3]; 256] { &self.palette } + 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) { - if self.palette_dirty { - let rgba: [u8; 256 * 4] = { - let mut buf = [0u8; 256 * 4]; - for (i, [r, g, b]) in self.palette.iter().enumerate() { - buf[i * 4] = *r; - buf[i * 4 + 1] = *g; - buf[i * 4 + 2] = *b; - buf[i * 4 + 3] = 255; - } - buf - }; - self.queue.write_texture( - wgpu::TexelCopyTextureInfoBase { texture: &self.palette_tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All }, - &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 }, - ); - self.palette_dirty = false; - } - 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 }, ); @@ -616,7 +573,6 @@ fn build_pipeline( count: None, }, tex_entry(1, wgpu::TextureSampleType::Uint), - tex_entry(2, wgpu::TextureSampleType::Float { filterable: false }), ], }); diff --git a/src/palette.rs b/src/palette.rs deleted file mode 100644 index 25e3bde..0000000 --- a/src/palette.rs +++ /dev/null @@ -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 -}; diff --git a/src/shader.wgsl b/src/shader.wgsl index c58120d..b44af18 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -5,8 +5,7 @@ struct Params { } @group(0) @binding(0) var params: Params; -@group(0) @binding(1) var src_indices: texture_2d; -@group(0) @binding(2) var palette_tex: texture_2d; +@group(0) @binding(1) var src_pixels: texture_2d; @vertex fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4 { @@ -25,8 +24,11 @@ fn fs_main(@builtin(position) frag_xy: vec4) -> @location(0) vec4 { 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(sx, sy), 0).r & 0xFFu; - let rgb = textureLoad(palette_tex, vec2(i32(idx), 0), 0).rgb; + // RGB565: RRRRRGGGGGGBBBBB + let px: u32 = textureLoad(src_pixels, vec2(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(rgb, 1.0); + return vec4(r, g, b, 1.0); }