Skip to content

chore: ISS via standalone descriptor + per-asset bundles - #8805

Closed
dalkia wants to merge 22 commits into
devfrom
chore/new-iss
Closed

chore: ISS via standalone descriptor + per-asset bundles#8805
dalkia wants to merge 22 commits into
devfrom
chore/new-iss

Conversation

@dalkia

@dalkia dalkia commented May 18, 2026

Copy link
Copy Markdown
Collaborator

Pull Request Description

What does this PR change?

Reworks the Initial Scene State (ISS) loading path so a scene's preview can be served either by:

  1. Bundle mode — the legacy single staticscene_{sceneId}{platform} asset bundle, OR
  2. Descriptor mode — a JSON descriptor (StreamingAssets/AssetBundles/iss_descriptors/{sceneId}_InitialSceneState.json) listing per-asset hashes + transforms, with each asset fetched as its own AB.

Per scene we pick a mode based on a HEAD probe of the legacy ISS bundle URL. Both paths feed the same LOD pipeline; the difference is only in how the AB requests are shaped.

Highlights

  • ISSDescriptor is now the single source of ISS truth. Built during scene-definition loading and stored on SceneEntityDefinition. Owns: descriptor Assets list, bridge-slot counters capped at per-hash multiplicity, bundle-asset hash set for O(1) AB-redirect lookups, and the scene-lifetime bundle release callback. Replaces both the old InitialSceneStateInfo struct and the IInitialSceneState indirection (deleted).
  • Bundle mode unchanged on the consumer side: PrepareAssetBundleLoadingParametersSystem still rewrites per-asset AB requests whose hash is in the ISS set to the shared bundle URL — it now consults ISSDescriptor.IsBundleAsset(hash) directly instead of a separate HashSet.
  • Descriptor mode (new): ResolveISSLODFromDescriptorSystem companion query in ResolveISSLODSystem spawns one AB promise per descriptor entry, each with the platform suffix appended; positions per the descriptor transform; counts failures (AddFailedAsset) so AllAssetsInstantiated settles even on 404s.
  • Bridge handoff (LOD → SDK runtime) is reservation-based. ISSDescriptor.TryReserveBridgeSlot(hash) caps at the descriptor's per-hash count, so SDK-instantiated copies of the same hash created at runtime never pollute the bridge.
  • AB v49 gate: descriptor + HEAD probe skipped for older manifests.
  • AssetBundleData.isISS flag plumbed through so ISS bundles aren't unloaded prematurely.

Files of interest

  • Explorer/Assets/DCL/NetworkDefinitions/ISSDescriptor.cs — descriptor + bridge state + JSON DTOs + ResolveAsync
  • Explorer/Assets/DCL/Infrastructure/ECS/SceneLifeCycle/SceneFacade/Systems/LoadSceneSystemLogicBase.csLoadISSBundleAsync eagerly fetches the shared AB in Bundle mode and attaches its Dereference to the descriptor
  • Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneFacade.cs — disposal calls ISSDescriptor.Dereference()
  • Explorer/Assets/DCL/LOD/Systems/ResolveISSLODSystem.cs — both bundle and descriptor query paths
  • Explorer/Assets/DCL/Infrastructure/ECS/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystem.cs — descriptor-aware AB hash rewrite

Why callback-typed bundle release?

AssetBundleData lives in the ECS asmdef; ISSDescriptor in DCL.Network. A typed AssetBundleData? field would cycle through ECS → SceneRunner.Scene → DCL.Network. AttachAssetBundle(Action) lets the loader supply the cleanup without dragging the type in.

Removed: ISS bundle prewarm (EarlySceneRequestSystem + EarlyAssetBundleRequestSystem)

The prewarm chain — eagerly fetch the start parcel's scene definition, then kick off its ISS asset-bundle download before anything in the world asked for it — only made sense when the ISS descriptor was resolved synchronously as part of scene-definition loading. Once descriptor resolution moved behind a lazy AssetPromise (driven by the LOD path / SDK runtime loader), the prewarm lost its anchor: there's no longer a deterministic point in the lifecycle where we know we'll need the ISS bundle far enough in advance to usefully prefetch it. The two systems stayed wired in but did nothing — EarlySceneRequestSystem.CompleteEarlySceneRequest stopped producing EarlyAssetBundleFlag entities, and EarlyAssetBundleRequestSystem's queries never matched. Both files, their flag types (EarlySceneFlag, EarlyAssetBundleFlag), and their InjectToWorld wiring in AssetBundlesPlugin and GlobalWorldFactory are removed here.

A follow-up PR will revive the prewarm in a form that fits the lazy pipeline — most likely by hooking it off the descriptor promise's resolution rather than off scene-definition load.

Temporarily forced: Descriptor mode in LoadISSDescriptorSystem

The HEAD probe that decided between Bundle and Descriptor modes (IsBundleReachableAsync against the legacy staticscene_{sceneId}{platform} URL) is commented out, and every ISS-capable scene now resolves directly to IISSDescriptor.State.Descriptor. Bundle mode is the legacy single-AB path; while we validate the per-asset descriptor pipeline end-to-end (cache reuse, bridging, deps-digest correctness), we want a deterministic single path so investigations aren't muddied by Bundle/Descriptor selection. The IsBundleReachableAsync helper and its call-site are left in place — re-enabling Bundle mode is a one-line uncomment in a later PR.

Test Instructions

Steps (standard run):
```bash
metaforge explorer run XXXX # ← replace with this PR number
```

