Skip to content

fix(core): disable cache for SceneLoader to avoid loadScene self-destroy#2993

Open
cptbtptpbcptdtptp wants to merge 2 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/scene-asset-cache-eviction
Open

fix(core): disable cache for SceneLoader to avoid loadScene self-destroy#2993
cptbtptpbcptdtptp wants to merge 2 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/scene-asset-cache-eviction

Conversation

@cptbtptpbcptdtptp
Copy link
Copy Markdown
Collaborator

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented May 11, 2026

Disable resource cache for SceneLoader so loadScene always produces a fresh Scene instance.

Bug: loadScene(url) with destroyOldScene=true, cached Scene is active scene -> resourceManager.load returns same instance -> then-handler destroys it -> rootEntities empty, PhysicsScene released, screen blank.

Fix: useCache true to false for SceneLoader. Consistent with PrimitiveMeshLoader and ProjectLoader.

Aligned with Unity/Unreal which all create fresh Scene instances.

Origin: Replaces evict workaround from commit 599a7e6 on fix/shaderlab (by @luzhuang) with root-cause fix.

Summary by CodeRabbit

  • Bug Fixes
    • Adjusted scene asset loader configuration
  • Tests
    • Added validation tests for scene asset loader behavior

Review Change Stack

@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.09%. Comparing base (1bc2b10) to head (63b6b1f).

Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #2993      +/-   ##
===========================================
- Coverage    78.25%   78.09%   -0.16%     
===========================================
  Files          900      900              
  Lines        99234    99234              
  Branches     10172    10198      +26     
===========================================
- Hits         77657    77499     -158     
- Misses       21406    21564     +158     
  Partials       171      171              
