From b855c2bf978550ca5c5739f126d61519b5fc44bb Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Wed, 21 Apr 2021 20:06:25 -0400 Subject: [PATCH] Add JPEG, PNG, and mixed compression for terrain. --- Cargo.lock | 7 + world/Cargo.toml | 2 +- world/src/bin/chunk_compression_benchmarks.rs | 342 +++++++++++++++++- 3 files changed, 344 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c49514c8c4..bf217c670b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2472,6 +2472,7 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "jpeg-decoder", "num-iter", "num-rational 0.3.2", "num-traits", @@ -2616,6 +2617,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" + [[package]] name = "js-sys" version = "0.3.50" diff --git a/world/Cargo.toml b/world/Cargo.toml index e08f498f08..2dde0532e4 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -19,7 +19,7 @@ bincode = "1.3.1" bitvec = "0.22" enum-iterator = "0.6" fxhash = "0.2.1" -image = { version = "0.23.12", default-features = false, features = ["png"] } +image = { version = "0.23.12", default-features = false, features = ["png", "jpeg"] } itertools = "0.10" vek = { version = "0.14.1", features = ["serde"] } noise = { version = "0.7", default-features = false } diff --git a/world/src/bin/chunk_compression_benchmarks.rs b/world/src/bin/chunk_compression_benchmarks.rs index 84c4038dcc..e1d0c9bb23 100644 --- a/world/src/bin/chunk_compression_benchmarks.rs +++ b/world/src/bin/chunk_compression_benchmarks.rs @@ -1,12 +1,17 @@ use common::{ spiral::Spiral2d, terrain::{chonk::Chonk, Block, BlockKind, SpriteKind}, - vol::{IntoVolIterator, RectVolSize, SizedVol, WriteVol}, - volumes::dyna::{Access, ColumnAccess, Dyna}, + vol::{BaseVol, IntoVolIterator, ReadVol, RectVolSize, SizedVol, WriteVol}, + volumes::{ + dyna::{Access, ColumnAccess, Dyna}, + vol_grid_2d::VolGrid2d, + }, }; use hashbrown::HashMap; use std::{ + fmt::Debug, io::{Read, Write}, + sync::Arc, time::Instant, }; use tracing::{debug, trace}; @@ -110,6 +115,254 @@ fn channelize_dyna( (blocks, r, g, b, sprites) } +/// Formula for packing voxel data into a 2d array +pub trait PackingFormula { + fn dimensions(&self, dims: Vec3) -> (u32, u32); + fn index(&self, dims: Vec3, x: u32, y: u32, z: u32) -> (u32, u32); +} + +/// A tall, thin image, with no wasted space, but which most image viewers don't +/// handle well. Z levels increase from top to bottom, xy-slices are stacked +/// vertically. +pub struct TallPacking { + /// Making the borders go back and forth based on z-parity preserves spatial + /// locality better, but is more confusing to look at + pub flip_y: bool, +} + +impl PackingFormula for TallPacking { + fn dimensions(&self, dims: Vec3) -> (u32, u32) { (dims.x, dims.y * dims.z) } + + fn index(&self, dims: Vec3, x: u32, y: u32, z: u32) -> (u32, u32) { + let i = x; + let j0 = if self.flip_y { + if z % 2 == 0 { y } else { dims.y - y - 1 } + } else { + y + }; + let j = z * dims.y + j0; + (i, j) + } +} + +/// A grid of the z levels, left to right, top to bottom, like English prose. +/// Convenient for visualizing terrain, but wastes space if the number of z +/// levels isn't a perfect square. +pub struct GridLtrPacking; + +impl PackingFormula for GridLtrPacking { + fn dimensions(&self, dims: Vec3) -> (u32, u32) { + let rootz = (dims.z as f64).sqrt().ceil() as u32; + (dims.x * rootz, dims.y * rootz) + } + + fn index(&self, dims: Vec3, x: u32, y: u32, z: u32) -> (u32, u32) { + let rootz = (dims.z as f64).sqrt().ceil() as u32; + let i = x + (z % rootz) * dims.x; + let j = y + (z / rootz) * dims.y; + (i, j) + } +} + +pub trait VoxelImageEncoding { + type Workspace; + type Output; + fn create(width: u32, height: u32) -> Self::Workspace; + fn put_solid(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, rgb: Rgb); + fn put_sprite(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, sprite: SpriteKind); + fn finish(ws: &Self::Workspace) -> Self::Output; +} + +pub struct PngEncoding; + +impl VoxelImageEncoding for PngEncoding { + type Output = Vec; + type Workspace = image::ImageBuffer, Vec>; + + fn create(width: u32, height: u32) -> Self::Workspace { + use image::{ImageBuffer, Rgba}; + ImageBuffer::, Vec>::new(width, height) + } + + fn put_solid(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, rgb: Rgb) { + ws.put_pixel(x, y, image::Rgba([rgb.r, rgb.g, rgb.b, 255 - kind as u8])); + } + + fn put_sprite(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, sprite: SpriteKind) { + ws.put_pixel(x, y, image::Rgba([kind as u8, sprite as u8, 255, 255])); + } + + fn finish(ws: &Self::Workspace) -> Self::Output { + use image::codecs::png::{CompressionType, FilterType}; + let mut buf = Vec::new(); + let png = image::codecs::png::PngEncoder::new_with_quality( + &mut buf, + CompressionType::Fast, + FilterType::Up, + ); + png.encode( + &*ws.as_raw(), + ws.width(), + ws.height(), + image::ColorType::Rgba8, + ) + .unwrap(); + buf + } +} + +pub struct JpegEncoding; + +impl VoxelImageEncoding for JpegEncoding { + type Output = Vec; + type Workspace = image::ImageBuffer, Vec>; + + fn create(width: u32, height: u32) -> Self::Workspace { + use image::{ImageBuffer, Rgba}; + ImageBuffer::, Vec>::new(width, height) + } + + fn put_solid(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, rgb: Rgb) { + ws.put_pixel(x, y, image::Rgba([rgb.r, rgb.g, rgb.b, 255 - kind as u8])); + } + + fn put_sprite(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, sprite: SpriteKind) { + ws.put_pixel(x, y, image::Rgba([kind as u8, sprite as u8, 255, 255])); + } + + fn finish(ws: &Self::Workspace) -> Self::Output { + let mut buf = Vec::new(); + let mut jpeg = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 1); + jpeg.encode_image(ws).unwrap(); + buf + } +} + +pub struct MixedEncoding; + +impl VoxelImageEncoding for MixedEncoding { + type Output = (Vec, usize); + type Workspace = ( + image::ImageBuffer, Vec>, + image::ImageBuffer, Vec>, + ); + + fn create(width: u32, height: u32) -> Self::Workspace { + use image::ImageBuffer; + ( + ImageBuffer::new(width, height), + ImageBuffer::new(width, height), + ) + } + + fn put_solid(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, rgb: Rgb) { + ws.0.put_pixel(x, y, image::LumaA([kind as u8, 0])); + ws.1.put_pixel(x, y, image::Rgb([rgb.r, rgb.g, rgb.b])); + } + + fn put_sprite(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, sprite: SpriteKind) { + ws.0.put_pixel(x, y, image::LumaA([kind as u8, sprite as u8])); + ws.1.put_pixel(x, y, image::Rgb([0; 3])); + } + + fn finish(ws: &Self::Workspace) -> Self::Output { + let mut buf = Vec::new(); + use image::codecs::png::{CompressionType, FilterType}; + let png = image::codecs::png::PngEncoder::new_with_quality( + &mut buf, + CompressionType::Fast, + FilterType::Up, + ); + png.encode( + &*ws.0.as_raw(), + ws.0.width(), + ws.0.height(), + image::ColorType::La8, + ) + .unwrap(); + let index = buf.len(); + let mut jpeg = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 1); + jpeg.encode_image(&ws.1).unwrap(); + //println!("Mixed {} {}", index, buf.len()); + (buf, index) + } +} + +fn image_terrain_chonk( + vie: VIE, + packing: P, + chonk: &Chonk, +) -> VIE::Output { + image_terrain( + vie, + packing, + chonk, + Vec3::new(0, 0, chonk.get_min_z() as u32), + Vec3::new(S::RECT_SIZE.x, S::RECT_SIZE.y, chonk.get_max_z() as u32), + ) +} + +fn image_terrain_volgrid< + S: RectVolSize + Debug, + M: Clone + Debug, + P: PackingFormula, + VIE: VoxelImageEncoding, +>( + vie: VIE, + packing: P, + volgrid: &VolGrid2d>, +) -> VIE::Output { + let mut lo = Vec3::broadcast(i32::MAX); + let mut hi = Vec3::broadcast(i32::MIN); + for (pos, chonk) in volgrid.iter() { + lo.x = lo.x.min(pos.x * S::RECT_SIZE.x as i32); + lo.y = lo.y.min(pos.y * S::RECT_SIZE.y as i32); + lo.z = lo.z.min(chonk.get_min_z()); + + hi.x = hi.x.max((pos.x + 1) * S::RECT_SIZE.x as i32); + hi.y = hi.y.max((pos.y + 1) * S::RECT_SIZE.y as i32); + hi.z = hi.z.max(chonk.get_max_z()); + } + println!("{:?} {:?}", lo, hi); + + image_terrain(vie, packing, volgrid, lo.as_(), hi.as_()) +} + +fn image_terrain + ReadVol, P: PackingFormula, VIE: VoxelImageEncoding>( + _: VIE, + packing: P, + vol: &V, + lo: Vec3, + hi: Vec3, +) -> VIE::Output { + let dims = hi - lo; + + let (width, height) = packing.dimensions(dims); + let mut image = VIE::create(width, height); + //println!("jpeg dims: {:?}", dims); + for z in 0..dims.z { + for y in 0..dims.y { + for x in 0..dims.x { + let (i, j) = packing.index(dims, x, y, z); + //println!("{:?} {:?}", (x, y, z), (i, j)); + + let block = *vol + .get(Vec3::new(x + lo.x, y + lo.y, z + lo.z).as_()) + .unwrap_or(&Block::empty()); + //println!("{} {} {} {:?}", x, y, z, block); + if let Some(rgb) = block.get_color() { + VIE::put_solid(&mut image, i, j, *block, rgb); + } else { + let sprite = block.get_sprite().unwrap(); + VIE::put_sprite(&mut image, i, j, *block, sprite); + } + } + } + } + + VIE::finish(&image) +} + fn histogram_to_dictionary(histogram: &HashMap, usize>, dictionary: &mut Vec) { let mut tmp: Vec<(Vec, usize)> = histogram.iter().map(|(k, v)| (k.clone(), *v)).collect(); tmp.sort_by_key(|(_, count)| *count); @@ -142,9 +395,10 @@ fn main() { let mut dictionary2 = vec![0xffu8; 1 << 16]; let k = 32; let sz = world.sim().get_size(); - let mut totals = [0.0; 5]; - let mut total_timings = [0.0; 2]; + let mut totals = [0.0; 10]; + let mut total_timings = [0.0; 7]; let mut count = 0; + let mut volgrid = VolGrid2d::new().unwrap(); for (i, (x, y)) in Spiral2d::new() .radius(20) .map(|v| (v.x + sz.x as i32 / 2, v.y + sz.y as i32 / 2)) @@ -182,6 +436,36 @@ fn main() { let deflate_dyna = do_deflate(&*ser_dyna); let deflate_channeled_dyna = do_deflate_flate2(&bincode::serialize(&channelize_dyna(&dyna)).unwrap()); + + let jpegchonkgrid_pre = Instant::now(); + let jpegchonkgrid = image_terrain_chonk(JpegEncoding, GridLtrPacking, &chunk); + let jpegchonkgrid_post = Instant::now(); + + if false { + use std::fs::File; + let mut f = File::create(&format!("chonkjpegs/tmp_{}_{}.jpg", x, y)).unwrap(); + f.write_all(&*jpegchonkgrid).unwrap(); + } + + let jpegchonktall_pre = Instant::now(); + let jpegchonktall = + image_terrain_chonk(JpegEncoding, TallPacking { flip_y: false }, &chunk); + let jpegchonktall_post = Instant::now(); + + let jpegchonkflip_pre = Instant::now(); + let jpegchonkflip = + image_terrain_chonk(JpegEncoding, TallPacking { flip_y: true }, &chunk); + let jpegchonkflip_post = Instant::now(); + + let mixedchonk_pre = Instant::now(); + let mixedchonk = + image_terrain_chonk(MixedEncoding, TallPacking { flip_y: true }, &chunk); + let mixedchonk_post = Instant::now(); + + let pngchonk_pre = Instant::now(); + let pngchonk = image_terrain_chonk(PngEncoding, GridLtrPacking, &chunk); + let pngchonk_post = Instant::now(); + let n = uncompressed.len(); let sizes = [ lz4_chonk.len() as f32 / n as f32, @@ -189,6 +473,11 @@ fn main() { lz4_dyna.len() as f32 / n as f32, deflate_dyna.len() as f32 / n as f32, deflate_channeled_dyna.len() as f32 / n as f32, + jpegchonkgrid.len() as f32 / n as f32, + jpegchonktall.len() as f32 / n as f32, + jpegchonkflip.len() as f32 / n as f32, + mixedchonk.0.len() as f32 / n as f32, + pngchonk.len() as f32 / n as f32, ]; let best_idx = sizes .iter() @@ -204,6 +493,11 @@ fn main() { let timings = [ (lz4chonk_post - lz4chonk_pre).subsec_nanos(), (deflatechonk_post - deflatechonk_pre).subsec_nanos(), + (jpegchonkgrid_post - jpegchonkgrid_pre).subsec_nanos(), + (jpegchonktall_post - jpegchonktall_pre).subsec_nanos(), + (jpegchonkflip_post - jpegchonkflip_pre).subsec_nanos(), + (mixedchonk_post - mixedchonk_pre).subsec_nanos(), + (pngchonk_post - pngchonk_pre).subsec_nanos(), ]; trace!( "{} {}: uncompressed: {}, {:?} {} {:?}", @@ -214,13 +508,24 @@ fn main() { best_idx, timings ); - for j in 0..5 { + for j in 0..totals.len() { totals[j] += sizes[j]; } - for j in 0..2 { + for j in 0..total_timings.len() { total_timings[j] += timings[j] as f32; } count += 1; + let _ = volgrid.insert(Vec2::new(x, y), Arc::new(chunk)); + + if (1usize..10) + .into_iter() + .any(|i| (2 * i + 1) * (2 * i + 1) == count) + { + use std::fs::File; + let mut f = File::create(&format!("chonkjpegs/volgrid_{}.jpg", count)).unwrap(); + let jpeg_volgrid = image_terrain_volgrid(JpegEncoding, GridLtrPacking, &volgrid); + f.write_all(&*jpeg_volgrid).unwrap(); + } } if i % 64 == 0 { println!("Chunks processed: {}\n", count); @@ -232,6 +537,11 @@ fn main() { "Average deflate_channeled_dyna: {}", totals[4] / count as f32 ); + println!("Average jpeggridchonk: {}", totals[5] / count as f32); + println!("Average jpegtallchonk: {}", totals[6] / count as f32); + println!("Average jpegflipchonk: {}", totals[7] / count as f32); + println!("Average mixedchonk: {}", totals[8] / count as f32); + println!("Average pngchonk: {}", totals[9] / count as f32); println!(""); println!( "Average lz4_chonk nanos : {:02}", @@ -241,6 +551,26 @@ fn main() { "Average deflate_chonk nanos: {:02}", total_timings[1] / count as f32 ); + println!( + "Average jpeggridchonk nanos: {:02}", + total_timings[2] / count as f32 + ); + println!( + "Average jpegtallchonk nanos: {:02}", + total_timings[3] / count as f32 + ); + println!( + "Average jpegflipchonk nanos: {:02}", + total_timings[4] / count as f32 + ); + println!( + "Average mixedchonk nanos: {:02}", + total_timings[5] / count as f32 + ); + println!( + "Average pngchonk nanos: {:02}", + total_timings[6] / count as f32 + ); println!("-----"); } if i % 256 == 0 {