Expected result:

  • A scene with neither a descriptor JSON nor a static-scene AB loads via the legacy non-ISS LOD path (full regression — no ISS).
  • A scene with a reachable legacy ISS bundle loads via Bundle mode (LOD assets pulled from the shared AB; visually identical to main).
  • A scene with only a descriptor JSON + per-asset bundles in StreamingAssets/AssetBundles/ loads via Descriptor mode; assets appear at the descriptor's transforms.
  • Walking close enough to trigger the real scene load reuses LOD-bridged assets where the hash matches; no duplicate AB downloads.

Prerequisites

  • For Descriptor-mode testing: a scene with StreamingAssets/AssetBundles/iss_descriptors/{sceneId}_InitialSceneState.json plus the per-asset AB files ({hash}{platform}).
  • For Bundle-mode regression: any realm where the legacy staticscene_{sceneId}{platform} AB still exists.
  • AB manifest version ≥ v49 (older scenes deliberately skip ISS entirely).

Test Steps

  1. Run with a Bundle-mode scene → confirm logs show the HEAD probe succeeding and the existing bundle path runs.
  2. Run with a Descriptor-mode scene → confirm the descriptor JSON is fetched (file:// URL in logs), HEAD probe returns false, descriptor mode kicks in, and per-asset bundles resolve.
  3. Remove the descriptor JSON → scene falls back to the legacy non-ISS LOD path.
  4. Walk close to a Descriptor-mode scene and verify LOD assets transfer into the SDK scene without re-instantiation (cache hit via the bridge).

Additional Testing Notes

  • Bridge slot reservation is capped per-hash to Assets.Count(a => a.hash == h) — if a hash repeats in the descriptor, that many copies can bridge; SDK-instantiated extras shouldn't.
  • AB promise failures (404 / network) on individual per-asset bundles must still let AllAssetsInstantiated() settle so UnloadLODForISS can bridge the successful subset.

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (if required)
  • Performance impact has been considered
  • For SDK features: Test scene is included

Code Review Reference

Please review our Branch & PR Standards before submitting.

🤖 Generated with Claude Code

dalkia and others added 8 commits May 14, 2026 13:08
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Append platform suffix to per-asset bundle URL in descriptor mode so it
  matches the deployed AB filename.
- Use entry.hash as the in-bundle asset name in both bundle and descriptor
  modes — per-asset bundles are baked with the same name-by-hash convention.
- Count AB-promise failures via AddFailedAsset so the LOD can still reach
  RESOLVED and UnloadLODForISS gets a chance to bridge the successful assets.
- Expose InitialSceneStateLOD.SceneID for per-scene debug filtering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the per-asset IsISS flag. The descriptor owns a per-hash bridge-slot
counter capped at metadata.assets.Count(a => a.hash == hash) — so the
bridge never holds more copies of an asset than the scene actually needs.

- ISSDescriptor: TryReserveBridgeSlot / ReleaseBridgeSlot, HashCapacity.
- CleanUpGltfContainerSystem: putInBridge = partition close && slot
  successfully reserved.
- ResolveISSLODSystem: releases the slot when popping from gltfCache.
- GltfContainerAsset.IsISS and the bool isPartOfISS plumbing through
  Utils.TryCreateGltfObject and GltfContainerAsset.Create are gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the IInitialSceneState/InitialSceneStateInfo indirection: the bundle
ref and ISS-asset HashSet were duplicating what the descriptor already knows.
ISSDescriptor now owns scene-lifetime cleanup via AttachAssetBundle(Action)
+ Dereference, exposes IsBundleAsset for O(1) hash redirects, and surfaces
Assets directly (drops the redundant Metadata property).

Callback is typed as Action rather than AssetBundleData to keep DCL.Network
clear of the ECS asmdef (would otherwise cycle through SceneRunner.Scene).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dalkia
dalkia requested review from a team as code owners May 18, 2026 16:44
@dalkia dalkia self-assigned this May 18, 2026
dalkia and others added 4 commits May 18, 2026 19:46
ISSDescriptor moves out of DCL.Network into ECS InitialSceneState folder so
it can hold the typed AssetBundleData ref directly. Field is removed from
SceneEntityDefinition (which is a JSON DTO, not a state holder) and replaced
with a static ISSDescriptorCache.INSTANCE keyed by scene id.

A LoadISSDescriptorSystem (LoadSystemBase) owns the JSON fetch + HEAD probe
and the v49 manifest gate; pre-v49 short-circuits to NONE without HTTP work.
Two trigger sites lazily fire promises: LoadSceneSystemLogicBase before its
bundle prefetch, and UpdateSceneLODInfoSystem at level 0 (new AWAITING_DESCRIPTOR
state on InitialSceneStateLOD avoids re-spawning entities per tick). Cache
dedupes the two via GetISSDescriptor.Equals by scene id.

Sync consumers (TransformsPlugin, CleanUpGltfContainerSystem, ResolveISSLODSystem,
PrepareAssetBundleLoadingParametersSystem, EarlySceneRequestSystem,
SceneFacade.Dispose) read via the cache; GetISSDescriptor is registered with
GlobalDeferredLoadingSystem so its state transitions to Allowed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n cache

ISSDescriptor's typed AssetBundleData ref forced an asmdef cycle if the field
lived on SceneEntityDefinition / SceneData. The fix mirrors the old
IInitialSceneState shape: an IISSDescriptor interface in SceneRunner.Scene
that the concrete ISSDescriptor (in ECS InitialSceneState) implements.
ISceneData/SceneData now carries an IISSDescriptor? — scene-runtime consumers
(plugins, CleanUp, SceneFacade.Dispose) read through it.

LoadISSDescriptorSystem takes IStreamableCache via DI (a NoCache with
useOngoingRequestCache so concurrent triggers share the resolved instance).
The singleton ISSDescriptorCache is gone.

For global-world LOD systems that don't have ISceneData, the resolved
descriptor settles onto SceneDefinitionComponent via a new
ResolveISSDescriptorSystem that lazily spawns + consumes the promise
per definition entity. UpdateSceneLODInfoSystem and ResolveISSLODSystem
read from SceneDefinitionComponent.ISSDescriptor directly.

EarlySceneRequestSystem drops its pre-warm branch — descriptor resolution
is now lazy so the early-warm always missed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LoadISSDescriptorSystem now writes resolved descriptors through a typed
IDiskCache<ISSDescriptor>: GetISSDescriptor.DiskHashCompute keys by scene id,
ISSDescriptorDiskSerializer stores a single JSON document carrying both
state (Bundle/Descriptor/None) and assets so the cache files open in any
text editor. ISSDescriptorDiskCache wraps the disk cache to skip writes
for NONE results — non-ISS scenes incur a quiet 404 on revisit instead of
littering the cache with empty stubs.

Descriptor URL is hardcoded to the LOD manifest bucket
(https://lod-unity-bucket-dev-0871c25.s3.us-east-1.amazonaws.com/lods-unity/manifests/
{sceneId}_StaticSceneDescriptor.json); StreamingAssets path is gone.
404s suppress their underlying log (expected case for non-ISS scenes).

ResolveISSDescriptorSystem gains an ISSDescriptorResolved flag on
SceneDefinitionComponent so the resolved-to-NONE case doesn't keep
re-spawning the promise every tick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Juan Ignacio Molteni <juanignaciomolteni@gmail.com>
@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

badge

New build in progress, come back later!

@dalkia dalkia changed the title ISS via standalone descriptor + per-asset bundles chore: ISS via standalone descriptor + per-asset bundles May 19, 2026
@claude

This comment has been minimized.

bundleReachable ? IISSDescriptor.State.Bundle : IISSDescriptor.State.Descriptor,
metadata.Value);

