Improve quadpng by adding WidePacking, which makes a wider image, which is faster due to PNG compressing by row. Heuristically switch between quadpng and deflate based on chunk height to reduce variance.

This commit is contained in:
Avi Weinstock 2021-04-26 23:52:10 -04:00
parent dffc7db8f5
commit cdc2eccda8
7 changed files with 79 additions and 45 deletions

View File

@ -89,9 +89,11 @@ pub struct TallPacking {
} }
impl PackingFormula for TallPacking { impl PackingFormula for TallPacking {
#[inline(always)]
fn dimensions(&self, dims: Vec3<u32>) -> (u32, u32) { (dims.x, dims.y * dims.z) } fn dimensions(&self, dims: Vec3<u32>) -> (u32, u32) { (dims.x, dims.y * dims.z) }
#[allow(clippy::many_single_char_names)] #[allow(clippy::many_single_char_names)]
#[inline(always)]
fn index(&self, dims: Vec3<u32>, x: u32, y: u32, z: u32) -> (u32, u32) { fn index(&self, dims: Vec3<u32>, x: u32, y: u32, z: u32) -> (u32, u32) {
let i = x; let i = x;
let j0 = if self.flip_y { let j0 = if self.flip_y {
@ -104,6 +106,27 @@ impl PackingFormula for TallPacking {
} }
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct WidePacking<const FLIP_X: bool>();
impl<const FLIP_X: bool> PackingFormula for WidePacking<FLIP_X> {
#[inline(always)]
fn dimensions(&self, dims: Vec3<u32>) -> (u32, u32) { (dims.x * dims.z, dims.y) }
#[allow(clippy::many_single_char_names)]
#[inline(always)]
fn index(&self, dims: Vec3<u32>, x: u32, y: u32, z: u32) -> (u32, u32) {
let i0 = if FLIP_X {
if z % 2 == 0 { x } else { dims.x - x - 1 }
} else {
x
};
let i = z * dims.x + i0;
let j = y;
(i, j)
}
}
/// A grid of the z levels, left to right, top to bottom, like English prose. /// 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 /// Convenient for visualizing terrain, but wastes space if the number of z
/// levels isn't a perfect square. /// levels isn't a perfect square.
@ -111,12 +134,14 @@ impl PackingFormula for TallPacking {
pub struct GridLtrPacking; pub struct GridLtrPacking;
impl PackingFormula for GridLtrPacking { impl PackingFormula for GridLtrPacking {
#[inline(always)]
fn dimensions(&self, dims: Vec3<u32>) -> (u32, u32) { fn dimensions(&self, dims: Vec3<u32>) -> (u32, u32) {
let rootz = (dims.z as f64).sqrt().ceil() as u32; let rootz = (dims.z as f64).sqrt().ceil() as u32;
(dims.x * rootz, dims.y * rootz) (dims.x * rootz, dims.y * rootz)
} }
#[allow(clippy::many_single_char_names)] #[allow(clippy::many_single_char_names)]
#[inline(always)]
fn index(&self, dims: Vec3<u32>, x: u32, y: u32, z: u32) -> (u32, u32) { fn index(&self, dims: Vec3<u32>, x: u32, y: u32, z: u32) -> (u32, u32) {
let rootz = (dims.z as f64).sqrt().ceil() as u32; let rootz = (dims.z as f64).sqrt().ceil() as u32;
let i = x + (z % rootz) * dims.x; let i = x + (z % rootz) * dims.x;
@ -371,13 +396,15 @@ impl<const N: u32> VoxelImageEncoding for QuadPngEncoding<N> {
) )
} }
#[inline(always)]
fn put_solid(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, rgb: Rgb<u8>) { fn put_solid(ws: &mut Self::Workspace, x: u32, y: u32, kind: BlockKind, rgb: Rgb<u8>) {
ws.0.put_pixel(x, y, image::Luma([kind as u8])); ws.0.put_pixel(x, y, image::Luma([kind as u8]));
ws.1.put_pixel(x, y, image::Luma([0])); //ws.1.put_pixel(x, y, image::Luma([0]));
ws.2.put_pixel(x, y, image::Luma([0])); //ws.2.put_pixel(x, y, image::Luma([0]));
ws.3.put_pixel(x / N, y / N, image::Rgb([rgb.r, rgb.g, rgb.b])); ws.3.put_pixel(x / N, y / N, image::Rgb([rgb.r, rgb.g, rgb.b]));
} }
#[inline(always)]
fn put_sprite( fn put_sprite(
ws: &mut Self::Workspace, ws: &mut Self::Workspace,
x: u32, x: u32,
@ -414,7 +441,7 @@ impl<const N: u32> VoxelImageEncoding for QuadPngEncoding<N> {
let png = image::codecs::png::PngEncoder::new_with_quality( let png = image::codecs::png::PngEncoder::new_with_quality(
&mut buf, &mut buf,
CompressionType::Rle, CompressionType::Rle,
FilterType::Paeth, FilterType::Sub,
); );
png.encode( png.encode(
&*ws.3.as_raw(), &*ws.3.as_raw(),

View File

@ -9,7 +9,7 @@ pub use self::{
client::{ClientGeneral, ClientMsg, ClientRegister, ClientType}, client::{ClientGeneral, ClientMsg, ClientRegister, ClientType},
compression::{ compression::{
CompressedData, GridLtrPacking, JpegEncoding, MixedEncoding, PackingFormula, PngEncoding, CompressedData, GridLtrPacking, JpegEncoding, MixedEncoding, PackingFormula, PngEncoding,
QuadPngEncoding, TallPacking, TriPngEncoding, VoxelImageEncoding, WireChonk, QuadPngEncoding, TallPacking, TriPngEncoding, VoxelImageEncoding, WidePacking, WireChonk,
}, },
ecs_packet::EcsCompPacket, ecs_packet::EcsCompPacket,
server::{ server::{

View File

@ -1,6 +1,6 @@
use super::{ use super::{
world_msg::EconomyInfo, ClientType, CompressedData, EcsCompPacket, MixedEncoding, PingMsg, world_msg::EconomyInfo, ClientType, CompressedData, EcsCompPacket, PingMsg, QuadPngEncoding,
QuadPngEncoding, TallPacking, TriPngEncoding, WireChonk, TriPngEncoding, WidePacking, WireChonk,
}; };
use crate::sync; use crate::sync;
use common::{ use common::{
@ -69,39 +69,25 @@ pub type ServerRegisterAnswer = Result<(), RegisterError>;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SerializedTerrainChunk { pub enum SerializedTerrainChunk {
DeflatedChonk(CompressedData<TerrainChunk>), DeflatedChonk(CompressedData<TerrainChunk>),
PngPngPngJpeg(WireChonk<MixedEncoding, TallPacking, TerrainChunkMeta, TerrainChunkSize>), QuadPng(WireChonk<QuadPngEncoding<4>, WidePacking<true>, TerrainChunkMeta, TerrainChunkSize>),
QuadPng(WireChonk<QuadPngEncoding<4>, TallPacking, TerrainChunkMeta, TerrainChunkSize>), TriPng(WireChonk<TriPngEncoding, WidePacking<true>, TerrainChunkMeta, TerrainChunkSize>),
TriPng(WireChonk<TriPngEncoding, TallPacking, TerrainChunkMeta, TerrainChunkSize>),
} }
impl SerializedTerrainChunk { impl SerializedTerrainChunk {
pub fn image(chunk: &TerrainChunk) -> Self { pub fn via_heuristic(chunk: &TerrainChunk) -> Self {
match 2 { if chunk.get_max_z() - chunk.get_min_z() < 128 {
0 => Self::deflate(chunk), Self::quadpng(chunk)
1 => Self::jpeg(chunk),
2 => Self::quadpng(chunk),
_ => Self::tripng(chunk),
}
}
pub fn deflate(chunk: &TerrainChunk) -> Self {
Self::DeflatedChonk(CompressedData::compress(chunk, 5))
}
pub fn jpeg(chunk: &TerrainChunk) -> Self {
if let Some(wc) = WireChonk::from_chonk(MixedEncoding, TallPacking { flip_y: true }, chunk)
{
Self::PngPngPngJpeg(wc)
} else { } else {
warn!("Image encoding failure occurred, falling back to deflate");
Self::deflate(chunk) Self::deflate(chunk)
} }
} }
pub fn deflate(chunk: &TerrainChunk) -> Self {
Self::DeflatedChonk(CompressedData::compress(chunk, 1))
}
pub fn quadpng(chunk: &TerrainChunk) -> Self { pub fn quadpng(chunk: &TerrainChunk) -> Self {
if let Some(wc) = if let Some(wc) = WireChonk::from_chonk(QuadPngEncoding(), WidePacking(), chunk) {
WireChonk::from_chonk(QuadPngEncoding(), TallPacking { flip_y: true }, chunk)
{
Self::QuadPng(wc) Self::QuadPng(wc)
} else { } else {
warn!("Image encoding failure occurred, falling back to deflate"); warn!("Image encoding failure occurred, falling back to deflate");
@ -110,8 +96,7 @@ impl SerializedTerrainChunk {
} }
pub fn tripng(chunk: &TerrainChunk) -> Self { pub fn tripng(chunk: &TerrainChunk) -> Self {
if let Some(wc) = WireChonk::from_chonk(TriPngEncoding, TallPacking { flip_y: true }, chunk) if let Some(wc) = WireChonk::from_chonk(TriPngEncoding, WidePacking(), chunk) {
{
Self::TriPng(wc) Self::TriPng(wc)
} else { } else {
warn!("Image encoding failure occurred, falling back to deflate"); warn!("Image encoding failure occurred, falling back to deflate");
@ -122,7 +107,6 @@ impl SerializedTerrainChunk {
pub fn to_chunk(&self) -> Option<TerrainChunk> { pub fn to_chunk(&self) -> Option<TerrainChunk> {
match self { match self {
Self::DeflatedChonk(chonk) => chonk.decompress(), Self::DeflatedChonk(chonk) => chonk.decompress(),
Self::PngPngPngJpeg(wc) => wc.to_chonk(),
Self::QuadPng(wc) => wc.to_chonk(), Self::QuadPng(wc) => wc.to_chonk(),
Self::TriPng(wc) => wc.to_chonk(), Self::TriPng(wc) => wc.to_chonk(),
} }

View File

@ -79,7 +79,9 @@ impl<'a> System<'a> for Sys {
network_metrics.chunks_served_from_memory.inc(); network_metrics.chunks_served_from_memory.inc();
client.send(ServerGeneral::TerrainChunkUpdate { client.send(ServerGeneral::TerrainChunkUpdate {
key, key,
chunk: Ok(SerializedTerrainChunk::image(&chunk)), chunk: Ok(SerializedTerrainChunk::via_heuristic(
&chunk,
)),
})? })?
}, },
None => { None => {

View File

@ -224,7 +224,7 @@ impl<'a> System<'a> for Sys {
new_chunks.into_par_iter().for_each(|(key, chunk)| { new_chunks.into_par_iter().for_each(|(key, chunk)| {
let mut msg = Some(ServerGeneral::TerrainChunkUpdate { let mut msg = Some(ServerGeneral::TerrainChunkUpdate {
key, key,
chunk: Ok(SerializedTerrainChunk::image(&*chunk)), chunk: Ok(SerializedTerrainChunk::via_heuristic(&*chunk)),
}); });
let mut lazy_msg = None; let mut lazy_msg = None;

View File

@ -38,7 +38,7 @@ impl<'a> System<'a> for Sys {
lazy_msg = Some(client.prepare(ServerGeneral::TerrainChunkUpdate { lazy_msg = Some(client.prepare(ServerGeneral::TerrainChunkUpdate {
key: *chunk_key, key: *chunk_key,
chunk: Ok(match terrain.get_key(*chunk_key) { chunk: Ok(match terrain.get_key(*chunk_key) {
Some(chunk) => SerializedTerrainChunk::image(&chunk), Some(chunk) => SerializedTerrainChunk::via_heuristic(&chunk),
None => break 'chunk, None => break 'chunk,
}), }),
})); }));

View File

@ -10,6 +10,7 @@ use common::{
use common_net::msg::compression::{ use common_net::msg::compression::{
image_terrain_chonk, image_terrain_volgrid, CompressedData, GridLtrPacking, JpegEncoding, image_terrain_chonk, image_terrain_volgrid, CompressedData, GridLtrPacking, JpegEncoding,
MixedEncoding, PngEncoding, QuadPngEncoding, TallPacking, TriPngEncoding, VoxelImageEncoding, MixedEncoding, PngEncoding, QuadPngEncoding, TallPacking, TriPngEncoding, VoxelImageEncoding,
WidePacking,
}; };
use hashbrown::HashMap; use hashbrown::HashMap;
use image::ImageBuffer; use image::ImageBuffer;
@ -417,7 +418,7 @@ fn main() {
bucket.0 += 1; bucket.0 += 1;
bucket.1 += (lz4chonk_post - lz4chonk_pre).subsec_nanos() as f32; bucket.1 += (lz4chonk_post - lz4chonk_pre).subsec_nanos() as f32;
} }
{ if false {
let bucket = z_buckets let bucket = z_buckets
.entry("rle") .entry("rle")
.or_default() .or_default()
@ -426,7 +427,7 @@ fn main() {
bucket.0 += 1; bucket.0 += 1;
bucket.1 += (rlechonk_post - rlechonk_pre).subsec_nanos() as f32; bucket.1 += (rlechonk_post - rlechonk_pre).subsec_nanos() as f32;
} }
{ if false {
let bucket = z_buckets let bucket = z_buckets
.entry("deflate0") .entry("deflate0")
.or_default() .or_default()
@ -604,24 +605,32 @@ fn main() {
.unwrap(); .unwrap();
let quadpnghalf_post = Instant::now(); let quadpnghalf_post = Instant::now();
let quadpngquart_pre = Instant::now(); let quadpngquarttall_pre = Instant::now();
let quadpngquart = image_terrain_chonk( let quadpngquarttall = image_terrain_chonk(
QuadPngEncoding::<4>(), QuadPngEncoding::<4>(),
TallPacking { flip_y: true }, TallPacking { flip_y: true },
&chunk, &chunk,
) )
.unwrap(); .unwrap();
let quadpngquart_post = Instant::now(); let quadpngquarttall_post = Instant::now();
let quadpngquartwide_pre = Instant::now();
let quadpngquartwide =
image_terrain_chonk(QuadPngEncoding::<4>(), WidePacking::<true>(), &chunk)
.unwrap();
let quadpngquartwide_post = Instant::now();
let tripng_pre = Instant::now(); let tripng_pre = Instant::now();
let tripng = let tripng =
image_terrain_chonk(TriPngEncoding, TallPacking { flip_y: true }, &chunk) image_terrain_chonk(TriPngEncoding, TallPacking { flip_y: true }, &chunk)
.unwrap(); .unwrap();
let tripng_post = Instant::now(); let tripng_post = Instant::now();
#[rustfmt::skip]
sizes.extend_from_slice(&[ sizes.extend_from_slice(&[
("quadpngfull", quadpngfull.data.len() as f32 / n as f32), ("quadpngfull", quadpngfull.data.len() as f32 / n as f32),
("quadpnghalf", quadpnghalf.data.len() as f32 / n as f32), ("quadpnghalf", quadpnghalf.data.len() as f32 / n as f32),
("quadpngquart", quadpngquart.data.len() as f32 / n as f32), ("quadpngquarttall", quadpngquarttall.data.len() as f32 / n as f32),
("quadpngquartwide", quadpngquartwide.data.len() as f32 / n as f32),
("tripng", tripng.data.len() as f32 / n as f32), ("tripng", tripng.data.len() as f32 / n as f32),
]); ]);
let best_idx = sizes let best_idx = sizes
@ -639,19 +648,31 @@ fn main() {
timings.extend_from_slice(&[ timings.extend_from_slice(&[
("quadpngfull", (quadpngfull_post - quadpngfull_pre).subsec_nanos()), ("quadpngfull", (quadpngfull_post - quadpngfull_pre).subsec_nanos()),
("quadpnghalf", (quadpnghalf_post - quadpnghalf_pre).subsec_nanos()), ("quadpnghalf", (quadpnghalf_post - quadpnghalf_pre).subsec_nanos()),
("quadpngquart", (quadpngquart_post - quadpngquart_pre).subsec_nanos()), ("quadpngquarttall", (quadpngquarttall_post - quadpngquarttall_pre).subsec_nanos()),
("quadpngquartwide", (quadpngquartwide_post - quadpngquartwide_pre).subsec_nanos()),
("tripng", (tripng_post - tripng_pre).subsec_nanos()), ("tripng", (tripng_post - tripng_pre).subsec_nanos()),
]); ]);
{ {
let bucket = z_buckets let bucket = z_buckets
.entry("quadpngquart") .entry("quadpngquarttall")
.or_default() .or_default()
.entry(chunk.get_max_z() - chunk.get_min_z()) .entry(chunk.get_max_z() - chunk.get_min_z())
.or_insert((0, 0.0)); .or_insert((0, 0.0));
bucket.0 += 1; bucket.0 += 1;
bucket.1 += (quadpngquart_post - quadpngquart_pre).subsec_nanos() as f32; bucket.1 +=
(quadpngquarttall_post - quadpngquarttall_pre).subsec_nanos() as f32;
} }
{ {
let bucket = z_buckets
.entry("quadpngquartwide")
.or_default()
.entry(chunk.get_max_z() - chunk.get_min_z())
.or_insert((0, 0.0));
bucket.0 += 1;
bucket.1 +=
(quadpngquartwide_post - quadpngquartwide_pre).subsec_nanos() as f32;
}
if false {
let bucket = z_buckets let bucket = z_buckets
.entry("tripng") .entry("tripng")
.or_default() .or_default()