579 lines
20 KiB
Rust
579 lines
20 KiB
Rust
//! Second Life terrain patch codec and layer packet construction.
|
|
|
|
#![allow(clippy::missing_errors_doc)]
|
|
#![allow(clippy::must_use_candidate)]
|
|
#![allow(clippy::needless_pass_by_value)]
|
|
#![allow(clippy::cast_possible_truncation)]
|
|
#![allow(clippy::cast_precision_loss)]
|
|
#![allow(clippy::cast_possible_wrap)] // Bit widths and bounded patch coordinates are range-checked.
|
|
#![allow(clippy::cast_sign_loss)] // Patch coordinates are validated before indexing.
|
|
|
|
use crate::{BitPack, Error, TerrainPatchLayerType};
|
|
|
|
const PATCH_SIZE: usize = 16;
|
|
const PATCH_VALUES: usize = PATCH_SIZE * PATCH_SIZE;
|
|
const STRIDE: i32 = 264;
|
|
const OO_SQRT2: f32 = std::f32::consts::FRAC_1_SQRT_2;
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct TerrainPatch {
|
|
pub data: Vec<f32>,
|
|
pub x: i32,
|
|
pub y: i32,
|
|
}
|
|
impl TerrainPatch {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self {
|
|
data: Vec::new(),
|
|
x: 0,
|
|
y: 0,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct TerrainPatchGroupHeader {
|
|
pub stride: i32,
|
|
pub patch_size: i32,
|
|
pub type_: TerrainPatchLayerType,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct TerrainPatchHeader {
|
|
pub dc_offset: f32,
|
|
pub range: i32,
|
|
pub quant_w_bits: i32,
|
|
pub patch_i_ds: i32,
|
|
pub word_bits: u32,
|
|
large_region: bool,
|
|
}
|
|
impl Default for TerrainPatchHeader {
|
|
fn default() -> Self {
|
|
Self {
|
|
dc_offset: 0.0,
|
|
range: 0,
|
|
quant_w_bits: 0,
|
|
patch_i_ds: 0,
|
|
word_bits: 0,
|
|
large_region: false,
|
|
}
|
|
}
|
|
}
|
|
impl TerrainPatchHeader {
|
|
pub fn set_patch_i_ds(&mut self, xx: i32, yy: i32) -> Result<i32, Error> {
|
|
let valid = if self.large_region {
|
|
(0..=65_535).contains(&xx) && (0..=65_535).contains(&yy)
|
|
} else {
|
|
(0..=31).contains(&xx) && (0..=31).contains(&yy)
|
|
};
|
|
if !valid {
|
|
return Err(Error::Argument);
|
|
}
|
|
self.patch_i_ds = if self.large_region {
|
|
(xx << 16) | (yy & 0xffff)
|
|
} else {
|
|
(xx << 5) | (yy & 0x1f)
|
|
};
|
|
Ok(self.patch_i_ds)
|
|
}
|
|
pub fn large_region(&self) -> bool {
|
|
self.large_region
|
|
}
|
|
pub fn set_large_region(&mut self, value: bool) {
|
|
self.large_region = value;
|
|
}
|
|
pub fn x(&self) -> i32 {
|
|
self.patch_i_ds >> if self.large_region { 16 } else { 5 }
|
|
}
|
|
pub fn set_x(&mut self, value: i32) {
|
|
let _ = self.set_patch_i_ds(value, self.y());
|
|
}
|
|
pub fn y(&self) -> i32 {
|
|
self.patch_i_ds & if self.large_region { 0xffff } else { 0x1f }
|
|
}
|
|
pub fn set_y(&mut self, value: i32) {
|
|
let _ = self.set_patch_i_ds(self.x(), value);
|
|
}
|
|
}
|
|
|
|
pub struct TerrainCompressor;
|
|
impl TerrainCompressor {
|
|
pub const PATCHES_PER_EDGE: i32 = 16;
|
|
pub const END_OF_PATCHES: i32 = 97;
|
|
|
|
fn copy_matrix() -> [usize; PATCH_VALUES] {
|
|
let mut result = [0; PATCH_VALUES];
|
|
let (mut diag, mut right, mut i, mut j, mut count) = (false, true, 0usize, 0usize, 0usize);
|
|
while i < PATCH_SIZE && j < PATCH_SIZE {
|
|
result[j * PATCH_SIZE + i] = count;
|
|
count += 1;
|
|
if !diag {
|
|
if right {
|
|
if i < PATCH_SIZE - 1 {
|
|
i += 1;
|
|
} else {
|
|
j += 1;
|
|
}
|
|
} else if j < PATCH_SIZE - 1 {
|
|
j += 1;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
right = !right;
|
|
diag = true;
|
|
} else if right {
|
|
i += 1;
|
|
j -= 1;
|
|
if i == PATCH_SIZE - 1 || j == 0 {
|
|
diag = false;
|
|
}
|
|
} else {
|
|
i -= 1;
|
|
j += 1;
|
|
if j == PATCH_SIZE - 1 || i == 0 {
|
|
diag = false;
|
|
}
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn cosine(u: usize, n: usize) -> f32 {
|
|
(((2 * n + 1) as f32) * (u as f32) * std::f32::consts::PI * 0.5 / PATCH_SIZE as f32).cos()
|
|
}
|
|
|
|
fn prescan(data: &[f32]) -> Result<TerrainPatchHeader, Error> {
|
|
if data.len() != PATCH_VALUES || data.iter().any(|value| !value.is_finite()) {
|
|
return Err(Error::Argument);
|
|
}
|
|
let mut min = f32::INFINITY;
|
|
let mut max = f32::NEG_INFINITY;
|
|
for value in data {
|
|
min = min.min(*value);
|
|
max = max.max(*value);
|
|
}
|
|
Ok(TerrainPatchHeader {
|
|
dc_offset: min,
|
|
range: ((max - min) + 1.0) as i32,
|
|
..TerrainPatchHeader::default()
|
|
})
|
|
}
|
|
|
|
fn compress(data: &[f32], header: &TerrainPatchHeader) -> [i32; PATCH_VALUES] {
|
|
let premult = 1024.0 / header.range.max(1) as f32;
|
|
let sub = 512.0 + header.dc_offset * premult;
|
|
let mut block = [0.0; PATCH_VALUES];
|
|
for (target, value) in block.iter_mut().zip(data) {
|
|
*target = *value * premult - sub;
|
|
}
|
|
let mut rows = [0.0; PATCH_VALUES];
|
|
for line in 0..PATCH_SIZE {
|
|
let base = line * PATCH_SIZE;
|
|
let total: f32 = block[base..base + PATCH_SIZE].iter().sum();
|
|
rows[base] = OO_SQRT2 * total;
|
|
for u in 1..PATCH_SIZE {
|
|
rows[base + u] = (0..PATCH_SIZE)
|
|
.map(|n| block[base + n] * Self::cosine(u, n))
|
|
.sum();
|
|
}
|
|
}
|
|
let copy = Self::copy_matrix();
|
|
let mut output = [0; PATCH_VALUES];
|
|
for column in 0..PATCH_SIZE {
|
|
let total: f32 = (0..PATCH_SIZE).map(|n| rows[n * PATCH_SIZE + column]).sum();
|
|
output[copy[column]] = (OO_SQRT2 * total * 0.125 / (1.0 + 2.0 * column as f32)) as i32;
|
|
for u in 1..PATCH_SIZE {
|
|
let total: f32 = (0..PATCH_SIZE)
|
|
.map(|n| rows[n * PATCH_SIZE + column] * Self::cosine(u, n))
|
|
.sum();
|
|
output[copy[u * PATCH_SIZE + column]] =
|
|
(total * 0.125 / (1.0 + 2.0 * (u + column) as f32)) as i32;
|
|
}
|
|
}
|
|
output
|
|
}
|
|
|
|
fn encode_header(
|
|
output: &mut BitPack,
|
|
mut header: TerrainPatchHeader,
|
|
patch: &[i32],
|
|
) -> Result<i32, Error> {
|
|
let base = (header.quant_w_bits & 0x0f) + 2;
|
|
let mut word_bits = base / 2;
|
|
for value in patch.iter().copied().filter(|value| *value != 0) {
|
|
let magnitude = value.unsigned_abs();
|
|
let bits = (u32::BITS - magnitude.leading_zeros()) as i32;
|
|
word_bits = word_bits.max(bits);
|
|
}
|
|
word_bits = (word_bits + 1).clamp(2, 17);
|
|
header.quant_w_bits = (header.quant_w_bits & 0xf0) | (word_bits - 2);
|
|
output.pack_bits_with_int32_int32(header.quant_w_bits, 8)?;
|
|
output.pack_float(header.dc_offset)?;
|
|
output.pack_bits_with_int32_int32(header.range, 16)?;
|
|
output.pack_bits_with_int32_int32(
|
|
header.patch_i_ds,
|
|
if header.large_region { 32 } else { 10 },
|
|
)?;
|
|
Ok(word_bits)
|
|
}
|
|
|
|
fn encode_patch_values(
|
|
output: &mut BitPack,
|
|
patch: &[i32],
|
|
word_bits: i32,
|
|
) -> Result<(), Error> {
|
|
for (index, value) in patch.iter().copied().enumerate() {
|
|
if value == 0 {
|
|
if patch[index..].iter().all(|candidate| *candidate == 0) {
|
|
output.pack_bits_with_int32_int32(2, 2)?;
|
|
return Ok(());
|
|
}
|
|
output.pack_bits_with_int32_int32(0, 1)?;
|
|
} else {
|
|
output.pack_bits_with_int32_int32(if value < 0 { 7 } else { 6 }, 3)?;
|
|
let maximum = (1_i32 << word_bits) - 1;
|
|
output.pack_bits_with_int32_int32(
|
|
i32::try_from(value.unsigned_abs())
|
|
.unwrap_or(maximum)
|
|
.min(maximum),
|
|
word_bits,
|
|
)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn encode_one(
|
|
output: &mut BitPack,
|
|
data: &[f32],
|
|
x: i32,
|
|
y: i32,
|
|
large_region: bool,
|
|
) -> Result<(), Error> {
|
|
let mut header = Self::prescan(data)?;
|
|
header.large_region = large_region;
|
|
header.quant_w_bits = 136;
|
|
header.set_patch_i_ds(x, y)?;
|
|
let patch = Self::compress(data, &header);
|
|
let word_bits = Self::encode_header(output, header, &patch)?;
|
|
Self::encode_patch_values(output, &patch, word_bits)
|
|
}
|
|
|
|
pub fn create_layer_data_packet(
|
|
patches: Vec<TerrainPatch>,
|
|
type_: TerrainPatchLayerType,
|
|
) -> Result<crate::packets::LayerDataPacket, Error> {
|
|
if patches.len() > 4096 {
|
|
return Err(Error::Argument);
|
|
}
|
|
let capacity = patches
|
|
.len()
|
|
.checked_mul(1024)
|
|
.and_then(|value| value.checked_add(32))
|
|
.ok_or(Error::Argument)?;
|
|
let mut bits = BitPack::new(vec![0; capacity], 0)?;
|
|
bits.pack_bits_with_int32_int32(STRIDE, 16)?;
|
|
bits.pack_bits_with_int32_int32(PATCH_SIZE as i32, 8)?;
|
|
bits.pack_bits_with_int32_int32(i32::from(type_.0), 8)?;
|
|
let large = matches!(type_.0, 57 | 58 | 77);
|
|
for patch in patches {
|
|
Self::encode_one(&mut bits, &patch.data, patch.x, patch.y, large)?;
|
|
}
|
|
bits.pack_bits_with_int32_int32(Self::END_OF_PATCHES, 8)?;
|
|
let used = usize::try_from(bits.byte_pos())
|
|
.map_err(|_| Error::Argument)?
|
|
.checked_add(1)
|
|
.ok_or(Error::Argument)?;
|
|
let mut packet = crate::packets::LayerDataPacket::new_with_constructor()?;
|
|
packet.layer_id.type_ = type_.0;
|
|
packet.layer_data.data = bits.data[..used.min(bits.data.len())].to_vec();
|
|
Ok(packet)
|
|
}
|
|
|
|
pub fn create_land_packet_with_single_array_int32_int32(
|
|
patch_data: Vec<f32>,
|
|
x: i32,
|
|
y: i32,
|
|
) -> Result<crate::packets::LayerDataPacket, Error> {
|
|
Self::create_layer_data_packet(
|
|
vec![TerrainPatch {
|
|
data: patch_data,
|
|
x,
|
|
y,
|
|
}],
|
|
TerrainPatchLayerType::LAND,
|
|
)
|
|
}
|
|
pub fn create_land_packet_with_single_array_int32_int32_80090bdf(
|
|
patch_data: Vec<f32>,
|
|
x: i32,
|
|
y: i32,
|
|
) -> Result<crate::packets::LayerDataPacket, Error> {
|
|
Self::create_land_packet_with_single_array_int32_int32(patch_data, x, y)
|
|
}
|
|
pub fn create_land_packet_with_single_array_int32_array(
|
|
heightmap: Vec<f32>,
|
|
patches: Vec<i32>,
|
|
) -> Result<crate::packets::LayerDataPacket, Error> {
|
|
let edge = (heightmap.len() as f64).sqrt() as usize;
|
|
if edge * edge != heightmap.len()
|
|
|| edge < PATCH_SIZE
|
|
|| !edge.is_multiple_of(PATCH_SIZE)
|
|
|| patches.len() > 4096
|
|
{
|
|
return Err(Error::Argument);
|
|
}
|
|
let per_edge = edge / PATCH_SIZE;
|
|
let mut values = Vec::with_capacity(patches.len());
|
|
for id in patches {
|
|
let id = usize::try_from(id).map_err(|_| Error::Argument)?;
|
|
let (x, y) = (id % per_edge, id / per_edge);
|
|
if y >= per_edge {
|
|
return Err(Error::Argument);
|
|
}
|
|
let mut data = Vec::with_capacity(PATCH_VALUES);
|
|
for row in 0..PATCH_SIZE {
|
|
let start = (y * PATCH_SIZE + row) * edge + x * PATCH_SIZE;
|
|
data.extend_from_slice(&heightmap[start..start + PATCH_SIZE]);
|
|
}
|
|
values.push(TerrainPatch {
|
|
data,
|
|
x: x as i32,
|
|
y: y as i32,
|
|
});
|
|
}
|
|
let layer = if edge > 256 {
|
|
TerrainPatchLayerType::LAND_EXTENDED
|
|
} else {
|
|
TerrainPatchLayerType::LAND
|
|
};
|
|
Self::create_layer_data_packet(values, layer)
|
|
}
|
|
|
|
pub fn create_patch_with_bit_pack_single_array_int32_int32(
|
|
mut output: BitPack,
|
|
patch_data: Vec<f32>,
|
|
x: i32,
|
|
y: i32,
|
|
) -> Result<(), Error> {
|
|
Self::encode_one(&mut output, &patch_data, x, y, false)
|
|
}
|
|
pub fn create_patch_with_bit_pack_single_array_int32_int32_boolean(
|
|
mut output: BitPack,
|
|
patch_data: Vec<f32>,
|
|
x: i32,
|
|
y: i32,
|
|
large_region: bool,
|
|
) -> Result<(), Error> {
|
|
Self::encode_one(&mut output, &patch_data, x, y, large_region)
|
|
}
|
|
pub fn create_patch_from_heightmap_with_bit_pack_single_array_int32_int32(
|
|
output: BitPack,
|
|
heightmap: Vec<f32>,
|
|
x: i32,
|
|
y: i32,
|
|
) -> Result<(), Error> {
|
|
Self::create_patch_from_heightmap_with_bit_pack_single_array_int32_int32_boolean(
|
|
output, heightmap, x, y, false,
|
|
)
|
|
}
|
|
pub fn create_patch_from_heightmap_with_bit_pack_single_array_int32_int32_boolean(
|
|
mut output: BitPack,
|
|
heightmap: Vec<f32>,
|
|
x: i32,
|
|
y: i32,
|
|
large_region: bool,
|
|
) -> Result<(), Error> {
|
|
if heightmap.len() != 65_536 || !(0..16).contains(&x) || !(0..16).contains(&y) {
|
|
return Err(Error::Argument);
|
|
}
|
|
let mut data = Vec::with_capacity(PATCH_VALUES);
|
|
for row in 0..PATCH_SIZE {
|
|
let start = (y as usize * PATCH_SIZE + row) * 256 + x as usize * PATCH_SIZE;
|
|
data.extend_from_slice(&heightmap[start..start + PATCH_SIZE]);
|
|
}
|
|
Self::encode_one(&mut output, &data, x, y, large_region)
|
|
}
|
|
|
|
pub fn decode_patch_header_with_bit_pack(
|
|
bitpack: BitPack,
|
|
) -> Result<TerrainPatchHeader, Error> {
|
|
Self::decode_patch_header_with_bit_pack_boolean(bitpack, false)
|
|
}
|
|
pub fn decode_patch_header_with_bit_pack_boolean(
|
|
mut bitpack: BitPack,
|
|
large_region: bool,
|
|
) -> Result<TerrainPatchHeader, Error> {
|
|
Self::decode_header(&mut bitpack, large_region)
|
|
}
|
|
pub(crate) fn decode_header(
|
|
bitpack: &mut BitPack,
|
|
large_region: bool,
|
|
) -> Result<TerrainPatchHeader, Error> {
|
|
let quant_w_bits = bitpack.unpack_bits(8)?;
|
|
if quant_w_bits == Self::END_OF_PATCHES {
|
|
return Ok(TerrainPatchHeader {
|
|
quant_w_bits,
|
|
large_region,
|
|
..TerrainPatchHeader::default()
|
|
});
|
|
}
|
|
let dc_offset = bitpack.unpack_float()?;
|
|
let range = bitpack.unpack_bits(16)?;
|
|
let patch_i_ds = bitpack.unpack_bits(if large_region { 32 } else { 10 })?;
|
|
Ok(TerrainPatchHeader {
|
|
dc_offset,
|
|
range,
|
|
quant_w_bits,
|
|
patch_i_ds,
|
|
word_bits: ((quant_w_bits & 0x0f) + 2) as u32,
|
|
large_region,
|
|
})
|
|
}
|
|
pub fn decode_patch(
|
|
mut patches: Vec<i32>,
|
|
mut bitpack: BitPack,
|
|
header: TerrainPatchHeader,
|
|
size: i32,
|
|
) -> Result<(), Error> {
|
|
Self::decode_values(&mut patches, &mut bitpack, header, size)
|
|
}
|
|
pub(crate) fn decode_values(
|
|
patches: &mut [i32],
|
|
bitpack: &mut BitPack,
|
|
header: TerrainPatchHeader,
|
|
size: i32,
|
|
) -> Result<(), Error> {
|
|
let count = usize::try_from(size.checked_mul(size).ok_or(Error::Argument)?)
|
|
.map_err(|_| Error::Argument)?;
|
|
if patches.len() < count
|
|
|| !(1..=32).contains(&size)
|
|
|| !(2..=17).contains(&header.word_bits)
|
|
{
|
|
return Err(Error::Argument);
|
|
}
|
|
for index in 0..count {
|
|
if bitpack.unpack_bits(1)? == 0 {
|
|
patches[index] = 0;
|
|
continue;
|
|
}
|
|
if bitpack.unpack_bits(1)? == 0 {
|
|
patches[index..count].fill(0);
|
|
break;
|
|
}
|
|
let negative = bitpack.unpack_bits(1)? != 0;
|
|
let value = bitpack.unpack_bits(header.word_bits as i32)?;
|
|
patches[index] = if negative { -value } else { value };
|
|
}
|
|
Ok(())
|
|
}
|
|
pub fn decompress_patch(
|
|
patches: Vec<i32>,
|
|
header: TerrainPatchHeader,
|
|
group: TerrainPatchGroupHeader,
|
|
) -> Result<Vec<f32>, Error> {
|
|
if group.patch_size != 16 || patches.len() < PATCH_VALUES {
|
|
return Err(Error::Argument);
|
|
}
|
|
let copy = Self::copy_matrix();
|
|
let mut block = [0.0; PATCH_VALUES];
|
|
for index in 0..PATCH_VALUES {
|
|
block[index] = patches[copy[index]] as f32
|
|
* (1.0 + 2.0 * ((index / PATCH_SIZE) + (index % PATCH_SIZE)) as f32);
|
|
}
|
|
let mut columns = [0.0; PATCH_VALUES];
|
|
for column in 0..PATCH_SIZE {
|
|
for n in 0..PATCH_SIZE {
|
|
columns[n * PATCH_SIZE + column] = OO_SQRT2 * block[column]
|
|
+ (1..PATCH_SIZE)
|
|
.map(|u| block[u * PATCH_SIZE + column] * Self::cosine(u, n))
|
|
.sum::<f32>();
|
|
}
|
|
}
|
|
let mut transformed = [0.0; PATCH_VALUES];
|
|
for line in 0..PATCH_SIZE {
|
|
let base = line * PATCH_SIZE;
|
|
for n in 0..PATCH_SIZE {
|
|
transformed[base + n] = (OO_SQRT2 * columns[base]
|
|
+ (1..PATCH_SIZE)
|
|
.map(|u| columns[base + u] * Self::cosine(u, n))
|
|
.sum::<f32>())
|
|
* 0.125;
|
|
}
|
|
}
|
|
let prequant = (header.quant_w_bits >> 4) + 2;
|
|
if !(1..31).contains(&prequant) {
|
|
return Err(Error::Argument);
|
|
}
|
|
let mult = header.range as f32 / (1_i32 << prequant) as f32;
|
|
let add = mult * (1_i32 << (prequant - 1)) as f32 + header.dc_offset;
|
|
Ok(transformed
|
|
.into_iter()
|
|
.map(|value| value * mult + add)
|
|
.collect())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
#[test]
|
|
fn terrain_codec_round_trip_stays_within_quantization_error() {
|
|
let source: Vec<f32> = (0..PATCH_VALUES)
|
|
.map(|index| 20.0 + index as f32 * 0.03125)
|
|
.collect();
|
|
let packet = TerrainCompressor::create_land_packet_with_single_array_int32_int32(
|
|
source.clone(),
|
|
3,
|
|
7,
|
|
)
|
|
.unwrap();
|
|
let mut bits = BitPack::new(packet.layer_data.data, 0).unwrap();
|
|
assert_eq!(bits.unpack_bits(16).unwrap(), STRIDE);
|
|
let size = bits.unpack_bits(8).unwrap();
|
|
assert_eq!(
|
|
bits.unpack_bits(8).unwrap(),
|
|
i32::from(TerrainPatchLayerType::LAND.0)
|
|
);
|
|
let header = TerrainCompressor::decode_header(&mut bits, false).unwrap();
|
|
let mut coefficients = vec![0; PATCH_VALUES];
|
|
TerrainCompressor::decode_values(&mut coefficients, &mut bits, header, size).unwrap();
|
|
let decoded = TerrainCompressor::decompress_patch(
|
|
coefficients,
|
|
header,
|
|
TerrainPatchGroupHeader {
|
|
stride: STRIDE,
|
|
patch_size: size,
|
|
type_: TerrainPatchLayerType::LAND,
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!((header.x(), header.y()), (3, 7));
|
|
assert!(
|
|
source
|
|
.iter()
|
|
.zip(decoded)
|
|
.all(|(expected, actual)| (expected - actual).abs() < 0.2)
|
|
);
|
|
}
|
|
#[test]
|
|
fn terrain_payloads_are_bounded_and_coordinates_validated() {
|
|
assert!(
|
|
TerrainCompressor::create_land_packet_with_single_array_int32_int32(
|
|
vec![0.0; 255],
|
|
0,
|
|
0
|
|
)
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
TerrainCompressor::create_land_packet_with_single_array_int32_int32(
|
|
vec![0.0; 256],
|
|
32,
|
|
0
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|