UnityEngine.Debug.Log($"[JUANI] ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md violation: Debug.Log must be replaced with ReportHub. This also includes the [JUANI] prefix which is developer-personal debug output.

Suggested change
UnityEngine.Debug.Log($"[JUANI] ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");
ReportHub.Log(ReportCategory.SCENE_LOADING, $"ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");

[Query]
private void ConvertFromAssetBundle(Entity entity, ISSAssetCreationHelper creationHelper, ref AssetBundlePromise assetBundleResult)
{
const string DEBUG_SCENE_ID = "bafkreift34mmemx7fvrf6mpoaab7qy2dceq5vwpwehq3wunv5dwulbjveu";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: DEBUG_SCENE_ID and the isDebugScene conditional at line 150–162 are leftover debug scaffolding. This also contains a Debug.Log (line 161) which violates CLAUDE.md — must use ReportHub.

Remove DEBUG_SCENE_ID, isDebugScene, and the if (isDebugScene) branch. The else path already logs the meaningful warning via ReportHub.

private readonly AssetBundleData[] Dependencies;

public AssetBundleData(AssetBundle assetBundle, InitialSceneStateMetadata? initialSceneState, Object[] loadedAssets, Type? assetType, AssetBundleData[] dependencies, string version = "", string source = "")
//TODO: Rehook isISS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking regression: isISS is never passed as true from LoadAssetBundleSystem.CreateAssetBundleDataAsync (the only call site), so every ISS bundle gets isISS = false and UnloadAB() is called on it. The original code used !InitialSceneStateMetadata.HasValue to guard the unload — this PR removes that guard without restoring it.

The PR description claims this flag is "plumbed through so ISS bundles aren't unloaded prematurely" but the code contradicts that. The //TODO: Rehook isISS comment confirms it's unfinished.

Wire isISS properly: LoadAssetBundleSystem.CreateAssetBundleDataAsync receives no ISS context right now. A simple fix would be to add an isISS parameter to CreateAssetBundleDataAsync, set by the caller via GetAssetBundleIntention (a new flag on the intention) or by detecting the bundle hash against the ISS URL.

runtimeInstance.SetIsDisposing();

DisposeInternal();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resource leak: The synchronous Dispose() path does not call SceneData.ISSDescriptor?.Dereference(), while DisposeAsync() at line 112 does. ISceneFacade inherits IDisposable, so any caller using the synchronous path (e.g. in tests or future call sites) will leak the shared ISS bundle.

Add the dereference here:

Suggested change
DisposeInternal();
DisposeInternal();
SceneData.ISSDescriptor?.Dereference();


// Hardcoded for this iteration — wire to DI once the dev/prod bucket split lands.
private static readonly URLDomain DESCRIPTOR_BASE_URL =
URLDomain.FromString("https://lod-unity-bucket-dev-0871c25.s3.us-east-1.amazonaws.com/lods-unity/manifests/");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded dev S3 URL: This URL points to lod-unity-bucket-dev-0871c25.s3.us-east-1.amazonaws.com (a dev bucket). Production builds will always fetch from dev, returning wrong (or missing) descriptors.

The inline comment acknowledges this, but shipping it as-is means Descriptor mode will be broken in prod/staging. This should be wired to DI before merging, or the entire Descriptor path should be gated behind a feature flag until the URL is configurable.

dalkia and others added 2 commits May 21, 2026 12:36
The GLTF container cache is keyed by "hash@digest" (ComposeCacheKey).
Looking up by bare hash from the LOD path missed any bridged entry the
SDK runtime left behind, so the LOD spawned a second instance of an
asset already resident in the cache — the visible "double" overlap.

Also populate DepsDigest on the per-asset ISS LOD AssetBundlePromise so
both LOD and SDK paths land in the same (Hash, DepsDigest) AssetBundleCache
slot. Without this, two parallel LoadAssetBundleSystem flows race for the
same physical bundle and Unity refuses the second one with "asset bundle
already loaded".

Misc: switch descriptor URL suffix from _StaticSceneDescriptor to
_InitialSceneState; drop suppressErrors on the descriptor GetAsync so
real failures surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Juan Ignacio Molteni <juanignaciomolteni@gmail.com>
@claude

This comment has been minimized.

bundleReachable ? IISSDescriptor.State.Bundle : IISSDescriptor.State.Descriptor,
metadata.Value);

UnityEngine.Debug.Log($"[JUANI] ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (CLAUDE.md violation): Debug.Log with personal [JUANI] tag. Must use ReportHub.

Suggested change
UnityEngine.Debug.Log($"[JUANI] ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");
ReportHub.Log(ReportCategory.SCENE_LOADING, $"ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");

Comment on lines +157 to +176
const string DEBUG_SCENE_ID = "bafkreift34mmemx7fvrf6mpoaab7qy2dceq5vwpwehq3wunv5dwulbjveu";

if (!instantiationFrameTimeBudget.TrySpendBudget() || !memoryBudget.TrySpendBudget())
return;

if (assetBundleResult.TryConsume(World, out StreamableLoadingResult<AssetBundleData> Result))
if (!assetBundleResult.TryConsume(World, out StreamableLoadingResult<AssetBundleData> Result))
return;

bool isDebugScene = creationHelper.InitialSceneStateLOD.SceneID == DEBUG_SCENE_ID;
bool stillRelevant = creationHelper.Generation == creationHelper.InitialSceneStateLOD.Generation
&& creationHelper.InitialSceneStateLOD.ParentContainer != null;

if (Result.Succeeded)
{
if (Result.Succeeded)
if (stillRelevant)
{
if (creationHelper.Generation == creationHelper.InitialSceneStateLOD.Generation
&& creationHelper.InitialSceneStateLOD.ParentContainer != null)
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetNameInBundle, out GltfContainerAsset asset))
{
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetHash, out GltfContainerAsset asset))
PositionAsset(creationHelper.InitialSceneStateLOD, creationHelper.AssetHash, asset,
creationHelper.InitialSceneStateLOD.ParentContainer.transform, Result.Asset.InitialSceneStateMetadata.Value, creationHelper.IndexToCreate);
else
{
ReportHub.LogWarning(GetReportData(), $"Failed to load {creationHelper.AssetHash} for LOD, the result may not look correct");
creationHelper.InitialSceneStateLOD.AddFailedAsset(creationHelper.AssetHash);
}
if (isDebugScene)
UnityEngine.Debug.Log($"[Juani] ConvertFromAssetBundle OK {creationHelper.Entry.hash} (counted via AddResolvedAsset)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (CLAUDE.md violation): DEBUG_SCENE_ID, isDebugScene, and the if (isDebugScene) branch with Debug.Log("[Juani] ...") are leftover debug scaffolding with a personal tag. Remove entirely and make the ReportHub.LogWarning path unconditional.

Suggested change
const string DEBUG_SCENE_ID = "bafkreift34mmemx7fvrf6mpoaab7qy2dceq5vwpwehq3wunv5dwulbjveu";
if (!instantiationFrameTimeBudget.TrySpendBudget() || !memoryBudget.TrySpendBudget())
return;
if (assetBundleResult.TryConsume(World, out StreamableLoadingResult<AssetBundleData> Result))
if (!assetBundleResult.TryConsume(World, out StreamableLoadingResult<AssetBundleData> Result))
return;
bool isDebugScene = creationHelper.InitialSceneStateLOD.SceneID == DEBUG_SCENE_ID;
bool stillRelevant = creationHelper.Generation == creationHelper.InitialSceneStateLOD.Generation
&& creationHelper.InitialSceneStateLOD.ParentContainer != null;
if (Result.Succeeded)
{
if (Result.Succeeded)
if (stillRelevant)
{
if (creationHelper.Generation == creationHelper.InitialSceneStateLOD.Generation
&& creationHelper.InitialSceneStateLOD.ParentContainer != null)
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetNameInBundle, out GltfContainerAsset asset))
{
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetHash, out GltfContainerAsset asset))
PositionAsset(creationHelper.InitialSceneStateLOD, creationHelper.AssetHash, asset,
creationHelper.InitialSceneStateLOD.ParentContainer.transform, Result.Asset.InitialSceneStateMetadata.Value, creationHelper.IndexToCreate);
else
{
ReportHub.LogWarning(GetReportData(), $"Failed to load {creationHelper.AssetHash} for LOD, the result may not look correct");
creationHelper.InitialSceneStateLOD.AddFailedAsset(creationHelper.AssetHash);
}
if (isDebugScene)
UnityEngine.Debug.Log($"[Juani] ConvertFromAssetBundle OK {creationHelper.Entry.hash} (counted via AddResolvedAsset)");
if (Result.Succeeded)
{
if (stillRelevant)
{
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetNameInBundle, out GltfContainerAsset asset))
{
PositionAsset(creationHelper.InitialSceneStateLOD, creationHelper.Entry, creationHelper.CacheKey, asset,
creationHelper.InitialSceneStateLOD.ParentContainer.transform);
}
else
{
ReportHub.LogWarning(GetReportData(), $"Failed to load {creationHelper.Entry.hash} for LOD, the result may not look correct");
creationHelper.InitialSceneStateLOD.AddFailedAsset(creationHelper.Entry.hash);
}
}

