Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ Seven crates under `crates/` (ownership table in `docs/architecture.md`):
deps), `crust-jit` (Cranelift JIT for `crust-mtlx` programs, feature `jit`), `crust-core`
(the engine library: import, integrator, materials, lights, volumes, guiding, stats),
`crust-assets` (every file decoder and texture cache, behind `crust_core::AssetLoader`),
`crust-render` (the CLI; `main.rs` only writes images) and `utils` (stateless math:
`crust-render` (the CLI; it only drives a render and writes images: `main.rs`,
`products.rs`, `logging.rs`) and `utils` (stateless math:
warps, MIS heuristics, the one Rec.709 `luminance`). `openqmc-rs` (all sampling) and
`opensubdiv-rs` / `ptex-rs` are external. Import from `crust_core::` roots; `lib.rs`
re-exports the public surface.
Expand Down
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ edition = "2024"
rust-version = "1.96"
# SPDX form of LICENSE, so cargo-deny can check our own crates too.
license = "MIT"
version = "0.5.0"
version = "0.5.1"
# Nothing here goes to crates.io: crust-core and crust-assets depend on git
# repositories, which crates.io refuses. Releases are the GitHub binaries.
publish = false
Expand Down
69 changes: 29 additions & 40 deletions crates/crust-assets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ mod image_file;
mod mip_filter;
mod ptex_stream;
mod ptex_texture;
pub mod tiled;
mod texture_cache;
mod tiled;
mod uv_texture;

pub use environment::{load_exr_environment, load_image_environment, read_exr_rgb, read_rgb_image};
Expand All @@ -40,18 +41,34 @@ pub use ies::{load_ies, parse_ies};
pub use ptex_stream::{
DEFAULT_CACHE_MB as PTEX_DEFAULT_CACHE_MB, DEFAULT_STREAM_MIN_MB as PTEX_DEFAULT_STREAM_MIN_MB,
MICRO_SLOTS as PTEX_MICRO_SLOTS, MipSpace as PtexMipSpace, PtexStream,
StreamStats as PtexStreamStats, cache_budget_from_env as ptex_cache_budget_from_env,
micro_reserve as ptex_micro_reserve, micro_retained_bytes as ptex_micro_retained_bytes,
micro_slot_max as ptex_micro_slot_max, micro_thread_bytes as ptex_micro_thread_bytes,
micro_threads as ptex_micro_threads, mip_space_from_env as ptex_mip_space_from_env,
stream_enabled as ptex_stream_enabled,
stream_min_bytes_from_env as ptex_stream_min_bytes_from_env,
StreamStats as PtexStreamStats, micro_reserve as ptex_micro_reserve,
micro_retained_bytes as ptex_micro_retained_bytes, micro_slot_max as ptex_micro_slot_max,
micro_thread_bytes as ptex_micro_thread_bytes, micro_threads as ptex_micro_threads,
};
pub use ptex_texture::{
DEFAULT_MAX_LOG2, PtexColor, max_log2_from_env, max_log2_from_env_opt, read_channel,
};
/// Offline `.tx` conversion: what `maketx` and `--auto-tx` write.
pub use tiled::{MadeTx, TxFormat, make_tx, make_tx_atomic};
pub use uv_texture::{DEFAULT_MAX_EDGE, UvTexture};

/// The files a texture path names: every `<UDIM>` / `<UVTILE>` tile on
/// disk (the 10x10 UDIM sweep every texture reader shares), or the one image
/// when it exists.
pub fn texture_files(path: &Path) -> Vec<std::path::PathBuf> {
let name = path.to_string_lossy();
if name.contains("<UDIM>") || name.contains("<UVTILE>") {
uv_texture::existing_tiles(&name)
.into_iter()
.map(|t| t.path)
.collect()
} else if path.exists() {
vec![path.to_path_buf()]
} else {
Vec::new()
}
}

use crust_core::{
AssetLoader, ColorSpace, EnvironmentMap, IesProfile, LightTexture, PtexTexture,
ResolvedColorSpace, Texture2D,
Expand Down Expand Up @@ -282,15 +299,15 @@ impl FileAssets {
debug!(
"Textures stream from a .tx beside them when one exists, through a {:.0} MiB \
tile cache (CRUST_TEX_STREAM=0 preloads everything)",
budget as f64 / (1024.0 * 1024.0)
texture_cache::bytes_to_mib(budget)
);
}
let ptex_streaming = config.ptex_stream;
let ptex_mip_space = config.ptex_mip_space;
if ptex_streaming {
info!(
"Streaming Ptex with a {:.0} MiB cache",
ptex_stream::budget_bytes(&config) as f64 / (1024.0 * 1024.0)
texture_cache::bytes_to_mib(ptex_stream::budget_bytes(&config) as u64)
);
// Said at construction rather than per texture, because under
// the default policy it is the line that explains a render where
Expand Down Expand Up @@ -370,30 +387,6 @@ impl FileAssets {
)
}

/// The files a texture path names: every `<UDIM>` / `<UVTILE>` tile on
/// disk, or the one image.
fn tile_sources(path: &Path) -> Vec<std::path::PathBuf> {
let name = path.to_string_lossy();
if name.contains("<UDIM>") || name.contains("<UVTILE>") {
let mut tiles = Vec::new();
for v in 0..10u32 {
for u in 0..10u32 {
if let Some(p) =
uv_texture::expand_token(&name, u, v).map(std::path::PathBuf::from)
&& p.exists()
{
tiles.push(p);
}
}
}
tiles
} else if path.exists() {
vec![path.to_path_buf()]
} else {
Vec::new()
}
}

/// Whether a complete set of `.tx` siblings stands beside `path`'s tiles,
/// converting the missing and stale ones first under `--auto-tx`.
///
Expand All @@ -412,7 +405,7 @@ impl FileAssets {
if is_tx {
return true;
}
let sources = Self::tile_sources(path);
let sources = texture_files(path);
if sources.is_empty() {
return false;
}
Expand Down Expand Up @@ -687,11 +680,7 @@ impl FileAssets {
.filter(|c| (c.as_path() == path) == (which == Candidates::Source));
for candidate in candidates {
let started = Instant::now();
let Some(tex) =
tiled::StreamingTexture::open(&candidate, space, self.cache.clone(), |u, v| {
let name = candidate.to_string_lossy();
uv_texture::expand_token(&name, u, v).map(std::path::PathBuf::from)
})
let Some(tex) = tiled::StreamingTexture::open(&candidate, space, self.cache.clone())
else {
continue;
};
Expand Down Expand Up @@ -906,7 +895,7 @@ impl AssetLoader for FileAssets {
// `DEFAULT_STREAM_MIN_MB` for the island distribution that
// makes this necessary rather than tidy.
let would = tex.preload_bytes(self.preload_max_log2());
let floor = self.config.ptex_stream_min_mb * 1024 * 1024;
let floor = ptex_stream::stream_min_bytes(&self.config);
if would < floor {
why = PreloadReason::TooSmall;
debug!(
Expand Down
73 changes: 21 additions & 52 deletions crates/crust-assets/src/ptex_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ use crate::ptex_texture::ptex_space;
use crate::read_channel;
use crust_core::{ColorSpace, PtexTexture, Vec3A};
use std::path::Path;

use crate::texture_cache::{Ways, mib_to_bytes};
use std::sync::atomic::{AtomicU32, Ordering};

/// Default cache budget, in MiB.
Expand All @@ -49,20 +51,10 @@ use std::sync::atomic::{AtomicU32, Ordering};
/// working set is the frame rather than a locality window.
pub const DEFAULT_CACHE_MB: usize = crust_core::config::DEFAULT_CACHE_MB;

/// `CRUST_PTEX_CACHE_MB` as parsed into [`crust_core::config()`], as a byte
/// count.
pub fn cache_budget_from_env() -> usize {
budget_bytes(crust_core::config())
}

/// [`cache_budget_from_env`] for a given configuration.
/// The render's whole Ptex budget `config` asks for (`CRUST_PTEX_CACHE_MB`),
/// in bytes.
pub fn budget_bytes(config: &crust_core::Config) -> usize {
config.ptex_cache_mb.get() * 1024 * 1024
}

/// Is the streaming backend on? `CRUST_PTEX_STREAM=1` turns it on.
pub fn stream_enabled() -> bool {
crust_core::config().ptex_stream
mib_to_bytes(config.ptex_cache_mb.get() as u64) as usize
}

/// Default admission threshold: a texture streams only if **preloading** it
Expand Down Expand Up @@ -96,23 +88,17 @@ pub fn stream_enabled() -> bool {
/// what reproduces the even-split behaviour for comparison.
pub const DEFAULT_STREAM_MIN_MB: usize = crust_core::config::DEFAULT_PTEX_STREAM_MIN_MB;

/// `CRUST_PTEX_STREAM_MIN_MB` as parsed into [`crust_core::config()`], as a
/// byte count. See [`DEFAULT_STREAM_MIN_MB`].
pub fn stream_min_bytes_from_env() -> usize {
crust_core::config().ptex_stream_min_mb * 1024 * 1024
/// The admission threshold `config` asks for (`CRUST_PTEX_STREAM_MIN_MB`), in
/// bytes. See [`DEFAULT_STREAM_MIN_MB`].
pub(crate) fn stream_min_bytes(config: &crust_core::Config) -> usize {
mib_to_bytes(config.ptex_stream_min_mb as u64) as usize
}

/// Which mip chain a streamed texture is allowed to read — the reasoning is
/// on [`crust_core::PtexMipSpace`], which `CRUST_PTEX_STREAM_MIPSPACE` parses
/// into.
pub use crust_core::PtexMipSpace as MipSpace;

/// `CRUST_PTEX_STREAM_MIPSPACE` as parsed into [`crust_core::config()`]. See
/// [`MipSpace`].
pub fn mip_space_from_env() -> MipSpace {
crust_core::config().ptex_mip_space
}

/// Mip levels a face of resolution `res` holds, halving each axis to a floor
/// of one texel.
///
Expand Down Expand Up @@ -190,7 +176,7 @@ struct TileId {
/// and leave the interior at 0.999, for one extra `Option` pair per thread
/// and a linear scan that finds its hit at index 0 either way. Pinned by
/// `the_microcache_absorbs_most_taps`.
type MicroSlots = [Option<(TileId, ptex::PixelData)>; MICRO_SLOTS];
type MicroSlots = Ways<TileId, ptex::PixelData, MICRO_SLOTS>;

/// See [`MicroSlots`]: four is the corner case's tap count, not a round
/// number. Dropping it to two is what the 0.000 above measures.
Expand Down Expand Up @@ -277,18 +263,12 @@ pub fn micro_retained_bytes() -> u64 {
/// same time — another test in the same binary, for one — moves it too. A
/// check on what one sequence of lookups retained has to read this instead.
pub fn micro_thread_bytes() -> u64 {
MICRO.with(|m| {
m.borrow()
.iter()
.flatten()
.map(|(_, data)| data.len() as u64)
.sum()
})
MICRO.with(|m| m.borrow().values().map(|data| data.len() as u64).sum())
}

thread_local! {
static MICRO: std::cell::RefCell<MicroSlots> =
const { std::cell::RefCell::new([const { None }; MICRO_SLOTS]) };
const { std::cell::RefCell::new(MicroSlots::EMPTY) };
}

/// Distinguishes textures in [`TileId`]. Wraps only after 4 billion `.ptx`
Expand Down Expand Up @@ -630,10 +610,7 @@ impl PtexStream {
let mut f = Some(f);
let hit = MICRO.with(|m| {
let slots = m.borrow();
let idx = slots
.iter()
.position(|s| matches!(s, Some((k, _)) if *k == id))?;
let (_, data) = slots[idx].as_ref()?;
let data = slots.get(&id)?;
Some(f.take()?(data))
});
if let Some(r) = hit {
Expand All @@ -658,18 +635,11 @@ impl PtexStream {
if data.len() > self.micro_max {
return Some(r);
}
MICRO.with(|m| {
let mut slots = m.borrow_mut();
// Take the entry about to fall off the end *before* rotating, so
// its bytes leave the accounting with it; `rotate_right` then
// puts that hole in front for the new tile.
if let Some((_, old)) = slots[MICRO_SLOTS - 1].take() {
MICRO_BYTES.sub(old.len() as u64);
}
slots.rotate_right(1);
MICRO_BYTES.add(data.len() as u64);
slots[0] = Some((id, data));
});
// The entry that falls off the end leaves the accounting with it.
MICRO_BYTES.add(data.len() as u64);
if let Some((_, old)) = MICRO.with(|m| m.borrow_mut().push(id, data)) {
MICRO_BYTES.sub(old.len() as u64);
}
Some(r)
}

Expand Down Expand Up @@ -882,10 +852,9 @@ mod tests {
}

#[test]
fn a_budget_is_read_from_the_environment_and_a_bad_one_falls_back() {
// No env mutation: `cache_budget_from_env` is the wrapper, and the
// policy it wraps is what matters. Kept as a compile-time check that
// the default is stated in one place and in MiB.
fn the_default_budget_is_stated_once_and_in_mib() {
// A compile-time check that the default is stated in one place and in
// MiB.
assert_eq!(DEFAULT_CACHE_MB * 1024 * 1024, 1024 * 1024 * 1024);
}
}
Loading
Loading