1use crate::{
9 BevyAnimationAssetLabelV1, GltfAnimationAddressabilityInventoryV1, GltfAnimationObservationV1,
10 ResolvedProfileSettingsV2, ResolvedSettingOriginV2, SettingIdV2, SettingValueV2,
11};
12use animsmith_core::{
13 Applicability, Check, CheckCtx, CheckEvaluation, CheckOutput, CheckSelection,
14 ConfigurationState, EnginePredictionBasisV4, EnginePredictionFacetStateV1,
15 EnginePredictionFacetV4, EnginePredictionV4, EngineSettingIdV2, EngineSettingValueOriginV3,
16 EngineSettingValueV2, EvaluationScope, EvaluationScopeCode, EvaluationState, InputIdentity,
17 LoadedSource, PREDICTION_V1_MAX_FACETS_PER_FILE, PredictionBasisReferenceV1,
18 PredictionBasisReferenceV2, PredictionBasisReferenceV4, PredictionProvenanceV4,
19 PredictionUnavailableReasonV2, RawGltfAddressabilityInventoryV1,
20 RawGltfDefaultSceneObservationV1, RawSourceBasisReferenceV1, RawSourceDomainV1,
21 RawSourceFieldIdV1, RawSourceKeyV1, ResolvedSettingLocationV1, SelectionState, ToolInfo,
22 evaluate_checks,
23};
24use serde::{Deserialize, Serialize};
25use serde_json::value::RawValue;
26use std::collections::{BTreeMap, BTreeSet};
27use std::fmt::Write as _;
28use std::io::Read;
29
30pub const GLTF_ADDRESSABILITY_V2_SCHEMA_VERSION: u32 = 2;
32pub const GLTF_ADDRESSABILITY_V2_ID: &str = "urn:animsmith:schema:gltf-addressability:2";
34pub const GLTF_ADDRESSABILITY_V2_MAX_REPORT_BYTES: u64 = 256 * 1024 * 1024;
36
37pub const GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS: usize = 4_096;
39pub const GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES: usize = 65_536;
42pub const GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES: usize =
44 5 * GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 5;
45pub const GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES: usize = 1_024;
47pub const GLTF_ADDRESSABILITY_V2_MAX_PATH_BYTES: usize = 4_096;
49pub const GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS: usize = 256;
51pub const GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES: usize = 1024 * 1024;
54
55const BEVY_TAG: &str = "v0.19.0";
56const BEVY_COMMIT: &str = "c6f634ca9f406d68ba5109d921247b654cb42c10";
57const BEVY_GLTF_CRATE: &str = "bevy_gltf 0.19.0";
58const GLTF_CRATE: &str = "gltf 1.4.1";
59const BEVY_RULE_BUNDLE_ID: &str = "urn:animsmith:bevy-gltf-addressability-rules:1";
60const BEVY_PROFILE_REVISION: u32 = 3;
61const FACET_BUDGET_SCOPE: &str = "engine-addressability:facet-budget";
62const ANIMATION_TARGET_NAMESPACE: [u8; 16] = [
63 0x31, 0x79, 0xf5, 0x19, 0xd9, 0x27, 0x4f, 0xf2, 0xb5, 0x96, 0x6f, 0xd0, 0x77, 0x02, 0x39, 0x11,
64];
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum TargetPointerWidth {
74 Bits32,
76 Bits64,
78}
79
80impl TargetPointerWidth {
81 const fn bytes(self) -> usize {
82 match self {
83 Self::Bits32 => 4,
84 Self::Bits64 => 8,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum BevyNamedMapDuplicatePolicyV1 {
93 LastWriteWins,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct BevyAddressabilityRuleSourceV1 {
101 id: String,
102 url: String,
103}
104
105impl BevyAddressabilityRuleSourceV1 {
106 pub fn id(&self) -> &str {
108 &self.id
109 }
110
111 pub fn url(&self) -> &str {
113 &self.url
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(deny_unknown_fields)]
120pub struct BevyGltfAddressabilityRulesV1 {
121 schema: String,
122 profile_revision: u32,
123 bevy_tag: String,
124 bevy_commit: String,
125 bevy_gltf_crate: String,
126 locked_gltf_crate: String,
127 bevy_animation_feature_required_for_targets: bool,
128 load_animations_required_for_targets: bool,
129 target_pointer_width: Option<TargetPointerWidth>,
130 named_scene_policy: BevyNamedMapDuplicatePolicyV1,
131 named_animation_policy: BevyNamedMapDuplicatePolicyV1,
132 named_skin_policy: BevyNamedMapDuplicatePolicyV1,
133 sources: Vec<BevyAddressabilityRuleSourceV1>,
134}
135
136impl BevyGltfAddressabilityRulesV1 {
137 pub fn frozen(target_pointer_width: Option<TargetPointerWidth>) -> Self {
140 let pinned =
141 |path: &str| format!("https://github.com/bevyengine/bevy/blob/{BEVY_COMMIT}/{path}");
142 let mut sources = vec![
143 BevyAddressabilityRuleSourceV1 {
144 id: "bevy-animation-target-id-0.19.0-c6f634ca".into(),
145 url: pinned("crates/bevy_animation/src/lib.rs"),
146 },
147 BevyAddressabilityRuleSourceV1 {
148 id: "bevy-gltf-labels-0.19.0-c6f634ca".into(),
149 url: pinned("crates/bevy_gltf/src/label.rs"),
150 },
151 BevyAddressabilityRuleSourceV1 {
152 id: "bevy-gltf-loader-0.19.0-c6f634ca".into(),
153 url: pinned("crates/bevy_gltf/src/loader/mod.rs"),
154 },
155 BevyAddressabilityRuleSourceV1 {
156 id: "bevy-gltf-node-path-0.19.0-c6f634ca".into(),
157 url: pinned("crates/bevy_gltf/src/loader/gltf_ext/scene.rs"),
158 },
159 BevyAddressabilityRuleSourceV1 {
160 id: "bevy-gltf-manifest-0.19.0-c6f634ca".into(),
161 url: pinned("crates/bevy_gltf/Cargo.toml"),
162 },
163 BevyAddressabilityRuleSourceV1 {
164 id: "bevy-lockfile-0.19.0-c6f634ca".into(),
165 url: pinned("Cargo.lock"),
166 },
167 ];
168 sources.sort_by(|left, right| left.id.cmp(&right.id));
169 Self {
170 schema: BEVY_RULE_BUNDLE_ID.into(),
171 profile_revision: BEVY_PROFILE_REVISION,
172 bevy_tag: BEVY_TAG.into(),
173 bevy_commit: BEVY_COMMIT.into(),
174 bevy_gltf_crate: BEVY_GLTF_CRATE.into(),
175 locked_gltf_crate: GLTF_CRATE.into(),
176 bevy_animation_feature_required_for_targets: true,
177 load_animations_required_for_targets: true,
178 target_pointer_width,
179 named_scene_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
180 named_animation_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
181 named_skin_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
182 sources,
183 }
184 }
185
186 pub fn contract_id(&self) -> &str {
188 &self.schema
189 }
190
191 pub const fn profile_revision(&self) -> u32 {
193 self.profile_revision
194 }
195
196 pub const fn target_pointer_width(&self) -> Option<TargetPointerWidth> {
198 self.target_pointer_width
199 }
200
201 pub fn sources(&self) -> &[BevyAddressabilityRuleSourceV1] {
203 &self.sources
204 }
205
206 fn validate(&self) -> bool {
207 self == &Self::frozen(self.target_pointer_width)
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(rename_all = "snake_case")]
214pub enum GltfAddressabilitySettingOriginV2 {
215 ExplicitConfig,
217 ProfileDefault,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(deny_unknown_fields)]
224pub struct BevyGltfAddressabilitySettingsV2 {
225 bevy_animation_feature: bool,
226 bevy_animation_feature_origin: GltfAddressabilitySettingOriginV2,
227 load_animations: bool,
228 load_animations_origin: GltfAddressabilitySettingOriginV2,
229}
230
231impl BevyGltfAddressabilitySettingsV2 {
232 fn from_resolved(
233 resolved: &ResolvedProfileSettingsV2,
234 ) -> Result<Self, GltfAddressabilityV2Error> {
235 let selection = resolved.profile().selection();
236 if selection.family() != "bevy"
237 || selection.profile_revision() != BEVY_PROFILE_REVISION
238 || selection.engine_version() != "0.19.0"
239 || selection.importer() != "gltf-asset-loader"
240 {
241 return Err(GltfAddressabilityV2Error::InvalidBevyProfile);
242 }
243 let boolean = |id| {
244 let setting = resolved
245 .document_settings()
246 .get(&id)
247 .ok_or(GltfAddressabilityV2Error::MissingRequiredSetting)?;
248 let SettingValueV2::Boolean(value) = setting.value() else {
249 return Err(GltfAddressabilityV2Error::MissingRequiredSetting);
250 };
251 Ok((*value, setting_origin(setting.origin())))
252 };
253 let (bevy_animation_feature, bevy_animation_feature_origin) =
254 boolean(SettingIdV2::BevyAnimationFeature)?;
255 let (load_animations, load_animations_origin) = boolean(SettingIdV2::LoadAnimations)?;
256 Ok(Self {
257 bevy_animation_feature,
258 bevy_animation_feature_origin,
259 load_animations,
260 load_animations_origin,
261 })
262 }
263
264 pub const fn bevy_animation_feature(&self) -> bool {
266 self.bevy_animation_feature
267 }
268
269 pub const fn bevy_animation_feature_origin(&self) -> GltfAddressabilitySettingOriginV2 {
271 self.bevy_animation_feature_origin
272 }
273
274 pub const fn load_animations(&self) -> bool {
276 self.load_animations
277 }
278
279 pub const fn load_animations_origin(&self) -> GltfAddressabilitySettingOriginV2 {
281 self.load_animations_origin
282 }
283}
284
285const fn setting_origin(value: ResolvedSettingOriginV2) -> GltfAddressabilitySettingOriginV2 {
286 match value {
287 ResolvedSettingOriginV2::ExplicitConfig => {
288 GltfAddressabilitySettingOriginV2::ExplicitConfig
289 }
290 ResolvedSettingOriginV2::ProfileDefault => {
291 GltfAddressabilitySettingOriginV2::ProfileDefault
292 }
293 }
294}
295
296pub fn bevy_animation_target_id_v1<'a>(
306 segments: impl IntoIterator<Item = &'a str>,
307 target_pointer_width: TargetPointerWidth,
308) -> Result<String, BevyAnimationTargetIdError> {
309 let mut hasher = blake3::Hasher::new();
310 hasher.update(&ANIMATION_TARGET_NAMESPACE);
311 let mut segment_count = 0usize;
312 let mut path_bytes = 0usize;
313 for segment in segments {
314 segment_count = segment_count
315 .checked_add(1)
316 .ok_or(BevyAnimationTargetIdError::ArithmeticOverflow)?;
317 if segment_count > GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS {
318 return Err(BevyAnimationTargetIdError::TooManySegments {
319 found: segment_count,
320 limit: GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS,
321 });
322 }
323 if segment.len() > GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES {
324 return Err(BevyAnimationTargetIdError::SegmentTooLong {
325 found: segment.len(),
326 limit: GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES,
327 });
328 }
329 path_bytes = path_bytes
330 .checked_add(segment.len())
331 .and_then(|value| value.checked_add(usize::from(segment_count > 1)))
332 .ok_or(BevyAnimationTargetIdError::ArithmeticOverflow)?;
333 if path_bytes > GLTF_ADDRESSABILITY_V2_MAX_PATH_BYTES {
334 return Err(BevyAnimationTargetIdError::PathTooLong {
335 found: path_bytes,
336 limit: GLTF_ADDRESSABILITY_V2_MAX_PATH_BYTES,
337 });
338 }
339 match target_pointer_width.bytes() {
340 4 => hasher.update(
341 &u32::try_from(segment.len())
342 .map_err(|_| BevyAnimationTargetIdError::SegmentLengthOverflow)?
343 .to_le_bytes(),
344 ),
345 8 => hasher.update(
346 &u64::try_from(segment.len())
347 .map_err(|_| BevyAnimationTargetIdError::SegmentLengthOverflow)?
348 .to_le_bytes(),
349 ),
350 _ => unreachable!("closed target pointer width"),
351 };
352 hasher.update(segment.as_bytes());
353 }
354 let mut uuid = [0u8; 16];
355 uuid.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
356 uuid[6] = (uuid[6] & 0x0f) | 0x50;
360 uuid[8] = (uuid[8] & 0x3f) | 0x80;
361 let mut value = String::with_capacity(36);
362 for (index, byte) in uuid.iter().enumerate() {
363 if matches!(index, 4 | 6 | 8 | 10) {
364 value.push('-');
365 }
366 write!(&mut value, "{byte:02x}").expect("writing to String cannot fail");
367 }
368 Ok(value)
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
373#[non_exhaustive]
374pub enum BevyAnimationTargetIdError {
375 #[error("target path has {found} segments; limit is {limit}")]
377 TooManySegments {
378 found: usize,
380 limit: usize,
382 },
383 #[error("target path segment has {found} bytes; limit is {limit}")]
385 SegmentTooLong {
386 found: usize,
388 limit: usize,
390 },
391 #[error("target path has {found} bytes; limit is {limit}")]
393 PathTooLong {
394 found: usize,
396 limit: usize,
398 },
399 #[error("target path segment length does not fit the selected pointer width")]
401 SegmentLengthOverflow,
402 #[error("target path bound arithmetic overflowed")]
404 ArithmeticOverflow,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
409#[serde(rename_all = "snake_case")]
410pub enum GltfAddressabilityUnavailableReasonV2 {
411 RawSourceIncomplete,
413 DependencyClosureIncomplete,
415 UnreachableTarget,
417 MultipleCandidatePaths,
419 DuplicateFullPath,
421 TargetIdCollision,
423 TargetPointerWidthMissing,
425 BevyAnimationFeatureDisabled,
427 LoadAnimationsDisabled,
429 PathBoundsExceeded,
431 NamedMapIncomplete,
433 TargetDomainTruncated,
435 ProjectionBoundsExceeded,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
441#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
442pub enum GltfAddressabilityProjectionV2<T> {
443 Available {
445 value: T,
447 },
448 ProvenAbsent,
450 RequiredUnavailable {
452 reasons: Vec<GltfAddressabilityUnavailableReasonV2>,
454 },
455}
456
457impl<T> GltfAddressabilityProjectionV2<T> {
458 fn unavailable(mut reasons: Vec<GltfAddressabilityUnavailableReasonV2>) -> Self {
459 reasons.sort();
460 reasons.dedup();
461 debug_assert!(!reasons.is_empty());
462 Self::RequiredUnavailable { reasons }
463 }
464
465 fn is_unavailable(&self) -> bool {
466 matches!(self, Self::RequiredUnavailable { .. })
467 }
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(deny_unknown_fields)]
473pub struct GltfAddressabilitySceneV2 {
474 source_scene_index: u64,
475 label: String,
476}
477
478impl GltfAddressabilitySceneV2 {
479 pub const fn source_scene_index(&self) -> u64 {
481 self.source_scene_index
482 }
483
484 pub fn label(&self) -> &str {
486 &self.label
487 }
488}
489
490#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
492#[serde(deny_unknown_fields)]
493pub struct GltfAddressabilitySkinV2 {
494 source_skin_index: u64,
495 explicit_skeleton_root_node_index: Option<u64>,
496 skin_label: GltfAddressabilityProjectionV2<String>,
497 inverse_bind_matrices_label: String,
498}
499
500impl GltfAddressabilitySkinV2 {
501 pub const fn source_skin_index(&self) -> u64 {
503 self.source_skin_index
504 }
505
506 pub const fn explicit_skeleton_root_node_index(&self) -> Option<u64> {
508 self.explicit_skeleton_root_node_index
509 }
510
511 pub const fn skin_label(&self) -> &GltfAddressabilityProjectionV2<String> {
513 &self.skin_label
514 }
515
516 pub fn inverse_bind_matrices_label(&self) -> &str {
518 &self.inverse_bind_matrices_label
519 }
520}
521
522#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
524#[serde(rename_all = "snake_case")]
525pub enum GltfAddressabilityNamedMapKindV2 {
526 Scene,
528 Animation,
530 Skin,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(deny_unknown_fields)]
537pub struct GltfAddressabilityNamedMapWinnerV2 {
538 name: String,
539 source_index: u64,
540 typed_label: String,
541}
542
543impl GltfAddressabilityNamedMapWinnerV2 {
544 pub fn name(&self) -> &str {
546 &self.name
547 }
548
549 pub const fn source_index(&self) -> u64 {
551 self.source_index
552 }
553
554 pub fn typed_label(&self) -> &str {
556 &self.typed_label
557 }
558}
559
560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
562#[serde(deny_unknown_fields)]
563pub struct GltfAddressabilityNamedMapV2 {
564 kind: GltfAddressabilityNamedMapKindV2,
565 duplicate_policy: BevyNamedMapDuplicatePolicyV1,
566 winners: GltfAddressabilityProjectionV2<Vec<GltfAddressabilityNamedMapWinnerV2>>,
567}
568
569impl GltfAddressabilityNamedMapV2 {
570 pub const fn kind(&self) -> GltfAddressabilityNamedMapKindV2 {
572 self.kind
573 }
574
575 pub const fn duplicate_policy(&self) -> BevyNamedMapDuplicatePolicyV1 {
577 self.duplicate_policy
578 }
579
580 pub const fn winners(
582 &self,
583 ) -> &GltfAddressabilityProjectionV2<Vec<GltfAddressabilityNamedMapWinnerV2>> {
584 &self.winners
585 }
586}
587
588#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
590#[serde(deny_unknown_fields)]
591pub struct GltfAddressabilityTargetChannelV2 {
592 source_animation_index: u64,
593 source_channel_index: u64,
594}
595
596impl GltfAddressabilityTargetChannelV2 {
597 pub const fn source_animation_index(&self) -> u64 {
599 self.source_animation_index
600 }
601
602 pub const fn source_channel_index(&self) -> u64 {
604 self.source_channel_index
605 }
606}
607
608#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
610#[serde(deny_unknown_fields)]
611pub struct GltfAddressabilityTargetValueV2 {
612 segments: Vec<String>,
613 path: String,
614 uuid: String,
615}
616
617impl GltfAddressabilityTargetValueV2 {
618 pub fn segments(&self) -> &[String] {
620 &self.segments
621 }
622
623 pub fn path(&self) -> &str {
625 &self.path
626 }
627
628 pub fn uuid(&self) -> &str {
630 &self.uuid
631 }
632}
633
634#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(deny_unknown_fields)]
637pub struct GltfAddressabilityTargetV2 {
638 source_node_index: u64,
639 contributing_channels: Vec<GltfAddressabilityTargetChannelV2>,
640 projection: GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2>,
641}
642
643impl GltfAddressabilityTargetV2 {
644 pub const fn source_node_index(&self) -> u64 {
646 self.source_node_index
647 }
648
649 pub fn contributing_channels(&self) -> &[GltfAddressabilityTargetChannelV2] {
651 &self.contributing_channels
652 }
653
654 pub const fn projection(
656 &self,
657 ) -> &GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2> {
658 &self.projection
659 }
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
664#[serde(deny_unknown_fields)]
665pub struct BevyGltfAddressabilityProjectionV2 {
666 scenes: Vec<GltfAddressabilitySceneV2>,
667 default_scene_route: GltfAddressabilityProjectionV2<String>,
668 skins: Vec<GltfAddressabilitySkinV2>,
669 named_maps: Vec<GltfAddressabilityNamedMapV2>,
670 target_coverage: GltfAddressabilityProjectionV2<()>,
671 targets: Vec<GltfAddressabilityTargetV2>,
672}
673
674impl BevyGltfAddressabilityProjectionV2 {
675 pub fn from_inventories(
678 raw: &RawGltfAddressabilityInventoryV1,
679 animations: &GltfAnimationAddressabilityInventoryV1,
680 bevy_animation_enabled: bool,
681 load_animations: bool,
682 pointer_width: Option<TargetPointerWidth>,
683 ) -> Result<Self, GltfAddressabilityV2Error> {
684 validate_inventory_binding(raw, animations)?;
685 let scenes = raw
686 .scenes()
687 .iter()
688 .map(|scene| GltfAddressabilitySceneV2 {
689 source_scene_index: scene.source_scene_index(),
690 label: format!("Scene{}", scene.source_scene_index()),
691 })
692 .collect();
693 let default_scene_route = match raw.default_scene() {
694 RawGltfDefaultSceneObservationV1::Absent => {
695 GltfAddressabilityProjectionV2::ProvenAbsent
696 }
697 RawGltfDefaultSceneObservationV1::Selected { source_scene_index }
698 if raw
699 .scenes()
700 .get(source_scene_index as usize)
701 .is_some_and(|scene| scene.source_scene_index() == source_scene_index) =>
702 {
703 GltfAddressabilityProjectionV2::Available {
704 value: format!("Scene{source_scene_index}"),
705 }
706 }
707 RawGltfDefaultSceneObservationV1::Selected { .. }
708 | RawGltfDefaultSceneObservationV1::Unavailable { .. } => {
709 GltfAddressabilityProjectionV2::unavailable(vec![
710 GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
711 ])
712 }
713 };
714
715 let attached = raw
716 .attachments()
717 .iter()
718 .map(|row| row.source_skin_index())
719 .collect::<BTreeSet<_>>();
720 let skins = raw
721 .skins()
722 .iter()
723 .map(|skin| {
724 let source_skin_index = skin.source_skin_index();
725 let skin_label = if attached.contains(&source_skin_index) {
726 GltfAddressabilityProjectionV2::Available {
727 value: format!("Skin{source_skin_index}"),
728 }
729 } else if raw.attachment_coverage().is_complete() {
730 GltfAddressabilityProjectionV2::ProvenAbsent
731 } else {
732 GltfAddressabilityProjectionV2::unavailable(vec![
733 GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
734 ])
735 };
736 GltfAddressabilitySkinV2 {
737 source_skin_index,
738 explicit_skeleton_root_node_index: skin.skeleton_root_node_index(),
739 skin_label,
740 inverse_bind_matrices_label: format!(
741 "Skin{source_skin_index}/InverseBindMatrices"
742 ),
743 }
744 })
745 .collect();
746
747 let named_maps = project_named_maps(raw, animations);
748 let (target_coverage, targets) = project_targets(
749 raw,
750 animations,
751 bevy_animation_enabled,
752 load_animations,
753 pointer_width,
754 )?;
755 let mut projection = Self {
756 scenes,
757 default_scene_route,
758 skins,
759 named_maps,
760 target_coverage,
761 targets,
762 };
763 normalize_projection_bounds(&mut projection);
764 Ok(projection)
765 }
766
767 pub fn scenes(&self) -> &[GltfAddressabilitySceneV2] {
769 &self.scenes
770 }
771
772 pub const fn default_scene_route(&self) -> &GltfAddressabilityProjectionV2<String> {
774 &self.default_scene_route
775 }
776
777 pub fn skins(&self) -> &[GltfAddressabilitySkinV2] {
779 &self.skins
780 }
781
782 pub fn named_maps(&self) -> &[GltfAddressabilityNamedMapV2] {
784 &self.named_maps
785 }
786
787 pub fn targets(&self) -> &[GltfAddressabilityTargetV2] {
789 &self.targets
790 }
791
792 pub const fn target_coverage(&self) -> &GltfAddressabilityProjectionV2<()> {
794 &self.target_coverage
795 }
796
797 pub fn has_required_unavailable(&self) -> bool {
799 self.default_scene_route.is_unavailable()
800 || self.skins.iter().any(|row| row.skin_label.is_unavailable())
801 || self
802 .named_maps
803 .iter()
804 .any(|row| row.winners.is_unavailable())
805 || self
806 .targets
807 .iter()
808 .any(|row| row.projection.is_unavailable())
809 || self.target_coverage.is_unavailable()
810 }
811}
812
813#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
816#[serde(deny_unknown_fields)]
817pub struct GltfAddressabilityInventoryV2 {
818 schema: String,
819 raw: RawGltfAddressabilityInventoryV1,
820 animations: GltfAnimationAddressabilityInventoryV1,
821}
822
823impl GltfAddressabilityInventoryV2 {
824 pub fn new(
826 raw: RawGltfAddressabilityInventoryV1,
827 animations: GltfAnimationAddressabilityInventoryV1,
828 ) -> Result<Self, GltfAddressabilityV2Error> {
829 validate_inventory_binding(&raw, &animations)?;
830 Ok(Self {
831 schema: GLTF_ADDRESSABILITY_V2_ID.into(),
832 raw,
833 animations,
834 })
835 }
836
837 pub fn contract_id(&self) -> &str {
839 &self.schema
840 }
841
842 pub const fn raw(&self) -> &RawGltfAddressabilityInventoryV1 {
844 &self.raw
845 }
846
847 pub const fn animations(&self) -> &GltfAnimationAddressabilityInventoryV1 {
849 &self.animations
850 }
851
852 fn validate(&self) -> Result<(), GltfAddressabilityV2Error> {
853 if self.schema != GLTF_ADDRESSABILITY_V2_ID {
854 return Err(GltfAddressabilityV2Error::InvalidSchema);
855 }
856 validate_inventory_binding(&self.raw, &self.animations)
857 }
858}
859
860#[derive(Debug, Clone, Serialize)]
862#[serde(deny_unknown_fields)]
863pub struct GltfAddressabilityBevyAdapterV2 {
864 rules: BevyGltfAddressabilityRulesV1,
865 settings: BevyGltfAddressabilitySettingsV2,
866 prediction_provenance: PredictionProvenanceV4,
867 check: CheckEvaluation,
868 projection: BevyGltfAddressabilityProjectionV2,
869}
870
871impl GltfAddressabilityBevyAdapterV2 {
872 pub const fn rules(&self) -> &BevyGltfAddressabilityRulesV1 {
874 &self.rules
875 }
876
877 pub const fn settings(&self) -> &BevyGltfAddressabilitySettingsV2 {
879 &self.settings
880 }
881
882 pub const fn prediction_provenance(&self) -> &PredictionProvenanceV4 {
884 &self.prediction_provenance
885 }
886
887 pub const fn check(&self) -> &CheckEvaluation {
889 &self.check
890 }
891
892 pub const fn projection(&self) -> &BevyGltfAddressabilityProjectionV2 {
894 &self.projection
895 }
896}
897
898#[derive(Debug, Clone, Serialize)]
900#[serde(deny_unknown_fields)]
901pub struct GltfAddressabilityV2 {
902 schema_version: u32,
903 schema: &'static str,
904 tool: ToolInfo,
905 command: &'static str,
906 input: InputIdentity,
907 inventory: GltfAddressabilityInventoryV2,
908 bevy: Option<GltfAddressabilityBevyAdapterV2>,
909}
910
911impl GltfAddressabilityV2 {
912 pub fn new(
914 tool: ToolInfo,
915 raw: RawGltfAddressabilityInventoryV1,
916 animations: GltfAnimationAddressabilityInventoryV1,
917 bevy: Option<GltfAddressabilityBevyAdapterV2>,
918 ) -> Result<Self, GltfAddressabilityV2Error> {
919 let inventory = GltfAddressabilityInventoryV2::new(raw, animations)?;
920 if let Some(adapter) = &bevy {
921 validate_adapter(adapter, &inventory)?;
922 }
923 Ok(Self {
924 schema_version: GLTF_ADDRESSABILITY_V2_SCHEMA_VERSION,
925 schema: GLTF_ADDRESSABILITY_V2_ID,
926 tool,
927 command: crate::GLTF_ANIMATION_ADDRESSABILITY_COMMAND,
928 input: inventory.raw.primary_input().clone(),
929 inventory,
930 bevy,
931 })
932 }
933
934 pub const fn input(&self) -> &InputIdentity {
936 &self.input
937 }
938
939 pub const fn inventory(&self) -> &GltfAddressabilityInventoryV2 {
941 &self.inventory
942 }
943
944 pub const fn bevy(&self) -> Option<&GltfAddressabilityBevyAdapterV2> {
946 self.bevy.as_ref()
947 }
948
949 pub fn has_required_prediction_unavailable(&self) -> bool {
951 self.bevy
952 .as_ref()
953 .is_some_and(|adapter| adapter.check.has_required_prediction_unavailable())
954 }
955
956 pub fn read_from(
958 reader: impl Read,
959 ) -> Result<GltfAddressabilityReadbackV2, GltfAddressabilityReadErrorV2> {
960 Self::read_from_with_limit(reader, GLTF_ADDRESSABILITY_V2_MAX_REPORT_BYTES)
961 }
962
963 fn read_from_with_limit(
964 reader: impl Read,
965 limit: u64,
966 ) -> Result<GltfAddressabilityReadbackV2, GltfAddressabilityReadErrorV2> {
967 let mut bounded = reader.take(limit + 1);
968 let mut bytes = Vec::new();
969 bounded
970 .read_to_end(&mut bytes)
971 .map_err(|source| GltfAddressabilityReadErrorV2::Io { source })?;
972 if bytes.len() as u64 > limit {
973 return Err(GltfAddressabilityReadErrorV2::ReportTooLarge { limit });
974 }
975 let wire: GltfAddressabilityWireV2 = serde_json::from_slice(&bytes)
976 .map_err(|source| GltfAddressabilityReadErrorV2::InvalidJson { source })?;
977 GltfAddressabilityReadbackV2::from_wire(wire)
978 .map_err(GltfAddressabilityReadErrorV2::Contract)
979 }
980}
981
982pub fn build_bevy_addressability_adapter_v2(
984 source: &LoadedSource,
985 raw: &RawGltfAddressabilityInventoryV1,
986 animations: &GltfAnimationAddressabilityInventoryV1,
987 resolved: &ResolvedProfileSettingsV2,
988 prediction_provenance: PredictionProvenanceV4,
989 pointer_width: Option<TargetPointerWidth>,
990 ctx: &CheckCtx<'_>,
991) -> Result<Option<GltfAddressabilityBevyAdapterV2>, GltfAddressabilityV2Error> {
992 validate_inventory_binding(raw, animations)?;
993 let settings = BevyGltfAddressabilitySettingsV2::from_resolved(resolved)?;
994 validate_provenance(source, raw, &prediction_provenance)?;
995 let rules = BevyGltfAddressabilityRulesV1::frozen(pointer_width);
996 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
997 raw,
998 animations,
999 settings.bevy_animation_feature,
1000 settings.load_animations,
1001 pointer_width,
1002 )?;
1003 let output = build_check_output(source, raw, animations, &prediction_provenance, &projection)?;
1004 let check = RichAddressabilityCheck { output };
1005 let checks: [Box<dyn Check>; 1] = [Box::new(check)];
1006 let evaluation = evaluate_checks(ctx, &checks, CheckSelection::All)
1007 .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?
1008 .pop()
1009 .expect("one static check produces one evaluation");
1010 let adapter = GltfAddressabilityBevyAdapterV2 {
1011 rules,
1012 settings,
1013 prediction_provenance,
1014 check: evaluation,
1015 projection,
1016 };
1017 validate_adapter(
1018 &adapter,
1019 &GltfAddressabilityInventoryV2::new(raw.clone(), animations.clone())?,
1020 )?;
1021 Ok(Some(adapter))
1022}
1023
1024struct RichAddressabilityCheck {
1025 output: CheckOutput,
1026}
1027
1028impl Check for RichAddressabilityCheck {
1029 fn id(&self) -> &'static str {
1030 crate::ENGINE_ADDRESSABILITY_CHECK_ID
1031 }
1032
1033 fn applicability(&self, _ctx: &CheckCtx<'_>) -> Applicability {
1034 Applicability::Applicable
1035 }
1036
1037 fn evaluate(&self, _ctx: &CheckCtx<'_>) -> CheckOutput {
1038 self.output.clone()
1039 }
1040}
1041
1042fn build_check_output(
1043 source: &LoadedSource,
1044 raw: &RawGltfAddressabilityInventoryV1,
1045 animations: &GltfAnimationAddressabilityInventoryV1,
1046 provenance: &PredictionProvenanceV4,
1047 projection: &BevyGltfAddressabilityProjectionV2,
1048) -> Result<CheckOutput, GltfAddressabilityV2Error> {
1049 let facts = source.source_facts();
1050 let mut scopes = Vec::new();
1051 let mut facets = Vec::new();
1052 let mut add = |scope: EvaluationScope, available: bool, reasons: Vec<_>, clip_index| {
1053 if available {
1054 scopes.push(scope.clone());
1055 return Ok::<(), GltfAddressabilityV2Error>(());
1056 }
1057 let basis = rich_addressability_basis_v4(facts, clip_index);
1058 let facet = EnginePredictionFacetV4::required_unavailable(scope, basis, reasons)
1059 .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1060 facets.push(facet);
1061 Ok::<(), GltfAddressabilityV2Error>(())
1062 };
1063
1064 if matches!(
1065 animations.animations().coverage().state(),
1066 crate::GltfAnimationCoverageStateV1::Complete
1067 ) {
1068 for animation in animations.animations().rows() {
1069 let label = BevyAnimationAssetLabelV1::new(animation.source_clip_index() as usize)
1070 .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1071 add(
1072 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL)
1073 .subject(label.as_str().to_owned()),
1074 true,
1075 Vec::new(),
1076 Some(animation.source_clip_index() as usize),
1077 )?;
1078 }
1079 } else {
1080 add(
1081 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
1082 false,
1083 vec![PredictionUnavailableReasonV2::RawSourceIncomplete],
1084 None,
1085 )?;
1086 }
1087
1088 for scene in &projection.scenes {
1089 add(
1090 EvaluationScope::new(EvaluationScopeCode::SCENE_ASSET_LABEL)
1091 .subject(scene.label.clone()),
1092 true,
1093 Vec::new(),
1094 None,
1095 )?;
1096 }
1097 add_projection_facet(
1098 &mut add,
1099 EvaluationScope::new(EvaluationScopeCode::DEFAULT_SCENE_ROUTE).subject("default_scene"),
1100 &projection.default_scene_route,
1101 )?;
1102 for skin in &projection.skins {
1103 add_projection_facet(
1104 &mut add,
1105 EvaluationScope::new(EvaluationScopeCode::SKIN_ASSET_LABEL)
1106 .subject(format!("Skin{}", skin.source_skin_index)),
1107 &skin.skin_label,
1108 )?;
1109 add(
1110 EvaluationScope::new(EvaluationScopeCode::INVERSE_BIND_MATRICES_ASSET_LABEL)
1111 .subject(skin.inverse_bind_matrices_label.clone()),
1112 true,
1113 Vec::new(),
1114 None,
1115 )?;
1116 }
1117 for map in &projection.named_maps {
1118 add_projection_facet(
1119 &mut add,
1120 EvaluationScope::new(EvaluationScopeCode::NAMED_ADDRESSABILITY_MAP).subject(match map
1121 .kind
1122 {
1123 GltfAddressabilityNamedMapKindV2::Scene => "scene",
1124 GltfAddressabilityNamedMapKindV2::Animation => "animation",
1125 GltfAddressabilityNamedMapKindV2::Skin => "skin",
1126 }),
1127 &map.winners,
1128 )?;
1129 }
1130 for target in &projection.targets {
1131 add_projection_facet(
1132 &mut add,
1133 EvaluationScope::new(EvaluationScopeCode::ANIMATION_TARGET_ID)
1134 .subject(format!("Node{}", target.source_node_index)),
1135 &target.projection,
1136 )?;
1137 }
1138 if !raw.scene_coverage().is_complete()
1139 || !raw.node_coverage().is_complete()
1140 || !raw.skin_coverage().is_complete()
1141 || !raw.attachment_coverage().is_complete()
1142 || !raw.path_candidate_coverage().is_complete()
1143 || animation_inventory_incomplete(animations)
1144 || projection.target_coverage.is_unavailable()
1145 {
1146 add(
1147 EvaluationScope::new(EvaluationScopeCode::GLTF_ADDRESSABILITY_INVENTORY),
1148 false,
1149 vec![PredictionUnavailableReasonV2::RawSourceIncomplete],
1150 None,
1151 )?;
1152 }
1153 if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
1154 let summary_scope = EvaluationScope::new(EvaluationScopeCode::custom(FACET_BUDGET_SCOPE));
1158 facets = vec![
1159 EnginePredictionFacetV4::required_unavailable(
1160 summary_scope,
1161 rich_addressability_basis_v4(facts, None),
1162 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
1163 )
1164 .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?,
1165 ];
1166 }
1167 let output = CheckOutput::from_coverage(Vec::new(), scopes, Vec::new());
1168 if facets.is_empty() {
1169 Ok(output)
1170 } else {
1171 let prediction = EnginePredictionV4::new(provenance.identity().clone(), facets)
1172 .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1173 Ok(output.with_engine_prediction_v4(prediction))
1174 }
1175}
1176
1177fn rich_addressability_basis_v4(
1178 facts: animsmith_core::SourceFactsViewV1<'_>,
1179 source_animation_index: Option<usize>,
1180) -> EnginePredictionBasisV4 {
1181 let mut references = vec![PredictionBasisReferenceV4::v2(
1182 PredictionBasisReferenceV2::v1(
1183 PredictionBasisReferenceV1::primary_source("bevy-gltf-loader-0.19.0-c6f634ca")
1184 .expect("static primary-source id is valid"),
1185 ),
1186 )];
1187 for setting_id in [
1188 EngineSettingIdV2::BevyAnimationFeature,
1189 EngineSettingIdV2::LoadAnimations,
1190 ] {
1191 references.push(PredictionBasisReferenceV4::v2(
1192 PredictionBasisReferenceV2::v1(
1193 PredictionBasisReferenceV1::resolved_setting(
1194 ResolvedSettingLocationV1::Document,
1195 setting_id.as_str(),
1196 )
1197 .expect("static setting id is valid"),
1198 ),
1199 ));
1200 }
1201 if let Some(source_animation_index) = source_animation_index {
1202 let source_name = RawSourceBasisReferenceV1::from_source(
1203 RawSourceDomainV1::Clip,
1204 RawSourceKeyV1::Clip {
1205 source_clip_index: source_animation_index as u64,
1206 },
1207 RawSourceFieldIdV1::new("source_name.state").expect("static field is valid"),
1208 facts,
1209 )
1210 .expect("retained animation row resolves its same-load source witness");
1211 references.push(PredictionBasisReferenceV4::v2(
1212 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::raw_source(source_name)),
1213 ));
1214 }
1215 EnginePredictionBasisV4::new(references).expect("static rich addressability basis is canonical")
1216}
1217
1218fn add_projection_facet<T>(
1219 add: &mut impl FnMut(
1220 EvaluationScope,
1221 bool,
1222 Vec<PredictionUnavailableReasonV2>,
1223 Option<usize>,
1224 ) -> Result<(), GltfAddressabilityV2Error>,
1225 scope: EvaluationScope,
1226 projection: &GltfAddressabilityProjectionV2<T>,
1227) -> Result<(), GltfAddressabilityV2Error> {
1228 match projection {
1229 GltfAddressabilityProjectionV2::Available { .. }
1230 | GltfAddressabilityProjectionV2::ProvenAbsent => add(scope, true, Vec::new(), None),
1231 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons } => add(
1232 scope,
1233 false,
1234 reasons.iter().map(prediction_reason).collect(),
1235 None,
1236 ),
1237 }
1238}
1239
1240fn prediction_reason(
1241 reason: &GltfAddressabilityUnavailableReasonV2,
1242) -> PredictionUnavailableReasonV2 {
1243 match reason {
1244 GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete
1245 | GltfAddressabilityUnavailableReasonV2::NamedMapIncomplete
1246 | GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated => {
1247 PredictionUnavailableReasonV2::RawSourceIncomplete
1248 }
1249 GltfAddressabilityUnavailableReasonV2::DependencyClosureIncomplete => {
1250 PredictionUnavailableReasonV2::DependencyClosureIncomplete
1251 }
1252 GltfAddressabilityUnavailableReasonV2::BevyAnimationFeatureDisabled
1253 | GltfAddressabilityUnavailableReasonV2::LoadAnimationsDisabled
1254 | GltfAddressabilityUnavailableReasonV2::TargetPointerWidthMissing => {
1255 PredictionUnavailableReasonV2::ProjectIntentUnavailable
1256 }
1257 GltfAddressabilityUnavailableReasonV2::UnreachableTarget => {
1258 PredictionUnavailableReasonV2::SourceSelectorNoMatch
1259 }
1260 GltfAddressabilityUnavailableReasonV2::MultipleCandidatePaths
1261 | GltfAddressabilityUnavailableReasonV2::DuplicateFullPath
1262 | GltfAddressabilityUnavailableReasonV2::TargetIdCollision => {
1263 PredictionUnavailableReasonV2::SourceSelectorAmbiguous
1264 }
1265 GltfAddressabilityUnavailableReasonV2::PathBoundsExceeded
1266 | GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded => {
1267 PredictionUnavailableReasonV2::MeasurementUnavailable
1268 }
1269 }
1270}
1271
1272fn validate_provenance(
1273 source: &LoadedSource,
1274 raw: &RawGltfAddressabilityInventoryV1,
1275 provenance: &PredictionProvenanceV4,
1276) -> Result<(), GltfAddressabilityV2Error> {
1277 provenance
1278 .validate()
1279 .map_err(|_| GltfAddressabilityV2Error::InvalidPredictionProvenance)?;
1280 let selection = provenance.profile().selection();
1281 if selection.family() != "bevy"
1282 || selection.profile_revision() != BEVY_PROFILE_REVISION
1283 || selection.engine_version() != "0.19.0"
1284 || selection.importer() != "gltf-asset-loader"
1285 {
1286 return Err(GltfAddressabilityV2Error::InvalidBevyProfile);
1287 }
1288 if provenance.raw_source().primary_input() != raw.primary_input()
1289 || provenance.dependency_closure() != raw.dependency_closure()
1290 || source.dependency_closure() != raw.dependency_closure()
1291 || source.source_facts().primary_identity() != raw.primary_input()
1292 {
1293 return Err(GltfAddressabilityV2Error::InventoryBindingMismatch);
1294 }
1295 Ok(())
1296}
1297
1298fn validate_adapter(
1299 adapter: &GltfAddressabilityBevyAdapterV2,
1300 inventory: &GltfAddressabilityInventoryV2,
1301) -> Result<(), GltfAddressabilityV2Error> {
1302 inventory.validate()?;
1303 if !adapter.rules.validate() {
1304 return Err(GltfAddressabilityV2Error::InvalidRuleBundle);
1305 }
1306 adapter
1307 .prediction_provenance
1308 .validate()
1309 .map_err(|_| GltfAddressabilityV2Error::InvalidPredictionProvenance)?;
1310 let selection = adapter.prediction_provenance.profile().selection();
1311 if selection.family() != "bevy"
1312 || selection.profile_revision() != BEVY_PROFILE_REVISION
1313 || selection.engine_version() != "0.19.0"
1314 || selection.importer() != "gltf-asset-loader"
1315 || adapter.prediction_provenance.raw_source().primary_input()
1316 != inventory.raw.primary_input()
1317 || adapter.prediction_provenance.dependency_closure() != inventory.raw.dependency_closure()
1318 {
1319 return Err(GltfAddressabilityV2Error::InvalidPredictionProvenance);
1320 }
1321 validate_settings_binding(&adapter.settings, &adapter.prediction_provenance)?;
1322 let expected = BevyGltfAddressabilityProjectionV2::from_inventories(
1323 &inventory.raw,
1324 &inventory.animations,
1325 adapter.settings.bevy_animation_feature,
1326 adapter.settings.load_animations,
1327 adapter.rules.target_pointer_width,
1328 )?;
1329 if expected != adapter.projection
1330 || adapter.check.check_id() != crate::ENGINE_ADDRESSABILITY_CHECK_ID
1331 || adapter.check.selection() != SelectionState::Selected
1332 || adapter.check.configuration() != ConfigurationState::Enabled
1333 || adapter.check.applicability() != Applicability::Applicable
1334 || !adapter.check.findings().is_empty()
1335 || !adapter.check.gaps().is_empty()
1336 {
1337 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1338 }
1339 let prediction = adapter.check.engine_prediction_v4();
1340 if let Some(prediction) = prediction {
1341 prediction
1342 .validate_against_provenance(&adapter.prediction_provenance)
1343 .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1344 }
1345 validate_facet_states(
1346 prediction,
1347 adapter.check.evaluated_scopes(),
1348 adapter.check.evaluation(),
1349 &expected_facet_specs(inventory, &adapter.projection),
1350 )
1351}
1352
1353fn validate_settings_binding(
1354 settings: &BevyGltfAddressabilitySettingsV2,
1355 provenance: &PredictionProvenanceV4,
1356) -> Result<(), GltfAddressabilityV2Error> {
1357 let matches = |id, expected_value, expected_origin| {
1358 provenance
1359 .settings()
1360 .document_setting(id)
1361 .is_some_and(|row| {
1362 row.value() == &EngineSettingValueV2::Boolean(expected_value)
1363 && row.value_origin()
1364 == match expected_origin {
1365 GltfAddressabilitySettingOriginV2::ExplicitConfig => {
1366 EngineSettingValueOriginV3::ExplicitConfig
1367 }
1368 GltfAddressabilitySettingOriginV2::ProfileDefault => {
1369 EngineSettingValueOriginV3::ProfileDefault
1370 }
1371 }
1372 })
1373 };
1374 if !matches(
1375 EngineSettingIdV2::BevyAnimationFeature,
1376 settings.bevy_animation_feature,
1377 settings.bevy_animation_feature_origin,
1378 ) || !matches(
1379 EngineSettingIdV2::LoadAnimations,
1380 settings.load_animations,
1381 settings.load_animations_origin,
1382 ) {
1383 return Err(GltfAddressabilityV2Error::InvalidPredictionProvenance);
1384 }
1385 Ok(())
1386}
1387
1388#[derive(Debug, Clone)]
1389struct ExpectedFacetV2 {
1390 scope: EvaluationScope,
1391 state: EnginePredictionFacetStateV1,
1392 reasons: Vec<PredictionUnavailableReasonV2>,
1393}
1394
1395fn validate_facet_states(
1396 prediction: Option<&EnginePredictionV4>,
1397 evaluated_scopes: &[EvaluationScope],
1398 evaluation: EvaluationState,
1399 expected: &[ExpectedFacetV2],
1400) -> Result<(), GltfAddressabilityV2Error> {
1401 if evaluated_scopes.len() > GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES {
1402 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1403 }
1404 let expected_available = expected
1405 .iter()
1406 .filter(|facet| facet.state == EnginePredictionFacetStateV1::Available)
1407 .collect::<Vec<_>>();
1408 let expected_unavailable = expected
1409 .iter()
1410 .filter(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
1411 .collect::<Vec<_>>();
1412 let actual_facets = prediction.map_or(&[][..], EnginePredictionV4::facets);
1413 let compacted = expected_unavailable.len() > PREDICTION_V1_MAX_FACETS_PER_FILE;
1414 let evaluated_set = evaluated_scopes.iter().collect::<BTreeSet<_>>();
1415 let expected_available_set = expected_available
1416 .iter()
1417 .map(|facet| &facet.scope)
1418 .collect::<BTreeSet<_>>();
1419 if evaluated_set.len() != evaluated_scopes.len()
1420 || expected_available_set.len() != expected_available.len()
1421 {
1422 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1423 }
1424
1425 if compacted {
1426 if actual_facets.len() != 1
1427 || actual_facets[0].scope().code.as_str() != FACET_BUDGET_SCOPE
1428 || actual_facets[0].scope().subject.is_some()
1429 || actual_facets[0].state()
1430 != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
1431 || actual_facets[0].result().is_some()
1432 || actual_facets[0].reasons() != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
1433 || evaluated_set != expected_available_set
1434 {
1435 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1436 }
1437 let expected_evaluation = if expected_available.is_empty() {
1438 EvaluationState::NotEvaluated
1439 } else {
1440 EvaluationState::Partial
1441 };
1442 return if evaluation == expected_evaluation {
1443 Ok(())
1444 } else {
1445 Err(GltfAddressabilityV2Error::InvalidCheckEvaluation)
1446 };
1447 }
1448
1449 let expected_unavailable_set = expected_unavailable
1450 .iter()
1451 .map(|facet| &facet.scope)
1452 .collect::<BTreeSet<_>>();
1453 let actual_by_scope = actual_facets
1454 .iter()
1455 .map(|facet| (facet.scope(), facet))
1456 .collect::<BTreeMap<_, _>>();
1457 if expected_unavailable_set.len() != expected_unavailable.len()
1458 || actual_by_scope.len() != actual_facets.len()
1459 || evaluated_set != expected_available_set
1460 || actual_by_scope.keys().copied().collect::<BTreeSet<_>>() != expected_unavailable_set
1461 || !evaluated_set.is_disjoint(&expected_unavailable_set)
1462 {
1463 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1464 }
1465
1466 for expected in &expected_unavailable {
1467 let Some(actual) = actual_by_scope.get(&expected.scope) else {
1468 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1469 };
1470 if actual.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
1471 || actual.result().is_some()
1472 || actual.reasons() != expected.reasons
1473 {
1474 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1475 }
1476 }
1477
1478 if actual_facets
1479 .iter()
1480 .any(|facet| facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
1481 {
1482 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1483 }
1484
1485 let expected_evaluation = if expected_unavailable.is_empty() {
1486 EvaluationState::Complete
1487 } else if expected_available.is_empty() {
1488 EvaluationState::NotEvaluated
1489 } else {
1490 EvaluationState::Partial
1491 };
1492 if evaluation != expected_evaluation {
1493 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1494 }
1495 Ok(())
1496}
1497
1498fn expected_facet_specs(
1499 inventory: &GltfAddressabilityInventoryV2,
1500 projection: &BevyGltfAddressabilityProjectionV2,
1501) -> Vec<ExpectedFacetV2> {
1502 let mut expected = Vec::new();
1503 if matches!(
1504 inventory.animations.animations().coverage().state(),
1505 crate::GltfAnimationCoverageStateV1::Complete
1506 ) {
1507 for animation in inventory.animations.animations().rows() {
1508 expected.push(ExpectedFacetV2 {
1509 scope: EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL)
1510 .subject(format!("Animation{}", animation.source_clip_index())),
1511 state: EnginePredictionFacetStateV1::Available,
1512 reasons: Vec::new(),
1513 });
1514 }
1515 } else {
1516 expected.push(ExpectedFacetV2 {
1517 scope: EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
1518 state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
1519 reasons: vec![PredictionUnavailableReasonV2::RawSourceIncomplete],
1520 });
1521 }
1522 for scene in &projection.scenes {
1523 expected.push(ExpectedFacetV2 {
1524 scope: EvaluationScope::new(EvaluationScopeCode::SCENE_ASSET_LABEL)
1525 .subject(scene.label.clone()),
1526 state: EnginePredictionFacetStateV1::Available,
1527 reasons: Vec::new(),
1528 });
1529 }
1530 push_expected_projection(
1531 &mut expected,
1532 EvaluationScope::new(EvaluationScopeCode::DEFAULT_SCENE_ROUTE).subject("default_scene"),
1533 &projection.default_scene_route,
1534 );
1535 for skin in &projection.skins {
1536 push_expected_projection(
1537 &mut expected,
1538 EvaluationScope::new(EvaluationScopeCode::SKIN_ASSET_LABEL)
1539 .subject(format!("Skin{}", skin.source_skin_index)),
1540 &skin.skin_label,
1541 );
1542 expected.push(ExpectedFacetV2 {
1543 scope: EvaluationScope::new(EvaluationScopeCode::INVERSE_BIND_MATRICES_ASSET_LABEL)
1544 .subject(skin.inverse_bind_matrices_label.clone()),
1545 state: EnginePredictionFacetStateV1::Available,
1546 reasons: Vec::new(),
1547 });
1548 }
1549 for map in &projection.named_maps {
1550 let subject = match map.kind {
1551 GltfAddressabilityNamedMapKindV2::Scene => "scene",
1552 GltfAddressabilityNamedMapKindV2::Animation => "animation",
1553 GltfAddressabilityNamedMapKindV2::Skin => "skin",
1554 };
1555 push_expected_projection(
1556 &mut expected,
1557 EvaluationScope::new(EvaluationScopeCode::NAMED_ADDRESSABILITY_MAP).subject(subject),
1558 &map.winners,
1559 );
1560 }
1561 for target in &projection.targets {
1562 push_expected_projection(
1563 &mut expected,
1564 EvaluationScope::new(EvaluationScopeCode::ANIMATION_TARGET_ID)
1565 .subject(format!("Node{}", target.source_node_index)),
1566 &target.projection,
1567 );
1568 }
1569 if !inventory.raw.scene_coverage().is_complete()
1570 || !inventory.raw.node_coverage().is_complete()
1571 || !inventory.raw.skin_coverage().is_complete()
1572 || !inventory.raw.attachment_coverage().is_complete()
1573 || !inventory.raw.path_candidate_coverage().is_complete()
1574 || animation_inventory_incomplete(&inventory.animations)
1575 || projection.target_coverage.is_unavailable()
1576 {
1577 expected.push(ExpectedFacetV2 {
1578 scope: EvaluationScope::new(EvaluationScopeCode::GLTF_ADDRESSABILITY_INVENTORY),
1579 state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
1580 reasons: vec![PredictionUnavailableReasonV2::RawSourceIncomplete],
1581 });
1582 }
1583 expected
1584}
1585
1586fn animation_inventory_incomplete(animations: &GltfAnimationAddressabilityInventoryV1) -> bool {
1587 !matches!(
1588 animations.animations().coverage().state(),
1589 crate::GltfAnimationCoverageStateV1::Complete
1590 ) || animations.animations().rows().iter().any(|animation| {
1591 !matches!(
1592 animation.channels().coverage().state(),
1593 crate::GltfAnimationCoverageStateV1::Complete
1594 )
1595 })
1596}
1597
1598fn push_expected_projection<T>(
1599 expected: &mut Vec<ExpectedFacetV2>,
1600 scope: EvaluationScope,
1601 projection: &GltfAddressabilityProjectionV2<T>,
1602) {
1603 let (state, mut reasons) = match projection {
1604 GltfAddressabilityProjectionV2::Available { .. }
1605 | GltfAddressabilityProjectionV2::ProvenAbsent => {
1606 (EnginePredictionFacetStateV1::Available, Vec::new())
1607 }
1608 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons } => (
1609 EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
1610 reasons.iter().map(prediction_reason).collect(),
1611 ),
1612 };
1613 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
1614 reasons.dedup();
1615 expected.push(ExpectedFacetV2 {
1616 scope,
1617 state,
1618 reasons,
1619 });
1620}
1621
1622#[derive(Debug, Deserialize)]
1623#[serde(deny_unknown_fields)]
1624struct ToolSourceWireV2 {
1625 revision: Option<String>,
1626 dirty: Option<bool>,
1627}
1628
1629#[derive(Debug, Deserialize)]
1630#[serde(deny_unknown_fields)]
1631struct ToolWireV2 {
1632 name: String,
1633 version: String,
1634 source: ToolSourceWireV2,
1635}
1636
1637#[derive(Debug, Deserialize)]
1638#[serde(deny_unknown_fields)]
1639struct AdapterWireV2 {
1640 rules: BevyGltfAddressabilityRulesV1,
1641 settings: BevyGltfAddressabilitySettingsV2,
1642 prediction_provenance: PredictionProvenanceV4,
1643 check: Box<RawValue>,
1644 projection: BevyGltfAddressabilityProjectionV2,
1645}
1646
1647#[derive(Debug, Deserialize)]
1648#[serde(deny_unknown_fields)]
1649struct GltfAddressabilityWireV2 {
1650 schema_version: u32,
1651 schema: String,
1652 tool: ToolWireV2,
1653 command: String,
1654 input: InputIdentity,
1655 inventory: GltfAddressabilityInventoryV2,
1656 bevy: Option<AdapterWireV2>,
1657}
1658
1659#[derive(Debug, Deserialize)]
1660#[serde(deny_unknown_fields)]
1661struct CheckWireV2 {
1662 check_id: String,
1663 selection: SelectionState,
1664 configuration: ConfigurationState,
1665 applicability: Applicability,
1666 evaluation: EvaluationState,
1667 findings: Vec<serde_json::Value>,
1668 #[serde(default)]
1669 evaluated_scopes: Vec<EvaluationScope>,
1670 #[serde(default)]
1671 gaps: Vec<serde_json::Value>,
1672 #[serde(default)]
1673 prediction: Option<EnginePredictionV4>,
1674}
1675
1676#[derive(Debug, Clone)]
1678pub struct GltfAddressabilityCheckReadbackV2 {
1679 check_id: String,
1680 selection: SelectionState,
1681 configuration: ConfigurationState,
1682 applicability: Applicability,
1683 evaluation: EvaluationState,
1684 evaluated_scopes: Vec<EvaluationScope>,
1685 prediction: Option<EnginePredictionV4>,
1686}
1687
1688impl GltfAddressabilityCheckReadbackV2 {
1689 pub fn check_id(&self) -> &str {
1691 &self.check_id
1692 }
1693
1694 pub const fn selection(&self) -> SelectionState {
1696 self.selection
1697 }
1698
1699 pub const fn configuration(&self) -> ConfigurationState {
1701 self.configuration
1702 }
1703
1704 pub const fn applicability(&self) -> Applicability {
1706 self.applicability
1707 }
1708
1709 pub const fn evaluation(&self) -> EvaluationState {
1711 self.evaluation
1712 }
1713
1714 pub const fn prediction(&self) -> Option<&EnginePredictionV4> {
1716 self.prediction.as_ref()
1717 }
1718
1719 pub fn evaluated_scopes(&self) -> &[EvaluationScope] {
1721 &self.evaluated_scopes
1722 }
1723}
1724
1725#[derive(Debug, Clone)]
1727pub struct GltfAddressabilityBevyReadbackV2 {
1728 rules: BevyGltfAddressabilityRulesV1,
1729 settings: BevyGltfAddressabilitySettingsV2,
1730 prediction_provenance: PredictionProvenanceV4,
1731 check: GltfAddressabilityCheckReadbackV2,
1732 projection: BevyGltfAddressabilityProjectionV2,
1733}
1734
1735impl GltfAddressabilityBevyReadbackV2 {
1736 pub const fn rules(&self) -> &BevyGltfAddressabilityRulesV1 {
1738 &self.rules
1739 }
1740
1741 pub const fn settings(&self) -> &BevyGltfAddressabilitySettingsV2 {
1743 &self.settings
1744 }
1745
1746 pub const fn prediction_provenance(&self) -> &PredictionProvenanceV4 {
1748 &self.prediction_provenance
1749 }
1750
1751 pub const fn check(&self) -> &GltfAddressabilityCheckReadbackV2 {
1753 &self.check
1754 }
1755
1756 pub const fn projection(&self) -> &BevyGltfAddressabilityProjectionV2 {
1758 &self.projection
1759 }
1760}
1761
1762#[derive(Debug, Clone)]
1764pub struct GltfAddressabilityReadbackV2 {
1765 input: InputIdentity,
1766 inventory: GltfAddressabilityInventoryV2,
1767 bevy: Option<GltfAddressabilityBevyReadbackV2>,
1768}
1769
1770impl GltfAddressabilityReadbackV2 {
1771 fn from_wire(wire: GltfAddressabilityWireV2) -> Result<Self, GltfAddressabilityV2Error> {
1772 if wire.schema_version != GLTF_ADDRESSABILITY_V2_SCHEMA_VERSION
1773 || wire.schema != GLTF_ADDRESSABILITY_V2_ID
1774 || wire.command != crate::GLTF_ANIMATION_ADDRESSABILITY_COMMAND
1775 || wire.tool.name != "animsmith"
1776 || !valid_semver(&wire.tool.version)
1777 || wire.tool.source.revision.as_ref().is_some_and(|revision| {
1778 revision.len() != 40 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit())
1779 })
1780 {
1781 return Err(GltfAddressabilityV2Error::InvalidSchema);
1782 }
1783 let _dirty = wire.tool.source.dirty;
1784 wire.inventory.validate()?;
1785 if &wire.input != wire.inventory.raw.primary_input() {
1786 return Err(GltfAddressabilityV2Error::InventoryBindingMismatch);
1787 }
1788 let bevy = wire
1789 .bevy
1790 .map(|adapter| read_adapter(adapter, &wire.inventory))
1791 .transpose()?;
1792 Ok(Self {
1793 input: wire.input,
1794 inventory: wire.inventory,
1795 bevy,
1796 })
1797 }
1798
1799 pub const fn input(&self) -> &InputIdentity {
1801 &self.input
1802 }
1803
1804 pub const fn inventory(&self) -> &GltfAddressabilityInventoryV2 {
1806 &self.inventory
1807 }
1808
1809 pub const fn bevy(&self) -> Option<&GltfAddressabilityBevyReadbackV2> {
1811 self.bevy.as_ref()
1812 }
1813}
1814
1815fn read_adapter(
1816 wire: AdapterWireV2,
1817 inventory: &GltfAddressabilityInventoryV2,
1818) -> Result<GltfAddressabilityBevyReadbackV2, GltfAddressabilityV2Error> {
1819 if !wire.rules.validate() {
1820 return Err(GltfAddressabilityV2Error::InvalidRuleBundle);
1821 }
1822 wire.prediction_provenance
1823 .validate()
1824 .map_err(|_| GltfAddressabilityV2Error::InvalidPredictionProvenance)?;
1825 let selection = wire.prediction_provenance.profile().selection();
1826 if selection.family() != "bevy"
1827 || selection.profile_revision() != BEVY_PROFILE_REVISION
1828 || selection.engine_version() != "0.19.0"
1829 || selection.importer() != "gltf-asset-loader"
1830 || wire.prediction_provenance.raw_source().primary_input() != inventory.raw.primary_input()
1831 || wire.prediction_provenance.dependency_closure() != inventory.raw.dependency_closure()
1832 {
1833 return Err(GltfAddressabilityV2Error::InvalidPredictionProvenance);
1834 }
1835 validate_settings_binding(&wire.settings, &wire.prediction_provenance)?;
1836 let expected = BevyGltfAddressabilityProjectionV2::from_inventories(
1837 &inventory.raw,
1838 &inventory.animations,
1839 wire.settings.bevy_animation_feature,
1840 wire.settings.load_animations,
1841 wire.rules.target_pointer_width,
1842 )?;
1843 if expected != wire.projection {
1844 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1845 }
1846 let check: CheckWireV2 = serde_json::from_str(wire.check.get())
1847 .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1848 if check.check_id != crate::ENGINE_ADDRESSABILITY_CHECK_ID
1849 || check.selection != SelectionState::Selected
1850 || check.configuration != ConfigurationState::Enabled
1851 || check.applicability != Applicability::Applicable
1852 || !check.findings.is_empty()
1853 || !check.gaps.is_empty()
1854 {
1855 return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1856 }
1857 if let Some(prediction) = &check.prediction {
1858 prediction
1859 .validate_against_provenance(&wire.prediction_provenance)
1860 .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1861 }
1862 validate_facet_states(
1863 check.prediction.as_ref(),
1864 &check.evaluated_scopes,
1865 check.evaluation,
1866 &expected_facet_specs(inventory, &wire.projection),
1867 )?;
1868 Ok(GltfAddressabilityBevyReadbackV2 {
1869 rules: wire.rules,
1870 settings: wire.settings,
1871 prediction_provenance: wire.prediction_provenance,
1872 check: GltfAddressabilityCheckReadbackV2 {
1873 check_id: check.check_id,
1874 selection: check.selection,
1875 configuration: check.configuration,
1876 applicability: check.applicability,
1877 evaluation: check.evaluation,
1878 evaluated_scopes: check.evaluated_scopes,
1879 prediction: check.prediction,
1880 },
1881 projection: wire.projection,
1882 })
1883}
1884
1885fn valid_semver(value: &str) -> bool {
1886 let core = value
1887 .split_once('+')
1888 .map_or(value, |(core, _)| core)
1889 .split_once('-')
1890 .map_or_else(
1891 || value.split_once('+').map_or(value, |(core, _)| core),
1892 |(core, _)| core,
1893 );
1894 let mut parts = core.split('.');
1895 (0..3).all(|_| {
1896 parts
1897 .next()
1898 .is_some_and(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
1899 }) && parts.next().is_none()
1900}
1901
1902#[derive(Debug, thiserror::Error)]
1904#[non_exhaustive]
1905pub enum GltfAddressabilityReadErrorV2 {
1906 #[error("cannot read rich addressability report: {source}")]
1908 Io {
1909 source: std::io::Error,
1911 },
1912 #[error("rich addressability report exceeds byte limit {limit}")]
1914 ReportTooLarge {
1915 limit: u64,
1917 },
1918 #[error("invalid rich addressability JSON: {source}")]
1920 InvalidJson {
1921 source: serde_json::Error,
1923 },
1924 #[error(transparent)]
1926 Contract(#[from] GltfAddressabilityV2Error),
1927}
1928
1929fn validate_inventory_binding(
1930 raw: &RawGltfAddressabilityInventoryV1,
1931 animations: &GltfAnimationAddressabilityInventoryV1,
1932) -> Result<(), GltfAddressabilityV2Error> {
1933 raw.validate()
1934 .map_err(|_| GltfAddressabilityV2Error::InvalidRawInventory)?;
1935 if raw.primary_input() != animations.primary_input()
1936 || raw.dependency_closure() != animations.dependency_closure()
1937 {
1938 return Err(GltfAddressabilityV2Error::InventoryBindingMismatch);
1939 }
1940 Ok(())
1941}
1942
1943fn project_named_maps(
1944 raw: &RawGltfAddressabilityInventoryV1,
1945 animations: &GltfAnimationAddressabilityInventoryV1,
1946) -> Vec<GltfAddressabilityNamedMapV2> {
1947 let mut scene_winners = BTreeMap::new();
1948 for row in raw.scenes() {
1949 if let Some(name) = row.name() {
1950 scene_winners.insert(
1951 name.to_owned(),
1952 GltfAddressabilityNamedMapWinnerV2 {
1953 name: name.to_owned(),
1954 source_index: row.source_scene_index(),
1955 typed_label: format!("Scene{}", row.source_scene_index()),
1956 },
1957 );
1958 }
1959 }
1960 let scenes = named_map(
1961 GltfAddressabilityNamedMapKindV2::Scene,
1962 raw.scene_coverage().is_complete(),
1963 scene_winners,
1964 );
1965
1966 let mut animation_winners = BTreeMap::new();
1967 for row in animations.animations().rows() {
1968 if let GltfAnimationObservationV1::Observed { value: name } = row.source_name() {
1969 animation_winners.insert(
1970 name.clone(),
1971 GltfAddressabilityNamedMapWinnerV2 {
1972 name: name.clone(),
1973 source_index: row.source_clip_index(),
1974 typed_label: format!("Animation{}", row.source_clip_index()),
1975 },
1976 );
1977 }
1978 }
1979 let animations = named_map(
1980 GltfAddressabilityNamedMapKindV2::Animation,
1981 matches!(
1982 animations.animations().coverage().state(),
1983 crate::GltfAnimationCoverageStateV1::Complete
1984 ),
1985 animation_winners,
1986 );
1987
1988 let skins_by_index = raw
1992 .skins()
1993 .iter()
1994 .map(|skin| (skin.source_skin_index(), skin))
1995 .collect::<BTreeMap<_, _>>();
1996 let mut seen = BTreeSet::new();
1997 let mut skin_winners = BTreeMap::new();
1998 for attachment in raw.attachments() {
1999 let index = attachment.source_skin_index();
2000 if seen.insert(index)
2001 && let Some(skin) = skins_by_index.get(&index)
2002 && let Some(name) = skin.name()
2003 {
2004 skin_winners.insert(
2005 name.to_owned(),
2006 GltfAddressabilityNamedMapWinnerV2 {
2007 name: name.to_owned(),
2008 source_index: index,
2009 typed_label: format!("Skin{index}"),
2010 },
2011 );
2012 }
2013 }
2014 let skins = named_map(
2015 GltfAddressabilityNamedMapKindV2::Skin,
2016 raw.skin_coverage().is_complete() && raw.attachment_coverage().is_complete(),
2017 skin_winners,
2018 );
2019 vec![scenes, animations, skins]
2020}
2021
2022fn named_map(
2023 kind: GltfAddressabilityNamedMapKindV2,
2024 complete: bool,
2025 winners: BTreeMap<String, GltfAddressabilityNamedMapWinnerV2>,
2026) -> GltfAddressabilityNamedMapV2 {
2027 let keys_bounded = winners
2028 .keys()
2029 .all(|name| name.len() <= GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES);
2030 GltfAddressabilityNamedMapV2 {
2031 kind,
2032 duplicate_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
2033 winners: if !keys_bounded {
2034 GltfAddressabilityProjectionV2::unavailable(vec![
2035 GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2036 ])
2037 } else if complete {
2038 GltfAddressabilityProjectionV2::Available {
2039 value: winners.into_values().collect(),
2040 }
2041 } else {
2042 GltfAddressabilityProjectionV2::unavailable(vec![
2043 GltfAddressabilityUnavailableReasonV2::NamedMapIncomplete,
2044 ])
2045 },
2046 }
2047}
2048
2049fn normalize_projection_bounds(projection: &mut BevyGltfAddressabilityProjectionV2) {
2050 if projection_structural_references(projection)
2051 > GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES
2052 {
2053 make_named_maps_bounds_unavailable(projection);
2054 let mut retained_references = 0usize;
2055 let mut retained = 0usize;
2056 for target in &projection.targets {
2057 let target_references = target.contributing_channels.len()
2058 + match &target.projection {
2059 GltfAddressabilityProjectionV2::Available { value } => value.segments.len(),
2060 GltfAddressabilityProjectionV2::ProvenAbsent
2061 | GltfAddressabilityProjectionV2::RequiredUnavailable { .. } => 0,
2062 };
2063 if retained_references.saturating_add(target_references)
2064 > GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES
2065 {
2066 break;
2067 }
2068 retained_references += target_references;
2069 retained += 1;
2070 }
2071 if retained < projection.targets.len() {
2072 projection.targets.truncate(retained);
2073 projection.target_coverage = GltfAddressabilityProjectionV2::unavailable(vec![
2074 GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2075 ]);
2076 }
2077 if projection.target_coverage.is_unavailable() {
2079 make_targets_bounds_unavailable(projection);
2080 }
2081 }
2082 if projection_text_bytes(projection) > GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES {
2083 make_named_maps_bounds_unavailable(projection);
2084 make_targets_bounds_unavailable(projection);
2085 }
2086 debug_assert!(
2087 projection_structural_references(projection)
2088 <= GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES
2089 );
2090 debug_assert!(projection_text_bytes(projection) <= GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES);
2091}
2092
2093fn make_named_maps_bounds_unavailable(projection: &mut BevyGltfAddressabilityProjectionV2) {
2094 for map in &mut projection.named_maps {
2095 map.winners = GltfAddressabilityProjectionV2::unavailable(vec![
2096 GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2097 ]);
2098 }
2099}
2100
2101fn make_targets_bounds_unavailable(projection: &mut BevyGltfAddressabilityProjectionV2) {
2102 projection.target_coverage = GltfAddressabilityProjectionV2::unavailable(vec![
2103 GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2104 ]);
2105 for target in &mut projection.targets {
2106 target.projection = GltfAddressabilityProjectionV2::unavailable(vec![
2107 GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2108 ]);
2109 }
2110}
2111
2112fn projection_structural_references(projection: &BevyGltfAddressabilityProjectionV2) -> usize {
2113 let named = projection
2114 .named_maps
2115 .iter()
2116 .map(|map| match &map.winners {
2117 GltfAddressabilityProjectionV2::Available { value } => value.len(),
2118 GltfAddressabilityProjectionV2::ProvenAbsent
2119 | GltfAddressabilityProjectionV2::RequiredUnavailable { .. } => 0,
2120 })
2121 .sum::<usize>();
2122 named
2123 + projection
2124 .targets
2125 .iter()
2126 .map(|target| {
2127 target.contributing_channels.len()
2128 + match &target.projection {
2129 GltfAddressabilityProjectionV2::Available { value } => value.segments.len(),
2130 GltfAddressabilityProjectionV2::ProvenAbsent
2131 | GltfAddressabilityProjectionV2::RequiredUnavailable { .. } => 0,
2132 }
2133 })
2134 .sum::<usize>()
2135}
2136
2137fn projection_text_bytes(projection: &BevyGltfAddressabilityProjectionV2) -> usize {
2138 let mut bytes = projection
2139 .scenes
2140 .iter()
2141 .map(|row| row.label.len())
2142 .sum::<usize>();
2143 if let GltfAddressabilityProjectionV2::Available { value } = &projection.default_scene_route {
2144 bytes = bytes.saturating_add(value.len());
2145 }
2146 for skin in &projection.skins {
2147 bytes = bytes.saturating_add(skin.inverse_bind_matrices_label.len());
2148 if let GltfAddressabilityProjectionV2::Available { value } = &skin.skin_label {
2149 bytes = bytes.saturating_add(value.len());
2150 }
2151 }
2152 for map in &projection.named_maps {
2153 if let GltfAddressabilityProjectionV2::Available { value } = &map.winners {
2154 for winner in value {
2155 bytes = bytes
2156 .saturating_add(winner.name.len())
2157 .saturating_add(winner.typed_label.len());
2158 }
2159 }
2160 }
2161 for target in &projection.targets {
2162 if let GltfAddressabilityProjectionV2::Available { value } = &target.projection {
2163 bytes = bytes
2164 .saturating_add(value.path.len())
2165 .saturating_add(value.uuid.len())
2166 .saturating_add(value.segments.iter().map(String::len).sum::<usize>());
2167 }
2168 }
2169 bytes
2170}
2171
2172fn project_targets(
2173 raw: &RawGltfAddressabilityInventoryV1,
2174 animations: &GltfAnimationAddressabilityInventoryV1,
2175 bevy_animation_enabled: bool,
2176 load_animations: bool,
2177 pointer_width: Option<TargetPointerWidth>,
2178) -> Result<
2179 (
2180 GltfAddressabilityProjectionV2<()>,
2181 Vec<GltfAddressabilityTargetV2>,
2182 ),
2183 GltfAddressabilityV2Error,
2184> {
2185 let mut contributors = BTreeMap::<u64, Vec<GltfAddressabilityTargetChannelV2>>::new();
2186 for animation in animations.animations().rows() {
2187 for channel in animation.channels().rows() {
2188 contributors
2189 .entry(channel.target().index())
2190 .or_default()
2191 .push(GltfAddressabilityTargetChannelV2 {
2192 source_animation_index: animation.source_clip_index(),
2193 source_channel_index: channel.source_channel_index(),
2194 });
2195 }
2196 }
2197 let contributor_count = contributors.len();
2198 let truncated = contributor_count > GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS;
2199 let target_domain_complete = target_domain_evidence_complete(raw, animations);
2200 let mut candidate_paths = BTreeMap::<u64, BTreeSet<Vec<u64>>>::new();
2201 for candidate in raw.path_candidates() {
2202 if let Some(target) = candidate.target_node_index() {
2203 candidate_paths
2204 .entry(target)
2205 .or_default()
2206 .insert(candidate.source_node_indices().to_vec());
2207 }
2208 }
2209 let nodes = raw
2210 .nodes()
2211 .iter()
2212 .map(|node| (node.source_node_index(), node))
2213 .collect::<BTreeMap<_, _>>();
2214 let base_reasons = target_base_reasons(
2215 raw,
2216 animations,
2217 bevy_animation_enabled,
2218 load_animations,
2219 pointer_width,
2220 );
2221 let collision_index = if base_reasons.is_empty() {
2222 let contributing_nodes = contributors.keys().copied().collect::<BTreeSet<_>>();
2223 Some(build_candidate_collision_index(
2224 &candidate_paths,
2225 &contributing_nodes,
2226 &nodes,
2227 pointer_width.expect("complete target prerequisites include pointer width"),
2228 )?)
2229 } else {
2230 None
2231 };
2232 let mut targets = Vec::with_capacity(
2233 contributors
2234 .len()
2235 .min(GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS),
2236 );
2237 for (source_node_index, contributing_channels) in contributors
2238 .into_iter()
2239 .take(GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS)
2240 {
2241 let mut reasons = base_reasons.clone();
2242 let paths = candidate_paths.get(&source_node_index);
2243 let unique_path = match paths {
2244 None => {
2245 reasons.push(GltfAddressabilityUnavailableReasonV2::UnreachableTarget);
2246 None
2247 }
2248 Some(paths) if paths.len() != 1 => {
2249 reasons.push(GltfAddressabilityUnavailableReasonV2::MultipleCandidatePaths);
2250 None
2251 }
2252 Some(paths) => paths.first(),
2253 };
2254 let projection = if reasons.is_empty() {
2255 let node_path = unique_path.expect("one path exists without path reasons");
2256 let segments = node_path
2257 .iter()
2258 .map(|index| {
2259 nodes
2260 .get(index)
2261 .map(|node| {
2262 node.name()
2263 .map_or_else(|| format!("GltfNode{index}"), str::to_owned)
2264 })
2265 .ok_or(GltfAddressabilityV2Error::InventoryBindingMismatch)
2266 })
2267 .collect::<Result<Vec<_>, _>>()?;
2268 let width = pointer_width.expect("width is present without base reason");
2269 project_target_segments(segments, width)
2270 } else {
2271 GltfAddressabilityProjectionV2::unavailable(reasons)
2272 };
2273 targets.push(GltfAddressabilityTargetV2 {
2274 source_node_index,
2275 contributing_channels,
2276 projection,
2277 });
2278 }
2279 if truncated {
2280 for target in &mut targets {
2284 let mut reasons = vec![GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated];
2285 if !target_domain_complete {
2286 reasons.push(GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete);
2287 }
2288 target.projection = GltfAddressabilityProjectionV2::unavailable(reasons);
2289 }
2290 } else if collision_index
2291 .as_ref()
2292 .is_some_and(|index| !index.complete)
2293 {
2294 for target in &mut targets {
2298 if matches!(
2299 target.projection,
2300 GltfAddressabilityProjectionV2::Available { .. }
2301 ) {
2302 target.projection = GltfAddressabilityProjectionV2::unavailable(vec![
2303 GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2304 ]);
2305 }
2306 }
2307 } else {
2308 if let Some(index) = &collision_index {
2309 invalidate_target_collisions_with_index(&mut targets, index);
2310 }
2311 }
2312 let coverage = target_coverage_projection(target_domain_complete, contributor_count);
2313 Ok((coverage, targets))
2314}
2315
2316fn project_target_segments(
2317 segments: Vec<String>,
2318 pointer_width: TargetPointerWidth,
2319) -> GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2> {
2320 let path = segments.join("/");
2321 match bevy_animation_target_id_v1(segments.iter().map(String::as_str), pointer_width) {
2322 Ok(uuid) => GltfAddressabilityProjectionV2::Available {
2323 value: GltfAddressabilityTargetValueV2 {
2324 segments,
2325 path,
2326 uuid,
2327 },
2328 },
2329 Err(_) => GltfAddressabilityProjectionV2::unavailable(vec![
2330 GltfAddressabilityUnavailableReasonV2::PathBoundsExceeded,
2331 ]),
2332 }
2333}
2334
2335fn target_coverage_projection(
2336 target_domain_complete: bool,
2337 contributor_count: usize,
2338) -> GltfAddressabilityProjectionV2<()> {
2339 let mut coverage_reasons = Vec::new();
2340 if !target_domain_complete {
2341 coverage_reasons.push(GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete);
2342 }
2343 if contributor_count > GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS {
2344 coverage_reasons.push(GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated);
2345 }
2346 if coverage_reasons.is_empty() {
2347 GltfAddressabilityProjectionV2::Available { value: () }
2348 } else {
2349 GltfAddressabilityProjectionV2::unavailable(coverage_reasons)
2350 }
2351}
2352
2353fn target_domain_evidence_complete(
2354 raw: &RawGltfAddressabilityInventoryV1,
2355 animations: &GltfAnimationAddressabilityInventoryV1,
2356) -> bool {
2357 raw.node_coverage().is_complete()
2358 && raw.scene_coverage().is_complete()
2359 && raw.path_candidate_coverage().is_complete()
2360 && !animation_inventory_incomplete(animations)
2361}
2362
2363fn target_base_reasons(
2364 raw: &RawGltfAddressabilityInventoryV1,
2365 animations: &GltfAnimationAddressabilityInventoryV1,
2366 bevy_animation_enabled: bool,
2367 load_animations: bool,
2368 pointer_width: Option<TargetPointerWidth>,
2369) -> Vec<GltfAddressabilityUnavailableReasonV2> {
2370 let mut reasons = Vec::new();
2371 if !raw.node_coverage().is_complete()
2372 || !raw.scene_coverage().is_complete()
2373 || !raw.path_candidate_coverage().is_complete()
2374 || !matches!(
2375 animations.animations().coverage().state(),
2376 crate::GltfAnimationCoverageStateV1::Complete
2377 )
2378 || animations.animations().rows().iter().any(|animation| {
2379 !matches!(
2380 animation.channels().coverage().state(),
2381 crate::GltfAnimationCoverageStateV1::Complete
2382 )
2383 })
2384 {
2385 reasons.push(GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete);
2386 }
2387 if !raw.dependency_closure().coverage().is_complete() {
2388 reasons.push(GltfAddressabilityUnavailableReasonV2::DependencyClosureIncomplete);
2389 }
2390 if !bevy_animation_enabled {
2391 reasons.push(GltfAddressabilityUnavailableReasonV2::BevyAnimationFeatureDisabled);
2392 }
2393 if !load_animations {
2394 reasons.push(GltfAddressabilityUnavailableReasonV2::LoadAnimationsDisabled);
2395 }
2396 if pointer_width.is_none() {
2397 reasons.push(GltfAddressabilityUnavailableReasonV2::TargetPointerWidthMissing);
2398 }
2399 reasons
2400}
2401
2402struct CandidateCollisionIndexV2 {
2403 paths: BTreeMap<Vec<String>, BTreeSet<u64>>,
2404 uuids: BTreeMap<String, BTreeSet<u64>>,
2405 complete: bool,
2406}
2407
2408fn build_candidate_collision_index(
2409 candidate_paths: &BTreeMap<u64, BTreeSet<Vec<u64>>>,
2410 contributing_nodes: &BTreeSet<u64>,
2411 nodes: &BTreeMap<u64, &animsmith_core::RawGltfNodeRowV1>,
2412 pointer_width: TargetPointerWidth,
2413) -> Result<CandidateCollisionIndexV2, GltfAddressabilityV2Error> {
2414 let mut index = CandidateCollisionIndexV2 {
2415 paths: BTreeMap::new(),
2416 uuids: BTreeMap::new(),
2417 complete: true,
2418 };
2419 for source_node_index in contributing_nodes {
2420 let Some(paths) = candidate_paths.get(source_node_index) else {
2421 continue;
2422 };
2423 for node_path in paths {
2424 let segments = node_path
2425 .iter()
2426 .map(|node_index| {
2427 nodes
2428 .get(node_index)
2429 .map(|node| {
2430 node.name()
2431 .map_or_else(|| format!("GltfNode{node_index}"), str::to_owned)
2432 })
2433 .ok_or(GltfAddressabilityV2Error::InventoryBindingMismatch)
2434 })
2435 .collect::<Result<Vec<_>, _>>()?;
2436 match bevy_animation_target_id_v1(segments.iter().map(String::as_str), pointer_width) {
2437 Ok(uuid) => {
2438 index
2439 .paths
2440 .entry(segments)
2441 .or_default()
2442 .insert(*source_node_index);
2443 index
2444 .uuids
2445 .entry(uuid)
2446 .or_default()
2447 .insert(*source_node_index);
2448 }
2449 Err(_) => index.complete = false,
2450 }
2451 }
2452 }
2453 Ok(index)
2454}
2455
2456#[cfg(test)]
2457fn invalidate_target_collisions(targets: &mut [GltfAddressabilityTargetV2]) {
2458 let mut index = CandidateCollisionIndexV2 {
2459 paths: BTreeMap::new(),
2460 uuids: BTreeMap::new(),
2461 complete: true,
2462 };
2463 for target in targets.iter() {
2464 if let GltfAddressabilityProjectionV2::Available { value } = &target.projection {
2465 index
2466 .paths
2467 .entry(value.segments.clone())
2468 .or_default()
2469 .insert(target.source_node_index);
2470 index
2471 .uuids
2472 .entry(value.uuid.clone())
2473 .or_default()
2474 .insert(target.source_node_index);
2475 }
2476 }
2477 invalidate_target_collisions_with_index(targets, &index);
2478}
2479
2480fn invalidate_target_collisions_with_index(
2481 targets: &mut [GltfAddressabilityTargetV2],
2482 index: &CandidateCollisionIndexV2,
2483) {
2484 for target in targets.iter_mut() {
2485 let GltfAddressabilityProjectionV2::Available { value } = &target.projection else {
2486 continue;
2487 };
2488 let mut reasons = Vec::new();
2489 if index
2490 .paths
2491 .get(&value.segments)
2492 .is_some_and(|owners| owners.len() > 1)
2493 {
2494 reasons.push(GltfAddressabilityUnavailableReasonV2::DuplicateFullPath);
2495 }
2496 if index
2497 .uuids
2498 .get(&value.uuid)
2499 .is_some_and(|owners| owners.len() > 1)
2500 {
2501 reasons.push(GltfAddressabilityUnavailableReasonV2::TargetIdCollision);
2502 }
2503 if !reasons.is_empty() {
2504 target.projection = GltfAddressabilityProjectionV2::unavailable(reasons);
2505 }
2506 }
2507}
2508
2509#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2511#[non_exhaustive]
2512pub enum GltfAddressabilityV2Error {
2513 #[error("invalid rich glTF addressability schema or header")]
2515 InvalidSchema,
2516 #[error("invalid raw glTF addressability inventory")]
2518 InvalidRawInventory,
2519 #[error("raw and animation addressability inventories are not from the same load")]
2521 InventoryBindingMismatch,
2522 #[error("rich addressability requires exact Bevy 0.19.0 profile revision 3")]
2524 InvalidBevyProfile,
2525 #[error("rich addressability is missing a required Bevy setting")]
2527 MissingRequiredSetting,
2528 #[error("invalid rich addressability prediction provenance")]
2530 InvalidPredictionProvenance,
2531 #[error("invalid Bevy glTF addressability rule bundle")]
2533 InvalidRuleBundle,
2534 #[error("invalid rich engine-addressability check evaluation")]
2536 InvalidCheckEvaluation,
2537}
2538
2539#[cfg(test)]
2540pub(crate) mod tests {
2541 use super::*;
2542 use animsmith_core::{
2543 Clip, Config, DependencyClosureBuilderV1, Document, MetricGrids,
2544 RawGltfAddressabilityCoverageV1, RawGltfAddressabilityInventoryInputV1,
2545 RawGltfInverseBindMatricesObservationV1, RawGltfNodeRowV1, RawGltfScenePathCandidateRowV1,
2546 RawGltfSceneRowV1, RawGltfSkinAttachmentRowV1, RawGltfSkinRowV1, RawSourceFactsBuilderV1,
2547 ResolvedRoles, SourceFactDomainV1, SourceFactSetV1, SourceFormatV1,
2548 SourceLoaderDispositionV1, SourceObservationV1, SourceProvenanceV1, SourceTargetKindV1,
2549 SourceTargetV1, SourceUnavailableReasonV1, ToolSource,
2550 };
2551 use std::io::Cursor;
2552
2553 fn empty_loaded_source() -> LoadedSource {
2554 let primary = InputIdentity::from_bytes(b"rich-addressability");
2555 let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary.clone());
2556 facts.mark_complete(SourceFactDomainV1::Clips);
2557 let closure = DependencyClosureBuilderV1::new(
2558 primary,
2559 facts.resource_coverage(),
2560 facts.resource_rows().len(),
2561 )
2562 .finish()
2563 .unwrap();
2564 facts
2565 .finish_with_dependency_closure(Document::default(), closure)
2566 .unwrap()
2567 }
2568
2569 fn loaded_source_with_targets(
2570 target_node_indices: impl IntoIterator<Item = u64>,
2571 channels_complete: bool,
2572 ) -> LoadedSource {
2573 loaded_source_with_targets_and_closure(target_node_indices, channels_complete, true)
2574 }
2575
2576 fn loaded_source_with_targets_and_closure(
2577 target_node_indices: impl IntoIterator<Item = u64>,
2578 channels_complete: bool,
2579 closure_complete: bool,
2580 ) -> LoadedSource {
2581 let primary = InputIdentity::from_bytes(b"rich-addressability-targets");
2582 let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary.clone());
2583 let channels = target_node_indices
2584 .into_iter()
2585 .enumerate()
2586 .map(|(channel_index, target_node_index)| {
2587 animsmith_core::SourceChannelFactV1::new(
2588 channel_index,
2589 SourceTargetV1::new(SourceTargetKindV1::Node, target_node_index),
2590 animsmith_core::SourceChannelPropertyV1::Translation,
2591 animsmith_core::SourceComponentMaskV1::new(true, true, true),
2592 SourceObservationV1::proven_absent(SourceProvenanceV1::format_defined()),
2593 SourceLoaderDispositionV1::Preserved,
2594 SourceProvenanceV1::format_defined(),
2595 )
2596 .with_accessors(channel_index * 2, channel_index * 2 + 1)
2597 })
2598 .collect::<Vec<_>>();
2599 let channel_set = if channels_complete {
2600 SourceFactSetV1::complete(channels)
2601 } else {
2602 SourceFactSetV1::partial(
2603 channels,
2604 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2605 )
2606 };
2607 assert!(facts.push_clip(animsmith_core::SourceClipFactV1::new(
2608 0,
2609 SourceObservationV1::proven_absent(SourceProvenanceV1::format_defined()),
2610 SourceObservationV1::observed(
2611 0,
2612 SourceProvenanceV1::format_defined(),
2613 SourceLoaderDispositionV1::Normalized,
2614 ),
2615 SourceObservationV1::proven_absent(SourceProvenanceV1::format_defined()),
2616 SourceObservationV1::proven_absent(SourceProvenanceV1::format_defined()),
2617 channel_set,
2618 )));
2619 facts.mark_complete(SourceFactDomainV1::Clips);
2620 facts.mark_complete(SourceFactDomainV1::Constructs);
2621 facts.mark_complete(SourceFactDomainV1::Resources);
2622 let closure = DependencyClosureBuilderV1::new(
2623 primary,
2624 facts.resource_coverage(),
2625 facts.resource_rows().len(),
2626 )
2627 .finish()
2628 .unwrap();
2629 let document = Document {
2630 clips: vec![Clip {
2631 name: "clip-0".into(),
2632 duration_s: 0.0,
2633 tracks: Vec::new(),
2634 }],
2635 ..Document::default()
2636 };
2637 if closure_complete {
2638 facts
2639 .finish_with_dependency_closure(document, closure)
2640 .unwrap()
2641 } else {
2642 facts.finish(document).unwrap()
2643 }
2644 }
2645
2646 fn raw_for_graph(
2647 source: &LoadedSource,
2648 scenes: Vec<RawGltfSceneRowV1>,
2649 scene_coverage: RawGltfAddressabilityCoverageV1,
2650 nodes: Vec<RawGltfNodeRowV1>,
2651 node_coverage: RawGltfAddressabilityCoverageV1,
2652 path_candidates: Vec<RawGltfScenePathCandidateRowV1>,
2653 path_coverage: RawGltfAddressabilityCoverageV1,
2654 ) -> RawGltfAddressabilityInventoryV1 {
2655 RawGltfAddressabilityInventoryV1::new(
2656 source.source_facts().primary_identity().clone(),
2657 source.dependency_closure().clone(),
2658 RawGltfAddressabilityInventoryInputV1 {
2659 default_scene: RawGltfDefaultSceneObservationV1::Absent,
2660 scene_coverage,
2661 scenes,
2662 node_coverage,
2663 nodes,
2664 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
2665 skins: Vec::new(),
2666 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
2667 attachments: Vec::new(),
2668 path_candidate_coverage: path_coverage,
2669 path_candidates,
2670 },
2671 )
2672 .unwrap()
2673 }
2674
2675 fn simple_target_raw(
2676 source: &LoadedSource,
2677 target_name: Option<String>,
2678 ) -> RawGltfAddressabilityInventoryV1 {
2679 raw_for_graph(
2680 source,
2681 vec![RawGltfSceneRowV1::new(0, None, vec![0])],
2682 RawGltfAddressabilityCoverageV1::Complete,
2683 vec![RawGltfNodeRowV1::new(0, target_name, None, Vec::new())],
2684 RawGltfAddressabilityCoverageV1::Complete,
2685 vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![0])],
2686 RawGltfAddressabilityCoverageV1::Complete,
2687 )
2688 }
2689
2690 fn wide_target_raw(
2691 source: &LoadedSource,
2692 retained_target_count: usize,
2693 target_domain_complete: bool,
2694 ) -> RawGltfAddressabilityInventoryV1 {
2695 let retained_indices = (0..retained_target_count as u64).collect::<Vec<_>>();
2696 let target_coverage = if target_domain_complete {
2697 RawGltfAddressabilityCoverageV1::Complete
2698 } else {
2699 RawGltfAddressabilityCoverageV1::budget_exceeded()
2700 };
2701 raw_for_graph(
2702 source,
2703 vec![RawGltfSceneRowV1::new(0, None, retained_indices.clone())],
2704 RawGltfAddressabilityCoverageV1::Complete,
2705 retained_indices
2706 .iter()
2707 .map(|index| RawGltfNodeRowV1::new(*index, None, None, Vec::new()))
2708 .collect(),
2709 target_coverage,
2710 retained_indices
2711 .iter()
2712 .enumerate()
2713 .map(|(ordinal, index)| {
2714 RawGltfScenePathCandidateRowV1::new(ordinal as u64, 0, vec![*index])
2715 })
2716 .collect(),
2717 target_coverage,
2718 )
2719 }
2720
2721 pub(crate) fn adapter_report_json(
2722 pointer_width: Option<TargetPointerWidth>,
2723 ) -> serde_json::Value {
2724 let source = loaded_source_with_targets([0], true);
2725 let raw = simple_target_raw(&source, Some("target".into()));
2726 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
2727 let resolved = crate::resolve_static_v2(crate::EngineDeclarationV2 {
2728 selection: Some(crate::ProfileSelection::new(
2729 "bevy",
2730 3,
2731 "0.19.0",
2732 "gltf-asset-loader",
2733 )),
2734 document_settings: Some(BTreeMap::from([
2735 (
2736 "bevy_animation_feature".into(),
2737 crate::SettingValueV2::Boolean(true),
2738 ),
2739 (
2740 "extension_handler_environment".into(),
2741 crate::SettingValueV2::HandlerEnvironment(
2742 crate::BevyGltfHandlerEnvironmentV2::BareEmpty,
2743 ),
2744 ),
2745 ])),
2746 ..crate::EngineDeclarationV2::default()
2747 })
2748 .unwrap()
2749 .unwrap()
2750 .resolve_input(SourceFormatV1::GltfJson)
2751 .unwrap();
2752 let provenance =
2753 crate::project_prediction_provenance_v4(&resolved, &source, Vec::new()).unwrap();
2754 let grids = MetricGrids::new(source.document());
2755 let roles = ResolvedRoles::default();
2756 let config = Config::default();
2757 let adapter = build_bevy_addressability_adapter_v2(
2758 &source,
2759 &raw,
2760 &animations,
2761 &resolved,
2762 provenance,
2763 pointer_width,
2764 &CheckCtx::new(&grids, &roles, &config),
2765 )
2766 .unwrap()
2767 .unwrap();
2768 let report = GltfAddressabilityV2::new(
2769 ToolInfo::animsmith(ToolSource::new(None, None)),
2770 raw,
2771 animations,
2772 Some(adapter),
2773 )
2774 .unwrap();
2775 let bytes = serde_json::to_vec(&report).unwrap();
2776 GltfAddressabilityV2::read_from(Cursor::new(&bytes)).unwrap();
2777 serde_json::from_slice(&bytes).unwrap()
2778 }
2779
2780 fn prediction_provenance_fixture() -> PredictionProvenanceV4 {
2781 serde_json::from_value(
2782 adapter_report_json(Some(TargetPointerWidth::Bits64))["bevy"]["prediction_provenance"]
2783 .clone(),
2784 )
2785 .unwrap()
2786 }
2787
2788 fn analytic_raw(source: &LoadedSource) -> RawGltfAddressabilityInventoryV1 {
2789 RawGltfAddressabilityInventoryV1::new(
2790 source.source_facts().primary_identity().clone(),
2791 source.dependency_closure().clone(),
2792 RawGltfAddressabilityInventoryInputV1 {
2793 default_scene: RawGltfDefaultSceneObservationV1::Selected {
2794 source_scene_index: 0,
2795 },
2796 scene_coverage: RawGltfAddressabilityCoverageV1::Complete,
2797 scenes: vec![
2798 RawGltfSceneRowV1::new(0, Some("World".into()), vec![0]),
2799 RawGltfSceneRowV1::new(1, Some("World".into()), vec![0]),
2800 ],
2801 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
2802 nodes: vec![RawGltfNodeRowV1::new(0, None, None, Vec::new())],
2803 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
2804 skins: vec![
2805 RawGltfSkinRowV1::new(
2806 0,
2807 Some("Rig".into()),
2808 vec![0],
2809 Some(0),
2810 RawGltfInverseBindMatricesObservationV1::Absent,
2811 ),
2812 RawGltfSkinRowV1::new(
2813 1,
2814 Some("Rig".into()),
2815 vec![0],
2816 None,
2817 RawGltfInverseBindMatricesObservationV1::Absent,
2818 ),
2819 ],
2820 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
2821 attachments: vec![RawGltfSkinAttachmentRowV1::new(0, 0)],
2822 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
2823 path_candidates: vec![
2824 RawGltfScenePathCandidateRowV1::new(0, 0, vec![0]),
2825 RawGltfScenePathCandidateRowV1::new(1, 1, vec![0]),
2826 ],
2827 },
2828 )
2829 .unwrap()
2830 }
2831
2832 #[test]
2833 fn frozen_rule_bundle_is_strict_and_commit_pinned() {
2834 for width in [
2835 None,
2836 Some(TargetPointerWidth::Bits32),
2837 Some(TargetPointerWidth::Bits64),
2838 ] {
2839 let bundle = BevyGltfAddressabilityRulesV1::frozen(width);
2840 assert!(bundle.validate());
2841 assert_eq!(bundle.profile_revision(), 3);
2842 assert_eq!(bundle.sources().len(), 6);
2843 assert!(
2844 bundle
2845 .sources()
2846 .windows(2)
2847 .all(|pair| pair[0].id() < pair[1].id())
2848 );
2849 assert!(
2850 bundle
2851 .sources()
2852 .iter()
2853 .all(|source| source.url().contains(BEVY_COMMIT))
2854 );
2855 }
2856 }
2857
2858 #[test]
2859 fn target_id_golden_vectors_cover_both_pointer_widths_and_boundaries() {
2860 let paths: &[&[&str]] = &[
2864 &[],
2865 &["Root"],
2866 &["Root", "Hips", "GltfNode7"],
2867 &["ab", "c"],
2868 &["a", "bc"],
2869 ];
2870 let expected_32 = [
2871 "77da024c-dd93-5d43-8f9a-2af3d4b17c5b",
2872 "2514296d-dbd8-5ea8-a8cc-1fadaf961d2f",
2873 "15c3912b-7127-55c1-b5ff-83950e3d0f9a",
2874 "fd324118-d34f-53d0-9787-d5a1594985e7",
2875 "2ceb26c6-8d62-53c7-b4c5-ee3b9b81784e",
2876 ];
2877 let expected_64 = [
2878 "77da024c-dd93-5d43-8f9a-2af3d4b17c5b",
2879 "05c24be8-bacd-5ee9-9a65-f8215983900e",
2880 "90fed833-c803-5800-883d-adee90aab446",
2881 "0d54b499-ea4e-5a49-afec-0a87e56da473",
2882 "c22eb6e6-9f59-509b-9cfc-79d424c0d445",
2883 ];
2884 for ((path, expected_32), expected_64) in paths.iter().zip(expected_32).zip(expected_64) {
2885 assert_eq!(
2886 bevy_animation_target_id_v1(path.iter().copied(), TargetPointerWidth::Bits32)
2887 .unwrap(),
2888 expected_32
2889 );
2890 assert_eq!(
2891 bevy_animation_target_id_v1(path.iter().copied(), TargetPointerWidth::Bits64)
2892 .unwrap(),
2893 expected_64
2894 );
2895 }
2896
2897 let max_segment = "x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES);
2898 assert!(
2899 bevy_animation_target_id_v1([max_segment.as_str()], TargetPointerWidth::Bits64).is_ok()
2900 );
2901 let over_segment = format!("{max_segment}x");
2902 assert!(matches!(
2903 bevy_animation_target_id_v1([over_segment.as_str()], TargetPointerWidth::Bits64),
2904 Err(BevyAnimationTargetIdError::SegmentTooLong { .. })
2905 ));
2906
2907 let at_path_limit = [
2908 "a".repeat(1024),
2909 "b".repeat(1024),
2910 "c".repeat(1024),
2911 "d".repeat(1021),
2912 ];
2913 assert!(
2914 bevy_animation_target_id_v1(
2915 at_path_limit.iter().map(String::as_str),
2916 TargetPointerWidth::Bits64,
2917 )
2918 .is_ok()
2919 );
2920 let over_path_limit = [
2921 "a".repeat(1024),
2922 "b".repeat(1024),
2923 "c".repeat(1024),
2924 "d".repeat(1022),
2925 ];
2926 assert!(matches!(
2927 bevy_animation_target_id_v1(
2928 over_path_limit.iter().map(String::as_str),
2929 TargetPointerWidth::Bits64,
2930 ),
2931 Err(BevyAnimationTargetIdError::PathTooLong {
2932 found: 4097,
2933 limit: 4096
2934 })
2935 ));
2936 assert!(
2937 bevy_animation_target_id_v1(
2938 std::iter::repeat_n("", GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS),
2939 TargetPointerWidth::Bits64,
2940 )
2941 .is_ok()
2942 );
2943 assert!(matches!(
2944 bevy_animation_target_id_v1(
2945 std::iter::repeat_n("", GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS + 1),
2946 TargetPointerWidth::Bits64,
2947 ),
2948 Err(BevyAnimationTargetIdError::TooManySegments {
2949 found: 257,
2950 limit: 256
2951 })
2952 ));
2953 }
2954
2955 #[test]
2956 fn analytic_labels_default_route_and_named_winners_follow_bevy_rules() {
2957 let source = empty_loaded_source();
2958 let raw = analytic_raw(&source);
2959 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
2960 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
2961 &raw,
2962 &animations,
2963 true,
2964 true,
2965 Some(TargetPointerWidth::Bits64),
2966 )
2967 .unwrap();
2968 assert_eq!(
2969 projection
2970 .scenes()
2971 .iter()
2972 .map(GltfAddressabilitySceneV2::label)
2973 .collect::<Vec<_>>(),
2974 ["Scene0", "Scene1"]
2975 );
2976 assert_eq!(
2977 projection.default_scene_route(),
2978 &GltfAddressabilityProjectionV2::Available {
2979 value: "Scene0".into()
2980 }
2981 );
2982 assert!(matches!(
2983 projection.skins()[0].skin_label(),
2984 GltfAddressabilityProjectionV2::Available { value } if value == "Skin0"
2985 ));
2986 assert!(matches!(
2987 projection.skins()[1].skin_label(),
2988 GltfAddressabilityProjectionV2::ProvenAbsent
2989 ));
2990 assert_eq!(
2991 projection.skins()[1].inverse_bind_matrices_label(),
2992 "Skin1/InverseBindMatrices"
2993 );
2994 let scene_map = &projection.named_maps()[0];
2995 assert!(matches!(
2996 &scene_map.winners,
2997 GltfAddressabilityProjectionV2::Available { value }
2998 if value.len() == 1 && value[0].source_index() == 1
2999 ));
3000 let skin_map = &projection.named_maps()[2];
3001 assert!(matches!(
3002 &skin_map.winners,
3003 GltfAddressabilityProjectionV2::Available { value }
3004 if value.len() == 1 && value[0].source_index() == 0
3005 ));
3006 }
3007
3008 #[test]
3009 fn retained_default_scene_is_exact_under_partial_tail_coverage() {
3010 let source = empty_loaded_source();
3011 let raw = RawGltfAddressabilityInventoryV1::new(
3012 source.source_facts().primary_identity().clone(),
3013 source.dependency_closure().clone(),
3014 RawGltfAddressabilityInventoryInputV1 {
3015 default_scene: RawGltfDefaultSceneObservationV1::Selected {
3016 source_scene_index: 0,
3017 },
3018 scene_coverage: RawGltfAddressabilityCoverageV1::budget_exceeded(),
3019 scenes: vec![RawGltfSceneRowV1::new(0, None, Vec::new())],
3020 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
3021 nodes: Vec::new(),
3022 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
3023 skins: Vec::new(),
3024 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
3025 attachments: Vec::new(),
3026 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
3027 path_candidates: Vec::new(),
3028 },
3029 )
3030 .unwrap();
3031 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3032 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3033 &raw,
3034 &animations,
3035 true,
3036 true,
3037 Some(TargetPointerWidth::Bits64),
3038 )
3039 .unwrap();
3040 assert_eq!(
3041 projection.default_scene_route(),
3042 &GltfAddressabilityProjectionV2::Available {
3043 value: "Scene0".into()
3044 }
3045 );
3046 }
3047
3048 #[test]
3049 fn named_map_keys_enforce_exact_segment_bound() {
3050 let winner = |name: String| {
3051 let mut winners = BTreeMap::new();
3052 winners.insert(
3053 name.clone(),
3054 GltfAddressabilityNamedMapWinnerV2 {
3055 name,
3056 source_index: 0,
3057 typed_label: "Scene0".into(),
3058 },
3059 );
3060 named_map(GltfAddressabilityNamedMapKindV2::Scene, true, winners)
3061 };
3062 assert!(matches!(
3063 winner("x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES)).winners(),
3064 GltfAddressabilityProjectionV2::Available { .. }
3065 ));
3066 assert!(matches!(
3067 winner("x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES + 1)).winners(),
3068 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3069 if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3070 ));
3071 }
3072
3073 #[test]
3074 fn incomplete_target_evidence_never_proves_complete_empty_or_prefix_coverage() {
3075 for targets in [Vec::new(), vec![0]] {
3076 let source = loaded_source_with_targets(targets, false);
3077 let raw = if source.source_facts().clips().rows()[0]
3078 .channels()
3079 .rows()
3080 .is_empty()
3081 {
3082 raw_for_graph(
3083 &source,
3084 Vec::new(),
3085 RawGltfAddressabilityCoverageV1::Complete,
3086 Vec::new(),
3087 RawGltfAddressabilityCoverageV1::Complete,
3088 Vec::new(),
3089 RawGltfAddressabilityCoverageV1::Complete,
3090 )
3091 } else {
3092 simple_target_raw(&source, Some("target".into()))
3093 };
3094 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3095 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3096 &raw,
3097 &animations,
3098 true,
3099 true,
3100 Some(TargetPointerWidth::Bits64),
3101 )
3102 .unwrap();
3103 assert!(matches!(
3104 projection.target_coverage(),
3105 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3106 if reasons == &[GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3107 ));
3108 }
3109
3110 assert!(matches!(
3111 target_coverage_projection(true, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS),
3112 GltfAddressabilityProjectionV2::Available { .. }
3113 ));
3114 assert!(matches!(
3115 target_coverage_projection(true, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 1),
3116 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3117 if reasons == [GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated]
3118 ));
3119 assert!(matches!(
3120 target_coverage_projection(false, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 1),
3121 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3122 if reasons == [
3123 GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
3124 GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated,
3125 ]
3126 ));
3127 }
3128
3129 #[test]
3130 fn production_target_projection_retains_exact_n_and_truncates_n_plus_one() {
3131 let exact_count = GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS;
3132 let exact_source = loaded_source_with_targets(0..exact_count as u64, true);
3133 let exact_raw = wide_target_raw(&exact_source, exact_count, true);
3134 let exact_animations =
3135 GltfAnimationAddressabilityInventoryV1::from_source(&exact_source).unwrap();
3136 let exact = BevyGltfAddressabilityProjectionV2::from_inventories(
3137 &exact_raw,
3138 &exact_animations,
3139 true,
3140 true,
3141 Some(TargetPointerWidth::Bits64),
3142 )
3143 .unwrap();
3144 assert!(matches!(
3145 exact.target_coverage(),
3146 GltfAddressabilityProjectionV2::Available { .. }
3147 ));
3148 assert_eq!(exact.targets().len(), exact_count);
3149 assert_eq!(
3150 exact
3151 .targets()
3152 .iter()
3153 .map(GltfAddressabilityTargetV2::source_node_index)
3154 .collect::<Vec<_>>(),
3155 (0..exact_count as u64).collect::<Vec<_>>()
3156 );
3157 assert!(exact.targets().iter().all(|target| matches!(
3158 target.projection(),
3159 GltfAddressabilityProjectionV2::Available { .. }
3160 )));
3161
3162 let over_count = exact_count + 1;
3163 let over_source = loaded_source_with_targets(0..over_count as u64, true);
3164 let over_raw = wide_target_raw(&over_source, exact_count, false);
3168 let over_animations =
3169 GltfAnimationAddressabilityInventoryV1::from_source(&over_source).unwrap();
3170 let over = BevyGltfAddressabilityProjectionV2::from_inventories(
3171 &over_raw,
3172 &over_animations,
3173 true,
3174 true,
3175 Some(TargetPointerWidth::Bits64),
3176 )
3177 .unwrap();
3178 assert_eq!(over.targets().len(), exact_count);
3179 assert_eq!(
3180 over.targets()
3181 .iter()
3182 .map(GltfAddressabilityTargetV2::source_node_index)
3183 .collect::<Vec<_>>(),
3184 (0..exact_count as u64).collect::<Vec<_>>()
3185 );
3186 let expected_reasons = [
3187 GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
3188 GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated,
3189 ];
3190 assert!(matches!(
3191 over.target_coverage(),
3192 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3193 if reasons.as_slice() == expected_reasons
3194 ));
3195 assert!(over.targets().iter().all(|target| matches!(
3196 target.projection(),
3197 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3198 if reasons.as_slice() == expected_reasons
3199 )));
3200 assert_eq!(
3201 over.targets().last().unwrap().contributing_channels(),
3202 &[GltfAddressabilityTargetChannelV2 {
3203 source_animation_index: 0,
3204 source_channel_index: (exact_count - 1) as u64,
3205 }]
3206 );
3207 }
3208
3209 #[test]
3210 fn unnamed_target_nodes_use_bevys_exact_source_index_fallback() {
3211 let source = loaded_source_with_targets([7], true);
3212 let raw = raw_for_graph(
3213 &source,
3214 vec![RawGltfSceneRowV1::new(0, None, vec![7])],
3215 RawGltfAddressabilityCoverageV1::Complete,
3216 (0..=7)
3217 .map(|index| RawGltfNodeRowV1::new(index, None, None, Vec::new()))
3218 .collect(),
3219 RawGltfAddressabilityCoverageV1::Complete,
3220 vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![7])],
3221 RawGltfAddressabilityCoverageV1::Complete,
3222 );
3223 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3224 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3225 &raw,
3226 &animations,
3227 true,
3228 true,
3229 Some(TargetPointerWidth::Bits64),
3230 )
3231 .unwrap();
3232 let GltfAddressabilityProjectionV2::Available { value } =
3233 projection.targets()[0].projection()
3234 else {
3235 panic!("complete unnamed target must be exactly projectable");
3236 };
3237 assert_eq!(value.segments(), ["GltfNode7"]);
3238 assert_eq!(value.path(), "GltfNode7");
3239 assert_eq!(
3240 value.uuid(),
3241 bevy_animation_target_id_v1(["GltfNode7"], TargetPointerWidth::Bits64).unwrap()
3242 );
3243 }
3244
3245 #[test]
3246 fn each_raw_target_domain_and_dependency_closure_fail_closed_with_causal_reason() {
3247 for partial_domain in ["scene", "node", "path"] {
3248 let source = loaded_source_with_targets([0], true);
3249 let partial = RawGltfAddressabilityCoverageV1::budget_exceeded();
3250 let complete = RawGltfAddressabilityCoverageV1::Complete;
3251 let raw = raw_for_graph(
3252 &source,
3253 vec![RawGltfSceneRowV1::new(0, None, vec![0])],
3254 if partial_domain == "scene" {
3255 partial
3256 } else {
3257 complete
3258 },
3259 vec![RawGltfNodeRowV1::new(
3260 0,
3261 Some("target".into()),
3262 None,
3263 Vec::new(),
3264 )],
3265 if partial_domain == "node" {
3266 partial
3267 } else {
3268 complete
3269 },
3270 vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![0])],
3271 if partial_domain == "path" {
3272 partial
3273 } else {
3274 complete
3275 },
3276 );
3277 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3278 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3279 &raw,
3280 &animations,
3281 true,
3282 true,
3283 Some(TargetPointerWidth::Bits64),
3284 )
3285 .unwrap();
3286 assert!(
3287 matches!(
3288 projection.target_coverage(),
3289 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3290 if reasons.as_slice()
3291 == [GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3292 ),
3293 "partial {partial_domain} coverage must invalidate target coverage"
3294 );
3295 assert!(
3296 matches!(
3297 projection.targets()[0].projection(),
3298 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3299 if reasons.as_slice()
3300 == [GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3301 ),
3302 "partial {partial_domain} coverage must invalidate every target row"
3303 );
3304 }
3305
3306 let source = loaded_source_with_targets_and_closure([0], true, false);
3307 assert!(!source.dependency_closure().coverage().is_complete());
3308 let raw = simple_target_raw(&source, Some("target".into()));
3309 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3310 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3311 &raw,
3312 &animations,
3313 true,
3314 true,
3315 Some(TargetPointerWidth::Bits64),
3316 )
3317 .unwrap();
3318 assert!(matches!(
3319 projection.target_coverage(),
3320 GltfAddressabilityProjectionV2::Available { .. }
3321 ));
3322 assert!(matches!(
3323 projection.targets()[0].projection(),
3324 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3325 if reasons.as_slice() == [
3326 GltfAddressabilityUnavailableReasonV2::DependencyClosureIncomplete,
3327 ]
3328 ));
3329 }
3330
3331 #[test]
3332 fn rich_unavailable_facets_compact_only_after_the_exact_file_limit() {
3333 let source = loaded_source_with_targets([0], true);
3334 let raw = simple_target_raw(&source, Some("target".into()));
3335 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3336 let provenance = prediction_provenance_fixture();
3337 let inventory =
3338 GltfAddressabilityInventoryV2::new(raw.clone(), animations.clone()).unwrap();
3339 let base = BevyGltfAddressabilityProjectionV2::from_inventories(
3340 &raw,
3341 &animations,
3342 true,
3343 true,
3344 Some(TargetPointerWidth::Bits64),
3345 )
3346 .unwrap();
3347
3348 for count in [
3349 PREDICTION_V1_MAX_FACETS_PER_FILE,
3350 PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3351 ] {
3352 let mut projection = base.clone();
3353 projection.targets = (0..count as u64)
3354 .map(|source_node_index| GltfAddressabilityTargetV2 {
3355 source_node_index,
3356 contributing_channels: vec![GltfAddressabilityTargetChannelV2 {
3357 source_animation_index: 0,
3358 source_channel_index: source_node_index,
3359 }],
3360 projection: GltfAddressabilityProjectionV2::unavailable(vec![
3361 GltfAddressabilityUnavailableReasonV2::TargetPointerWidthMissing,
3362 ]),
3363 })
3364 .collect();
3365 let output =
3366 build_check_output(&source, &raw, &animations, &provenance, &projection).unwrap();
3367 let check =
3368 CheckEvaluation::evaluated(crate::ENGINE_ADDRESSABILITY_CHECK_ID, output).unwrap();
3369 let expected = expected_facet_specs(&inventory, &projection);
3370 validate_facet_states(
3371 check.engine_prediction_v4(),
3372 check.evaluated_scopes(),
3373 check.evaluation(),
3374 &expected,
3375 )
3376 .unwrap();
3377
3378 let facets = check.engine_prediction_v4().unwrap().facets();
3379 if count == PREDICTION_V1_MAX_FACETS_PER_FILE {
3380 assert_eq!(facets.len(), PREDICTION_V1_MAX_FACETS_PER_FILE);
3381 assert!(facets.iter().all(|facet| {
3382 facet.scope().code == EvaluationScopeCode::ANIMATION_TARGET_ID
3383 && facet.reasons()
3384 == [PredictionUnavailableReasonV2::ProjectIntentUnavailable]
3385 }));
3386 } else {
3387 assert_eq!(facets.len(), 1);
3388 assert_eq!(facets[0].scope().code.as_str(), FACET_BUDGET_SCOPE);
3389 assert!(facets[0].scope().subject.is_none());
3390 assert_eq!(
3391 facets[0].reasons(),
3392 [PredictionUnavailableReasonV2::FacetBudgetExceeded]
3393 );
3394 }
3395 }
3396 }
3397
3398 #[test]
3399 fn evaluated_scope_guard_accepts_exact_n_and_rejects_n_plus_one() {
3400 let scopes = (0..=GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES)
3401 .map(|index| {
3402 EvaluationScope::new(EvaluationScopeCode::SCENE_ASSET_LABEL)
3403 .subject(format!("Scene{index}"))
3404 })
3405 .collect::<Vec<_>>();
3406 let expected = scopes
3407 .iter()
3408 .cloned()
3409 .map(|scope| ExpectedFacetV2 {
3410 scope,
3411 state: EnginePredictionFacetStateV1::Available,
3412 reasons: Vec::new(),
3413 })
3414 .collect::<Vec<_>>();
3415 validate_facet_states(
3416 None,
3417 &scopes[..GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES],
3418 EvaluationState::Complete,
3419 &expected[..GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES],
3420 )
3421 .unwrap();
3422 assert_eq!(
3423 validate_facet_states(None, &scopes, EvaluationState::Complete, &expected),
3424 Err(GltfAddressabilityV2Error::InvalidCheckEvaluation)
3425 );
3426 }
3427
3428 #[test]
3429 fn feature_and_loader_settings_are_independently_required_for_targets() {
3430 let source = loaded_source_with_targets([0], true);
3431 let raw = simple_target_raw(&source, Some("target".into()));
3432 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3433 for (feature, load, expected) in [
3434 (
3435 false,
3436 true,
3437 GltfAddressabilityUnavailableReasonV2::BevyAnimationFeatureDisabled,
3438 ),
3439 (
3440 true,
3441 false,
3442 GltfAddressabilityUnavailableReasonV2::LoadAnimationsDisabled,
3443 ),
3444 ] {
3445 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3446 &raw,
3447 &animations,
3448 feature,
3449 load,
3450 Some(TargetPointerWidth::Bits64),
3451 )
3452 .unwrap();
3453 assert!(matches!(
3454 projection.targets()[0].projection(),
3455 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3456 if reasons.contains(&expected)
3457 ));
3458 }
3459 }
3460
3461 #[test]
3462 fn overlong_runtime_path_maps_to_typed_unavailable_after_the_exact_boundary() {
3463 let segments = vec![
3464 "a".repeat(1024),
3465 "b".repeat(1024),
3466 "c".repeat(1024),
3467 "d".repeat(1022),
3468 ];
3469 let projection = project_target_segments(segments, TargetPointerWidth::Bits64);
3470 assert!(matches!(
3471 projection,
3472 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3473 if reasons == [GltfAddressabilityUnavailableReasonV2::PathBoundsExceeded]
3474 ));
3475 }
3476
3477 #[test]
3478 fn slash_joined_display_paths_do_not_alias_segment_vectors() {
3479 let target = |source_node_index, segments: Vec<&str>, width| {
3480 let owned = segments
3481 .iter()
3482 .map(|segment| (*segment).to_owned())
3483 .collect::<Vec<_>>();
3484 GltfAddressabilityTargetV2 {
3485 source_node_index,
3486 contributing_channels: vec![GltfAddressabilityTargetChannelV2 {
3487 source_animation_index: 0,
3488 source_channel_index: source_node_index,
3489 }],
3490 projection: GltfAddressabilityProjectionV2::Available {
3491 value: GltfAddressabilityTargetValueV2 {
3492 path: owned.join("/"),
3493 uuid: bevy_animation_target_id_v1(segments, width).unwrap(),
3494 segments: owned,
3495 },
3496 },
3497 }
3498 };
3499 let mut targets = vec![
3500 target(0, vec!["a/b", "c"], TargetPointerWidth::Bits64),
3501 target(1, vec!["a", "b/c"], TargetPointerWidth::Bits64),
3502 ];
3503 invalidate_target_collisions(&mut targets);
3504 assert!(targets.iter().all(|target| matches!(
3505 target.projection(),
3506 GltfAddressabilityProjectionV2::Available { .. }
3507 )));
3508 }
3509
3510 #[test]
3511 fn multi_path_candidates_participate_in_global_collision_analysis() {
3512 let source = loaded_source_with_targets([1, 3], true);
3513 let raw = raw_for_graph(
3514 &source,
3515 vec![
3516 RawGltfSceneRowV1::new(0, None, vec![0]),
3517 RawGltfSceneRowV1::new(1, None, vec![1]),
3518 RawGltfSceneRowV1::new(2, None, vec![2]),
3519 ],
3520 RawGltfAddressabilityCoverageV1::Complete,
3521 vec![
3522 RawGltfNodeRowV1::new(0, Some("a".into()), None, vec![1]),
3523 RawGltfNodeRowV1::new(1, Some("b".into()), Some(0), Vec::new()),
3524 RawGltfNodeRowV1::new(2, Some("a".into()), None, vec![3]),
3525 RawGltfNodeRowV1::new(3, Some("b".into()), Some(2), Vec::new()),
3526 ],
3527 RawGltfAddressabilityCoverageV1::Complete,
3528 vec![
3529 RawGltfScenePathCandidateRowV1::new(0, 0, vec![0]),
3530 RawGltfScenePathCandidateRowV1::new(1, 0, vec![0, 1]),
3531 RawGltfScenePathCandidateRowV1::new(2, 1, vec![1]),
3532 RawGltfScenePathCandidateRowV1::new(3, 2, vec![2]),
3533 RawGltfScenePathCandidateRowV1::new(4, 2, vec![2, 3]),
3534 ],
3535 RawGltfAddressabilityCoverageV1::Complete,
3536 );
3537 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3538 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3539 &raw,
3540 &animations,
3541 true,
3542 true,
3543 Some(TargetPointerWidth::Bits64),
3544 )
3545 .unwrap();
3546 assert!(matches!(
3547 projection.targets()[0].projection(),
3548 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3549 if reasons.contains(&GltfAddressabilityUnavailableReasonV2::MultipleCandidatePaths)
3550 ));
3551 assert!(matches!(
3552 projection.targets()[1].projection(),
3553 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3554 if reasons.contains(&GltfAddressabilityUnavailableReasonV2::DuplicateFullPath)
3555 && reasons.contains(&GltfAddressabilityUnavailableReasonV2::TargetIdCollision)
3556 ));
3557 }
3558
3559 #[test]
3560 fn target_id_collision_helper_invalidates_distinct_paths() {
3561 let mut targets = vec![
3562 GltfAddressabilityTargetV2 {
3563 source_node_index: 0,
3564 contributing_channels: Vec::new(),
3565 projection: GltfAddressabilityProjectionV2::Available {
3566 value: GltfAddressabilityTargetValueV2 {
3567 segments: vec!["left".into()],
3568 path: "left".into(),
3569 uuid: "00000000-0000-5000-8000-000000000000".into(),
3570 },
3571 },
3572 },
3573 GltfAddressabilityTargetV2 {
3574 source_node_index: 1,
3575 contributing_channels: Vec::new(),
3576 projection: GltfAddressabilityProjectionV2::Available {
3577 value: GltfAddressabilityTargetValueV2 {
3578 segments: vec!["right".into()],
3579 path: "right".into(),
3580 uuid: "00000000-0000-5000-8000-000000000000".into(),
3581 },
3582 },
3583 },
3584 ];
3585 invalidate_target_collisions(&mut targets);
3586 assert!(targets.iter().all(|target| matches!(
3587 target.projection(),
3588 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3589 if reasons == &[GltfAddressabilityUnavailableReasonV2::TargetIdCollision]
3590 )));
3591 }
3592
3593 #[test]
3594 fn named_map_incomplete_and_projection_reference_bounds_are_typed() {
3595 let source = empty_loaded_source();
3596 let raw = raw_for_graph(
3597 &source,
3598 vec![RawGltfSceneRowV1::new(
3599 0,
3600 Some("retained".into()),
3601 Vec::new(),
3602 )],
3603 RawGltfAddressabilityCoverageV1::budget_exceeded(),
3604 Vec::new(),
3605 RawGltfAddressabilityCoverageV1::Complete,
3606 Vec::new(),
3607 RawGltfAddressabilityCoverageV1::Complete,
3608 );
3609 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3610 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3611 &raw,
3612 &animations,
3613 true,
3614 true,
3615 Some(TargetPointerWidth::Bits64),
3616 )
3617 .unwrap();
3618 assert!(matches!(
3619 projection.named_maps()[0].winners(),
3620 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3621 if reasons == &[GltfAddressabilityUnavailableReasonV2::NamedMapIncomplete]
3622 ));
3623
3624 let bounded_projection = |channel_count| BevyGltfAddressabilityProjectionV2 {
3625 scenes: Vec::new(),
3626 default_scene_route: GltfAddressabilityProjectionV2::ProvenAbsent,
3627 skins: Vec::new(),
3628 named_maps: Vec::new(),
3629 target_coverage: GltfAddressabilityProjectionV2::Available { value: () },
3630 targets: vec![GltfAddressabilityTargetV2 {
3631 source_node_index: 0,
3632 contributing_channels: vec![
3633 GltfAddressabilityTargetChannelV2 {
3634 source_animation_index: 0,
3635 source_channel_index: 0,
3636 };
3637 channel_count
3638 ],
3639 projection: GltfAddressabilityProjectionV2::Available {
3640 value: GltfAddressabilityTargetValueV2 {
3641 segments: vec!["target".into()],
3642 path: "target".into(),
3643 uuid: "00000000-0000-5000-8000-000000000000".into(),
3644 },
3645 },
3646 }],
3647 };
3648 let mut exact = bounded_projection(GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES - 1);
3649 normalize_projection_bounds(&mut exact);
3650 assert!(matches!(
3651 exact.targets()[0].projection(),
3652 GltfAddressabilityProjectionV2::Available { .. }
3653 ));
3654 let mut over = bounded_projection(GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES);
3655 normalize_projection_bounds(&mut over);
3656 assert!(matches!(
3657 over.target_coverage(),
3658 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3659 if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3660 ));
3661 }
3662
3663 #[test]
3664 fn rich_projection_dynamic_text_accepts_one_mib_and_refuses_one_mib_plus_one() {
3665 let projection_with_first_name = |first_name_bytes: usize| {
3666 let winners = (0..1024)
3667 .map(|index| GltfAddressabilityNamedMapWinnerV2 {
3668 name: "x".repeat(if index == 0 { first_name_bytes } else { 1023 }),
3669 source_index: index,
3670 typed_label: "x".into(),
3671 })
3672 .collect();
3673 BevyGltfAddressabilityProjectionV2 {
3674 scenes: Vec::new(),
3675 default_scene_route: GltfAddressabilityProjectionV2::ProvenAbsent,
3676 skins: Vec::new(),
3677 named_maps: vec![GltfAddressabilityNamedMapV2 {
3678 kind: GltfAddressabilityNamedMapKindV2::Scene,
3679 duplicate_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
3680 winners: GltfAddressabilityProjectionV2::Available { value: winners },
3681 }],
3682 target_coverage: GltfAddressabilityProjectionV2::Available { value: () },
3683 targets: Vec::new(),
3684 }
3685 };
3686 let mut exact = projection_with_first_name(1023);
3687 assert_eq!(
3688 projection_text_bytes(&exact),
3689 GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES
3690 );
3691 normalize_projection_bounds(&mut exact);
3692 assert!(matches!(
3693 exact.named_maps()[0].winners(),
3694 GltfAddressabilityProjectionV2::Available { .. }
3695 ));
3696
3697 let mut over = projection_with_first_name(1024);
3698 assert_eq!(
3699 projection_text_bytes(&over),
3700 GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES + 1
3701 );
3702 normalize_projection_bounds(&mut over);
3703 assert!(matches!(
3704 over.named_maps()[0].winners(),
3705 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3706 if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3707 ));
3708 }
3709
3710 #[test]
3711 fn empty_authored_node_name_is_an_exact_bevy_segment_and_round_trips() {
3712 let source = loaded_source_with_targets([0], true);
3713 let raw = simple_target_raw(&source, Some(String::new()));
3714 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3715 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3716 &raw,
3717 &animations,
3718 true,
3719 true,
3720 Some(TargetPointerWidth::Bits64),
3721 )
3722 .unwrap();
3723 let target = projection.targets()[0].projection();
3724 assert!(matches!(
3725 target,
3726 GltfAddressabilityProjectionV2::Available { value }
3727 if value.segments() == [String::new()] && value.path().is_empty()
3728 ));
3729 let encoded = serde_json::to_vec(target).unwrap();
3730 let decoded: GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2> =
3731 serde_json::from_slice(&encoded).unwrap();
3732 assert_eq!(&decoded, target);
3733 let mut non_strict = serde_json::to_value(target).unwrap();
3734 non_strict["value"]["invented"] = serde_json::json!(true);
3735 assert!(
3736 serde_json::from_value::<
3737 GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2>,
3738 >(non_strict)
3739 .is_err()
3740 );
3741 assert_ne!(
3742 bevy_animation_target_id_v1([""], TargetPointerWidth::Bits64).unwrap(),
3743 bevy_animation_target_id_v1(std::iter::empty(), TargetPointerWidth::Bits64).unwrap()
3744 );
3745 }
3746
3747 #[test]
3748 fn standalone_inventory_round_trip_is_strict_and_identity_bound() {
3749 let source = empty_loaded_source();
3750 let raw = analytic_raw(&source);
3751 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3752 let report = GltfAddressabilityV2::new(
3753 ToolInfo::animsmith(ToolSource::new(None, None)),
3754 raw,
3755 animations,
3756 None,
3757 )
3758 .unwrap();
3759 let json = serde_json::to_value(&report).unwrap();
3760 let readback =
3761 GltfAddressabilityV2::read_from(Cursor::new(serde_json::to_vec(&json).unwrap()))
3762 .unwrap();
3763 assert_eq!(readback.input(), report.input());
3764 assert_eq!(readback.inventory(), report.inventory());
3765 assert!(readback.bevy().is_none());
3766
3767 let mut wrong_schema = json.clone();
3768 wrong_schema["schema"] = serde_json::json!("urn:animsmith:schema:gltf-addressability:1");
3769 assert!(
3770 GltfAddressabilityV2::read_from(Cursor::new(
3771 serde_json::to_vec(&wrong_schema).unwrap()
3772 ))
3773 .is_err()
3774 );
3775
3776 let mut wrong_binding = json;
3777 wrong_binding["input"]["bytes"] = serde_json::json!(999);
3778 assert!(
3779 GltfAddressabilityV2::read_from(Cursor::new(
3780 serde_json::to_vec(&wrong_binding).unwrap()
3781 ))
3782 .is_err()
3783 );
3784 }
3785
3786 #[test]
3787 fn adapter_strict_readback_rejects_each_authority_projection_and_check_mutation() {
3788 let complete = adapter_report_json(Some(TargetPointerWidth::Bits64));
3789 assert!(
3790 crate::validate_bevy_readback_v1(serde_json::to_vec(&complete).unwrap().as_slice())
3791 .is_err()
3792 );
3793 let reject = |value: serde_json::Value| {
3794 let bytes = serde_json::to_vec(&value).unwrap();
3795 assert!(GltfAddressabilityV2::read_from(Cursor::new(bytes)).is_err());
3796 };
3797
3798 let mut rules = complete.clone();
3799 rules["bevy"]["rules"]["bevy_commit"] = serde_json::json!("0".repeat(40));
3800 reject(rules);
3801
3802 let mut setting_value = complete.clone();
3803 setting_value["bevy"]["settings"]["load_animations"] = serde_json::json!(false);
3804 reject(setting_value);
3805
3806 let mut setting_origin = complete.clone();
3807 setting_origin["bevy"]["settings"]["bevy_animation_feature_origin"] =
3808 serde_json::json!("profile_default");
3809 reject(setting_origin);
3810
3811 let mut provenance = complete.clone();
3812 provenance["bevy"]["prediction_provenance"]["identity"]["bytes"] = serde_json::json!(1);
3813 reject(provenance);
3814
3815 let mut projection = complete.clone();
3816 projection["bevy"]["projection"]["targets"][0]["projection"]["value"]["path"] =
3817 serde_json::json!("mutated");
3818 reject(projection);
3819
3820 let mut scopes = complete.clone();
3821 let duplicate = scopes["bevy"]["check"]["evaluated_scopes"][0].clone();
3822 scopes["bevy"]["check"]["evaluated_scopes"]
3823 .as_array_mut()
3824 .unwrap()
3825 .push(duplicate);
3826 reject(scopes);
3827
3828 let mut evaluation = complete;
3829 evaluation["bevy"]["check"]["evaluation"] = serde_json::json!("partial");
3830 reject(evaluation);
3831
3832 let mut reasons = adapter_report_json(None);
3833 let target_facet = reasons["bevy"]["check"]["prediction"]["facets"]
3834 .as_array_mut()
3835 .unwrap()
3836 .iter_mut()
3837 .find(|facet| facet["scope"]["code"] == "animation_target_id")
3838 .unwrap();
3839 target_facet["reasons"] = serde_json::json!(["raw_source_incomplete"]);
3840 reject(reasons);
3841 }
3842
3843 #[test]
3844 fn staged_v2_reader_accepts_n_and_rejects_n_plus_one_before_json_decode() {
3845 assert_eq!(GLTF_ADDRESSABILITY_V2_MAX_REPORT_BYTES, 256 * 1024 * 1024);
3846 let exact = GltfAddressabilityV2::read_from_with_limit(Cursor::new(b"null"), 4);
3847 assert!(matches!(
3848 exact,
3849 Err(GltfAddressabilityReadErrorV2::InvalidJson { .. })
3850 | Err(GltfAddressabilityReadErrorV2::Contract(_))
3851 ));
3852 let over = GltfAddressabilityV2::read_from_with_limit(Cursor::new(b"null "), 4);
3853 assert!(matches!(
3854 over,
3855 Err(GltfAddressabilityReadErrorV2::ReportTooLarge { limit: 4 })
3856 ));
3857 }
3858
3859 #[test]
3860 fn public_schema_caps_each_raw_path_candidate_at_256_segments() {
3861 let schema: serde_json::Value = serde_json::from_str(include_str!(
3862 "../../../docs/schemas/gltf-addressability-v2.schema.json"
3863 ))
3864 .unwrap();
3865 let candidate_schema =
3866 schema["$defs"]["path_candidate_row"]["properties"]["source_node_indices"].clone();
3867 assert_eq!(candidate_schema["maxItems"], 256);
3868 let standalone = serde_json::json!({
3869 "$defs": {"u64": schema["$defs"]["u64"].clone()},
3870 "allOf": [candidate_schema.clone()]
3871 });
3872 let validator = jsonschema::options().build(&standalone).unwrap();
3873 assert!(validator.is_valid(&serde_json::json!(vec![0; 256])));
3874 assert!(!validator.is_valid(&serde_json::json!(vec![0; 257])));
3875
3876 let mut weakened = candidate_schema;
3877 weakened["maxItems"] = serde_json::json!(257);
3878 let weakened = serde_json::json!({
3879 "$defs": {"u64": schema["$defs"]["u64"].clone()},
3880 "allOf": [weakened]
3881 });
3882 let weakened = jsonschema::options().build(&weakened).unwrap();
3883 assert!(weakened.is_valid(&serde_json::json!(vec![0; 257])));
3884 }
3885}