Summary
On Apple-silicon macOS, any scene whose partition bucket reaches the 0 FPS tier stops ticking permanently. It never resumes when the player comes back into range; only /reload (a fresh SceneFacade) revives it. The scene's own tick-driven logic, network sync and console output all stop, while renderer-driven things (Tweens, avatars, comms) keep working, so the scene looks alive.
The freeze support in SceneFacade assumes that (int)(1000f / 0) yields int.MinValue. That is the x86/x64 result. On arm64 the float→int conversion saturates to int.MaxValue, so the freeze branch is skipped and the update loop sleeps for ~24 days instead.
Environment
- Explorer
v0.178.0-alpha (also reproduced on 0.177) via DecentralandLauncherLight, macOS, Apple M4 Max, IL2CPP universal binary running arm64.
- Code references against
main @ a9ff4354c.
Steps to reproduce
Needs a world: worlds instantiate all their scenes at spawn regardless of distance, so a far scene is running at FPS 0. (Genesis scenes are not loaded until the player is closer, so they don't start in the 0 FPS bucket.)
- Enter a world at a parcel ~10 parcels away from a scene that animates something from a system (per-tick code) and something else with a Tween. Test scene:
robtfm.dcl.eth, spawn at 50,40, the scene is at 50,50 (green cube rotated from engine.addSystem, blue cube rotated by a Tween, 5 s console heartbeat).
- Walk to the scene.
Alternative, same world: spawn inside the scene (both cubes spin), walk 6+ parcels away, come back.
Expected
Both cubes spin, the heartbeat logs every 5 s, the scene's state sync catches up.
Actual
Only the Tween cube spins. The system-driven cube is frozen in its first-tick pose, the heartbeat never prints, and (for an authoritative-multiplayer scene) the client never syncs state. /reload fixes it immediately. Spawning at 52,48 (inside the 20 FPS bucket) or directly in the scene works.
Root cause
RealmPartitionSettingsAsset.fpsBuckets = { 30, 20, 10, 5, 0 } over PartitionSettingsAsset.distanceBuckets (16, 32, 48, 64, 80 … m in the shipped asset): bucket ≥ 4 ⇒ target FPS 0. Worlds instantiate every scene at spawn, so a far world scene starts at FPS 0; a loaded scene the player walks away from drops into it as well.
SceneFacade.SetTargetFPS(int fps): intervalMS = (int)(1000f / fps); — for 0 this casts +Infinity to int.
SceneFacade.IdleWhileRunningAsync:
// Support scene freeze (0 FPS, int.MinValue)
while (intervalMS < 0) { … await DCLTask.Delay(10, ct); }
The guard only triggers for a negative interval. On x86/x64 the out-of-range conversion produces int.MinValue (0x80000000) and this works. On arm64 the conversion saturates to int.MaxValue, so the loop falls through to
int sleepMS = Math.Max(intervalMS - (int)stopWatch.ElapsedMilliseconds, 0);
await DCLTask.Delay(sleepMS, ct);
with sleepMS ≈ int.MaxValue (~24.8 days). A later SetTargetFPS(30) from ControlSceneUpdateLoopSystem.ChangeSceneFps only rewrites the field; nothing wakes the pending delay.
Net effect: the scene runs main() and exactly one update, then sleeps until disposal.
Suggested fix
Represent the freeze explicitly instead of relying on the cast, e.g. intervalMS = fps <= 0 ? -1 : (int)(1000f / fps) (or a dedicated flag) so IdleWhileRunningAsync idles on every platform. Note also that on x86 the same subtraction intervalMS - elapsed would wrap when the guard is bypassed, so a guard on the sign of the value is fragile in both directions.
Notes
- Production
ReportsHandlingSettings keeps only severity 4/1 for SCENE_LOADING, so nothing about this reaches Player.log; the [ALWAYS] lifecycle lines show the scene "started" once and never "disposed" until /reload.
- Discovered while testing local-scene-development / authoritative-server presence: the symptom there is "client never receives server CRDT after walking into the scene", which is the frozen SDK runtime, not comms.
Summary
On Apple-silicon macOS, any scene whose partition bucket reaches the 0 FPS tier stops ticking permanently. It never resumes when the player comes back into range; only
/reload(a freshSceneFacade) revives it. The scene's own tick-driven logic, network sync and console output all stop, while renderer-driven things (Tweens, avatars, comms) keep working, so the scene looks alive.The freeze support in
SceneFacadeassumes that(int)(1000f / 0)yieldsint.MinValue. That is the x86/x64 result. On arm64 the float→int conversion saturates toint.MaxValue, so the freeze branch is skipped and the update loop sleeps for ~24 days instead.Environment
v0.178.0-alpha(also reproduced on 0.177) via DecentralandLauncherLight, macOS, Apple M4 Max, IL2CPP universal binary running arm64.main@a9ff4354c.Steps to reproduce
Needs a world: worlds instantiate all their scenes at spawn regardless of distance, so a far scene is running at FPS 0. (Genesis scenes are not loaded until the player is closer, so they don't start in the 0 FPS bucket.)
robtfm.dcl.eth, spawn at50,40, the scene is at50,50(green cube rotated fromengine.addSystem, blue cube rotated by aTween, 5 s console heartbeat).Alternative, same world: spawn inside the scene (both cubes spin), walk 6+ parcels away, come back.
Expected
Both cubes spin, the heartbeat logs every 5 s, the scene's state sync catches up.
Actual
Only the Tween cube spins. The system-driven cube is frozen in its first-tick pose, the heartbeat never prints, and (for an authoritative-multiplayer scene) the client never syncs state.
/reloadfixes it immediately. Spawning at52,48(inside the 20 FPS bucket) or directly in the scene works.Root cause
RealmPartitionSettingsAsset.fpsBuckets = { 30, 20, 10, 5, 0 }overPartitionSettingsAsset.distanceBuckets(16, 32, 48, 64, 80 … m in the shipped asset): bucket ≥ 4 ⇒ target FPS 0. Worlds instantiate every scene at spawn, so a far world scene starts at FPS 0; a loaded scene the player walks away from drops into it as well.SceneFacade.SetTargetFPS(int fps):intervalMS = (int)(1000f / fps);— for 0 this casts+Infinitytoint.SceneFacade.IdleWhileRunningAsync:int.MinValue(0x80000000) and this works. On arm64 the conversion saturates toint.MaxValue, so the loop falls through tosleepMS ≈ int.MaxValue(~24.8 days). A laterSetTargetFPS(30)fromControlSceneUpdateLoopSystem.ChangeSceneFpsonly rewrites the field; nothing wakes the pending delay.Net effect: the scene runs
main()and exactly one update, then sleeps until disposal.Suggested fix
Represent the freeze explicitly instead of relying on the cast, e.g.
intervalMS = fps <= 0 ? -1 : (int)(1000f / fps)(or a dedicated flag) soIdleWhileRunningAsyncidles on every platform. Note also that on x86 the same subtractionintervalMS - elapsedwould wrap when the guard is bypassed, so a guard on the sign of the value is fragile in both directions.Notes
ReportsHandlingSettingskeeps only severity 4/1 forSCENE_LOADING, so nothing about this reachesPlayer.log; the[ALWAYS]lifecycle lines show the scene "started" once and never "disposed" until/reload.