diff --git a/Cargo.lock b/Cargo.lock index 9017f2f008..2b38f490c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5516,6 +5516,7 @@ dependencies = [ "flate2", "hashbrown", "image", + "num-traits", "serde", "specs", "specs-idvs", diff --git a/client/src/lib.rs b/client/src/lib.rs index ad321d4c67..2b6d6f8529 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1952,7 +1952,7 @@ impl Client { fn handle_server_terrain_msg(&mut self, msg: ServerGeneral) -> Result<(), Error> { match msg { ServerGeneral::TerrainChunkUpdate { key, chunk } => { - if let Some(chunk) = chunk.ok().and_then(|c| c.decompress()) { + if let Some(chunk) = chunk.ok().and_then(|c| c.to_chunk()) { self.state.insert_chunk(key, Arc::new(chunk)); } self.pending_chunks.remove(&key); diff --git a/common/net/Cargo.toml b/common/net/Cargo.toml index 3c0905f351..4f7370f6a2 100644 --- a/common/net/Cargo.toml +++ b/common/net/Cargo.toml @@ -17,9 +17,11 @@ common = {package = "veloren-common", path = "../../common"} bincode = "1.3.3" flate2 = "1.0.20" image = { version = "0.23.12", default-features = false, features = ["png", "jpeg"] } +num-traits = "0.2" sum_type = "0.2.0" vek = { version = "=0.14.1", features = ["serde"] } tracing = { version = "0.1", default-features = false } +#inline_tweak = "1.0.2" # Data structures hashbrown = { version = "0.9", features = ["rayon", "serde", "nightly"] } diff --git a/common/net/src/msg/compression.rs b/common/net/src/msg/compression.rs index 2196ee40c2..1071a8f4a0 100644 --- a/common/net/src/msg/compression.rs +++ b/common/net/src/msg/compression.rs @@ -1,16 +1,17 @@ use common::{ terrain::{chonk::Chonk, Block, BlockKind, SpriteKind}, - vol::{BaseVol, IntoVolIterator, ReadVol, RectVolSize, SizedVol, WriteVol}, + vol::{BaseVol, ReadVol, RectVolSize, WriteVol}, volumes::vol_grid_2d::VolGrid2d, }; -use hashbrown::HashMap; +use image::{ImageBuffer, ImageDecoder, Pixel}; +use num_traits::cast::FromPrimitive; use serde::{Deserialize, Serialize}; use std::{ fmt::Debug, io::{Read, Write}, marker::PhantomData, }; -use tracing::trace; +use tracing::{trace, warn}; use vek::*; /// Wrapper for compressed, serialized data (for stuff that doesn't use the @@ -71,7 +72,7 @@ impl Deserialize<'a>> CompressedData { } /// Formula for packing voxel data into a 2d array -pub trait PackingFormula { +pub trait PackingFormula: Copy { fn dimensions(&self, dims: Vec3) -> (u32, u32); fn index(&self, dims: Vec3, x: u32, y: u32, z: u32) -> (u32, u32); } @@ -79,6 +80,7 @@ pub trait PackingFormula { /// 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. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] 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 @@ -88,6 +90,7 @@ pub struct TallPacking { impl PackingFormula for TallPacking { fn dimensions(&self, dims: Vec3) -> (u32, u32) { (dims.x, dims.y * dims.z) } + #[allow(clippy::many_single_char_names)] fn index(&self, dims: Vec3, x: u32, y: u32, z: u32) -> (u32, u32) { let i = x; let j0 = if self.flip_y { @@ -103,6 +106,7 @@ impl PackingFormula for TallPacking { /// 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. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct GridLtrPacking; impl PackingFormula for GridLtrPacking { @@ -111,6 +115,7 @@ impl PackingFormula for GridLtrPacking { (dims.x * rootz, dims.y * rootz) } + #[allow(clippy::many_single_char_names)] 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; @@ -119,23 +124,36 @@ impl PackingFormula for GridLtrPacking { } } -pub trait VoxelImageEncoding { +pub trait VoxelImageEncoding: Copy { 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, ori: Option); - fn finish(ws: &Self::Workspace) -> Self::Output; + fn put_sprite( + ws: &mut Self::Workspace, + x: u32, + y: u32, + kind: BlockKind, + sprite: SpriteKind, + ori: Option, + ); + fn finish(ws: &Self::Workspace) -> Option; } +pub trait VoxelImageDecoding: VoxelImageEncoding { + fn start(ws: &Self::Output) -> Option; + fn get_block(ws: &Self::Workspace, x: u32, y: u32) -> Block; +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct PngEncoding; impl VoxelImageEncoding for PngEncoding { type Output = Vec; - type Workspace = image::ImageBuffer, Vec>; + type Workspace = ImageBuffer, Vec>; fn create(width: u32, height: u32) -> Self::Workspace { - use image::{ImageBuffer, Rgba}; + use image::Rgba; ImageBuffer::, Vec>::new(width, height) } @@ -143,11 +161,22 @@ impl VoxelImageEncoding for PngEncoding { 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, ori: Option) { - ws.put_pixel(x, y, image::Rgba([kind as u8, sprite as u8, ori.unwrap_or(0), 255])); + fn put_sprite( + ws: &mut Self::Workspace, + x: u32, + y: u32, + kind: BlockKind, + sprite: SpriteKind, + ori: Option, + ) { + ws.put_pixel( + x, + y, + image::Rgba([kind as u8, sprite as u8, ori.unwrap_or(0), 255]), + ); } - fn finish(ws: &Self::Workspace) -> Self::Output { + fn finish(ws: &Self::Workspace) -> Option { use image::codecs::png::{CompressionType, FilterType}; let mut buf = Vec::new(); let png = image::codecs::png::PngEncoder::new_with_quality( @@ -161,19 +190,20 @@ impl VoxelImageEncoding for PngEncoding { ws.height(), image::ColorType::Rgba8, ) - .unwrap(); - buf + .ok()?; + Some(buf) } } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct JpegEncoding; impl VoxelImageEncoding for JpegEncoding { type Output = Vec; - type Workspace = image::ImageBuffer, Vec>; + type Workspace = ImageBuffer, Vec>; fn create(width: u32, height: u32) -> Self::Workspace { - use image::{ImageBuffer, Rgba}; + use image::Rgba; ImageBuffer::, Vec>::new(width, height) } @@ -181,31 +211,39 @@ impl VoxelImageEncoding for JpegEncoding { 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, _: Option) { + fn put_sprite( + ws: &mut Self::Workspace, + x: u32, + y: u32, + kind: BlockKind, + sprite: SpriteKind, + _: Option, + ) { ws.put_pixel(x, y, image::Rgba([kind as u8, sprite as u8, 255, 255])); } - fn finish(ws: &Self::Workspace) -> Self::Output { + fn finish(ws: &Self::Workspace) -> Option { let mut buf = Vec::new(); let mut jpeg = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 1); - jpeg.encode_image(ws).unwrap(); - buf + jpeg.encode_image(ws).ok()?; + Some(buf) } } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct MixedEncoding; impl VoxelImageEncoding for MixedEncoding { type Output = (Vec, [usize; 3]); + #[allow(clippy::type_complexity)] type Workspace = ( - image::ImageBuffer, Vec>, - image::ImageBuffer, Vec>, - image::ImageBuffer, Vec>, - image::ImageBuffer, Vec>, + ImageBuffer, Vec>, + ImageBuffer, Vec>, + ImageBuffer, Vec>, + ImageBuffer, Vec>, ); fn create(width: u32, height: u32) -> Self::Workspace { - use image::ImageBuffer; ( ImageBuffer::new(width, height), ImageBuffer::new(width, height), @@ -221,39 +259,174 @@ impl VoxelImageEncoding for MixedEncoding { ws.3.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, ori: Option) { + fn put_sprite( + ws: &mut Self::Workspace, + x: u32, + y: u32, + kind: BlockKind, + sprite: SpriteKind, + ori: Option, + ) { ws.0.put_pixel(x, y, image::Luma([kind as u8])); ws.1.put_pixel(x, y, image::Luma([sprite as u8])); ws.2.put_pixel(x, y, image::Luma([ori.unwrap_or(0)])); ws.3.put_pixel(x, y, image::Rgb([0; 3])); } - fn finish(ws: &Self::Workspace) -> Self::Output { + fn finish(ws: &Self::Workspace) -> Option { let mut buf = Vec::new(); use image::codecs::png::{CompressionType, FilterType}; let mut indices = [0; 3]; - let mut f = |x: &image::ImageBuffer<_, Vec>, i| { + let mut f = |x: &ImageBuffer<_, Vec>, i| { let png = image::codecs::png::PngEncoder::new_with_quality( &mut buf, CompressionType::Fast, FilterType::Up, ); - png.encode( - &*x.as_raw(), - x.width(), - x.height(), - image::ColorType::L8, - ) - .unwrap(); + png.encode(&*x.as_raw(), x.width(), x.height(), image::ColorType::L8) + .ok()?; indices[i] = buf.len(); + Some(()) }; - f(&ws.0, 0); - f(&ws.1, 1); - f(&ws.2, 2); + f(&ws.0, 0)?; + f(&ws.1, 1)?; + f(&ws.2, 2)?; - let mut jpeg = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 1); - jpeg.encode_image(&ws.3).unwrap(); - (buf, indices) + let mut jpeg = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 10); + jpeg.encode_image(&ws.3).ok()?; + Some((buf, indices)) + } +} + +fn image_from_bytes<'a, I: ImageDecoder<'a>, P: 'static + Pixel>( + decoder: I, +) -> Option>> { + let (w, h) = decoder.dimensions(); + let mut buf = vec![0; decoder.total_bytes() as usize]; + decoder.read_image(&mut buf).ok()?; + ImageBuffer::from_raw(w, h, buf) +} + +impl VoxelImageDecoding for MixedEncoding { + fn start((quad, indices): &Self::Output) -> Option { + use image::codecs::{jpeg::JpegDecoder, png::PngDecoder}; + let ranges: [_; 4] = [ + 0..indices[0], + indices[0]..indices[1], + indices[1]..indices[2], + indices[2]..quad.len(), + ]; + tracing::info!("{:?} {:?}", ranges, indices); + let a = image_from_bytes(PngDecoder::new(&quad[ranges[0].clone()]).ok()?)?; + let b = image_from_bytes(PngDecoder::new(&quad[ranges[1].clone()]).ok()?)?; + let c = image_from_bytes(PngDecoder::new(&quad[ranges[2].clone()]).ok()?)?; + let d = image_from_bytes(JpegDecoder::new(&quad[ranges[3].clone()]).ok()?)?; + Some((a, b, c, d)) + } + + fn get_block(ws: &Self::Workspace, x: u32, y: u32) -> Block { + if let Some(kind) = BlockKind::from_u8(ws.0.get_pixel(x, y).0[0]) { + if kind.is_filled() { + let rgb = ws.3.get_pixel(x, y); + Block::new(kind, Rgb { + r: rgb[0], + g: rgb[1], + b: rgb[2], + }) + } else { + let mut block = Block::new(kind, Rgb { r: 0, g: 0, b: 0 }); + if let Some(spritekind) = SpriteKind::from_u8(ws.1.get_pixel(x, y).0[0]) { + block = block.with_sprite(spritekind); + } + if let Some(oriblock) = block.with_ori(ws.2.get_pixel(x, y).0[0]) { + block = oriblock; + } + block + } + } else { + Block::empty() + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct QuadPngEncoding; + +impl VoxelImageEncoding for QuadPngEncoding { + type Output = CompressedData<(Vec, [usize; 3])>; + #[allow(clippy::type_complexity)] + type Workspace = ( + ImageBuffer, Vec>, + ImageBuffer, Vec>, + ImageBuffer, Vec>, + ImageBuffer, Vec>, + ); + + fn create(width: u32, height: u32) -> Self::Workspace { + ( + ImageBuffer::new(width, height), + ImageBuffer::new(width, height), + 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::Luma([kind as u8])); + ws.1.put_pixel(x, y, image::Luma([0])); + ws.2.put_pixel(x, y, image::Luma([0])); + ws.3.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, + ori: Option, + ) { + ws.0.put_pixel(x, y, image::Luma([kind as u8])); + ws.1.put_pixel(x, y, image::Luma([sprite as u8])); + ws.2.put_pixel(x, y, image::Luma([ori.unwrap_or(0)])); + ws.3.put_pixel(x, y, image::Rgb([0; 3])); + } + + fn finish(ws: &Self::Workspace) -> Option { + let mut buf = Vec::new(); + use image::codecs::png::{CompressionType, FilterType}; + let mut indices = [0; 3]; + let mut f = |x: &ImageBuffer<_, Vec>, i| { + let png = image::codecs::png::PngEncoder::new_with_quality( + &mut buf, + CompressionType::Fast, + FilterType::Up, + ); + png.encode(&*x.as_raw(), x.width(), x.height(), image::ColorType::L8) + .ok()?; + indices[i] = buf.len(); + Some(()) + }; + f(&ws.0, 0)?; + f(&ws.1, 1)?; + f(&ws.2, 2)?; + + { + let png = image::codecs::png::PngEncoder::new_with_quality( + &mut buf, + CompressionType::Fast, + FilterType::Paeth, + ); + png.encode( + &*ws.3.as_raw(), + ws.3.width(), + ws.3.height(), + image::ColorType::Rgb8, + ) + .ok()?; + } + + Some(CompressedData::compress(&(buf, indices), 4)) } } @@ -261,7 +434,7 @@ pub fn image_terrain_chonk, -) -> VIE::Output { +) -> Option { image_terrain( vie, packing, @@ -280,7 +453,7 @@ pub fn image_terrain_volgrid< vie: VIE, packing: P, volgrid: &VolGrid2d>, -) -> VIE::Output { +) -> Option { let mut lo = Vec3::broadcast(i32::MAX); let mut hi = Vec3::broadcast(i32::MIN); for (pos, chonk) in volgrid.iter() { @@ -306,8 +479,13 @@ pub fn image_terrain< vol: &V, lo: Vec3, hi: Vec3, -) -> VIE::Output { - let dims = hi - lo; +) -> Option { + tracing::info!("image_terrain: {:?} {:?}", lo, hi); + let dims = Vec3::new( + hi.x.wrapping_sub(lo.x), + hi.y.wrapping_sub(lo.y), + hi.z.wrapping_sub(lo.z), + ); let (width, height) = packing.dimensions(dims); let mut image = VIE::create(width, height); @@ -317,7 +495,14 @@ pub fn image_terrain< let (i, j) = packing.index(dims, x, y, z); let block = *vol - .get(Vec3::new(x + lo.x, y + lo.y, z + lo.z).as_()) + .get( + Vec3::new( + x.wrapping_add(lo.x), + y.wrapping_add(lo.y), + z.wrapping_add(lo.z), + ) + .as_(), + ) .unwrap_or(&Block::empty()); match (block.get_color(), block.get_sprite()) { (Some(rgb), None) => { @@ -339,69 +524,85 @@ pub fn image_terrain< VIE::finish(&image) } -pub struct MixedEncodingDenseSprites; - -impl VoxelImageEncoding for MixedEncodingDenseSprites { - type Output = (Vec, [usize; 3]); - type Workspace = ( - image::ImageBuffer, Vec>, - Vec, - Vec, - image::ImageBuffer, Vec>, +pub fn write_image_terrain< + V: BaseVol + WriteVol, + P: PackingFormula, + VIE: VoxelImageEncoding + VoxelImageDecoding, +>( + _: VIE, + packing: P, + vol: &mut V, + data: &VIE::Output, + lo: Vec3, + hi: Vec3, +) -> Option<()> { + let ws = VIE::start(data)?; + let dims = Vec3::new( + hi.x.wrapping_sub(lo.x), + hi.y.wrapping_sub(lo.y), + hi.z.wrapping_sub(lo.z), ); + 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); + let block = VIE::get_block(&ws, i, j); + if let Err(e) = vol.set(lo.as_() + Vec3::new(x, y, z).as_(), block) { + warn!( + "Error placing a block into a volume at {:?}: {:?}", + (x, y, z), + e + ); + } + } + } + } + Some(()) +} - fn create(width: u32, height: u32) -> Self::Workspace { - use image::ImageBuffer; - ( - ImageBuffer::new(width, height), - Vec::new(), - Vec::new(), - ImageBuffer::new(width, height), - ) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WireChonk { + zmin: i32, + zmax: i32, + data: VIE::Output, + below: Block, + above: Block, + meta: M, + vie: VIE, + packing: P, + size: PhantomData, +} + +impl + WireChonk +{ + pub fn from_chonk(vie: VIE, packing: P, chonk: &Chonk) -> Option { + let data = image_terrain_chonk(vie, packing, chonk)?; + Some(Self { + zmin: chonk.get_min_z(), + zmax: chonk.get_max_z(), + data, + below: *chonk + .get(Vec3::new(0, 0, chonk.get_min_z().saturating_sub(1))) + .ok()?, + above: *chonk.get(Vec3::new(0, 0, chonk.get_max_z() + 1)).ok()?, + meta: chonk.meta().clone(), + vie, + packing, + size: PhantomData, + }) } - fn put_solid(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, rgb: Rgb) { - ws.0.put_pixel(x, y, image::Luma([kind as u8])); - ws.3.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, ori: Option) { - ws.0.put_pixel(x, y, image::Luma([kind as u8])); - ws.1.push(sprite as u8); - ws.2.push(ori.unwrap_or(0)); - ws.3.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 mut indices = [0; 3]; - let mut f = |x: &image::ImageBuffer<_, Vec>, i| { - let png = image::codecs::png::PngEncoder::new_with_quality( - &mut buf, - CompressionType::Fast, - FilterType::Up, - ); - png.encode( - &*x.as_raw(), - x.width(), - x.height(), - image::ColorType::L8, - ) - .unwrap(); - indices[i] = buf.len(); - }; - f(&ws.0, 0); - let mut g = |x: &[u8], i| { - buf.extend_from_slice(&*CompressedData::compress(&x, 4).data); - indices[i] = buf.len(); - }; - - g(&ws.1, 1); - g(&ws.2, 2); - - let mut jpeg = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 1); - jpeg.encode_image(&ws.3).unwrap(); - (buf, indices) + pub fn to_chonk(&self) -> Option> { + let mut chonk = Chonk::new(self.zmin, self.below, self.above, self.meta.clone()); + write_image_terrain( + self.vie, + self.packing, + &mut chonk, + &self.data, + Vec3::new(0, 0, self.zmin as u32), + Vec3::new(S::RECT_SIZE.x, S::RECT_SIZE.y, self.zmax as u32), + )?; + Some(chonk) } } diff --git a/common/net/src/msg/mod.rs b/common/net/src/msg/mod.rs index 25d315da2d..63c8708500 100644 --- a/common/net/src/msg/mod.rs +++ b/common/net/src/msg/mod.rs @@ -9,12 +9,13 @@ pub use self::{ client::{ClientGeneral, ClientMsg, ClientRegister, ClientType}, compression::{ CompressedData, GridLtrPacking, JpegEncoding, MixedEncoding, PackingFormula, PngEncoding, - TallPacking, VoxelImageEncoding, + TallPacking, VoxelImageEncoding, WireChonk, }, ecs_packet::EcsCompPacket, server::{ CharacterInfo, DisconnectReason, InviteAnswer, Notification, PlayerInfo, PlayerListUpdate, - RegisterError, ServerGeneral, ServerInfo, ServerInit, ServerMsg, ServerRegisterAnswer, + RegisterError, SerializedTerrainChunk, ServerGeneral, ServerInfo, ServerInit, ServerMsg, + ServerRegisterAnswer, }, world_msg::WorldMapMsg, }; diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index 033ed0892e..79e2f59094 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -1,4 +1,7 @@ -use super::{world_msg::EconomyInfo, ClientType, CompressedData, EcsCompPacket, PingMsg}; +use super::{ + world_msg::EconomyInfo, ClientType, CompressedData, EcsCompPacket, MixedEncoding, PingMsg, + TallPacking, WireChonk, +}; use crate::sync; use common::{ character::{self, CharacterItem}, @@ -6,13 +9,14 @@ use common::{ outcome::Outcome, recipe::RecipeBook, resources::TimeOfDay, - terrain::{Block, TerrainChunk}, + terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, trade::{PendingTrade, SitePrices, TradeId, TradeResult}, uid::Uid, }; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; use std::time::Duration; +use tracing::warn; use vek::*; ///This struct contains all messages the server might send (on different @@ -62,6 +66,35 @@ pub enum ServerInit { pub type ServerRegisterAnswer = Result<(), RegisterError>; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SerializedTerrainChunk { + DeflatedChonk(CompressedData), + PngPngPngJpeg(WireChonk), +} + +impl SerializedTerrainChunk { + pub fn deflate(chunk: &TerrainChunk) -> Self { + Self::DeflatedChonk(CompressedData::compress(chunk, 5)) + } + + pub fn image(chunk: &TerrainChunk) -> Self { + if let Some(wc) = WireChonk::from_chonk(MixedEncoding, TallPacking { flip_y: true }, chunk) + { + Self::PngPngPngJpeg(wc) + } else { + warn!("Image encoding failure occurred, falling back to deflate"); + Self::deflate(chunk) + } + } + + pub fn to_chunk(&self) -> Option { + match self { + Self::DeflatedChonk(chonk) => chonk.decompress(), + Self::PngPngPngJpeg(wc) => wc.to_chonk(), + } + } +} + /// Messages sent from the server to the client #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ServerGeneral { @@ -106,7 +139,7 @@ pub enum ServerGeneral { // Ingame related AND terrain stream TerrainChunkUpdate { key: Vec2, - chunk: Result, ()>, + chunk: Result, }, TerrainBlockUpdates(CompressedData, Block>>), // Always possible diff --git a/server/src/sys/msg/terrain.rs b/server/src/sys/msg/terrain.rs index 120098b444..6793f7c743 100644 --- a/server/src/sys/msg/terrain.rs +++ b/server/src/sys/msg/terrain.rs @@ -6,7 +6,7 @@ use common::{ vol::RectVolSize, }; use common_ecs::{Job, Origin, ParMode, Phase, System}; -use common_net::msg::{ClientGeneral, CompressedData, ServerGeneral}; +use common_net::msg::{ClientGeneral, SerializedTerrainChunk, ServerGeneral}; use rayon::iter::ParallelIterator; use specs::{Entities, Join, ParJoin, Read, ReadExpect, ReadStorage}; use tracing::{debug, trace}; @@ -79,7 +79,7 @@ impl<'a> System<'a> for Sys { network_metrics.chunks_served_from_memory.inc(); client.send(ServerGeneral::TerrainChunkUpdate { key, - chunk: Ok(CompressedData::compress(&chunk, 1)), + chunk: Ok(SerializedTerrainChunk::image(&chunk)), })? }, None => { diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 3cb95f40d6..88e3beca63 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -14,7 +14,7 @@ use common::{ LoadoutBuilder, SkillSetBuilder, }; use common_ecs::{Job, Origin, Phase, System}; -use common_net::msg::{CompressedData, ServerGeneral}; +use common_net::msg::{SerializedTerrainChunk, ServerGeneral}; use common_state::TerrainChanges; use comp::Behavior; use specs::{Join, Read, ReadStorage, Write, WriteExpect}; @@ -224,7 +224,7 @@ impl<'a> System<'a> for Sys { new_chunks.into_par_iter().for_each(|(key, chunk)| { let mut msg = Some(ServerGeneral::TerrainChunkUpdate { key, - chunk: Ok(CompressedData::compress(&*chunk, 1)), + chunk: Ok(SerializedTerrainChunk::image(&*chunk)), }); let mut lazy_msg = None; diff --git a/server/src/sys/terrain_sync.rs b/server/src/sys/terrain_sync.rs index 1992b68d99..ca8fd251a8 100644 --- a/server/src/sys/terrain_sync.rs +++ b/server/src/sys/terrain_sync.rs @@ -1,7 +1,7 @@ use crate::{client::Client, presence::Presence}; use common::{comp::Pos, terrain::TerrainGrid}; use common_ecs::{Job, Origin, Phase, System}; -use common_net::msg::{CompressedData, ServerGeneral}; +use common_net::msg::{CompressedData, SerializedTerrainChunk, ServerGeneral}; use common_state::TerrainChanges; use specs::{Join, Read, ReadExpect, ReadStorage}; @@ -38,7 +38,7 @@ impl<'a> System<'a> for Sys { lazy_msg = Some(client.prepare(ServerGeneral::TerrainChunkUpdate { key: *chunk_key, chunk: Ok(match terrain.get_key(*chunk_key) { - Some(chunk) => CompressedData::compress(&chunk, 1), + Some(chunk) => SerializedTerrainChunk::image(&chunk), None => break 'chunk, }), })); diff --git a/world/src/bin/chunk_compression_benchmarks.rs b/world/src/bin/chunk_compression_benchmarks.rs index 03541fb8d7..3c57338c5f 100644 --- a/world/src/bin/chunk_compression_benchmarks.rs +++ b/world/src/bin/chunk_compression_benchmarks.rs @@ -9,10 +9,11 @@ use common::{ }; use common_net::msg::compression::{ image_terrain, image_terrain_chonk, image_terrain_volgrid, CompressedData, GridLtrPacking, - JpegEncoding, MixedEncoding, MixedEncodingDenseSprites, PackingFormula, PngEncoding, - TallPacking, VoxelImageEncoding, + JpegEncoding, MixedEncoding, PackingFormula, PngEncoding, QuadPngEncoding, TallPacking, + VoxelImageEncoding, }; use hashbrown::HashMap; +use image::ImageBuffer; use std::{ io::{Read, Write}, sync::Arc, @@ -120,6 +121,7 @@ fn channelize_dyna( (blocks, r, g, b, sprites) } +#[derive(Debug, Clone, Copy)] pub struct MixedEncodingSparseSprites; impl VoxelImageEncoding for MixedEncodingSparseSprites { @@ -135,7 +137,6 @@ impl VoxelImageEncoding for MixedEncodingSparseSprites { ); fn create(width: u32, height: u32) -> Self::Workspace { - use image::ImageBuffer; ( ImageBuffer::new(width, height), ImageBuffer::new(width, height), @@ -161,7 +162,7 @@ impl VoxelImageEncoding for MixedEncodingSparseSprites { ws.2.insert(Vec2::new(x, y), (sprite, ori.unwrap_or(0))); } - fn finish(ws: &Self::Workspace) -> Self::Output { + fn finish(ws: &Self::Workspace) -> Option { let mut buf = Vec::new(); use image::codecs::png::{CompressionType, FilterType}; let png = image::codecs::png::PngEncoder::new_with_quality( @@ -175,11 +176,81 @@ impl VoxelImageEncoding for MixedEncodingSparseSprites { ws.0.height(), image::ColorType::L8, ) - .unwrap(); + .ok()?; let index = buf.len(); let mut jpeg = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 1); - jpeg.encode_image(&ws.1).unwrap(); - (buf, index, CompressedData::compress(&ws.2, 4)) + jpeg.encode_image(&ws.1).ok()?; + Some((buf, index, CompressedData::compress(&ws.2, 4))) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct MixedEncodingDenseSprites; + +impl VoxelImageEncoding for MixedEncodingDenseSprites { + type Output = (Vec, [usize; 3]); + type Workspace = ( + ImageBuffer, Vec>, + Vec, + Vec, + ImageBuffer, Vec>, + ); + + fn create(width: u32, height: u32) -> Self::Workspace { + ( + ImageBuffer::new(width, height), + Vec::new(), + Vec::new(), + 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::Luma([kind as u8])); + ws.3.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, + ori: Option, + ) { + ws.0.put_pixel(x, y, image::Luma([kind as u8])); + ws.1.push(sprite as u8); + ws.2.push(ori.unwrap_or(0)); + ws.3.put_pixel(x, y, image::Rgb([0; 3])); + } + + fn finish(ws: &Self::Workspace) -> Option { + let mut buf = Vec::new(); + use image::codecs::png::{CompressionType, FilterType}; + let mut indices = [0; 3]; + let mut f = |x: &ImageBuffer<_, Vec>, i| { + let png = image::codecs::png::PngEncoder::new_with_quality( + &mut buf, + CompressionType::Fast, + FilterType::Up, + ); + png.encode(&*x.as_raw(), x.width(), x.height(), image::ColorType::L8) + .ok()?; + indices[i] = buf.len(); + Some(()) + }; + f(&ws.0, 0)?; + let mut g = |x: &[u8], i| { + buf.extend_from_slice(&*CompressedData::compress(&x, 4).data); + indices[i] = buf.len(); + }; + + g(&ws.1, 1); + g(&ws.2, 2); + + let mut jpeg = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 1); + jpeg.encode_image(&ws.3).ok()?; + Some((buf, indices)) } } @@ -258,8 +329,8 @@ fn main() { )); for (sitename, sitepos) in sites.iter() { - let mut totals = [0.0; 12]; - let mut total_timings = [0.0; 9]; + let mut totals = [0.0; 13]; + let mut total_timings = [0.0; 10]; let mut count = 0; let mut volgrid = VolGrid2d::new().unwrap(); for (i, spiralpos) in Spiral2d::new() @@ -305,7 +376,8 @@ fn main() { do_deflate_flate2(&bincode::serialize(&channelize_dyna(&dyna)).unwrap()); let jpegchonkgrid_pre = Instant::now(); - let jpegchonkgrid = image_terrain_chonk(JpegEncoding, GridLtrPacking, &chunk); + let jpegchonkgrid = + image_terrain_chonk(JpegEncoding, GridLtrPacking, &chunk).unwrap(); let jpegchonkgrid_post = Instant::now(); if false { @@ -320,29 +392,42 @@ fn main() { let jpegchonktall_pre = Instant::now(); let jpegchonktall = - image_terrain_chonk(JpegEncoding, TallPacking { flip_y: false }, &chunk); + image_terrain_chonk(JpegEncoding, TallPacking { flip_y: false }, &chunk) + .unwrap(); let jpegchonktall_post = Instant::now(); let jpegchonkflip_pre = Instant::now(); let jpegchonkflip = - image_terrain_chonk(JpegEncoding, TallPacking { flip_y: true }, &chunk); + image_terrain_chonk(JpegEncoding, TallPacking { flip_y: true }, &chunk) + .unwrap(); let jpegchonkflip_post = Instant::now(); let mixedchonk_pre = Instant::now(); let mixedchonk = - image_terrain_chonk(MixedEncoding, TallPacking { flip_y: true }, &chunk); + image_terrain_chonk(MixedEncoding, TallPacking { flip_y: true }, &chunk) + .unwrap(); let mixedchonk_post = Instant::now(); let mixeddeflate = CompressedData::compress(&mixedchonk, 1); let mixeddeflate_post = Instant::now(); let mixeddense_pre = Instant::now(); - let mixeddense = - image_terrain_chonk(MixedEncodingDenseSprites, TallPacking { flip_y: true }, &chunk); + let mixeddense = image_terrain_chonk( + MixedEncodingDenseSprites, + TallPacking { flip_y: true }, + &chunk, + ) + .unwrap(); let mixeddense_post = Instant::now(); + let quadpng_pre = Instant::now(); + let quadpng = + image_terrain_chonk(QuadPngEncoding, TallPacking { flip_y: true }, &chunk) + .unwrap(); + let quadpng_post = Instant::now(); + let pngchonk_pre = Instant::now(); - let pngchonk = image_terrain_chonk(PngEncoding, GridLtrPacking, &chunk); + let pngchonk = image_terrain_chonk(PngEncoding, GridLtrPacking, &chunk).unwrap(); let pngchonk_post = Instant::now(); let n = uncompressed.len(); @@ -358,6 +443,7 @@ fn main() { mixedchonk.0.len() as f32 / n as f32, mixeddeflate.data.len() as f32 / n as f32, mixeddense.0.len() as f32 / n as f32, + quadpng.data.len() as f32 / n as f32, pngchonk.len() as f32 / n as f32, ]; let best_idx = sizes @@ -380,6 +466,7 @@ fn main() { (mixedchonk_post - mixedchonk_pre).subsec_nanos(), (mixeddeflate_post - mixedchonk_pre).subsec_nanos(), (mixeddense_post - mixeddense_pre).subsec_nanos(), + (quadpng_post - quadpng_pre).subsec_nanos(), (pngchonk_post - pngchonk_pre).subsec_nanos(), ]; trace!( @@ -408,12 +495,12 @@ fn main() { let mut f = File::create(&format!("chonkjpegs/{}_{}.jpg", sitename, count)).unwrap(); let jpeg_volgrid = - image_terrain_volgrid(JpegEncoding, GridLtrPacking, &volgrid); + image_terrain_volgrid(JpegEncoding, GridLtrPacking, &volgrid).unwrap(); f.write_all(&*jpeg_volgrid).unwrap(); let mixedgrid_pre = Instant::now(); let (mixed_volgrid, indices) = - image_terrain_volgrid(MixedEncoding, GridLtrPacking, &volgrid); + image_terrain_volgrid(MixedEncoding, GridLtrPacking, &volgrid).unwrap(); let mixedgrid_post = Instant::now(); let seconds = (mixedgrid_post - mixedgrid_pre).as_secs_f64(); println!( @@ -455,7 +542,8 @@ fn main() { println!("Average mixedchonk: {}", totals[8] / count as f32); println!("Average mixeddeflate: {}", totals[9] / count as f32); println!("Average mixeddense: {}", totals[10] / count as f32); - println!("Average pngchonk: {}", totals[11] / count as f32); + println!("Average quadpng: {}", totals[11] / count as f32); + println!("Average pngchonk: {}", totals[12] / count as f32); println!(""); println!( "Average lz4_chonk nanos : {:02}", @@ -490,9 +578,13 @@ fn main() { total_timings[7] / count as f32 ); println!( - "Average pngchonk nanos: {:02}", + "Average quadpng nanos: {:02}", total_timings[8] / count as f32 ); + println!( + "Average pngchonk nanos: {:02}", + total_timings[9] / count as f32 + ); println!("-----"); } if i % 256 == 0 {