use super::error_diagnostic;
use crate::assets::{AssetLoadReport, DefaultAssetFetcher, SceneAsset, TextureHandle};
use crate::material::MaterialDesc;
use crate::scene::recipe::{RecipeBuildPolicy, SceneRecipeDiagnosticV1};
use crate::scene_host::SceneHostCore;
use crate::{GeometryHandle, MaterialHandle};
#[path = "policy/budget.rs"]
mod budget;
pub(in crate::scene_host::recipe) use budget::{RecipeBuildBudget, RecipeTextureBudget};
pub(super) fn asset_policy_diagnostics(
policy: &RecipeBuildPolicy,
build_budget: &mut RecipeBuildBudget,
texture_budget: &mut RecipeTextureBudget,
host: &SceneHostCore<DefaultAssetFetcher>,
report: &AssetLoadReport<SceneAsset>,
import_path: &str,
) -> Vec<SceneRecipeDiagnosticV1> {
let mut diagnostics = Vec::new();
let fetched_bytes = report.fetched_bytes()
+ report
.external_resources()
.iter()
.filter_map(|resource| resource.bytes)
.sum::<usize>();
if fetched_bytes > policy.fetch_byte_limit() {
diagnostics.push(error_diagnostic(
import_path,
"policy_violation",
format!(
"import fetched {fetched_bytes} bytes, exceeding RecipeBuildPolicy fetch_byte_limit {}",
policy.fetch_byte_limit()
),
"use a smaller asset or raise the operator-owned fetch_byte_limit policy",
));
}
let asset = report.asset();
let node_count = asset.node_count();
if node_count > policy.max_nodes() {
diagnostics.push(error_diagnostic(
import_path,
"policy_violation",
format!(
"import contains {node_count} nodes, exceeding RecipeBuildPolicy max_nodes {}",
policy.max_nodes()
),
"use a smaller asset or raise the operator-owned max_nodes policy",
));
}
if let Some(diagnostic) = build_budget.reserve_nodes(policy, import_path, node_count) {
diagnostics.push(diagnostic);
}
let instance_count = asset
.nodes()
.iter()
.map(|node| node.instance_transforms().len())
.sum::<usize>();
if instance_count > policy.max_instances() {
diagnostics.push(error_diagnostic(
import_path,
"policy_violation",
format!(
"import contains {instance_count} authored instances, exceeding RecipeBuildPolicy max_instances {}",
policy.max_instances()
),
"use fewer instances or raise the operator-owned max_instances policy",
));
}
if let Some(diagnostic) = build_budget.reserve_instances(policy, import_path, instance_count) {
diagnostics.push(diagnostic);
}
let mut geometries = Vec::<GeometryHandle>::new();
let mut materials = Vec::<MaterialHandle>::new();
for node in asset.nodes() {
for mesh in node.meshes() {
push_unique(&mut geometries, mesh.geometry());
push_unique(&mut materials, mesh.material());
for binding in mesh.material_variant_bindings() {
push_unique(&mut materials, binding.material());
}
}
}
check_material_count(policy, import_path, materials.len(), &mut diagnostics);
if let Some(diagnostic) = build_budget.reserve_materials(policy, import_path, materials.len()) {
diagnostics.push(diagnostic);
}
let (vertices, indices) =
check_geometry_budget(policy, host, import_path, geometries, &mut diagnostics);
if let Some(diagnostic) =
build_budget.reserve_geometry(policy, import_path, vertices as u64, indices as u64)
{
diagnostics.push(diagnostic);
}
let (textures, decoded_texture_bytes) =
check_texture_budget(policy, host, import_path, materials, &mut diagnostics);
if let Some(diagnostic) =
texture_budget.reserve_loaded_textures(policy, import_path, textures, decoded_texture_bytes)
{
diagnostics.push(diagnostic);
}
diagnostics
}
fn check_material_count(
policy: &RecipeBuildPolicy,
import_path: &str,
count: usize,
diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) {
if count > policy.max_materials() {
diagnostics.push(error_diagnostic(
import_path,
"policy_violation",
format!(
"import references {count} materials, exceeding RecipeBuildPolicy max_materials {}",
policy.max_materials()
),
"use fewer materials or raise the operator-owned max_materials policy",
));
}
}
fn check_geometry_budget(
policy: &RecipeBuildPolicy,
host: &SceneHostCore<DefaultAssetFetcher>,
import_path: &str,
geometries: Vec<GeometryHandle>,
diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) -> (usize, usize) {
let mut vertex_count = 0usize;
let mut index_count = 0usize;
for geometry in geometries {
if let Some(desc) = host.assets.geometry(geometry) {
vertex_count = vertex_count.saturating_add(desc.vertices().len());
index_count = index_count.saturating_add(desc.indices().len());
}
}
if vertex_count > policy.max_vertices() {
diagnostics.push(error_diagnostic(
import_path,
"policy_violation",
format!(
"import references {vertex_count} vertices, exceeding RecipeBuildPolicy max_vertices {}",
policy.max_vertices()
),
"use lower-detail geometry or raise the operator-owned max_vertices policy",
));
}
if index_count > policy.max_indices() {
diagnostics.push(error_diagnostic(
import_path,
"policy_violation",
format!(
"import references {index_count} indices, exceeding RecipeBuildPolicy max_indices {}",
policy.max_indices()
),
"use lower-detail geometry or raise the operator-owned max_indices policy",
));
}
(vertex_count, index_count)
}
fn check_texture_budget(
policy: &RecipeBuildPolicy,
host: &SceneHostCore<DefaultAssetFetcher>,
import_path: &str,
materials: Vec<MaterialHandle>,
diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) -> (usize, usize) {
let mut textures = Vec::<TextureHandle>::new();
for material in materials {
if let Some(desc) = host.assets.material(material) {
collect_material_textures(&desc, &mut textures);
}
}
if textures.len() > policy.max_textures() {
diagnostics.push(error_diagnostic(
import_path,
"policy_violation",
format!(
"import references {} textures, exceeding RecipeBuildPolicy max_textures {}",
textures.len(),
policy.max_textures()
),
"use fewer textures or raise the operator-owned max_textures policy",
));
}
let decoded_texture_bytes =
check_texture_dimensions(policy, host, import_path, &textures, diagnostics);
if decoded_texture_bytes > policy.max_texture_bytes() {
diagnostics.push(error_diagnostic(
import_path,
"policy_violation",
format!(
"decoded textures use {decoded_texture_bytes} bytes, exceeding RecipeBuildPolicy max_texture_bytes {}",
policy.max_texture_bytes()
),
"use smaller textures or raise the operator-owned max_texture_bytes policy",
));
}
(textures.len(), decoded_texture_bytes)
}
fn check_texture_dimensions(
policy: &RecipeBuildPolicy,
host: &SceneHostCore<DefaultAssetFetcher>,
import_path: &str,
textures: &[TextureHandle],
diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) -> usize {
let mut decoded_texture_bytes = 0usize;
for texture in textures {
let Some(desc) = host.assets.texture(*texture) else {
continue;
};
if let Some((width, height, rgba8)) = desc.decoded_rgba8() {
decoded_texture_bytes = decoded_texture_bytes.saturating_add(rgba8.len());
check_texture_dimension(policy, import_path, width, height, diagnostics);
} else if let Some((width, height)) = desc.decoded_dimensions() {
check_texture_dimension(policy, import_path, width, height, diagnostics);
}
}
decoded_texture_bytes
}
fn check_texture_dimension(
policy: &RecipeBuildPolicy,
import_path: &str,
width: u32,
height: u32,
diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) {
let max_dimension = width.max(height);
if max_dimension > policy.max_image_dimension() {
diagnostics.push(error_diagnostic(
import_path,
"policy_violation",
format!(
"texture dimensions {width}x{height} exceed RecipeBuildPolicy max_image_dimension {}",
policy.max_image_dimension()
),
"use smaller textures or raise the operator-owned max_image_dimension policy",
));
}
}
fn collect_material_textures(material: &MaterialDesc, textures: &mut Vec<TextureHandle>) {
for texture in [
material.base_color_texture(),
material.normal_texture(),
material.metallic_roughness_texture(),
material.occlusion_texture(),
material.emissive_texture(),
material.clearcoat_texture(),
material.clearcoat_roughness_texture(),
material.clearcoat_normal_texture(),
material.sheen_color_texture(),
material.sheen_roughness_texture(),
material.anisotropy_texture(),
material.iridescence_texture(),
material.iridescence_thickness_texture(),
material.transmission_texture(),
material.thickness_texture(),
]
.into_iter()
.flatten()
{
push_unique(textures, texture);
}
}
fn push_unique<T: Copy + PartialEq>(values: &mut Vec<T>, value: T) {
if !values.contains(&value) {
values.push(value);
}
}