private readonly AssetBundleData[] Dependencies;

public AssetBundleData(AssetBundle assetBundle, InitialSceneStateMetadata? initialSceneState, Object[] loadedAssets, Type? assetType, AssetBundleData[] dependencies, string version = "", string source = "")
//TODO: Rehook isISS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (unfinished TODO): //TODO: Rehook isISS confirms this isn't wired. LoadAssetBundleSystem.CreateAssetBundleDataAsync (line 170) never passes isISS: true, so every ISS bundle is created with the default false and UnloadAB() is called on it immediately. The PR description says this flag is "plumbed through so ISS bundles aren't unloaded prematurely" — the code contradicts that.

Wire it: add an isISS parameter to CreateAssetBundleDataAsync and detect ISS bundles at the call site (e.g. by checking whether intention.Hash starts with "staticscene_" or by adding an IsISS flag to GetAssetBundleIntention).

DisposeInternal();
SceneData.InitialSceneStateInfo.Dispose();
// Release the shared ISS bundle (if any). Null when no ISS was active for this scene.
SceneData.ISSDescriptor?.Dereference();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (resource leak): DisposeAsync() correctly calls Dereference() here, but the synchronous Dispose() method (around line 87) does not. ISceneFacade inherits IDisposable, so any consumer using the sync path leaks the shared ISS bundle.

