chore: opti - reuse persistent native containers RenderSystem (landscape) - #9697
chore: opti - reuse persistent native containers RenderSystem (landscape)#9697NickKhalow wants to merge 4 commits into
Conversation
…stem RenderGroundInternal allocated and disposed a NativeArray<int> and a NativeList<Matrix4x4> with Allocator.TempJob every frame the ground was visible; hold them as Allocator.Persistent fields instead, clearing them each frame and disposing once in OnDispose.
🚦 CI StatusBuild failed! Check the logs to see what went wrong. Warnings not reduced: 13292 => 13296 — remove at least 5 warnings to merge. Warnings/errors in files changed by this PR (4)All Unity tests passed ✅
|
decentraland-bot
left a comment
There was a problem hiding this comment.
STEP 2 — Root-cause check
PASS. The PR correctly addresses the root cause: per-frame Allocator.TempJob allocation and disposal churn of native containers in RenderGroundInternal. The fix promotes them to Allocator.Persistent fields allocated once and cleared per frame — a direct elimination of the allocation overhead, not a workaround.
STEP 3 — Design & integration
PASS. No new unit is introduced. The change modifies the existing RenderGroundSystem to hold persistent scratch buffers as a nullable tuple field, disposed in OnDispose. This matches the established pattern in the sibling LandscapeMiscCullingSystem (which holds nativeFrustumPlanes, cliffsBoundaries, waterBoundaries as Allocator.Persistent fields with disposal in OnDispose).
The containers are per-frame scratch buffers cleared each frame — they hold no entity/component state across frames, so CLAUDE.md §1 ("Systems must not hold persistent entity/component collections") is not violated.
TEARDOWN TRACE:
nativeContainers.instanceCounts→.Dispose()inOnDispose(line 193) ✅nativeContainers.transforms→.Dispose()inOnDispose(line 194) ✅- Pre-existing:
landscape.TerrainLoaded += OnTerrainLoaded(line 50) has no-=inOnDispose— this is a pre-existing leak, not introduced by this PR.
STEP 4 — Member audit
nativeContainers (private field, added): Used by 2 consumers — RenderGroundInternal (read/write, lines 110–127) and OnDispose (read + dispose, lines 187–195). No redundancy, no single-use smell.
STEP 5 — Line-level findings
See inline comments below.
Security review: No security issues found. No secrets, credentials, user input handling, or auth changes. Internal rendering-system optimization only.
STEP 6 — Complexity
SIMPLE — 2 meaningful files (1 system, 1 new test), ~50 lines of meaningful production changes, straightforward allocation-to-persistent promotion within a single system's private method. No ECS infrastructure, query, component, or execution-order changes.
STEP 7 — QA assessment
YES — Runtime rendering code is modified (ground rendering hot path). Ground/grass rendering should be visually verified.
STEP 8 — Non-blocking warnings
opti: which is not a recognized semantic commit type. CI is already flagging this (semantic / title-matches-convention ❌). Consider perf: per conventional commits.
TerrainLoaded event subscriptions (lines 50, 57) are never unsubscribed in OnDispose. Not introduced by this PR but worth a follow-up fix.
RenderGroundContainerReusePerformanceTest.cs.
REVIEW_RESULT: FAIL ❌
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Allocation-to-persistent promotion within RenderGroundSystem's private rendering method, plus a new performance test file
QA_REQUIRED: YES
Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub
| Assert.That(fields.Any(f => f.FieldType == typeof(NativeArray<int>)), Is.True, | ||
| "RenderGroundSystem must hold instanceCounts as a reused NativeArray<int> field " + | ||
| "rather than a per-frame Allocator.TempJob local."); | ||
|
|
||
| Assert.That(fields.Any(f => f.FieldType == typeof(NativeList<Matrix4x4>)), Is.True, | ||
| "RenderGroundSystem must hold transforms as a reused NativeList<Matrix4x4> field " + | ||
| "rather than a per-frame Allocator.TempJob local."); |
There was a problem hiding this comment.
[P1] Reflection assertions will always fail. The field nativeContainers is typed (NativeArray<int>, NativeList<Matrix4x4>)? — i.e., Nullable<ValueTuple<…>>. No field in RenderGroundSystem has FieldType == typeof(NativeArray<int>) because the tuple wrapper changes the reflected type. Both assertions evaluate to false → both fail → this test provides no regression protection.
| Assert.That(fields.Any(f => f.FieldType == typeof(NativeArray<int>)), Is.True, | |
| "RenderGroundSystem must hold instanceCounts as a reused NativeArray<int> field " + | |
| "rather than a per-frame Allocator.TempJob local."); | |
| Assert.That(fields.Any(f => f.FieldType == typeof(NativeList<Matrix4x4>)), Is.True, | |
| "RenderGroundSystem must hold transforms as a reused NativeList<Matrix4x4> field " + | |
| "rather than a per-frame Allocator.TempJob local."); | |
| Assert.That( | |
| fields.Any(f => f.FieldType == typeof((NativeArray<int>, NativeList<Matrix4x4>)?)), | |
| Is.True, | |
| "RenderGroundSystem must hold instanceCounts and transforms as a reused " + | |
| "nullable tuple field (NativeArray<int>, NativeList<Matrix4x4>)? " + | |
| "rather than per-frame Allocator.TempJob locals."); |
| instanceCounts.Dispose(); | ||
| transforms.Dispose(); | ||
| } |
There was a problem hiding this comment.
[P2] Null nativeContainers after disposal for double-dispose safety. After Dispose(), the field still holds a reference to the disposed containers. Setting it to null makes the null-check on line 189 a reliable guard against double-dispose and use-after-free if OnDispose is ever called twice or if RenderGroundInternal runs after disposal due to a lifecycle ordering bug.
| instanceCounts.Dispose(); | |
| transforms.Dispose(); | |
| } | |
| instanceCounts.Dispose(); | |
| transforms.Dispose(); | |
| nativeContainers = null; | |
| } |
…9705 #9706) (#9707) * opti(avatar-rendering): bound bone-matrix job by each avatar's actual bone count BoneMatrixCalculationJob recomputed all MAX_BONE_COUNT matrices per avatar every frame regardless of how many ComputeSkinning actually uploads; refresh and pass through the authoritative per-avatar count so the job only computes the uploaded range. * opti(transforms): assign world cache directly in SetWorldTransform Transform.SetPositionAndRotation is world-authoritative, so the resulting world pose equals the arguments passed in; skip the native position/rotation readback and assign the cache from them directly. * opti(landscape): reuse persistent native containers in RenderGroundSystem RenderGroundInternal allocated and disposed a NativeArray<int> and a NativeList<Matrix4x4> with Allocator.TempJob every frame the ground was visible; hold them as Allocator.Persistent fields instead, clearing them each frame and disposing once in OnDispose. * opti(web-requests): pre-size PartialDownloadHandler and grow geometrically ReceiveData rented exactly PartialData.Length + dataLength and copied the full old buffer on every regrow, causing O(N^2) rent+memcpy churn over a chunked download. Pre-size the buffer from the Content-Length header when available, and otherwise double the buffer on regrow, copying only the bytes written so far. Exercising the growth path requires driving DownloadHandlerScript's native callbacks, which has no stable unit-test seam in headless batch mode. * opti(comms-profiles): skip the remove-intentions lock on empty frames RemoteEntitiesExtensions.Remove ran every frame and unconditionally built an OwnedBunch<RemoveIntention>, whose ctor acquires MutexSync even when there is nothing to remove. Add a racy, lock-free NewBunchAvailable() pre-check (mirrors RemoteProfiles.NewBunchAvailable()) so the lock is skipped entirely on empty frames. * fix: reset stale hand point-at on teleport and scene reload (#9578) A cached absolute point-at world hit-point survived a teleport or same-realm scene reload, leaving an avatar's arm pinned at a stale position. Reset the point-at target from both the PlayerTeleportIntent path and the scene-reload path (ECSReloadScene, both overloads). Verified on real Unity 6000.4.0f1 (compile + EditMode/PlayMode regression tests). * opti(avatar-rendering): hoist frustum-plane extraction out of the outline query GetAvatarsVisibleWithOutline recalculated the camera's frustum planes and re-fetched the camera component for every avatar; both are identical across a frame, so compute them once in Update and pass the camera in via [Data]. * opti(avatar-animation): skip redundant point-at/rotation layer weight writes SetPointAtLayerWeight and SetRotationLayerWeight now shadow the last value written and skip the native Animator.SetLayerWeight call when it hasn't changed; ResetState clears the shadow so a rebind isn't skipped. * refactor(landscape): replace init flag with nullable tuple in RenderGroundSystem * docs(landscape): explain why persistent ground containers never need reallocation * Fix formatting (no code changes) * changed how reload/teleport reset is managed * opti(avatar-animation): finish indexed animator-layer API and drop string-based lookups Completes the layer-index scaffolding introduced with the weight guard: - AnimatorEmoteLayers gains the BASE_LAYER_INDEX constant the scaffolding referenced (fixes compilation) and loses GetFromEmoteMask plus the unused ALL_LAYERS / NON_BASE_LAYERS arrays. - GetAnimatorCurrentStateTag(string) and SetLayerWeight(string) resolved the layer via a native Animator.GetLayerIndex call on every invocation, per frame in HeadIKSystem and HandsIKSystem. All callers now use the cached index API (UpperBodyLayerIndex, GetEmoteLayerIndex, int overloads) and the string overloads are removed. - Removes the unused DclAvatarMask using alias. * Update Explorer/Assets/DCL/Multiplayer/Profiles/RemoveIntentions/IRemoveIntentions.cs Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com> Signed-off-by: Nick Khalow <71646502+NickKhalow@users.noreply.github.com> * docs(transforms): explain direct world cache write in SetWorldTransform * expected length "no double calculation" and comments * refactor(avatar-rendering): drop camera param from IsVisibleInCamera, pass CameraComponent as query data --------- Signed-off-by: Nick Khalow <71646502+NickKhalow@users.noreply.github.com> Co-authored-by: Esteban Ordano <esteban@decentraland.org> Co-authored-by: lorenzo-ranciaffi <lorenzo.ranciaffi@decentraland.org> Co-authored-by: lorenzo-ranciaffi <41125365+lorenzo-ranciaffi@users.noreply.github.com> Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Pull request was closed
Pull Request Description
What does this PR change?
RenderGroundSystem.RenderGroundInternalallocated and disposed aNativeArray<int>(per-mesh instance counts) and aNativeList<Matrix4x4>(instance transforms) withAllocator.TempJobevery frame the ground was visible. This PR holds them asAllocator.Persistentstate instead:(instanceCounts, transforms), allocated on first use.OnDispose.Why no reallocation on landscape change is needed:
instanceCountsholds one slot per ground mesh kind —GroundMeshesis enum-indexed (GroundMeshPiece: Middle/Edge/Corner) with a private setter, so its length is fixed for the lifetime of the app and never depends on the loaded terrain.transformsis aNativeListand grows on demand. This is documented in a code comment at the allocation site.Test Instructions
Steps (standard run):
Expected result:
devin Genesis City (walk/teleport around a few parcels, look at terrain edges and corners).metaforge explorer logs tail --filter "LANDSCAPE").Quality Checklist
Code Review Reference
Please review our Branch & PR Standards before submitting. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.