Switch to 16-bit truecolor framebuffer and PNG assets

pbio moves to git.bnd.wtf/irrlicht/pbio.git branch truecolor16: the
framebuffer is now RGB565 u16, no palette. Assets are loaded from PNG via
the png crate and quantized to RGB565 on load; transparency comes from the
alpha channel as a per-tile opacity mask, so any color (black included) can
be opaque. The TGA loader and the palette-order fragility are gone.

overworld.png is the unmodified Mini-Medieval Overworld sheet (same 24-column
layout as the old TGA, so map tile ids are unchanged); entities.png holds the
player sprite extracted from the old entities.tga.

Adds an ignored snapshot test that renders the first frame headlessly to a
PNG for eyeballing the renderer without a window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irrlicht
2026-09-18 23:17:41 +02:00
co-authored by Claude Opus 5
parent 64a6e1d42d
commit 5a055c048d
14 changed files with 212 additions and 96 deletions
+40 -11
View File
@@ -3,7 +3,7 @@ mod pixelhelper;
use std::collections::{HashMap, VecDeque};
use crate::assets::Image;
use crate::assets::{Image, Tile};
use crate::input::{GameAction, InputState};
use sim::entity::Entity;
use sim::map::TileMap;
@@ -25,8 +25,11 @@ const MOVE_INTERVAL_MS: usize = (TICK_MS * TICKS_PER_MOVE as f32) as usize;
/// drag) is dropped instead of spiralling into a burst of ticks.
const MAX_FRAME_MS: f32 = 250.0;
/// Palette index for the planned route. Dim blue in RGB332 (r=0, g=0, b=2).
const ROUTE_COLOR: u8 = 0b000_000_10;
/// Color of the planned route: dim blue.
const ROUTE_COLOR: u16 = pbio::rgb565(0, 0, 170);
/// Fill for tiles outside the tileset / world.
const VOID_COLOR: u16 = pbio::rgb565(0, 0, 0);
/// Viewport geometry: 30×30 tiles of 8 px, top-left of the framebuffer.
const VIEW_TILES: i32 = 30;
@@ -50,8 +53,8 @@ struct EntityLerp {
}
pub struct Game {
tileset: Vec<[u8; 64]>,
entity_tileset: Vec<[u8; 64]>,
tileset: Vec<Tile>,
entity_tileset: Vec<Tile>,
sim: Sim,
player_id: u32,
/// Frame time not yet consumed by whole ticks.
@@ -74,8 +77,8 @@ pub struct Game {
impl Game {
pub fn start() -> Self {
let tileset = Image::from_tga("assets/tilesets/overworld.tga").to_tileset();
let entity_tileset = Image::from_tga("assets/tilesets/entities.tga").to_tileset();
let tileset = Image::from_png("assets/tilesets/overworld.png").to_tileset();
let entity_tileset = Image::from_png("assets/tilesets/entities.png").to_tileset();
let map = TileMap::from_csv("assets/map_test");
println!("loaded map {}×{} ({} tiles)", map.width, map.height, map.tiles.len());
@@ -109,7 +112,7 @@ impl Game {
self.sim.world.tile_flags(wx as i16, wy as i16).map_or(true, |f| f.collidable())
}
pub fn update(&mut self, render_frame: &mut [u8], dt: usize, input: &InputState)
pub fn update(&mut self, render_frame: &mut [u16], dt: usize, input: &InputState)
-> Option<GameSignal>
{
if input.mouse_clicked() || input.mouse_held() {
@@ -315,8 +318,7 @@ impl Game {
}
}
fn render_viewport(&self, frame: &mut [u8]) {
const WALL: u8 = 0x00;
fn render_viewport(&self, frame: &mut [u16]) {
const W: usize = 320;
frame.fill(0);
@@ -345,7 +347,7 @@ impl Game {
} else {
for dy in 0..8 {
for dx in 0..8 {
pixelhelper::set_pixel(frame, W, px + dx, py + dy, WALL);
pixelhelper::set_pixel(frame, W, px + dx, py + dy, VOID_COLOR);
}
}
}
@@ -383,3 +385,30 @@ impl Game {
}
}
}
#[cfg(test)]
mod snapshot {
/// Renders the first frame headlessly to the PNG named by `$SNAPSHOT` — a way to
/// eyeball the renderer without a window:
/// `SNAPSHOT=/tmp/frame.png cargo test -p game snapshot -- --ignored`
#[test]
#[ignore]
fn dump_frame() {
std::env::set_current_dir("..").unwrap();
let mut g = super::Game::start();
let input = crate::input::InputState::new();
let mut frame = vec![0u16; 320 * 240];
g.update(&mut frame, 0, &input);
let out = std::env::var("SNAPSHOT").unwrap();
let mut rgb = Vec::with_capacity(320 * 240 * 3);
for p in &frame {
rgb.push(((p >> 11) as u8) << 3);
rgb.push((((p >> 5) & 63) as u8) << 2);
rgb.push(((p & 31) as u8) << 3);
}
let f = std::fs::File::create(out).unwrap();
let mut enc = png::Encoder::new(std::io::BufWriter::new(f), 320, 240);
enc.set_color(png::ColorType::Rgb);
enc.write_header().unwrap().write_image_data(&rgb).unwrap();
}
}