Add SceneData.ISSDescriptor?.Dereference(); after DisposeInternal() in the synchronous Dispose() path as well.


// Hardcoded for this iteration — wire to DI once the dev/prod bucket split lands.
private static readonly URLDomain DESCRIPTOR_BASE_URL =
URLDomain.FromString("https://lod-unity-bucket-dev-0871c25.s3.us-east-1.amazonaws.com/lods-unity/manifests/");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: Hardcoded dev S3 URL. The inline comment acknowledges this must be wired to DI, but as-is, production and staging builds will always fetch descriptors from the dev bucket — returning either wrong data or 404 for every production scene, silently making Descriptor mode dead in prod.

Either wire DESCRIPTOR_BASE_URL through DI before merging, or gate Descriptor mode behind a feature flag (FeatureFlagsConfiguration) until the URL is configurable.

return;
// Descriptor is populated by ResolveISSDescriptorSystem (lazy). While unresolved
// CurrentState is None — wait by returning early; UpdateLODLevel re-fires next tick.
if (sceneDefinitionComponent.ISSDescriptorPromise.Entity != Entity.Null) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning (ordering race): This guard returns early when the promise is already in flight, but not when it hasn't been spawned yet. On the first frame a scene entity exists, if UpdateSceneLODInfoSystem executes before ResolveISSDescriptorSystem.SpawnPromise within the same RealmGroup tick, ISSDescriptorPromise.Entity == Entity.Null and ISSDescriptor.CurrentState == None, so the code falls through to legacy LOD — silently bypassing ISS for that scene permanently.

ISSDescriptorResolved already exists on the component for exactly this purpose. Add the guard:

Suggested change
if (sceneDefinitionComponent.ISSDescriptorPromise.Entity != Entity.Null) return;
if (!sceneDefinitionComponent.ISSDescriptorResolved) return;
if (sceneDefinitionComponent.ISSDescriptorPromise.Entity != Entity.Null) return;

Alternatively, add [UpdateAfter(typeof(ResolveISSDescriptorSystem))] to this class.

@claude

This comment has been minimized.

bundleReachable ? IISSDescriptor.State.Bundle : IISSDescriptor.State.Descriptor,
metadata.Value);

