use crc32fast::Hasher;
use crush_core::error::{CrushError, Result};
pub const CGPU_MAGIC: [u8; 4] = [0x43, 0x47, 0x50, 0x55];
pub const FORMAT_VERSION: u32 = 2;
pub const DEFAULT_TILE_SIZE: u32 = 65536;
pub const DEFAULT_SUB_STREAM_COUNT: u8 = 32;
pub const TILE_ALIGNMENT: usize = 128;
pub const ENGINE_VERSION_STR: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EngineVersion {
pub major: u16,
pub minor: u16,
pub patch: u16,
pub pre: u8,
pub build: u8,
}
impl EngineVersion {
#[must_use]
pub fn current() -> Self {
let v = ENGINE_VERSION_STR;
let mut parts = v.split('.');
let major = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let minor = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let patch = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
Self {
major,
minor,
patch,
pre: 0,
build: 0,
}
}
#[must_use]
pub fn to_bytes(self) -> [u8; 8] {
let mut b = [0u8; 8];
b[0..2].copy_from_slice(&self.major.to_le_bytes());
b[2..4].copy_from_slice(&self.minor.to_le_bytes());
b[4..6].copy_from_slice(&self.patch.to_le_bytes());
b[6] = self.pre;
b[7] = self.build;
b
}
#[must_use]
pub fn from_bytes(b: &[u8; 8]) -> Self {
Self {
major: u16::from_le_bytes([b[0], b[1]]),
minor: u16::from_le_bytes([b[2], b[3]]),
patch: u16::from_le_bytes([b[4], b[5]]),
pre: b[6],
build: b[7],
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct GpuFileFlags(pub u8);
impl GpuFileFlags {
pub const CHECKSUMS_ENABLED: u8 = 0b0000_0001;
pub const VECTORIZE_USED: u8 = 0b0000_0010;
pub const ENTROPY_CHECKED: u8 = 0b0000_0100;
#[must_use]
pub fn checksums_enabled(self) -> bool {
self.0 & Self::CHECKSUMS_ENABLED != 0
}
#[must_use]
pub fn vectorize_used(self) -> bool {
self.0 & Self::VECTORIZE_USED != 0
}
#[must_use]
pub fn entropy_checked(self) -> bool {
self.0 & Self::ENTROPY_CHECKED != 0
}
#[must_use]
pub fn with_checksums(mut self) -> Self {
self.0 |= Self::CHECKSUMS_ENABLED;
self
}
#[must_use]
pub fn with_vectorize(mut self) -> Self {
self.0 |= Self::VECTORIZE_USED;
self
}
#[must_use]
pub fn with_entropy_checked(mut self) -> Self {
self.0 |= Self::ENTROPY_CHECKED;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GpuFileHeader {
pub magic: [u8; 4],
pub format_version: u32,
pub engine_version: EngineVersion,
pub tile_size: u32,
pub sub_stream_count: u8,
pub flags: GpuFileFlags,
pub uncompressed_size: u64,
pub tile_count: u64,
}
impl GpuFileHeader {
pub const SIZE: usize = 64;
#[must_use]
pub fn new(tile_count: u64, uncompressed_size: u64) -> Self {
Self {
magic: CGPU_MAGIC,
format_version: FORMAT_VERSION,
engine_version: EngineVersion::current(),
tile_size: DEFAULT_TILE_SIZE,
sub_stream_count: DEFAULT_SUB_STREAM_COUNT,
flags: GpuFileFlags::default()
.with_checksums()
.with_entropy_checked(),
uncompressed_size,
tile_count,
}
}
#[must_use]
pub fn to_bytes(&self) -> [u8; Self::SIZE] {
let mut b = [0u8; Self::SIZE];
b[0..4].copy_from_slice(&self.magic);
b[4..8].copy_from_slice(&self.format_version.to_le_bytes());
b[8..16].copy_from_slice(&self.engine_version.to_bytes());
b[16..20].copy_from_slice(&self.tile_size.to_le_bytes());
b[20] = self.sub_stream_count;
b[21] = self.flags.0;
b[24..32].copy_from_slice(&self.uncompressed_size.to_le_bytes());
b[32..40].copy_from_slice(&self.tile_count.to_le_bytes());
b
}
pub fn from_bytes(b: &[u8; Self::SIZE]) -> Result<Self> {
let magic = [b[0], b[1], b[2], b[3]];
if magic != CGPU_MAGIC {
return Err(CrushError::InvalidFormat(format!(
"expected CGPU magic {CGPU_MAGIC:?}, got {magic:?}"
)));
}
let format_version = u32::from_le_bytes([b[4], b[5], b[6], b[7]]);
if format_version != FORMAT_VERSION {
let ev = EngineVersion::from_bytes(
b[8..16]
.try_into()
.map_err(|_| CrushError::InvalidFormat("header too short".to_owned()))?,
);
return Err(CrushError::VersionMismatch {
file_version: format!(
"format v{format_version} (engine {}.{}.{})",
ev.major, ev.minor, ev.patch
),
current_version: format!("format v{FORMAT_VERSION} (engine {ENGINE_VERSION_STR})"),
});
}
let engine_version = EngineVersion::from_bytes(
b[8..16]
.try_into()
.map_err(|_| CrushError::InvalidFormat("header too short".to_owned()))?,
);
Ok(Self {
magic,
format_version,
engine_version,
tile_size: u32::from_le_bytes([b[16], b[17], b[18], b[19]]),
sub_stream_count: b[20],
flags: GpuFileFlags(b[21]),
uncompressed_size: u64::from_le_bytes(
b[24..32]
.try_into()
.map_err(|_| CrushError::InvalidFormat("header truncated".to_owned()))?,
),
tile_count: u64::from_le_bytes(
b[32..40]
.try_into()
.map_err(|_| CrushError::InvalidFormat("header truncated".to_owned()))?,
),
})
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TileFlags(pub u8);
impl TileFlags {
pub const STORED: u8 = 0b0000_0001;
pub const LAST_TILE: u8 = 0b0000_0010;
#[must_use]
pub fn stored(self) -> bool {
self.0 & Self::STORED != 0
}
#[must_use]
pub fn last_tile(self) -> bool {
self.0 & Self::LAST_TILE != 0
}
#[must_use]
pub fn with_stored(mut self) -> Self {
self.0 |= Self::STORED;
self
}
#[must_use]
pub fn with_last_tile(mut self) -> Self {
self.0 |= Self::LAST_TILE;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileHeader {
pub version: u8,
pub flags: TileFlags,
pub sub_stream_count: u8,
pub compressed_size: u32,
pub uncompressed_size: u32,
pub checksum: u32,
pub sub_stream_offsets_size: u32,
}
impl TileHeader {
pub const SIZE: usize = 32;
#[must_use]
pub fn to_bytes(self) -> [u8; Self::SIZE] {
let mut b = [0u8; Self::SIZE];
b[0] = self.version;
b[1] = self.flags.0;
b[2] = self.sub_stream_count;
b[4..8].copy_from_slice(&self.compressed_size.to_le_bytes());
b[8..12].copy_from_slice(&self.uncompressed_size.to_le_bytes());
b[12..16].copy_from_slice(&self.checksum.to_le_bytes());
b[16..20].copy_from_slice(&self.sub_stream_offsets_size.to_le_bytes());
b
}
#[must_use]
pub fn from_bytes(b: &[u8; Self::SIZE]) -> Self {
Self {
version: b[0],
flags: TileFlags(b[1]),
sub_stream_count: b[2],
compressed_size: u32::from_le_bytes([b[4], b[5], b[6], b[7]]),
uncompressed_size: u32::from_le_bytes([b[8], b[9], b[10], b[11]]),
checksum: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
sub_stream_offsets_size: u32::from_le_bytes([b[16], b[17], b[18], b[19]]),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileIndexEntry {
pub tile_offset: u64,
pub compressed_size: u32,
pub uncompressed_size: u32,
pub checksum: u32,
pub flags: u32,
}
impl TileIndexEntry {
pub const SIZE: usize = 24;
#[must_use]
pub fn to_bytes(self) -> [u8; Self::SIZE] {
let mut b = [0u8; Self::SIZE];
b[0..8].copy_from_slice(&self.tile_offset.to_le_bytes());
b[8..12].copy_from_slice(&self.compressed_size.to_le_bytes());
b[12..16].copy_from_slice(&self.uncompressed_size.to_le_bytes());
b[16..20].copy_from_slice(&self.checksum.to_le_bytes());
b[20..24].copy_from_slice(&self.flags.to_le_bytes());
b
}
#[must_use]
pub fn from_bytes(b: &[u8; Self::SIZE]) -> Self {
Self {
tile_offset: u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]),
compressed_size: u32::from_le_bytes([b[8], b[9], b[10], b[11]]),
uncompressed_size: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
checksum: u32::from_le_bytes([b[16], b[17], b[18], b[19]]),
flags: u32::from_le_bytes([b[20], b[21], b[22], b[23]]),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileIndexHeader {
pub entry_count: u32,
pub index_flags: u32,
}
impl TileIndexHeader {
pub const SIZE: usize = 8;
#[must_use]
pub fn to_bytes(self) -> [u8; Self::SIZE] {
let mut b = [0u8; Self::SIZE];
b[0..4].copy_from_slice(&self.entry_count.to_le_bytes());
b[4..8].copy_from_slice(&self.index_flags.to_le_bytes());
b
}
#[must_use]
pub fn from_bytes(b: &[u8; Self::SIZE]) -> Self {
Self {
entry_count: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
index_flags: u32::from_le_bytes([b[4], b[5], b[6], b[7]]),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GpuFileFooter {
pub index_offset: u64,
pub index_size: u32,
pub footer_checksum: u32,
pub format_version: u32,
pub magic: [u8; 4],
}
impl GpuFileFooter {
pub const SIZE: usize = 24;
#[must_use]
pub fn new(index_offset: u64, index_size: u32) -> Self {
let mut f = Self {
index_offset,
index_size,
footer_checksum: 0,
format_version: FORMAT_VERSION,
magic: CGPU_MAGIC,
};
f.footer_checksum = f.compute_checksum();
f
}
fn compute_checksum(self) -> u32 {
let b = self.to_bytes_unchecked();
let mut h = Hasher::new();
h.update(&b[0..12]);
h.finalize()
}
fn to_bytes_unchecked(self) -> [u8; Self::SIZE] {
let mut b = [0u8; Self::SIZE];
b[0..8].copy_from_slice(&self.index_offset.to_le_bytes());
b[8..12].copy_from_slice(&self.index_size.to_le_bytes());
b[12..16].copy_from_slice(&self.footer_checksum.to_le_bytes());
b[16..20].copy_from_slice(&self.format_version.to_le_bytes());
b[20..24].copy_from_slice(&self.magic);
b
}
#[must_use]
pub fn to_bytes(self) -> [u8; Self::SIZE] {
self.to_bytes_unchecked()
}
pub fn from_bytes(b: &[u8; Self::SIZE]) -> Result<Self> {
let magic = [b[20], b[21], b[22], b[23]];
if magic != CGPU_MAGIC {
return Err(CrushError::InvalidFormat(format!(
"footer magic {magic:?} does not match CGPU"
)));
}
let format_version = u32::from_le_bytes([b[16], b[17], b[18], b[19]]);
if format_version != FORMAT_VERSION {
return Err(CrushError::VersionMismatch {
file_version: format!("format v{format_version}"),
current_version: format!("format v{FORMAT_VERSION} (engine {ENGINE_VERSION_STR})"),
});
}
let footer = Self {
index_offset: u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]),
index_size: u32::from_le_bytes([b[8], b[9], b[10], b[11]]),
footer_checksum: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
format_version,
magic,
};
let expected = {
let mut h = Hasher::new();
h.update(&b[0..12]);
h.finalize()
};
if footer.footer_checksum != expected {
return Err(CrushError::IndexCorrupted(format!(
"GPU footer checksum mismatch: expected {expected:#010x}, got {:#010x}",
footer.footer_checksum
)));
}
Ok(footer)
}
}
#[must_use]
pub fn padding_to_alignment(size: usize) -> usize {
let remainder = size % TILE_ALIGNMENT;
if remainder == 0 {
0
} else {
TILE_ALIGNMENT - remainder
}
}