use std::collections::{BTreeSet, HashMap, VecDeque};
use std::ops::Deref;
use super::walk::{AssetKind, Visit, visit_asset_paths};
use crate::sdf;
use crate::usd::Stage;
use crate::{Error, Result, ar, pcp};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Dependencies {
pub layers: Vec<String>,
pub assets: Vec<String>,
pub unresolved: Vec<String>,
}
pub fn compute_all_dependencies(stage: &Stage, asset_path: &str) -> Result<Dependencies> {
let graph = stage.layers();
let policy = Discover {
inactive: false,
open_packages: true,
};
let discovery = discover(&graph, asset_path, policy)?;
let mut dependencies = Dependencies::default();
for source in &discovery.sources {
match &source.content {
Content::Layer { .. } | Content::Package { .. } => dependencies.layers.push(source.identifier.clone()),
Content::Asset(resolved) => dependencies.assets.push(resolved.to_string()),
}
}
dependencies.unresolved = discovery
.unresolved
.into_iter()
.map(|(identifier, _)| identifier)
.collect();
dependencies.layers[1..].sort();
dependencies.assets.sort();
dependencies.unresolved.sort();
Ok(dependencies)
}
#[derive(Clone, Copy)]
pub(super) struct Discover {
pub inactive: bool,
pub open_packages: bool,
}
pub(super) struct Discovery<'a> {
pub sources: Vec<Source<'a>>,
pub unresolved: Vec<(String, AssetKind)>,
}
pub(super) struct Source<'a> {
pub identifier: String,
pub placement: Placement,
pub content: Content<'a>,
}
pub(super) enum Placement {
Root,
Entry(String),
Named { by: usize, path: String },
}
pub(super) enum Content<'a> {
Layer {
layer: SourceLayer<'a>,
uses: HashMap<String, Use>,
},
Package {
resolved: ar::ResolvedPath,
inside: Discovery<'a>,
},
Asset(ar::ResolvedPath),
}
pub(super) struct Use {
pub source: usize,
pub packaged: Option<String>,
}
impl Content<'_> {
pub(super) fn location(&self) -> Option<&str> {
match self {
Self::Layer { layer, .. } => layer.resolved_path(),
Self::Package { resolved, .. } | Self::Asset(resolved) => resolved.to_str(),
}
}
fn anchor(&self) -> Option<ar::ResolvedPath> {
match self {
Self::Layer { layer, .. } => layer.anchor_location(),
Self::Package { .. } | Self::Asset(_) => None,
}
}
}
pub(super) fn discover<'a>(graph: &'a pcp::LayerGraph, asset_path: &str, policy: Discover) -> Result<Discovery<'a>> {
let _scope = ar::CacheScope::begin();
let identifier = root_identifier(graph, asset_path);
let Some(layer) = SourceLayer::open(graph, &identifier)? else {
return Err(Error::UnresolvedAsset(asset_path.to_owned()));
};
let root = Source {
identifier,
placement: Placement::Root,
content: Content::Layer {
layer,
uses: HashMap::new(),
},
};
discover_scope(graph, policy, vec![root], None, Vec::new())
}
fn root_identifier(graph: &pcp::LayerGraph, asset_path: &str) -> String {
if graph.id_of(asset_path).is_some() {
return asset_path.to_owned();
}
graph.layer_registry().create_identifier(asset_path, None)
}
fn discover_scope<'a>(
graph: &'a pcp::LayerGraph,
policy: Discover,
roots: Vec<Source<'a>>,
package: Option<String>,
unresolved: Vec<(String, AssetKind)>,
) -> Result<Discovery<'a>> {
let mut discoverer = Discoverer {
graph,
policy,
package: package.map(|package| {
let prefix = format!("{}[", package.trim_end_matches(']'));
(package, prefix)
}),
index: roots
.iter()
.enumerate()
.map(|(index, root)| (root.identifier.clone(), index))
.collect(),
sources: roots,
unresolved,
};
let mut queue: VecDeque<usize> = (0..discoverer.sources.len())
.filter(|&index| discoverer.sources[index].is_layer())
.collect();
while let Some(current) = queue.pop_front() {
for (path, kind) in discoverer.authored_in(current)? {
if let Some(found) = discoverer.find_or_add(current, &path, kind)? {
if found.source == discoverer.sources.len() - 1 && discoverer.sources[found.source].is_layer() {
queue.push_back(found.source);
}
discoverer.uses_of(current).insert(path, found);
}
}
}
Ok(Discovery {
sources: discoverer.sources,
unresolved: discoverer.unresolved,
})
}
fn discover_inside<'a>(graph: &'a pcp::LayerGraph, policy: Discover, package: &str) -> Result<Discovery<'a>> {
let prefix = format!("{}[", package.trim_end_matches(']'));
let mut roots = Vec::new();
let mut nested = BTreeSet::new();
for &id in graph.all_ids() {
let layer = graph.layer(id);
let Some(rest) = layer.resolved_path().and_then(|real| real.strip_prefix(&prefix)) else {
continue;
};
let entry = rest.trim_end_matches(']');
match entry.split_once('[') {
Some((package, _)) => {
nested.insert(package.to_owned());
}
None => roots.push(Source {
identifier: layer.identifier().to_owned(),
placement: Placement::Entry(entry.to_owned()),
content: Content::Layer {
layer: SourceLayer::Live(layer),
uses: HashMap::new(),
},
}),
}
}
let mut unresolved = Vec::new();
for name in nested {
let identifier = ar::nest_packaged_path(package, &name);
let inside = discover_inside(graph, policy, &identifier)?;
unresolved.extend(inside.unresolved.iter().cloned());
roots.push(Source {
placement: Placement::Entry(name),
content: Content::Package {
resolved: ar::ResolvedPath::new(&identifier),
inside,
},
identifier,
});
}
if roots.is_empty() {
return Ok(Discovery {
sources: Vec::new(),
unresolved,
});
}
discover_scope(graph, policy, roots, Some(package.to_owned()), unresolved)
}
struct Discoverer<'a> {
graph: &'a pcp::LayerGraph,
policy: Discover,
package: Option<(String, String)>,
index: HashMap<String, usize>,
sources: Vec<Source<'a>>,
unresolved: Vec<(String, AssetKind)>,
}
impl<'a> Discoverer<'a> {
fn authored_in(&self, current: usize) -> Result<Vec<(String, AssetKind)>> {
let Content::Layer { layer, uses } = &self.sources[current].content else {
return Ok(Vec::new());
};
let mut found = Vec::new();
visit_asset_paths(layer.data(), layer.identifier(), Visit::DISCOVER, |asset| {
if (asset.applied || self.policy.inactive)
&& !uses.contains_key(asset.path)
&& !found.iter().any(|(path, _)| path == asset.path)
{
found.push((asset.path.to_owned(), asset.kind));
}
Ok(None)
})?;
Ok(found)
}
fn uses_of(&mut self, current: usize) -> &mut HashMap<String, Use> {
match &mut self.sources[current].content {
Content::Layer { uses, .. } => uses,
Content::Package { .. } | Content::Asset(_) => unreachable!("only a layer is walked"),
}
}
fn find_or_add(&mut self, referrer: usize, path: &str, kind: AssetKind) -> Result<Option<Use>> {
let registry = self.graph.layer_registry();
let anchor = self.sources[referrer].content.anchor();
let target = registry.create_identifier(path, anchor.as_ref());
if let Some((package, prefix)) = &self.package
&& let Some(rest) = target.strip_prefix(prefix.as_str())
{
let rest = rest.trim_end_matches(']');
let (name, packaged) = match rest.split_once('[') {
Some((name, deeper)) => (name, Some(deeper.to_owned())),
None => (rest, None),
};
let identifier = ar::nest_packaged_path(package, name);
if let Some(&index) = self.index.get(&identifier) {
return Ok(Some(Use {
source: index,
packaged,
}));
}
let Some(resolved) = registry.resolve(&target) else {
self.record_unresolved(target, kind);
return Ok(None);
};
let content = if packaged.is_some() || AssetKind::of(name, false) == AssetKind::Package {
Content::Package {
resolved: ar::ResolvedPath::new(&identifier),
inside: Discovery {
sources: Vec::new(),
unresolved: Vec::new(),
},
}
} else {
Content::Asset(resolved)
};
let index = self.sources.len();
self.index.insert(identifier.clone(), index);
self.sources.push(Source {
identifier,
placement: Placement::Entry(name.to_owned()),
content,
});
return Ok(Some(Use {
source: index,
packaged,
}));
}
let (identifier, to_source, kind, packaged) = match ar::split_package_relative_path_outer(path) {
Some((package, inner)) if !self.policy.open_packages => {
if registry.resolve(&target).is_none() {
self.record_unresolved(target, kind);
return Ok(None);
}
let identifier = registry.create_identifier(&package, anchor.as_ref());
let packaged = packaged_part(&target, &identifier).unwrap_or(inner);
(identifier, package, AssetKind::Package, Some(packaged))
}
_ if self.policy.open_packages && kind == AssetKind::Package => {
(target, path.to_owned(), AssetKind::Layer, None)
}
_ => (target, path.to_owned(), kind, None),
};
if let Some(&index) = self.index.get(&identifier) {
return Ok(Some(Use {
source: index,
packaged,
}));
}
let content = match kind {
AssetKind::Layer => {
let Some(layer) = SourceLayer::open(self.graph, &identifier)? else {
self.record_unresolved(identifier, kind);
return Ok(None);
};
Content::Layer {
layer,
uses: HashMap::new(),
}
}
AssetKind::Package | AssetKind::Asset => {
let Some(resolved) = registry.resolve(&identifier) else {
self.record_unresolved(identifier, kind);
return Ok(None);
};
if kind == AssetKind::Package {
let inside = discover_inside(self.graph, self.policy, &identifier)?;
for (unresolved, kind) in &inside.unresolved {
self.record_unresolved(unresolved.clone(), *kind);
}
Content::Package { resolved, inside }
} else {
Content::Asset(resolved)
}
}
};
let index = self.sources.len();
self.index.insert(identifier.clone(), index);
self.sources.push(Source {
identifier,
placement: Placement::Named {
by: referrer,
path: to_source,
},
content,
});
Ok(Some(Use {
source: index,
packaged,
}))
}
fn record_unresolved(&mut self, identifier: String, kind: AssetKind) {
if !self.unresolved.iter().any(|(found, _)| *found == identifier) {
self.unresolved.push((identifier, kind));
}
}
}
impl Source<'_> {
fn is_layer(&self) -> bool {
matches!(self.content, Content::Layer { .. })
}
}
pub(super) enum SourceLayer<'a> {
Live(&'a sdf::Layer),
Read(sdf::Layer),
}
impl<'a> SourceLayer<'a> {
fn open(graph: &'a pcp::LayerGraph, identifier: &str) -> Result<Option<Self>> {
if let Some(id) = graph.id_of(identifier) {
return Ok(Some(Self::Live(graph.layer(id))));
}
Ok(graph.layer_registry().open_layer(identifier)?.map(Self::Read))
}
}
impl Deref for SourceLayer<'_> {
type Target = sdf::Layer;
fn deref(&self) -> &sdf::Layer {
match self {
Self::Live(layer) => layer,
Self::Read(layer) => layer,
}
}
}
fn packaged_part(target: &str, package: &str) -> Option<String> {
let base = package.trim_end_matches(']');
let closing = package.len() - base.len() + 1;
let rest = target.strip_prefix(base)?.strip_prefix('[')?;
let (part, tail) = rest.split_at_checked(rest.len().checked_sub(closing)?)?;
tail.bytes().all(|byte| byte == b']').then(|| part.to_owned())
}
#[cfg(test)]
pub(crate) mod tests {
use std::fs;
use std::path::{MAIN_SEPARATOR, Path};
use super::*;
use crate::sdf::{FieldKey, Value};
use crate::usd_utils::{DependencyError, modify_asset_paths};
use crate::usdz::ArchiveWriter;
pub(crate) fn write(dir: &Path, path: &str, contents: &str) {
let path = dir.join(path);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, contents).unwrap();
}
pub(crate) fn open(dir: &Path, path: &str) -> Result<(String, Stage)> {
let root = dir.join(path).to_str().unwrap().to_owned();
let stage = Stage::open(&root)?;
Ok((root, stage))
}
const ROOT: &str = r#"#usda 1.0
(
customLayerData = {
asset icon = @./meta.png@
}
subLayers = [@./layers/sub.usda@]
)
def "A" (
delete references = @./deleted.usda@
prepend references = @./ref.usda@</A>
)
{
asset sampled.timeSamples = {
1: @./sample.png@,
}
asset[] textures = [@./tex.png@, @./missing.png@]
}
def "P" (
prepend payload = @../outside/payload.usda@</P>
)
{
}
def "V" (
variants = {
string v = "one"
}
prepend variantSets = "v"
)
{
variantSet "v" = {
"one" (
prepend references = @./one.usda@</V>
) {
}
"two" (
prepend references = @./two.usda@</V>
) {
}
}
}
def "M" (
prepend references = @./missing.usda@
)
{
}
def "N" (
prepend references = @./pkg.usdz@
)
{
}
"#;
fn scene(dir: &Path) -> String {
write(dir, "scene/root.usda", ROOT);
write(
dir,
"scene/layers/sub.usda",
"#usda 1.0\ndef \"S\" {\n asset t = @../sub_tex.png@\n}\n",
);
write(
dir,
"scene/ref.usda",
"#usda 1.0\ndef \"A\" {\n asset t = @./ref_tex.png@\n}\n",
);
for name in ["deleted", "one", "two"] {
write(dir, &format!("scene/{name}.usda"), "#usda 1.0\ndef \"V\" {\n}\n");
}
write(dir, "outside/payload.usda", "#usda 1.0\ndef \"P\" {\n}\n");
for name in ["meta", "sample", "tex", "sub_tex", "ref_tex"] {
write(dir, &format!("scene/{name}.png"), "PNG");
}
let mut package = ArchiveWriter::create(dir.join("scene/pkg.usdz")).unwrap();
package
.add_layer(
"inner.usda",
b"#usda 1.0\n(defaultPrim = \"N\")\ndef \"N\" {\n asset t = @./inside.png@\n}\n",
)
.unwrap();
package.add_layer("inside.png", b"PNG").unwrap();
package.finish().unwrap();
dir.join("scene/root.usda").to_str().unwrap().to_owned()
}
fn relative(dir: &Path, paths: &[String]) -> Vec<String> {
let base = format!("{}{}", fs::canonicalize(dir).unwrap().display(), MAIN_SEPARATOR);
let mut paths: Vec<_> = paths
.iter()
.map(|path| path.replace(&base, "").replace('\\', "/"))
.collect();
paths.sort();
paths
}
#[test]
fn dependencies_match_cpp() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = scene(dir.path());
let stage = Stage::open(&root)?;
let dependencies = compute_all_dependencies(&stage, &root)?;
assert_eq!(
relative(dir.path(), &dependencies.layers),
[
"outside/payload.usda",
"scene/layers/sub.usda",
"scene/one.usda",
"scene/pkg.usdz",
"scene/ref.usda",
"scene/root.usda",
"scene/two.usda",
]
);
assert!(Path::new(&dependencies.layers[0]).ends_with("scene/root.usda"));
assert_eq!(
relative(dir.path(), &dependencies.assets),
[
"scene/meta.png",
"scene/pkg.usdz[inside.png]",
"scene/ref_tex.png",
"scene/sample.png",
"scene/sub_tex.png",
"scene/tex.png",
]
);
assert_eq!(
relative(dir.path(), &dependencies.unresolved),
["scene/missing.png", "scene/missing.usda"]
);
Ok(())
}
#[test]
fn unsaved_edits_are_walked() -> Result<()> {
let dir = tempfile::tempdir()?;
write(dir.path(), "root.usda", "#usda 1.0\ndef \"A\" {\n}\n");
write(dir.path(), "extra.usda", "#usda 1.0\ndef \"E\" {\n}\n");
let (_, stage) = open(dir.path(), "root.usda")?;
let identifier = stage.root_layer().identifier().to_owned();
stage.layer_mut(&identifier).unwrap().edit(|edit| {
edit.data_mut().set_field(
&sdf::Path::abs_root(),
FieldKey::SubLayers.as_str(),
Value::StringVec(vec!["./extra.usda".into()]),
);
Ok(())
})?;
let dependencies = compute_all_dependencies(&stage, &identifier)?;
assert_eq!(relative(dir.path(), &dependencies.layers), ["extra.usda", "root.usda"]);
Ok(())
}
#[test]
fn asset_info_identifier_skipped() -> Result<()> {
let dir = tempfile::tempdir()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n assetInfo = {\n asset identifier = @./nowhere.usda@\n asset icon = @./missing.png@\n }\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
let dependencies = compute_all_dependencies(&stage, &root)?;
assert_eq!(relative(dir.path(), &dependencies.layers), ["root.usda"]);
assert_eq!(relative(dir.path(), &dependencies.unresolved), ["missing.png"]);
let mut seen = Vec::new();
modify_asset_paths(
&mut stage.layer_mut(&dependencies.layers[0]).unwrap(),
|path| {
seen.push(path.to_owned());
path.to_owned()
},
true,
)?;
seen.sort();
assert_eq!(seen, ["./missing.png", "./nowhere.usda"]);
Ok(())
}
#[test]
fn deleted_expression_ignored() -> Result<()> {
let dir = tempfile::tempdir()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n delete references = @`\"./${NAME}.usda\"`@\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
let dependencies = compute_all_dependencies(&stage, &root)?;
assert_eq!(relative(dir.path(), &dependencies.layers), ["root.usda"]);
assert!(dependencies.unresolved.is_empty(), "{:?}", dependencies.unresolved);
Ok(())
}
#[test]
fn deleted_reference_metadata_ignored() -> Result<()> {
let dir = tempfile::tempdir()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n delete references = @./gone.usda@ (\n customData = {\n asset icon = @`\"./${X}.png\"`@\n }\n )\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
let dependencies = compute_all_dependencies(&stage, &root)?;
assert!(dependencies.assets.is_empty(), "{:?}", dependencies.assets);
assert!(dependencies.unresolved.is_empty(), "{:?}", dependencies.unresolved);
Ok(())
}
#[test]
fn empty_clip_template_allowed() -> Result<()> {
let dir = tempfile::tempdir()?;
write(
dir.path(),
"root.usda",
"#usda 1.0\ndef \"A\" (\n clips = {\n dictionary default = {\n string templateAssetPath = \"\"\n }\n }\n)\n{\n}\n",
);
let (root, stage) = open(dir.path(), "root.usda")?;
assert_eq!(compute_all_dependencies(&stage, &root)?.layers.len(), 1);
Ok(())
}
#[test]
fn expansions_are_unsupported() -> Result<()> {
let dir = tempfile::tempdir()?;
for (body, kind) in [
("asset t = @./tex.<UDIM>.png@", "UDIM pattern"),
("asset t = @`\"./${NAME}.png\"`@", "variable expression"),
] {
write(
dir.path(),
"root.usda",
&format!("#usda 1.0\ndef \"A\" {{\n {body}\n}}\n"),
);
let (root, stage) = open(dir.path(), "root.usda")?;
let error = compute_all_dependencies(&stage, &root).unwrap_err();
assert!(
matches!(&error, Error::Dependency(DependencyError::Unsupported { kind: k, .. }) if *k == kind),
"{error}"
);
}
Ok(())
}
}