UnityEngine.Debug.Log($"[JUANI] ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (CLAUDE.md violation, 3rd flag): Debug.Log with personal [JUANI] tag still present. Must use ReportHub.

Suggested change
UnityEngine.Debug.Log($"[JUANI] ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");
ReportHub.Log(GetReportData(), $"ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");

Comment on lines +157 to +176
const string DEBUG_SCENE_ID = "bafkreift34mmemx7fvrf6mpoaab7qy2dceq5vwpwehq3wunv5dwulbjveu";

if (!instantiationFrameTimeBudget.TrySpendBudget() || !memoryBudget.TrySpendBudget())
return;

if (assetBundleResult.TryConsume(World, out StreamableLoadingResult<AssetBundleData> Result))
if (!assetBundleResult.TryConsume(World, out StreamableLoadingResult<AssetBundleData> Result))
return;

bool isDebugScene = creationHelper.InitialSceneStateLOD.SceneID == DEBUG_SCENE_ID;
bool stillRelevant = creationHelper.Generation == creationHelper.InitialSceneStateLOD.Generation
&& creationHelper.InitialSceneStateLOD.ParentContainer != null;

if (Result.Succeeded)
{
if (Result.Succeeded)
if (stillRelevant)
{
if (creationHelper.Generation == creationHelper.InitialSceneStateLOD.Generation
&& creationHelper.InitialSceneStateLOD.ParentContainer != null)
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetNameInBundle, out GltfContainerAsset asset))
{
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetHash, out GltfContainerAsset asset))
PositionAsset(creationHelper.InitialSceneStateLOD, creationHelper.AssetHash, asset,
creationHelper.InitialSceneStateLOD.ParentContainer.transform, Result.Asset.InitialSceneStateMetadata.Value, creationHelper.IndexToCreate);
else
{
ReportHub.LogWarning(GetReportData(), $"Failed to load {creationHelper.AssetHash} for LOD, the result may not look correct");
creationHelper.InitialSceneStateLOD.AddFailedAsset(creationHelper.AssetHash);
}
if (isDebugScene)
UnityEngine.Debug.Log($"[Juani] ConvertFromAssetBundle OK {creationHelper.Entry.hash} (counted via AddResolvedAsset)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (CLAUDE.md violation, 3rd flag): DEBUG_SCENE_ID, isDebugScene, and Debug.Log("[Juani] ...") are leftover debug scaffolding with a personal tag. Remove the const, the bool, and the if (isDebugScene) branch entirely.

Suggested change
const string DEBUG_SCENE_ID = "bafkreift34mmemx7fvrf6mpoaab7qy2dceq5vwpwehq3wunv5dwulbjveu";
if (!instantiationFrameTimeBudget.TrySpendBudget() || !memoryBudget.TrySpendBudget())
return;
if (assetBundleResult.TryConsume(World, out StreamableLoadingResult<AssetBundleData> Result))
if (!assetBundleResult.TryConsume(World, out StreamableLoadingResult<AssetBundleData> Result))
return;
bool isDebugScene = creationHelper.InitialSceneStateLOD.SceneID == DEBUG_SCENE_ID;
bool stillRelevant = creationHelper.Generation == creationHelper.InitialSceneStateLOD.Generation
&& creationHelper.InitialSceneStateLOD.ParentContainer != null;
if (Result.Succeeded)
{
if (Result.Succeeded)
if (stillRelevant)
{
if (creationHelper.Generation == creationHelper.InitialSceneStateLOD.Generation
&& creationHelper.InitialSceneStateLOD.ParentContainer != null)
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetNameInBundle, out GltfContainerAsset asset))
{
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetHash, out GltfContainerAsset asset))
PositionAsset(creationHelper.InitialSceneStateLOD, creationHelper.AssetHash, asset,
creationHelper.InitialSceneStateLOD.ParentContainer.transform, Result.Asset.InitialSceneStateMetadata.Value, creationHelper.IndexToCreate);
else
{
ReportHub.LogWarning(GetReportData(), $"Failed to load {creationHelper.AssetHash} for LOD, the result may not look correct");
creationHelper.InitialSceneStateLOD.AddFailedAsset(creationHelper.AssetHash);
}
if (isDebugScene)
UnityEngine.Debug.Log($"[Juani] ConvertFromAssetBundle OK {creationHelper.Entry.hash} (counted via AddResolvedAsset)");
if (Result.Succeeded)
{
if (stillRelevant)
{
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetNameInBundle, out GltfContainerAsset asset))
{
PositionAsset(creationHelper.InitialSceneStateLOD, creationHelper.Entry, creationHelper.CacheKey, asset,
creationHelper.InitialSceneStateLOD.ParentContainer.transform);
}
else
{
ReportHub.LogWarning(GetReportData(), $"Failed to load {creationHelper.Entry.hash} for LOD, the result may not look correct");
creationHelper.InitialSceneStateLOD.AddFailedAsset(creationHelper.Entry.hash);
}
}


