Skip to content

chore: opti - reuse persistent native containers RenderSystem (landscape) - #9697

Closed
NickKhalow wants to merge 4 commits into
devfrom
opti/perf-hunt/08-render-ground-container-reuse
Closed

chore: opti - reuse persistent native containers RenderSystem (landscape)#9697
NickKhalow wants to merge 4 commits into
devfrom
opti/perf-hunt/08-render-ground-container-reuse

Conversation

@NickKhalow

@NickKhalow NickKhalow commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Pull Request Description

What does this PR change?

RenderGroundSystem.RenderGroundInternal allocated and disposed a NativeArray<int> (per-mesh instance counts) and a NativeList<Matrix4x4> (instance transforms) with Allocator.TempJob every frame the ground was visible. This PR holds them as Allocator.Persistent state instead:

  • Both containers live in a single nullable tuple field (instanceCounts, transforms), allocated on first use.
  • On subsequent frames the counts array is zeroed and the list cleared instead of reallocating.
  • Both are disposed once in OnDispose.

Why no reallocation on landscape change is needed: instanceCounts holds one slot per ground mesh kindGroundMeshes is 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. transforms is a NativeList and grows on demand. This is documented in a code comment at the allocation site.

Test Instructions

For testers: only the happy path needs testing — there are no edge cases reachable from user actions; the change is a frame-loop allocation optimization with identical rendering behavior.

Steps (standard run):

metaforge explorer run 9697

Expected result:

  • Ground and grass render exactly as on dev in Genesis City (walk/teleport around a few parcels, look at terrain edges and corners).
  • Switching to a World and back still shows correct ground.
  • No new errors/warnings in logs (metaforge explorer logs tail --filter "LANDSCAPE").

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. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.

eordano and others added 2 commits August 11, 2026 18:47
…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.
@NickKhalow
NickKhalow requested review from a team as code owners August 12, 2026 11:57
@github-actions
github-actions Bot requested a review from anicalbano August 12, 2026 11:57
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

Build failed! Check the logs to see what went wrong.
If the error repeats please consider the clean-build tag.

Lint

Warnings not reduced: 13292 => 13296 — remove at least 5 warnings to merge.

Warnings/errors in files changed by this PR (4)
Assets/DCL/Landscape/Tests/PerformanceTests/GroundContainerReuse/RenderGroundContainerReusePerformanceTest.cs:196  AccessToDisposedClosure  Captured variable is disposed in the outer scope
Assets/DCL/Landscape/Tests/PerformanceTests/GroundContainerReuse/RenderGroundContainerReusePerformanceTest.cs:196  AccessToDisposedClosure  Captured variable is disposed in the outer scope
Assets/DCL/Landscape/Tests/PerformanceTests/GroundContainerReuse/RenderGroundContainerReusePerformanceTest.cs:197  AccessToDisposedClosure  Captured variable is disposed in the outer scope
Assets/DCL/Landscape/Tests/PerformanceTests/GroundContainerReuse/RenderGroundContainerReusePerformanceTest.cs:198  AccessToDisposedClosure  Captured variable is disposed in the outer scope

Tests

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped
EditMode ✅ Passed 24941 0 13
PlayMode ✅ Passed 236 0 36

@NickKhalow NickKhalow changed the title opti(landscape): reuse persistent native containers in RenderGroundSy… opti: reuse persistent native containers RenderSystem (landscape) Aug 12, 2026

@decentraland-bot decentraland-bot left a comment

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.

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() in OnDispose (line 193) ✅
  • nativeContainers.transforms.Dispose() in OnDispose (line 194) ✅
  • Pre-existing: landscape.TerrainLoaded += OnTerrainLoaded (line 50) has no -= in OnDispose — 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

⚠️ PR title uses opti: which is not a recognized semantic commit type. CI is already flagging this (semantic / title-matches-convention ❌). Consider perf: per conventional commits.

⚠️ Pre-existing: TerrainLoaded event subscriptions (lines 50, 57) are never unsubscribed in OnDispose. Not introduced by this PR but worth a follow-up fix.

⚠️ Missing trailing newline at end of 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

Comment on lines +52 to +58
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.");

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.

[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.

Suggested change
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.");

Comment on lines 193 to 195
instanceCounts.Dispose();
transforms.Dispose();
}

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.

[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.

Suggested change
instanceCounts.Dispose();
transforms.Dispose();
}
instanceCounts.Dispose();
transforms.Dispose();
nativeContainers = null;
}

@NickKhalow NickKhalow changed the title opti: reuse persistent native containers RenderSystem (landscape) chore: opti - reuse persistent native containers RenderSystem (landscape) Aug 12, 2026
@NickKhalow
NickKhalow enabled auto-merge (squash) August 12, 2026 12:24
NickKhalow added a commit that referenced this pull request Aug 13, 2026
…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>
@eordano eordano closed this Aug 13, 2026
auto-merge was automatically disabled August 13, 2026 17:26

Pull request was closed

@lorenzo-ranciaffi
lorenzo-ranciaffi deleted the opti/perf-hunt/08-render-ground-container-reuse branch August 14, 2026 12:57
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.

4 participants