decentraland / decentraland/unity-explorer

Scenes in the 0 FPS partition bucket freeze permanently on Apple silicon (int.MaxValue interval, never resumes)

Open
#10,059 1 comment 0 reactions 0 assignees View on GitHub
1-high bug Mac Only sdk
Dominant language
C#
Stars
23
Forks
17
Avg merge
2d 16h
Merged PRs (30d)
101

Description

### 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.)

1. 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).
2. 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`:
```csharp
// 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
```csharp
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.

Contributor guide

Open the contributing guide

Research direction

Start in SceneFacade.SetTargetFPS and IdleWhileRunningAsync, then trace ControlSceneUpdateLoopSystem.ChangeSceneFps to understand how target FPS changes are applied. Reproduce the 0 FPS transition on arm64 if possible and verify that a scene resumes ticking after returning to range, while renderer-driven activity continues. Done means freeze behavior is explicit and platform-independent, and the described scene heartbeat, system-driven animation, and state sync recover without /reload.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
game-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.