public AssetBundleData(AssetBundle assetBundle, InitialSceneStateMetadata? initialSceneState, Object[] loadedAssets, Type? assetType, AssetBundleData[] dependencies, string version = "", string source = "")
//TODO: Rehook isISS
public AssetBundleData(AssetBundle assetBundle , Object[] loadedAssets, Type? assetType, AssetBundleData[] dependencies, string version = "", string source = "", bool isISS = false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (unfinished TODO, 3rd flag): //TODO: Rehook isISS still present. LoadAssetBundleSystem.CreateAssetBundleDataAsync at line 170 still passes no isISS argument, so every ISS bundle is constructed with isISS = false and UnloadAB() fires immediately on all of them. The PR description claims this flag is "plumbed through so ISS bundles aren't unloaded prematurely" — the code contradicts that.

Wire it: add an isISS parameter to CreateAssetBundleDataAsync, detect ISS bundles at the call site (e.g. by checking intention.Hash starts with "staticscene_" or by adding an IsISS flag to GetAssetBundleIntention), and remove the TODO comment.


// Hardcoded for this iteration — wire to DI once the dev/prod bucket split lands.
private static readonly URLDomain DESCRIPTOR_BASE_URL =
URLDomain.FromString("https://lod-unity-bucket-dev-0871c25.s3.us-east-1.amazonaws.com/lods-unity/manifests/");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (hardcoded dev S3 URL, 3rd flag): DESCRIPTOR_BASE_URL still points to lod-unity-bucket-dev-0871c25.s3.us-east-1.amazonaws.com. Production builds will silently receive 404s for every descriptor fetch, making Descriptor mode dead in prod. The inline comment acknowledges this is a known shortcut, but it cannot ship as-is.

Must be wired through DI before merging, or the entire Descriptor path must be gated behind a feature flag (FeatureFlagsConfiguration) until the URL is configurable.

return;
// Descriptor is populated by ResolveISSDescriptorSystem (lazy). While unresolved
// CurrentState is None — wait by returning early; UpdateLODLevel re-fires next tick.
if (sceneDefinitionComponent.ISSDescriptorPromise.Entity != Entity.Null) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning (ordering race, 2nd flag): This guard only fires when a promise is already in flight (Entity != Null). On the first frame a scene entity exists — if this system runs before ResolveISSDescriptorSystem.SpawnPromise in the same RealmGroup tick — both ISSDescriptorPromise.Entity == Entity.Null and ISSDescriptor == NONE, so SupportsDescriptor() and SupportsBundle() both return false and the code falls through to legacy LOD at line 100. Because sceneLODInfo.HasLOD(0) will be true on subsequent frames, ISS is silently bypassed for that scene permanently.

ISSDescriptorResolved exists on the component precisely for this. Add it before the existing guard:

Suggested change
if (sceneDefinitionComponent.ISSDescriptorPromise.Entity != Entity.Null) return;
if (!sceneDefinitionComponent.ISSDescriptorResolved) return;
if (sceneDefinitionComponent.ISSDescriptorPromise.Entity != Entity.Null) return;

@claude

claude Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @dalkia's task in 3m 13s —— View job


Code Review (re-review after sync)

  • Read CLAUDE.md and docs/README.md
  • Check previous blocking issues
  • Review new diff vs origin/dev
  • Post final review

Summary

What this PR fixes: Replaces the monolithic single-bundle ISS path with a dual-mode system (Bundle vs Descriptor). The root cause — ISS being tightly coupled to the shared bundle — is correctly addressed architecturally.

Status: All four blocking issues from previous review rounds are still unfixed. This is now the 4th review cycle. The one change in this sync is that SceneFacade.Dispose() now has a TODO comment acknowledging the resource leak — but a comment does not fix a leak. No new issues introduced.


Blocking Issues

1. Debug.Log with personal [JUANI]/[Juani] tags (CLAUDE.md violation)

  • LoadISSDescriptorSystem.cs:66UnityEngine.Debug.Log($"[JUANI] ISS resolved...")see inline
  • ResolveISSLODSystem.cs:157–176const string DEBUG_SCENE_ID, bool isDebugScene, Debug.Log("[Juani] ...")see inline

2. isISS not wired — ISS bundles unloaded prematurely

AssetBundleData.cs:24//TODO: Rehook isISS still present. LoadAssetBundleSystem.CreateAssetBundleDataAsync (line 170) still passes no isISS argument, so every ISS bundle is created with isISS = false and UnloadAB() fires immediately. The PR description says this flag is "plumbed through so ISS bundles aren't unloaded prematurely" — the code contradicts that. — see inline

3. Dispose() missing ISS dereference — resource leak

SceneFacade.cs:85 — The synchronous Dispose() method still does not call SceneData.ISSDescriptor?.Dereference(). A TODO comment was added acknowledging the leak this sync, but that does not fix it. DisposeAsync() at line 122 does call it — the sync path must too. — see inline

4. Hardcoded dev S3 URL

LoadISSDescriptorSystem.cs:37DESCRIPTOR_BASE_URL still points to lod-unity-bucket-dev-0871c25.s3.us-east-1.amazonaws.com. Every production/staging descriptor fetch returns 404, making Descriptor mode silently dead outside dev. Must be wired through DI or gated behind a feature flag before merging. — see inline


Non-Blocking Warning

Ordering race in UpdateSceneLODInfoSystem (3rd flag): ISSDescriptorResolved was added to SceneDefinitionComponent but is still not used in the guard at line 77. On the first frame a scene entity exists, if this system runs before ResolveISSDescriptorSystem.SpawnPromise, ISSDescriptorPromise.Entity == Entity.Null and the guard does not fire — the code falls through to legacy LOD permanently. Add if (!sceneDefinitionComponent.ISSDescriptorResolved) return; before the existing guard. — see inline


REVIEW_RESULT: FAIL ❌
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Touches asset bundle loading pipeline, ISS LOD systems, scene lifecycle/dispose paths, ECS component design, async loading, and per-asset descriptor loading infrastructure across 8+ subsystems.
QA_REQUIRED: YES

bundleReachable ? IISSDescriptor.State.Bundle : IISSDescriptor.State.Descriptor,
metadata.Value);