Flag Coverage Δ
unittests 78.09% <100.00%> (-0.16%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Walkthrough

The PR flips the @resourceLoader(AssetType.Scene, ["scene"], ...) decorator's third argument from true to false on SceneLoader and adds Vitest tests that assert loader useCache flags for scene, primitive mesh, and texture assets.

Changes

Scene Loader Registration

Layer / File(s) Summary
Scene Loader registration flag and validation tests
packages/loader/src/SceneLoader.ts, tests/src/core/resource/SceneLoaderCache.test.ts
The SceneLoader class's @resourceLoader(AssetType.Scene, ["scene"], ...) decorator changes its third boolean parameter from true to false. Added tests initialize a WebGLEngine and assert ResourceManager._loaders entries: AssetType.Scene and AssetType.PrimitiveMesh have useCache === false, while AssetType.Texture2D/AssetType.Texture has useCache === true.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 I flipped a flag, a quiet tweak,
Scenes will load fresh, not cling to the meek.
Tests hop in, they check and cheer,
Cache for textures, scenes stay clear.
A tiny change, a happier year.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: disabling cache for SceneLoader to prevent self-destruction during loadScene operations. It directly relates to the primary fix in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
packages/core/src/SceneManager.ts (3)

93-116: Consider documenting concurrent loadScene behavior.

If loadScene(url, true) is called multiple times concurrently with overlapping execution, the second promise resolution will destroy scenes added by the first (lines 108-111 destroy ALL scenes in _scenes). While this may be acceptable given typical usage patterns, it could cause unexpected behavior in edge cases.

Consider adding a guard or documentation noting that concurrent loadScene calls with destroyOldScene=true should be avoided, or implementing a loading state flag to prevent concurrent loads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/SceneManager.ts` around lines 93 - 116, The loadScene
method can concurrently destroy scenes added by overlapping calls when
destroyOldScene=true; add a simple guard to prevent concurrent destructive loads
by introducing a private flag (e.g., this._isLoadingDestructiveScene) or a
pending load tracker (e.g., this._pendingDestructiveLoadUrl) and check it at the
start of loadScene(url, destroyOldScene) to either reject/return the existing
promise or queue the new request; set the flag before calling
resourceManager.load and clear it in the scenePromise.then and its rejection
path, and use the existing symbols loadScene, destroyOldScene, scenePromise,
this._scenes and resourceManager to locate where to implement the guard.

99-99: ⚡ Quick win

Add defensive check for _virtualPathResourceMap existence.

If resourceManager._virtualPathResourceMap is undefined, accessing [url]?.path will throw a TypeError. Consider adding a defensive check or null coalescing for the entire map.

🛡️ Suggested defensive fix
-    const realPath = resourceManager._virtualPathResourceMap[url]?.path ?? url;
+    const realPath = resourceManager._virtualPathResourceMap?.[url]?.path ?? url;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/SceneManager.ts` at line 99, The code assumes
resourceManager._virtualPathResourceMap exists before indexing it which can
throw if undefined; update the realPath resolution in SceneManager (the line
referencing resourceManager._virtualPathResourceMap and url) to defensively
access the map (e.g., check or use optional chaining on the entire map before
indexing) so you use the mapped path when present and fall back to url
otherwise.

99-102: ⚡ Quick win

Use the existing _getRemoteUrl() helper method instead of duplicating its logic.

Line 99 duplicates the logic from ResourceManager._getRemoteUrl() (which does this._virtualPathResourceMap[url]?.path ?? url). Replace the inline logic with:

const realPath = resourceManager._getRemoteUrl(url);

This avoids duplicating the internal path resolution logic and keeps the code maintainable if the resolution strategy changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/SceneManager.ts` around lines 99 - 102, The code duplicates
ResourceManager path-resolution logic by inlining
this._virtualPathResourceMap[url]?.path ?? url to compute realPath; replace that
inline expression with a call to resourceManager._getRemoteUrl(url) so the
SceneManager uses ResourceManager's canonical resolution strategy (affecting
realPath, the subsequent getFromCache<Scene>(realPath) lookup, and the
_deleteAsset(cached) branch).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/SceneManager.ts`:
- Around line 93-116: The loadScene method can concurrently destroy scenes added
by overlapping calls when destroyOldScene=true; add a simple guard to prevent
concurrent destructive loads by introducing a private flag (e.g.,
this._isLoadingDestructiveScene) or a pending load tracker (e.g.,
this._pendingDestructiveLoadUrl) and check it at the start of loadScene(url,
destroyOldScene) to either reject/return the existing promise or queue the new
request; set the flag before calling resourceManager.load and clear it in the
scenePromise.then and its rejection path, and use the existing symbols
loadScene, destroyOldScene, scenePromise, this._scenes and resourceManager to
locate where to implement the guard.
- Line 99: The code assumes resourceManager._virtualPathResourceMap exists
before indexing it which can throw if undefined; update the realPath resolution
in SceneManager (the line referencing resourceManager._virtualPathResourceMap
and url) to defensively access the map (e.g., check or use optional chaining on
the entire map before indexing) so you use the mapped path when present and fall
back to url otherwise.
- Around line 99-102: The code duplicates ResourceManager path-resolution logic
by inlining this._virtualPathResourceMap[url]?.path ?? url to compute realPath;
replace that inline expression with a call to resourceManager._getRemoteUrl(url)
so the SceneManager uses ResourceManager's canonical resolution strategy
(affecting realPath, the subsequent getFromCache<Scene>(realPath) lookup, and
the _deleteAsset(cached) branch).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4f5f2e47-1909-4c4b-a9e3-acc848386503

📥 Commits

Reviewing files that changed from the base of the PR and between 1bc2b10 and 2dc6913.

📒 Files selected for processing (1)
  • packages/core/src/SceneManager.ts

GuoLei1990

This comment was marked as outdated.

@GuoLei1990 GuoLei1990 mentioned this pull request May 11, 2026
3 tasks
Scene is a live runtime tree, not an immutable asset. Caching the loaded
Scene caused a self-destroy race when loadScene(url) was called with a URL
whose cached Scene was the current active scene:

  1. resourceManager.load<Scene>({url}) returned the cached (= currently
     active) Scene instance
  2. SceneManager.loadScene then entered the destroyOldScene branch and
     destroyed the old scenes — including the one just returned
  3. The "new" scene was the same object, now destroyed: rootEntities
     empty, native PhysicsScene released, screen blank, no error logged

The root cause is the cache-vs-construct conflict. A Scene is both an
asset (a JSON blob on disk) and a constructed live runtime. Caching the
constructed instance and returning it for "load same URL again" violates
the user's intent ("reload this level").

Other engines (Unity SceneManager.LoadScene, Cocos director.loadScene,
Unreal OpenLevel) all create a fresh Scene per load — none cache.
Aligning Galacean to the same convention.

`PrimitiveMeshLoader` and `ProjectLoader` already use `useCache: false`
for similar reasons (constructed at load time, not immutable assets),
so the pattern is established in the codebase.
@cptbtptpbcptdtptp cptbtptpbcptdtptp force-pushed the fix/scene-asset-cache-eviction branch from 2dc6913 to e1b18a2 Compare May 12, 2026 02:43
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/loader/src/SceneLoader.ts`:
- Line 170: The resourceLoader registration currently sets useCache globally for
AssetType.Scene (the decorator at resourceLoader(AssetType.Scene, ["scene"],
false)), which disables caching for all SceneLoader instances and breaks
additive loads; instead revert the registration to allow normal caching and
implement targeted eviction inside SceneManager.loadScene: keep the
decorator/SceneLoader registration default (allow cache), and when
loadScene(...) is called with destroyOldScene === true, explicitly evict or
reload the specific scene from the loader/cache (e.g., call the SceneLoader
instance's cache invalidation method or a cache.clearFor(sceneId) function)
before loading; reference resourceLoader(AssetType.Scene,...), the SceneLoader
class, and SceneManager.loadScene(destroyOldScene) to locate and modify the
code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 93d29346-a827-4e96-b28c-9ee3b0cfecba

📥 Commits

Reviewing files that changed from the base of the PR and between 2dc6913 and e1b18a2.

📒 Files selected for processing (1)
  • packages/loader/src/SceneLoader.ts

}

@resourceLoader(AssetType.Scene, ["scene"], true)
@resourceLoader(AssetType.Scene, ["scene"], false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify resourceLoader third-arg semantics and scene load call-path impact (read-only).

set -euo pipefail

echo "== Locate resourceLoader declaration/signature =="
rg -n -C3 '\bfunction\s+resourceLoader\b|\bconst\s+resourceLoader\b|\bexport\s+function\s+resourceLoader\b'

echo
echo "== Find all Scene loader registrations and third argument values =="
rg -n -C2 '@resourceLoader\(\s*AssetType\.Scene'

echo
echo "== Find SceneManager.loadScene implementation and cache eviction logic =="
rg -n -C6 '\bclass\s+SceneManager\b|\bloadScene\s*\(' packages

echo
echo "== Find internal cache operations relevant to scene assets =="
rg -n -C4 '\b_deleteAsset\b|\bget\b.*Asset|virtualPath|cache' packages

Repository: galacean/engine

Length of output: 50371


The cache behavior change is global and affects additive scene loads unintentionally.

Setting useCache: false on the Scene loader registration at line 170 disables caching for all SceneManager.loadScene() calls, regardless of the destroyOldScene parameter. The resourceLoader decorator passes useCache to the loader constructor at registration time, not per-load call, so additive scene loads (destroyOldScene = false) will also bypass the cache. The PR objective specifies targeted cache eviction only when destroying old scenes, but this implementation affects both destructive and additive loads equally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/loader/src/SceneLoader.ts` at line 170, The resourceLoader
registration currently sets useCache globally for AssetType.Scene (the decorator
at resourceLoader(AssetType.Scene, ["scene"], false)), which disables caching
for all SceneLoader instances and breaks additive loads; instead revert the
registration to allow normal caching and implement targeted eviction inside
SceneManager.loadScene: keep the decorator/SceneLoader registration default
(allow cache), and when loadScene(...) is called with destroyOldScene === true,
explicitly evict or reload the specific scene from the loader/cache (e.g., call
the SceneLoader instance's cache invalidation method or a
cache.clearFor(sceneId) function) before loading; reference
resourceLoader(AssetType.Scene,...), the SceneLoader class, and
SceneManager.loadScene(destroyOldScene) to locate and modify the code.

@cptbtptpbcptdtptp cptbtptpbcptdtptp changed the title fix(core): evict active scene asset cache on loadScene to avoid self-destroy fix(core): disable cache for SceneLoader to avoid loadScene self-destroy May 12, 2026
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

Adds regression tests for SceneLoader cache policy:
- SceneLoader.useCache === false (the fix in this PR)
- PrimitiveMeshLoader.useCache === false (existing convention, contrast)
- Texture2D loader.useCache === true (immutable asset, contrast)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/src/core/resource/SceneLoaderCache.test.ts`:
- Around line 9-11: Add an afterAll teardown that destroys the WebGLEngine
created in beforeAll to prevent WebGL resource leaks: implement afterAll(async
() => { if (engine) await engine.destroy(); }); referencing the existing engine
variable and the WebGLEngine API (engine.destroy) so the test file
SceneLoaderCache.test.ts cleans up after itself and avoids cross-test
contamination.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bd3993f9-f2cd-4660-8771-990d197fcbcb

📥 Commits

Reviewing files that changed from the base of the PR and between e1b18a2 and 63b6b1f.

📒 Files selected for processing (1)
  • tests/src/core/resource/SceneLoaderCache.test.ts

Comment on lines +9 to +11
beforeAll(async () => {
engine = await WebGLEngine.create({ canvas: document.createElement("canvas") });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add teardown for WebGLEngine to avoid test resource leaks.

WebGLEngine is created in beforeAll but never destroyed. Please add afterAll(async () => { await engine.destroy(); }) (or the engine’s correct dispose API) to prevent cross-test contamination and WebGL resource leaks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/src/core/resource/SceneLoaderCache.test.ts` around lines 9 - 11, Add an
afterAll teardown that destroys the WebGLEngine created in beforeAll to prevent
WebGL resource leaks: implement afterAll(async () => { if (engine) await
engine.destroy(); }); referencing the existing engine variable and the
WebGLEngine API (engine.destroy) so the test file SceneLoaderCache.test.ts
cleans up after itself and avoids cross-test contamination.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

总结

缓存 Scene 后 loadScene(..., destroyOldScene=true) 会把刚加载的同一 Scene 实例当作旧 Scene 销毁——禁用缓存后每次 loadScene 得到新实例,旧/新 Scene 必然是不同对象,问题消除。一行改动,方向正确。

测试验证了核心断言(useCache === false),额外的 PrimitiveMesh/Texture2D 对照测试属于增量覆盖,不阻塞合并。

LGTM,可合入。

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