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)]
2540mod 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 fn adapter_report_json(pointer_width: Option<TargetPointerWidth>) -> serde_json::Value {
2722 let source = loaded_source_with_targets([0], true);
2723 let raw = simple_target_raw(&source, Some("target".into()));
2724 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
2725 let resolved = crate::resolve_static_v2(crate::EngineDeclarationV2 {
2726 selection: Some(crate::ProfileSelection::new(
2727 "bevy",
2728 3,
2729 "0.19.0",
2730 "gltf-asset-loader",
2731 )),
2732 document_settings: Some(BTreeMap::from([
2733 (
2734 "bevy_animation_feature".into(),
2735 crate::SettingValueV2::Boolean(true),
2736 ),
2737 (
2738 "extension_handler_environment".into(),
2739 crate::SettingValueV2::HandlerEnvironment(
2740 crate::BevyGltfHandlerEnvironmentV2::BareEmpty,
2741 ),
2742 ),
2743 ])),
2744 ..crate::EngineDeclarationV2::default()
2745 })
2746 .unwrap()
2747 .unwrap()
2748 .resolve_input(SourceFormatV1::GltfJson)
2749 .unwrap();
2750 let provenance =
2751 crate::project_prediction_provenance_v4(&resolved, &source, Vec::new()).unwrap();
2752 let grids = MetricGrids::new(source.document());
2753 let roles = ResolvedRoles::default();
2754 let config = Config::default();
2755 let adapter = build_bevy_addressability_adapter_v2(
2756 &source,
2757 &raw,
2758 &animations,
2759 &resolved,
2760 provenance,
2761 pointer_width,
2762 &CheckCtx::new(&grids, &roles, &config),
2763 )
2764 .unwrap()
2765 .unwrap();
2766 let report = GltfAddressabilityV2::new(
2767 ToolInfo::animsmith(ToolSource::new(None, None)),
2768 raw,
2769 animations,
2770 Some(adapter),
2771 )
2772 .unwrap();
2773 let bytes = serde_json::to_vec(&report).unwrap();
2774 GltfAddressabilityV2::read_from(Cursor::new(&bytes)).unwrap();
2775 serde_json::from_slice(&bytes).unwrap()
2776 }
2777
2778 fn prediction_provenance_fixture() -> PredictionProvenanceV4 {
2779 serde_json::from_value(
2780 adapter_report_json(Some(TargetPointerWidth::Bits64))["bevy"]["prediction_provenance"]
2781 .clone(),
2782 )
2783 .unwrap()
2784 }
2785
2786 fn analytic_raw(source: &LoadedSource) -> RawGltfAddressabilityInventoryV1 {
2787 RawGltfAddressabilityInventoryV1::new(
2788 source.source_facts().primary_identity().clone(),
2789 source.dependency_closure().clone(),
2790 RawGltfAddressabilityInventoryInputV1 {
2791 default_scene: RawGltfDefaultSceneObservationV1::Selected {
2792 source_scene_index: 0,
2793 },
2794 scene_coverage: RawGltfAddressabilityCoverageV1::Complete,
2795 scenes: vec![
2796 RawGltfSceneRowV1::new(0, Some("World".into()), vec![0]),
2797 RawGltfSceneRowV1::new(1, Some("World".into()), vec![0]),
2798 ],
2799 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
2800 nodes: vec![RawGltfNodeRowV1::new(0, None, None, Vec::new())],
2801 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
2802 skins: vec![
2803 RawGltfSkinRowV1::new(
2804 0,
2805 Some("Rig".into()),
2806 vec![0],
2807 Some(0),
2808 RawGltfInverseBindMatricesObservationV1::Absent,
2809 ),
2810 RawGltfSkinRowV1::new(
2811 1,
2812 Some("Rig".into()),
2813 vec![0],
2814 None,
2815 RawGltfInverseBindMatricesObservationV1::Absent,
2816 ),
2817 ],
2818 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
2819 attachments: vec![RawGltfSkinAttachmentRowV1::new(0, 0)],
2820 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
2821 path_candidates: vec![
2822 RawGltfScenePathCandidateRowV1::new(0, 0, vec![0]),
2823 RawGltfScenePathCandidateRowV1::new(1, 1, vec![0]),
2824 ],
2825 },
2826 )
2827 .unwrap()
2828 }
2829
2830 #[test]
2831 fn frozen_rule_bundle_is_strict_and_commit_pinned() {
2832 for width in [
2833 None,
2834 Some(TargetPointerWidth::Bits32),
2835 Some(TargetPointerWidth::Bits64),
2836 ] {
2837 let bundle = BevyGltfAddressabilityRulesV1::frozen(width);
2838 assert!(bundle.validate());
2839 assert_eq!(bundle.profile_revision(), 3);
2840 assert_eq!(bundle.sources().len(), 6);
2841 assert!(
2842 bundle
2843 .sources()
2844 .windows(2)
2845 .all(|pair| pair[0].id() < pair[1].id())
2846 );
2847 assert!(
2848 bundle
2849 .sources()
2850 .iter()
2851 .all(|source| source.url().contains(BEVY_COMMIT))
2852 );
2853 }
2854 }
2855
2856 #[test]
2857 fn target_id_golden_vectors_cover_both_pointer_widths_and_boundaries() {
2858 let paths: &[&[&str]] = &[
2862 &[],
2863 &["Root"],
2864 &["Root", "Hips", "GltfNode7"],
2865 &["ab", "c"],
2866 &["a", "bc"],
2867 ];
2868 let expected_32 = [
2869 "77da024c-dd93-5d43-8f9a-2af3d4b17c5b",
2870 "2514296d-dbd8-5ea8-a8cc-1fadaf961d2f",
2871 "15c3912b-7127-55c1-b5ff-83950e3d0f9a",
2872 "fd324118-d34f-53d0-9787-d5a1594985e7",
2873 "2ceb26c6-8d62-53c7-b4c5-ee3b9b81784e",
2874 ];
2875 let expected_64 = [
2876 "77da024c-dd93-5d43-8f9a-2af3d4b17c5b",
2877 "05c24be8-bacd-5ee9-9a65-f8215983900e",
2878 "90fed833-c803-5800-883d-adee90aab446",
2879 "0d54b499-ea4e-5a49-afec-0a87e56da473",
2880 "c22eb6e6-9f59-509b-9cfc-79d424c0d445",
2881 ];
2882 for ((path, expected_32), expected_64) in paths.iter().zip(expected_32).zip(expected_64) {
2883 assert_eq!(
2884 bevy_animation_target_id_v1(path.iter().copied(), TargetPointerWidth::Bits32)
2885 .unwrap(),
2886 expected_32
2887 );
2888 assert_eq!(
2889 bevy_animation_target_id_v1(path.iter().copied(), TargetPointerWidth::Bits64)
2890 .unwrap(),
2891 expected_64
2892 );
2893 }
2894
2895 let max_segment = "x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES);
2896 assert!(
2897 bevy_animation_target_id_v1([max_segment.as_str()], TargetPointerWidth::Bits64).is_ok()
2898 );
2899 let over_segment = format!("{max_segment}x");
2900 assert!(matches!(
2901 bevy_animation_target_id_v1([over_segment.as_str()], TargetPointerWidth::Bits64),
2902 Err(BevyAnimationTargetIdError::SegmentTooLong { .. })
2903 ));
2904
2905 let at_path_limit = [
2906 "a".repeat(1024),
2907 "b".repeat(1024),
2908 "c".repeat(1024),
2909 "d".repeat(1021),
2910 ];
2911 assert!(
2912 bevy_animation_target_id_v1(
2913 at_path_limit.iter().map(String::as_str),
2914 TargetPointerWidth::Bits64,
2915 )
2916 .is_ok()
2917 );
2918 let over_path_limit = [
2919 "a".repeat(1024),
2920 "b".repeat(1024),
2921 "c".repeat(1024),
2922 "d".repeat(1022),
2923 ];
2924 assert!(matches!(
2925 bevy_animation_target_id_v1(
2926 over_path_limit.iter().map(String::as_str),
2927 TargetPointerWidth::Bits64,
2928 ),
2929 Err(BevyAnimationTargetIdError::PathTooLong {
2930 found: 4097,
2931 limit: 4096
2932 })
2933 ));
2934 assert!(
2935 bevy_animation_target_id_v1(
2936 std::iter::repeat_n("", GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS),
2937 TargetPointerWidth::Bits64,
2938 )
2939 .is_ok()
2940 );
2941 assert!(matches!(
2942 bevy_animation_target_id_v1(
2943 std::iter::repeat_n("", GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS + 1),
2944 TargetPointerWidth::Bits64,
2945 ),
2946 Err(BevyAnimationTargetIdError::TooManySegments {
2947 found: 257,
2948 limit: 256
2949 })
2950 ));
2951 }
2952
2953 #[test]
2954 fn analytic_labels_default_route_and_named_winners_follow_bevy_rules() {
2955 let source = empty_loaded_source();
2956 let raw = analytic_raw(&source);
2957 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
2958 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
2959 &raw,
2960 &animations,
2961 true,
2962 true,
2963 Some(TargetPointerWidth::Bits64),
2964 )
2965 .unwrap();
2966 assert_eq!(
2967 projection
2968 .scenes()
2969 .iter()
2970 .map(GltfAddressabilitySceneV2::label)
2971 .collect::<Vec<_>>(),
2972 ["Scene0", "Scene1"]
2973 );
2974 assert_eq!(
2975 projection.default_scene_route(),
2976 &GltfAddressabilityProjectionV2::Available {
2977 value: "Scene0".into()
2978 }
2979 );
2980 assert!(matches!(
2981 projection.skins()[0].skin_label(),
2982 GltfAddressabilityProjectionV2::Available { value } if value == "Skin0"
2983 ));
2984 assert!(matches!(
2985 projection.skins()[1].skin_label(),
2986 GltfAddressabilityProjectionV2::ProvenAbsent
2987 ));
2988 assert_eq!(
2989 projection.skins()[1].inverse_bind_matrices_label(),
2990 "Skin1/InverseBindMatrices"
2991 );
2992 let scene_map = &projection.named_maps()[0];
2993 assert!(matches!(
2994 &scene_map.winners,
2995 GltfAddressabilityProjectionV2::Available { value }
2996 if value.len() == 1 && value[0].source_index() == 1
2997 ));
2998 let skin_map = &projection.named_maps()[2];
2999 assert!(matches!(
3000 &skin_map.winners,
3001 GltfAddressabilityProjectionV2::Available { value }
3002 if value.len() == 1 && value[0].source_index() == 0
3003 ));
3004 }
3005
3006 #[test]
3007 fn retained_default_scene_is_exact_under_partial_tail_coverage() {
3008 let source = empty_loaded_source();
3009 let raw = RawGltfAddressabilityInventoryV1::new(
3010 source.source_facts().primary_identity().clone(),
3011 source.dependency_closure().clone(),
3012 RawGltfAddressabilityInventoryInputV1 {
3013 default_scene: RawGltfDefaultSceneObservationV1::Selected {
3014 source_scene_index: 0,
3015 },
3016 scene_coverage: RawGltfAddressabilityCoverageV1::budget_exceeded(),
3017 scenes: vec![RawGltfSceneRowV1::new(0, None, Vec::new())],
3018 node_coverage: RawGltfAddressabilityCoverageV1::Complete,
3019 nodes: Vec::new(),
3020 skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
3021 skins: Vec::new(),
3022 attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
3023 attachments: Vec::new(),
3024 path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
3025 path_candidates: Vec::new(),
3026 },
3027 )
3028 .unwrap();
3029 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3030 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3031 &raw,
3032 &animations,
3033 true,
3034 true,
3035 Some(TargetPointerWidth::Bits64),
3036 )
3037 .unwrap();
3038 assert_eq!(
3039 projection.default_scene_route(),
3040 &GltfAddressabilityProjectionV2::Available {
3041 value: "Scene0".into()
3042 }
3043 );
3044 }
3045
3046 #[test]
3047 fn named_map_keys_enforce_exact_segment_bound() {
3048 let winner = |name: String| {
3049 let mut winners = BTreeMap::new();
3050 winners.insert(
3051 name.clone(),
3052 GltfAddressabilityNamedMapWinnerV2 {
3053 name,
3054 source_index: 0,
3055 typed_label: "Scene0".into(),
3056 },
3057 );
3058 named_map(GltfAddressabilityNamedMapKindV2::Scene, true, winners)
3059 };
3060 assert!(matches!(
3061 winner("x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES)).winners(),
3062 GltfAddressabilityProjectionV2::Available { .. }
3063 ));
3064 assert!(matches!(
3065 winner("x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES + 1)).winners(),
3066 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3067 if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3068 ));
3069 }
3070
3071 #[test]
3072 fn incomplete_target_evidence_never_proves_complete_empty_or_prefix_coverage() {
3073 for targets in [Vec::new(), vec![0]] {
3074 let source = loaded_source_with_targets(targets, false);
3075 let raw = if source.source_facts().clips().rows()[0]
3076 .channels()
3077 .rows()
3078 .is_empty()
3079 {
3080 raw_for_graph(
3081 &source,
3082 Vec::new(),
3083 RawGltfAddressabilityCoverageV1::Complete,
3084 Vec::new(),
3085 RawGltfAddressabilityCoverageV1::Complete,
3086 Vec::new(),
3087 RawGltfAddressabilityCoverageV1::Complete,
3088 )
3089 } else {
3090 simple_target_raw(&source, Some("target".into()))
3091 };
3092 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3093 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3094 &raw,
3095 &animations,
3096 true,
3097 true,
3098 Some(TargetPointerWidth::Bits64),
3099 )
3100 .unwrap();
3101 assert!(matches!(
3102 projection.target_coverage(),
3103 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3104 if reasons == &[GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3105 ));
3106 }
3107
3108 assert!(matches!(
3109 target_coverage_projection(true, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS),
3110 GltfAddressabilityProjectionV2::Available { .. }
3111 ));
3112 assert!(matches!(
3113 target_coverage_projection(true, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 1),
3114 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3115 if reasons == [GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated]
3116 ));
3117 assert!(matches!(
3118 target_coverage_projection(false, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 1),
3119 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3120 if reasons == [
3121 GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
3122 GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated,
3123 ]
3124 ));
3125 }
3126
3127 #[test]
3128 fn production_target_projection_retains_exact_n_and_truncates_n_plus_one() {
3129 let exact_count = GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS;
3130 let exact_source = loaded_source_with_targets(0..exact_count as u64, true);
3131 let exact_raw = wide_target_raw(&exact_source, exact_count, true);
3132 let exact_animations =
3133 GltfAnimationAddressabilityInventoryV1::from_source(&exact_source).unwrap();
3134 let exact = BevyGltfAddressabilityProjectionV2::from_inventories(
3135 &exact_raw,
3136 &exact_animations,
3137 true,
3138 true,
3139 Some(TargetPointerWidth::Bits64),
3140 )
3141 .unwrap();
3142 assert!(matches!(
3143 exact.target_coverage(),
3144 GltfAddressabilityProjectionV2::Available { .. }
3145 ));
3146 assert_eq!(exact.targets().len(), exact_count);
3147 assert_eq!(
3148 exact
3149 .targets()
3150 .iter()
3151 .map(GltfAddressabilityTargetV2::source_node_index)
3152 .collect::<Vec<_>>(),
3153 (0..exact_count as u64).collect::<Vec<_>>()
3154 );
3155 assert!(exact.targets().iter().all(|target| matches!(
3156 target.projection(),
3157 GltfAddressabilityProjectionV2::Available { .. }
3158 )));
3159
3160 let over_count = exact_count + 1;
3161 let over_source = loaded_source_with_targets(0..over_count as u64, true);
3162 let over_raw = wide_target_raw(&over_source, exact_count, false);
3166 let over_animations =
3167 GltfAnimationAddressabilityInventoryV1::from_source(&over_source).unwrap();
3168 let over = BevyGltfAddressabilityProjectionV2::from_inventories(
3169 &over_raw,
3170 &over_animations,
3171 true,
3172 true,
3173 Some(TargetPointerWidth::Bits64),
3174 )
3175 .unwrap();
3176 assert_eq!(over.targets().len(), exact_count);
3177 assert_eq!(
3178 over.targets()
3179 .iter()
3180 .map(GltfAddressabilityTargetV2::source_node_index)
3181 .collect::<Vec<_>>(),
3182 (0..exact_count as u64).collect::<Vec<_>>()
3183 );
3184 let expected_reasons = [
3185 GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
3186 GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated,
3187 ];
3188 assert!(matches!(
3189 over.target_coverage(),
3190 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3191 if reasons.as_slice() == expected_reasons
3192 ));
3193 assert!(over.targets().iter().all(|target| matches!(
3194 target.projection(),
3195 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3196 if reasons.as_slice() == expected_reasons
3197 )));
3198 assert_eq!(
3199 over.targets().last().unwrap().contributing_channels(),
3200 &[GltfAddressabilityTargetChannelV2 {
3201 source_animation_index: 0,
3202 source_channel_index: (exact_count - 1) as u64,
3203 }]
3204 );
3205 }
3206
3207 #[test]
3208 fn unnamed_target_nodes_use_bevys_exact_source_index_fallback() {
3209 let source = loaded_source_with_targets([7], true);
3210 let raw = raw_for_graph(
3211 &source,
3212 vec![RawGltfSceneRowV1::new(0, None, vec![7])],
3213 RawGltfAddressabilityCoverageV1::Complete,
3214 (0..=7)
3215 .map(|index| RawGltfNodeRowV1::new(index, None, None, Vec::new()))
3216 .collect(),
3217 RawGltfAddressabilityCoverageV1::Complete,
3218 vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![7])],
3219 RawGltfAddressabilityCoverageV1::Complete,
3220 );
3221 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3222 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3223 &raw,
3224 &animations,
3225 true,
3226 true,
3227 Some(TargetPointerWidth::Bits64),
3228 )
3229 .unwrap();
3230 let GltfAddressabilityProjectionV2::Available { value } =
3231 projection.targets()[0].projection()
3232 else {
3233 panic!("complete unnamed target must be exactly projectable");
3234 };
3235 assert_eq!(value.segments(), ["GltfNode7"]);
3236 assert_eq!(value.path(), "GltfNode7");
3237 assert_eq!(
3238 value.uuid(),
3239 bevy_animation_target_id_v1(["GltfNode7"], TargetPointerWidth::Bits64).unwrap()
3240 );
3241 }
3242
3243 #[test]
3244 fn each_raw_target_domain_and_dependency_closure_fail_closed_with_causal_reason() {
3245 for partial_domain in ["scene", "node", "path"] {
3246 let source = loaded_source_with_targets([0], true);
3247 let partial = RawGltfAddressabilityCoverageV1::budget_exceeded();
3248 let complete = RawGltfAddressabilityCoverageV1::Complete;
3249 let raw = raw_for_graph(
3250 &source,
3251 vec![RawGltfSceneRowV1::new(0, None, vec![0])],
3252 if partial_domain == "scene" {
3253 partial
3254 } else {
3255 complete
3256 },
3257 vec![RawGltfNodeRowV1::new(
3258 0,
3259 Some("target".into()),
3260 None,
3261 Vec::new(),
3262 )],
3263 if partial_domain == "node" {
3264 partial
3265 } else {
3266 complete
3267 },
3268 vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![0])],
3269 if partial_domain == "path" {
3270 partial
3271 } else {
3272 complete
3273 },
3274 );
3275 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3276 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3277 &raw,
3278 &animations,
3279 true,
3280 true,
3281 Some(TargetPointerWidth::Bits64),
3282 )
3283 .unwrap();
3284 assert!(
3285 matches!(
3286 projection.target_coverage(),
3287 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3288 if reasons.as_slice()
3289 == [GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3290 ),
3291 "partial {partial_domain} coverage must invalidate target coverage"
3292 );
3293 assert!(
3294 matches!(
3295 projection.targets()[0].projection(),
3296 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3297 if reasons.as_slice()
3298 == [GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3299 ),
3300 "partial {partial_domain} coverage must invalidate every target row"
3301 );
3302 }
3303
3304 let source = loaded_source_with_targets_and_closure([0], true, false);
3305 assert!(!source.dependency_closure().coverage().is_complete());
3306 let raw = simple_target_raw(&source, Some("target".into()));
3307 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3308 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3309 &raw,
3310 &animations,
3311 true,
3312 true,
3313 Some(TargetPointerWidth::Bits64),
3314 )
3315 .unwrap();
3316 assert!(matches!(
3317 projection.target_coverage(),
3318 GltfAddressabilityProjectionV2::Available { .. }
3319 ));
3320 assert!(matches!(
3321 projection.targets()[0].projection(),
3322 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3323 if reasons.as_slice() == [
3324 GltfAddressabilityUnavailableReasonV2::DependencyClosureIncomplete,
3325 ]
3326 ));
3327 }
3328
3329 #[test]
3330 fn rich_unavailable_facets_compact_only_after_the_exact_file_limit() {
3331 let source = loaded_source_with_targets([0], true);
3332 let raw = simple_target_raw(&source, Some("target".into()));
3333 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3334 let provenance = prediction_provenance_fixture();
3335 let inventory =
3336 GltfAddressabilityInventoryV2::new(raw.clone(), animations.clone()).unwrap();
3337 let base = BevyGltfAddressabilityProjectionV2::from_inventories(
3338 &raw,
3339 &animations,
3340 true,
3341 true,
3342 Some(TargetPointerWidth::Bits64),
3343 )
3344 .unwrap();
3345
3346 for count in [
3347 PREDICTION_V1_MAX_FACETS_PER_FILE,
3348 PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3349 ] {
3350 let mut projection = base.clone();
3351 projection.targets = (0..count as u64)
3352 .map(|source_node_index| GltfAddressabilityTargetV2 {
3353 source_node_index,
3354 contributing_channels: vec![GltfAddressabilityTargetChannelV2 {
3355 source_animation_index: 0,
3356 source_channel_index: source_node_index,
3357 }],
3358 projection: GltfAddressabilityProjectionV2::unavailable(vec![
3359 GltfAddressabilityUnavailableReasonV2::TargetPointerWidthMissing,
3360 ]),
3361 })
3362 .collect();
3363 let output =
3364 build_check_output(&source, &raw, &animations, &provenance, &projection).unwrap();
3365 let check =
3366 CheckEvaluation::evaluated(crate::ENGINE_ADDRESSABILITY_CHECK_ID, output).unwrap();
3367 let expected = expected_facet_specs(&inventory, &projection);
3368 validate_facet_states(
3369 check.engine_prediction_v4(),
3370 check.evaluated_scopes(),
3371 check.evaluation(),
3372 &expected,
3373 )
3374 .unwrap();
3375
3376 let facets = check.engine_prediction_v4().unwrap().facets();
3377 if count == PREDICTION_V1_MAX_FACETS_PER_FILE {
3378 assert_eq!(facets.len(), PREDICTION_V1_MAX_FACETS_PER_FILE);
3379 assert!(facets.iter().all(|facet| {
3380 facet.scope().code == EvaluationScopeCode::ANIMATION_TARGET_ID
3381 && facet.reasons()
3382 == [PredictionUnavailableReasonV2::ProjectIntentUnavailable]
3383 }));
3384 } else {
3385 assert_eq!(facets.len(), 1);
3386 assert_eq!(facets[0].scope().code.as_str(), FACET_BUDGET_SCOPE);
3387 assert!(facets[0].scope().subject.is_none());
3388 assert_eq!(
3389 facets[0].reasons(),
3390 [PredictionUnavailableReasonV2::FacetBudgetExceeded]
3391 );
3392 }
3393 }
3394 }
3395
3396 #[test]
3397 fn evaluated_scope_guard_accepts_exact_n_and_rejects_n_plus_one() {
3398 let scopes = (0..=GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES)
3399 .map(|index| {
3400 EvaluationScope::new(EvaluationScopeCode::SCENE_ASSET_LABEL)
3401 .subject(format!("Scene{index}"))
3402 })
3403 .collect::<Vec<_>>();
3404 let expected = scopes
3405 .iter()
3406 .cloned()
3407 .map(|scope| ExpectedFacetV2 {
3408 scope,
3409 state: EnginePredictionFacetStateV1::Available,
3410 reasons: Vec::new(),
3411 })
3412 .collect::<Vec<_>>();
3413 validate_facet_states(
3414 None,
3415 &scopes[..GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES],
3416 EvaluationState::Complete,
3417 &expected[..GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES],
3418 )
3419 .unwrap();
3420 assert_eq!(
3421 validate_facet_states(None, &scopes, EvaluationState::Complete, &expected),
3422 Err(GltfAddressabilityV2Error::InvalidCheckEvaluation)
3423 );
3424 }
3425
3426 #[test]
3427 fn feature_and_loader_settings_are_independently_required_for_targets() {
3428 let source = loaded_source_with_targets([0], true);
3429 let raw = simple_target_raw(&source, Some("target".into()));
3430 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3431 for (feature, load, expected) in [
3432 (
3433 false,
3434 true,
3435 GltfAddressabilityUnavailableReasonV2::BevyAnimationFeatureDisabled,
3436 ),
3437 (
3438 true,
3439 false,
3440 GltfAddressabilityUnavailableReasonV2::LoadAnimationsDisabled,
3441 ),
3442 ] {
3443 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3444 &raw,
3445 &animations,
3446 feature,
3447 load,
3448 Some(TargetPointerWidth::Bits64),
3449 )
3450 .unwrap();
3451 assert!(matches!(
3452 projection.targets()[0].projection(),
3453 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3454 if reasons.contains(&expected)
3455 ));
3456 }
3457 }
3458
3459 #[test]
3460 fn overlong_runtime_path_maps_to_typed_unavailable_after_the_exact_boundary() {
3461 let segments = vec![
3462 "a".repeat(1024),
3463 "b".repeat(1024),
3464 "c".repeat(1024),
3465 "d".repeat(1022),
3466 ];
3467 let projection = project_target_segments(segments, TargetPointerWidth::Bits64);
3468 assert!(matches!(
3469 projection,
3470 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3471 if reasons == [GltfAddressabilityUnavailableReasonV2::PathBoundsExceeded]
3472 ));
3473 }
3474
3475 #[test]
3476 fn slash_joined_display_paths_do_not_alias_segment_vectors() {
3477 let target = |source_node_index, segments: Vec<&str>, width| {
3478 let owned = segments
3479 .iter()
3480 .map(|segment| (*segment).to_owned())
3481 .collect::<Vec<_>>();
3482 GltfAddressabilityTargetV2 {
3483 source_node_index,
3484 contributing_channels: vec![GltfAddressabilityTargetChannelV2 {
3485 source_animation_index: 0,
3486 source_channel_index: source_node_index,
3487 }],
3488 projection: GltfAddressabilityProjectionV2::Available {
3489 value: GltfAddressabilityTargetValueV2 {
3490 path: owned.join("/"),
3491 uuid: bevy_animation_target_id_v1(segments, width).unwrap(),
3492 segments: owned,
3493 },
3494 },
3495 }
3496 };
3497 let mut targets = vec![
3498 target(0, vec!["a/b", "c"], TargetPointerWidth::Bits64),
3499 target(1, vec!["a", "b/c"], TargetPointerWidth::Bits64),
3500 ];
3501 invalidate_target_collisions(&mut targets);
3502 assert!(targets.iter().all(|target| matches!(
3503 target.projection(),
3504 GltfAddressabilityProjectionV2::Available { .. }
3505 )));
3506 }
3507
3508 #[test]
3509 fn multi_path_candidates_participate_in_global_collision_analysis() {
3510 let source = loaded_source_with_targets([1, 3], true);
3511 let raw = raw_for_graph(
3512 &source,
3513 vec![
3514 RawGltfSceneRowV1::new(0, None, vec![0]),
3515 RawGltfSceneRowV1::new(1, None, vec![1]),
3516 RawGltfSceneRowV1::new(2, None, vec![2]),
3517 ],
3518 RawGltfAddressabilityCoverageV1::Complete,
3519 vec![
3520 RawGltfNodeRowV1::new(0, Some("a".into()), None, vec![1]),
3521 RawGltfNodeRowV1::new(1, Some("b".into()), Some(0), Vec::new()),
3522 RawGltfNodeRowV1::new(2, Some("a".into()), None, vec![3]),
3523 RawGltfNodeRowV1::new(3, Some("b".into()), Some(2), Vec::new()),
3524 ],
3525 RawGltfAddressabilityCoverageV1::Complete,
3526 vec![
3527 RawGltfScenePathCandidateRowV1::new(0, 0, vec![0]),
3528 RawGltfScenePathCandidateRowV1::new(1, 0, vec![0, 1]),
3529 RawGltfScenePathCandidateRowV1::new(2, 1, vec![1]),
3530 RawGltfScenePathCandidateRowV1::new(3, 2, vec![2]),
3531 RawGltfScenePathCandidateRowV1::new(4, 2, vec![2, 3]),
3532 ],
3533 RawGltfAddressabilityCoverageV1::Complete,
3534 );
3535 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3536 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3537 &raw,
3538 &animations,
3539 true,
3540 true,
3541 Some(TargetPointerWidth::Bits64),
3542 )
3543 .unwrap();
3544 assert!(matches!(
3545 projection.targets()[0].projection(),
3546 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3547 if reasons.contains(&GltfAddressabilityUnavailableReasonV2::MultipleCandidatePaths)
3548 ));
3549 assert!(matches!(
3550 projection.targets()[1].projection(),
3551 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3552 if reasons.contains(&GltfAddressabilityUnavailableReasonV2::DuplicateFullPath)
3553 && reasons.contains(&GltfAddressabilityUnavailableReasonV2::TargetIdCollision)
3554 ));
3555 }
3556
3557 #[test]
3558 fn target_id_collision_helper_invalidates_distinct_paths() {
3559 let mut targets = vec![
3560 GltfAddressabilityTargetV2 {
3561 source_node_index: 0,
3562 contributing_channels: Vec::new(),
3563 projection: GltfAddressabilityProjectionV2::Available {
3564 value: GltfAddressabilityTargetValueV2 {
3565 segments: vec!["left".into()],
3566 path: "left".into(),
3567 uuid: "00000000-0000-5000-8000-000000000000".into(),
3568 },
3569 },
3570 },
3571 GltfAddressabilityTargetV2 {
3572 source_node_index: 1,
3573 contributing_channels: Vec::new(),
3574 projection: GltfAddressabilityProjectionV2::Available {
3575 value: GltfAddressabilityTargetValueV2 {
3576 segments: vec!["right".into()],
3577 path: "right".into(),
3578 uuid: "00000000-0000-5000-8000-000000000000".into(),
3579 },
3580 },
3581 },
3582 ];
3583 invalidate_target_collisions(&mut targets);
3584 assert!(targets.iter().all(|target| matches!(
3585 target.projection(),
3586 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3587 if reasons == &[GltfAddressabilityUnavailableReasonV2::TargetIdCollision]
3588 )));
3589 }
3590
3591 #[test]
3592 fn named_map_incomplete_and_projection_reference_bounds_are_typed() {
3593 let source = empty_loaded_source();
3594 let raw = raw_for_graph(
3595 &source,
3596 vec![RawGltfSceneRowV1::new(
3597 0,
3598 Some("retained".into()),
3599 Vec::new(),
3600 )],
3601 RawGltfAddressabilityCoverageV1::budget_exceeded(),
3602 Vec::new(),
3603 RawGltfAddressabilityCoverageV1::Complete,
3604 Vec::new(),
3605 RawGltfAddressabilityCoverageV1::Complete,
3606 );
3607 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3608 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3609 &raw,
3610 &animations,
3611 true,
3612 true,
3613 Some(TargetPointerWidth::Bits64),
3614 )
3615 .unwrap();
3616 assert!(matches!(
3617 projection.named_maps()[0].winners(),
3618 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3619 if reasons == &[GltfAddressabilityUnavailableReasonV2::NamedMapIncomplete]
3620 ));
3621
3622 let bounded_projection = |channel_count| BevyGltfAddressabilityProjectionV2 {
3623 scenes: Vec::new(),
3624 default_scene_route: GltfAddressabilityProjectionV2::ProvenAbsent,
3625 skins: Vec::new(),
3626 named_maps: Vec::new(),
3627 target_coverage: GltfAddressabilityProjectionV2::Available { value: () },
3628 targets: vec![GltfAddressabilityTargetV2 {
3629 source_node_index: 0,
3630 contributing_channels: vec![
3631 GltfAddressabilityTargetChannelV2 {
3632 source_animation_index: 0,
3633 source_channel_index: 0,
3634 };
3635 channel_count
3636 ],
3637 projection: GltfAddressabilityProjectionV2::Available {
3638 value: GltfAddressabilityTargetValueV2 {
3639 segments: vec!["target".into()],
3640 path: "target".into(),
3641 uuid: "00000000-0000-5000-8000-000000000000".into(),
3642 },
3643 },
3644 }],
3645 };
3646 let mut exact = bounded_projection(GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES - 1);
3647 normalize_projection_bounds(&mut exact);
3648 assert!(matches!(
3649 exact.targets()[0].projection(),
3650 GltfAddressabilityProjectionV2::Available { .. }
3651 ));
3652 let mut over = bounded_projection(GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES);
3653 normalize_projection_bounds(&mut over);
3654 assert!(matches!(
3655 over.target_coverage(),
3656 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3657 if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3658 ));
3659 }
3660
3661 #[test]
3662 fn rich_projection_dynamic_text_accepts_one_mib_and_refuses_one_mib_plus_one() {
3663 let projection_with_first_name = |first_name_bytes: usize| {
3664 let winners = (0..1024)
3665 .map(|index| GltfAddressabilityNamedMapWinnerV2 {
3666 name: "x".repeat(if index == 0 { first_name_bytes } else { 1023 }),
3667 source_index: index,
3668 typed_label: "x".into(),
3669 })
3670 .collect();
3671 BevyGltfAddressabilityProjectionV2 {
3672 scenes: Vec::new(),
3673 default_scene_route: GltfAddressabilityProjectionV2::ProvenAbsent,
3674 skins: Vec::new(),
3675 named_maps: vec![GltfAddressabilityNamedMapV2 {
3676 kind: GltfAddressabilityNamedMapKindV2::Scene,
3677 duplicate_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
3678 winners: GltfAddressabilityProjectionV2::Available { value: winners },
3679 }],
3680 target_coverage: GltfAddressabilityProjectionV2::Available { value: () },
3681 targets: Vec::new(),
3682 }
3683 };
3684 let mut exact = projection_with_first_name(1023);
3685 assert_eq!(
3686 projection_text_bytes(&exact),
3687 GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES
3688 );
3689 normalize_projection_bounds(&mut exact);
3690 assert!(matches!(
3691 exact.named_maps()[0].winners(),
3692 GltfAddressabilityProjectionV2::Available { .. }
3693 ));
3694
3695 let mut over = projection_with_first_name(1024);
3696 assert_eq!(
3697 projection_text_bytes(&over),
3698 GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES + 1
3699 );
3700 normalize_projection_bounds(&mut over);
3701 assert!(matches!(
3702 over.named_maps()[0].winners(),
3703 GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3704 if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3705 ));
3706 }
3707
3708 #[test]
3709 fn empty_authored_node_name_is_an_exact_bevy_segment_and_round_trips() {
3710 let source = loaded_source_with_targets([0], true);
3711 let raw = simple_target_raw(&source, Some(String::new()));
3712 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3713 let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3714 &raw,
3715 &animations,
3716 true,
3717 true,
3718 Some(TargetPointerWidth::Bits64),
3719 )
3720 .unwrap();
3721 let target = projection.targets()[0].projection();
3722 assert!(matches!(
3723 target,
3724 GltfAddressabilityProjectionV2::Available { value }
3725 if value.segments() == [String::new()] && value.path().is_empty()
3726 ));
3727 let encoded = serde_json::to_vec(target).unwrap();
3728 let decoded: GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2> =
3729 serde_json::from_slice(&encoded).unwrap();
3730 assert_eq!(&decoded, target);
3731 let mut non_strict = serde_json::to_value(target).unwrap();
3732 non_strict["value"]["invented"] = serde_json::json!(true);
3733 assert!(
3734 serde_json::from_value::<
3735 GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2>,
3736 >(non_strict)
3737 .is_err()
3738 );
3739 assert_ne!(
3740 bevy_animation_target_id_v1([""], TargetPointerWidth::Bits64).unwrap(),
3741 bevy_animation_target_id_v1(std::iter::empty(), TargetPointerWidth::Bits64).unwrap()
3742 );
3743 }
3744
3745 #[test]
3746 fn standalone_inventory_round_trip_is_strict_and_identity_bound() {
3747 let source = empty_loaded_source();
3748 let raw = analytic_raw(&source);
3749 let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3750 let report = GltfAddressabilityV2::new(
3751 ToolInfo::animsmith(ToolSource::new(None, None)),
3752 raw,
3753 animations,
3754 None,
3755 )
3756 .unwrap();
3757 let json = serde_json::to_value(&report).unwrap();
3758 let readback =
3759 GltfAddressabilityV2::read_from(Cursor::new(serde_json::to_vec(&json).unwrap()))
3760 .unwrap();
3761 assert_eq!(readback.input(), report.input());
3762 assert_eq!(readback.inventory(), report.inventory());
3763 assert!(readback.bevy().is_none());
3764
3765 let mut wrong_schema = json.clone();
3766 wrong_schema["schema"] = serde_json::json!("urn:animsmith:schema:gltf-addressability:1");
3767 assert!(
3768 GltfAddressabilityV2::read_from(Cursor::new(
3769 serde_json::to_vec(&wrong_schema).unwrap()
3770 ))
3771 .is_err()
3772 );
3773
3774 let mut wrong_binding = json;
3775 wrong_binding["input"]["bytes"] = serde_json::json!(999);
3776 assert!(
3777 GltfAddressabilityV2::read_from(Cursor::new(
3778 serde_json::to_vec(&wrong_binding).unwrap()
3779 ))
3780 .is_err()
3781 );
3782 }
3783
3784 #[test]
3785 fn adapter_strict_readback_rejects_each_authority_projection_and_check_mutation() {
3786 let complete = adapter_report_json(Some(TargetPointerWidth::Bits64));
3787 let reject = |value: serde_json::Value| {
3788 let bytes = serde_json::to_vec(&value).unwrap();
3789 assert!(GltfAddressabilityV2::read_from(Cursor::new(bytes)).is_err());
3790 };
3791
3792 let mut rules = complete.clone();
3793 rules["bevy"]["rules"]["bevy_commit"] = serde_json::json!("0".repeat(40));
3794 reject(rules);
3795
3796 let mut setting_value = complete.clone();
3797 setting_value["bevy"]["settings"]["load_animations"] = serde_json::json!(false);
3798 reject(setting_value);
3799
3800 let mut setting_origin = complete.clone();
3801 setting_origin["bevy"]["settings"]["bevy_animation_feature_origin"] =
3802 serde_json::json!("profile_default");
3803 reject(setting_origin);
3804
3805 let mut provenance = complete.clone();
3806 provenance["bevy"]["prediction_provenance"]["identity"]["bytes"] = serde_json::json!(1);
3807 reject(provenance);
3808
3809 let mut projection = complete.clone();
3810 projection["bevy"]["projection"]["targets"][0]["projection"]["value"]["path"] =
3811 serde_json::json!("mutated");
3812 reject(projection);
3813
3814 let mut scopes = complete.clone();
3815 let duplicate = scopes["bevy"]["check"]["evaluated_scopes"][0].clone();
3816 scopes["bevy"]["check"]["evaluated_scopes"]
3817 .as_array_mut()
3818 .unwrap()
3819 .push(duplicate);
3820 reject(scopes);
3821
3822 let mut evaluation = complete;
3823 evaluation["bevy"]["check"]["evaluation"] = serde_json::json!("partial");
3824 reject(evaluation);
3825
3826 let mut reasons = adapter_report_json(None);
3827 let target_facet = reasons["bevy"]["check"]["prediction"]["facets"]
3828 .as_array_mut()
3829 .unwrap()
3830 .iter_mut()
3831 .find(|facet| facet["scope"]["code"] == "animation_target_id")
3832 .unwrap();
3833 target_facet["reasons"] = serde_json::json!(["raw_source_incomplete"]);
3834 reject(reasons);
3835 }
3836
3837 #[test]
3838 fn staged_v2_reader_accepts_n_and_rejects_n_plus_one_before_json_decode() {
3839 assert_eq!(GLTF_ADDRESSABILITY_V2_MAX_REPORT_BYTES, 256 * 1024 * 1024);
3840 let exact = GltfAddressabilityV2::read_from_with_limit(Cursor::new(b"null"), 4);
3841 assert!(matches!(
3842 exact,
3843 Err(GltfAddressabilityReadErrorV2::InvalidJson { .. })
3844 | Err(GltfAddressabilityReadErrorV2::Contract(_))
3845 ));
3846 let over = GltfAddressabilityV2::read_from_with_limit(Cursor::new(b"null "), 4);
3847 assert!(matches!(
3848 over,
3849 Err(GltfAddressabilityReadErrorV2::ReportTooLarge { limit: 4 })
3850 ));
3851 }
3852
3853 #[test]
3854 fn public_schema_caps_each_raw_path_candidate_at_256_segments() {
3855 let schema: serde_json::Value = serde_json::from_str(include_str!(
3856 "../../../docs/schemas/gltf-addressability-v2.schema.json"
3857 ))
3858 .unwrap();
3859 let candidate_schema =
3860 schema["$defs"]["path_candidate_row"]["properties"]["source_node_indices"].clone();
3861 assert_eq!(candidate_schema["maxItems"], 256);
3862 let standalone = serde_json::json!({
3863 "$defs": {"u64": schema["$defs"]["u64"].clone()},
3864 "allOf": [candidate_schema.clone()]
3865 });
3866 let validator = jsonschema::options().build(&standalone).unwrap();
3867 assert!(validator.is_valid(&serde_json::json!(vec![0; 256])));
3868 assert!(!validator.is_valid(&serde_json::json!(vec![0; 257])));
3869
3870 let mut weakened = candidate_schema;
3871 weakened["maxItems"] = serde_json::json!(257);
3872 let weakened = serde_json::json!({
3873 "$defs": {"u64": schema["$defs"]["u64"].clone()},
3874 "allOf": [weakened]
3875 });
3876 let weakened = jsonschema::options().build(&weakened).unwrap();
3877 assert!(weakened.is_valid(&serde_json::json!(vec![0; 257])));
3878 }
3879}