UnityEngine.Debug.Log($"[JUANI] ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (CLAUDE.md violation, 4th flag): Debug.Log with personal [JUANI] tag still present. Must use ReportHub.

Suggested change
UnityEngine.Debug.Log($"[JUANI] ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");
ReportHub.Log(GetReportData(), $"ISS resolved for {intention.SceneId}: {descriptor.CurrentState} ({descriptor.Assets.Count} assets)");

creationHelper.InitialSceneStateLOD.AddFailedAsset(creationHelper.AssetHash);
}
if (isDebugScene)
UnityEngine.Debug.Log($"[Juani] ConvertFromAssetBundle OK {creationHelper.Entry.hash} (counted via AddResolvedAsset)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (CLAUDE.md violation, 4th flag): DEBUG_SCENE_ID, isDebugScene, and Debug.Log("[Juani] ...") are leftover debug scaffolding with a personal tag — still here for the 4th review. Remove the const string DEBUG_SCENE_ID on line 157, the bool isDebugScene on line 165, and the if (isDebugScene) branch entirely. The success path should call PositionAsset unconditionally.

Suggested change
UnityEngine.Debug.Log($"[Juani] ConvertFromAssetBundle OK {creationHelper.Entry.hash} (counted via AddResolvedAsset)");
if (Utils.TryCreateGltfObject(Result.Asset, creationHelper.AssetNameInBundle, out GltfContainerAsset asset))
{
PositionAsset(creationHelper.InitialSceneStateLOD, creationHelper.Entry, creationHelper.CacheKey, asset,
creationHelper.InitialSceneStateLOD.ParentContainer.transform);
}


public AssetBundleData(AssetBundle assetBundle, InitialSceneStateMetadata? initialSceneState, Object[] loadedAssets, Type? assetType, AssetBundleData[] dependencies, string version = "", string source = "")
//TODO: Rehook isISS
public AssetBundleData(AssetBundle assetBundle , Object[] loadedAssets, Type? assetType, AssetBundleData[] dependencies, string version = "", string source = "", bool isISS = false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (unfinished TODO, 4th flag): //TODO: Rehook isISS still present and UnloadAB() at line 39 still fires unconditionally for all bundles. LoadAssetBundleSystem.CreateAssetBundleDataAsync (line 170) still never passes isISS: true, so every ISS bundle is constructed with isISS = false and UnloadAB() fires immediately — the ISS bundle is unloaded before any per-asset extraction can happen.

The PR description says this flag is "plumbed through so ISS bundles aren't unloaded prematurely". The code contradicts that claim.

Fix: add an isISS flag to GetAssetBundleIntention (or detect via the staticscene_ hash prefix), thread it through CreateAssetBundleDataAsync, pass isISS: true when constructing AssetBundleData, and uncomment the if (!isISS) guard around UnloadAB(). Remove the TODO comment.


DisposeInternal();

// TODO: DisposeAsync dereferences SceneData.ISSDescriptor here; the sync path skips it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (resource leak, 4th flag): The synchronous Dispose() path still does not call SceneData.ISSDescriptor?.Dereference(). In this sync a TODO comment was added acknowledging the leak — but a comment does not fix a resource leak. DisposeAsync() at line 122 correctly calls Dereference(); this path must too.

ISceneFacade inherits IDisposable, so any consumer on the sync path leaks the shared ISS bundle.

Suggested change
// TODO: DisposeAsync dereferences SceneData.ISSDescriptor here; the sync path skips it.
DisposeInternal();
SceneData.ISSDescriptor?.Dereference();


// Hardcoded for this iteration — wire to DI once the dev/prod bucket split lands.
private static readonly URLDomain DESCRIPTOR_BASE_URL =
URLDomain.FromString("https://lod-unity-bucket-dev-0871c25.s3.us-east-1.amazonaws.com/lods-unity/manifests/");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (hardcoded dev S3 URL, 4th flag): DESCRIPTOR_BASE_URL still points to lod-unity-bucket-dev-0871c25.s3.us-east-1.amazonaws.com. Every production and staging descriptor fetch silently returns 404, making Descriptor mode dead outside dev. The inline comment acknowledges this is a known shortcut; it cannot ship as-is.

Must be wired through DI before merging, or the entire Descriptor path must be gated behind a feature flag (FeatureFlagsConfiguration) until the URL is configurable.

return;
// Descriptor is populated by ResolveISSDescriptorSystem (lazy). While unresolved
// CurrentState is None — wait by returning early; UpdateLODLevel re-fires next tick.
if (sceneDefinitionComponent.ISSDescriptorPromise.Entity != Entity.Null) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning (ordering race, 3rd flag): This guard only returns early when a promise is already in flight (Entity != Null). When UpdateSceneLODInfoSystem runs before ResolveISSDescriptorSystem.SpawnPromise on the first frame a scene entity exists, both ISSDescriptorPromise.Entity == Entity.Null and ISSDescriptorResolved == false — the guard does not fire, SupportsDescriptor()/SupportsBundle() both return false, and the code falls through to legacy LOD permanently.

ISSDescriptorResolved was added precisely for this. Add it before the existing guard:

Suggested change
if (sceneDefinitionComponent.ISSDescriptorPromise.Entity != Entity.Null) return;
if (!sceneDefinitionComponent.ISSDescriptorResolved) return;
if (sceneDefinitionComponent.ISSDescriptorPromise.Entity != Entity.Null) return;

@m3taphysics

Copy link
Copy Markdown
Contributor

PR #8805, run #26470065303

Builds: Windows change, Windows baseline, macOS change, macOS baseline

Framework 13 i7

Metric Change Baseline Delta Improvement
Samples 2701 2700
CPU average 33.3 ms 33.3 ms -0.0 ms 0.0%
CPU 1% worst 33.4 ms 33.5 ms -0.1 ms 0.3%
CPU 0.1% worst 33.8 ms 34.9 ms -1.1 ms 3.1% 🟢
GPU average 8.4 ms 8.8 ms -0.4 ms 4.3% 🟢
GPU 1% worst 16.3 ms 18.3 ms -2.0 ms 10.7% 🟢
GPU 0.1% worst 17.8 ms 24.3 ms -6.5 ms 26.7% 🟢

@dalkia
dalkia marked this pull request as draft May 27, 2026 02:24
dalkia and others added 4 commits May 26, 2026 23:29
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both systems and their flag types (EarlySceneFlag, EarlyAssetBundleFlag)
become dead code once ISS descriptor resolution is lazy: the eagerly-
resolved descriptor that justified prewarming the ISS bundle is gone,
EarlySceneRequestSystem stops producing EarlyAssetBundleFlag entities,
and EarlyAssetBundleRequestSystem then has nothing to consume.

Also drops the corresponding using imports and InjectToWorld calls in
AssetBundlesPlugin and GlobalWorldFactory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Temporarily disables the IsBundleReachableAsync HEAD probe in
LoadISSDescriptorSystem so every ISS-capable scene resolves to
Descriptor mode. The helper is kept in place for a later PR that
re-enables Bundle-mode selection; rationale lives in the PR description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the SupportsDepsDigests shape: cached version-number check on
the manifest, short-circuited when the manifest fetch itself failed.
LoadISSDescriptorSystem calls it directly.

Also makes GetISSDescriptor.ManifestVersion non-nullable. By the time
the intention is constructed the scene definition (and its manifest)
has been loaded; a null manifest at this point means an upstream load
already failed and should surface there rather than be silently
swallowed here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dalkia dalkia closed this May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants