pub(crate) mod compression;
pub mod config;
pub(crate) mod deduplication;
pub(crate) mod geometry;
pub(crate) mod tile_processor;
pub use config::{BcFormat, SourceTexture, TileCompressionPreference, TileSetConfiguration};
use crate::error::{Error, Result};
use crate::virtual_texture::types::{GtsCodec, GtsFlatTileInfo, VTexPhase, VTexProgress};
use crate::virtual_texture::writer::{
fourcc::build_metadata_tree,
gtp_writer::{Chunk, GtpWriter},
gts_writer::{
GtsWriter, LayerInfo, LevelInfo as GtsLevelInfo, PageFileInfo, create_bc_parameter_block,
},
};
use rayon::prelude::*;
use std::fs::File;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use uuid::Uuid;
use self::compression::{CompressedTile, compress_tile};
use self::deduplication::build_dedup_map;
use self::geometry::calculate_geometry;
use self::tile_processor::{DdsTexture, ProcessedTile, extract_tiles_from_dds};
#[derive(Debug)]
pub struct BuildResult {
pub gts_path: PathBuf,
pub gtp_paths: Vec<PathBuf>,
pub tile_count: usize,
pub unique_tile_count: usize,
pub total_size_bytes: u64,
}
pub struct VirtualTextureBuilder {
config: TileSetConfiguration,
textures: Vec<SourceTexture>,
guid: [u8; 16],
name: Option<String>,
}
impl Default for VirtualTextureBuilder {
fn default() -> Self {
Self::new()
}
}
impl VirtualTextureBuilder {
#[must_use]
pub fn new() -> Self {
let uuid = Uuid::new_v4();
Self {
config: TileSetConfiguration::default(),
textures: Vec::new(),
guid: *uuid.as_bytes(),
name: None,
}
}
#[must_use]
pub fn with_config(config: TileSetConfiguration) -> Self {
let uuid = Uuid::new_v4();
Self {
config,
textures: Vec::new(),
guid: *uuid.as_bytes(),
name: None,
}
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn add_texture(mut self, texture: SourceTexture) -> Self {
self.textures.push(texture);
self
}
#[must_use]
pub fn compression(mut self, compression: TileCompressionPreference) -> Self {
self.config.compression = compression;
self
}
#[must_use]
pub fn tile_size(mut self, width: u32, height: u32) -> Self {
self.config.tile_width = width;
self.config.tile_height = height;
self
}
#[must_use]
pub fn embed_mip(mut self, enable: bool) -> Self {
self.config.embed_mip = enable;
self
}
#[must_use]
pub fn deduplicate(mut self, enable: bool) -> Self {
self.config.deduplicate = enable;
self
}
pub fn build<P: AsRef<Path>>(self, output_dir: P) -> Result<BuildResult> {
self.build_with_progress(output_dir, |_| {})
}
pub fn build_with_progress<P, F>(self, output_dir: P, progress: F) -> Result<BuildResult>
where
P: AsRef<Path>,
F: Fn(&VTexProgress) + Send + Sync,
{
let output_dir = output_dir.as_ref();
progress(&VTexProgress::new(VTexPhase::Validating, 1, 1));
self.validate()?;
let name = self
.name
.as_ref()
.or_else(|| self.textures.first().map(|t| &t.name))
.cloned()
.unwrap_or_else(|| "VirtualTexture".to_string());
std::fs::create_dir_all(output_dir)?;
let texture = &self.textures[0];
let layers_present = [
texture.base_map.is_some(),
texture.normal_map.is_some(),
texture.physical_map.is_some(),
];
progress(&VTexProgress::new(VTexPhase::CalculatingGeometry, 1, 1));
let (first_dds, _first_layer_idx) = self.load_first_layer(texture)?;
let tex_info = (texture.name.clone(), first_dds.width, first_dds.height);
let geometry = calculate_geometry(
&[tex_info],
layers_present,
&self.config,
Some(first_dds.mip_count),
);
progress(&VTexProgress::new(VTexPhase::LoadingTiles, 0, 3));
let estimated_tiles: usize = geometry
.tiles_per_layer
.iter()
.map(std::vec::Vec::len)
.sum();
let mut all_tiles: Vec<ProcessedTile> = Vec::with_capacity(estimated_tiles);
let layer_paths = texture.layer_paths();
let mut dds_textures: [Option<DdsTexture>; 3] = [None, None, None];
for (i, path) in layer_paths.iter().enumerate() {
if let Some(p) = path {
progress(&VTexProgress::with_file(
VTexPhase::LoadingTiles,
i + 1,
3,
format!("Loading layer {i}"),
));
dds_textures[i] = Some(DdsTexture::load(p)?);
}
}
for (layer_idx, dds_opt) in dds_textures.iter().enumerate() {
if let Some(dds) = dds_opt {
let coords = &geometry.tiles_per_layer[layer_idx];
if !coords.is_empty() {
progress(&VTexProgress::with_file(
VTexPhase::LoadingTiles,
layer_idx + 1,
3,
format!("Extracting {} tiles from layer {}", coords.len(), layer_idx),
));
let tiles = extract_tiles_from_dds(dds, coords, &self.config)?;
all_tiles.extend(tiles);
}
}
}
let total_tile_count = all_tiles.len();
progress(&VTexProgress::new(
VTexPhase::Deduplicating,
1,
total_tile_count,
));
let (is_first, unique_idx) = if self.config.deduplicate {
build_dedup_map(&all_tiles)
} else {
let is_first: Vec<bool> = vec![true; all_tiles.len()];
let unique_idx: Vec<usize> = (0..all_tiles.len()).collect();
(is_first, unique_idx)
};
let unique_tile_count = is_first.iter().filter(|&&x| x).count();
let unique_indices: Vec<usize> = is_first
.iter()
.enumerate()
.filter_map(|(i, &first)| if first { Some(i) } else { None })
.collect();
progress(&VTexProgress::new(
VTexPhase::Compressing,
0,
unique_tile_count,
));
let processed = AtomicUsize::new(0);
let compression = self.config.compression;
let compressed_unique: Result<Vec<CompressedTile>> = unique_indices
.par_iter()
.map(|&idx| {
let current = processed.fetch_add(1, Ordering::Relaxed) + 1;
if current % 100 == 0 {
progress(&VTexProgress::new(
VTexPhase::Compressing,
current,
unique_tile_count,
));
}
compress_tile(&all_tiles[idx].full_data(), compression)
})
.collect();
let compressed_unique = compressed_unique?;
progress(&VTexProgress::new(
VTexPhase::Compressing,
unique_tile_count,
unique_tile_count,
));
progress(&VTexProgress::new(VTexPhase::WritingGtp, 1, 1));
let gtp_hash = self.guid.iter().fold(String::new(), |mut acc, b| {
let _ = std::fmt::Write::write_fmt(&mut acc, format_args!("{b:02x}"));
acc
});
let gtp_filename = format!("{name}_{gtp_hash}.gtp");
let gtp_path = output_dir.join(>p_filename);
let mut gtp_writer = GtpWriter::new(self.guid, self.config.page_size);
let (compression1, compression2) = self.config.compression.compression_strings();
let mut chunk_locations: Vec<(u16, u16)> = Vec::with_capacity(unique_tile_count);
for compressed in &compressed_unique {
let chunk = Chunk {
codec: GtsCodec::Bc,
parameter_block_id: 0,
data: compressed.data.clone(),
};
let (page_idx, chunk_idx) = gtp_writer.add_chunk(chunk);
chunk_locations.push((page_idx, chunk_idx));
}
let mut flat_tile_infos: Vec<(GtsFlatTileInfo, usize, u32)> =
Vec::with_capacity(total_tile_count);
for (i, tile) in all_tiles.iter().enumerate() {
let u_idx = unique_idx[i];
let (page_idx, chunk_idx) = chunk_locations[u_idx];
flat_tile_infos.push((
GtsFlatTileInfo {
page_file_index: 0, page_index: page_idx,
chunk_index: chunk_idx,
d: 0,
packed_tile_id_index: 0, },
tile.coord.level as usize,
tile.packed_id,
));
}
let gtp_file = File::create(>p_path)?;
let mut gtp_buf = BufWriter::new(gtp_file);
gtp_writer.write(&mut gtp_buf)?;
drop(gtp_buf);
progress(&VTexProgress::new(VTexPhase::WritingGts, 1, 1));
let gts_path = output_dir.join(format!("{name}.gts"));
let mut gts_writer = GtsWriter::new(
self.guid,
self.config.tile_width as i32,
self.config.tile_height as i32,
self.config.tile_border as i32,
self.config.page_size,
);
for (i, present) in layers_present.iter().enumerate() {
if *present {
let data_type = match i {
1 => 12, _ => 6, };
gts_writer.add_layer(LayerInfo { data_type });
}
}
for level in &geometry.levels {
gts_writer.add_level(GtsLevelInfo {
width: level.width_tiles,
height: level.height_tiles,
width_pixels: level.width_pixels,
height_pixels: level.height_pixels,
});
}
let param_block = create_bc_parameter_block(
compression1,
compression2,
6, BcFormat::Bc3.fourcc(),
self.config.embed_mip,
);
gts_writer.add_parameter_block(param_block);
gts_writer.add_page_file(PageFileInfo {
filename: gtp_filename,
num_pages: gtp_writer.num_pages(),
guid: self.guid,
});
for (mut info, level, packed_id) in flat_tile_infos {
let packed_idx = gts_writer.add_packed_tile_id(packed_id);
info.packed_tile_id_index = packed_idx;
gts_writer.add_flat_tile_info(info, level);
}
let layer_info: Vec<(&str, &str)> = layers_present
.iter()
.enumerate()
.filter(|(_, present)| **present)
.map(|(i, _)| match i {
0 => ("BaseMap", "BaseColor"),
1 => ("NormalMap", "NormalMap"),
2 => ("PhysicalMap", "PhysicalMap"),
_ => ("Unknown", "Unknown"),
})
.collect();
let fourcc_tree = build_metadata_tree(
&texture.name,
geometry.total_width,
geometry.total_height,
0, 0, &layer_info,
&self.guid,
);
gts_writer.set_fourcc_tree(fourcc_tree);
let gts_file = File::create(>s_path)?;
let mut gts_buf = BufWriter::new(gts_file);
gts_writer.write(&mut gts_buf)?;
drop(gts_buf);
let gts_size = std::fs::metadata(>s_path)?.len();
let gtp_size = std::fs::metadata(>p_path)?.len();
let total_size = gts_size + gtp_size;
progress(&VTexProgress::new(VTexPhase::Complete, 1, 1));
Ok(BuildResult {
gts_path,
gtp_paths: vec![gtp_path],
tile_count: total_tile_count,
unique_tile_count,
total_size_bytes: total_size,
})
}
fn load_first_layer(&self, texture: &SourceTexture) -> Result<(DdsTexture, usize)> {
for (i, path) in texture.layer_paths().iter().enumerate() {
if let Some(p) = path {
let dds = DdsTexture::load(p)?;
return Ok((dds, i));
}
}
Err(Error::VirtualTexture(
"No layers found in texture".to_string(),
))
}
fn validate(&self) -> Result<()> {
self.config.validate().map_err(Error::VirtualTexture)?;
if self.textures.is_empty() {
return Err(Error::VirtualTexture(
"No textures added to builder".to_string(),
));
}
for tex in &self.textures {
if !tex.has_any_layer() {
return Err(Error::VirtualTexture(format!(
"Texture '{}' has no layers defined",
tex.name
)));
}
}
for tex in &self.textures {
for (i, path) in tex.layer_paths().iter().enumerate() {
if let Some(p) = path {
if !p.exists() {
let layer_name = match i {
0 => "base_map",
1 => "normal_map",
2 => "physical_map",
_ => "unknown",
};
return Err(Error::VirtualTexture(format!(
"Texture '{}' {}: file not found: {}",
tex.name,
layer_name,
p.display()
)));
}
}
}
}
Ok(())
}
}