Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6bca968
opti(avatar-rendering): bound bone-matrix job by each avatar's actual…
eordano Aug 7, 2026
d8f8686
opti(transforms): assign world cache directly in SetWorldTransform
eordano Aug 7, 2026
f97d9fc
opti(landscape): reuse persistent native containers in RenderGroundSy…
eordano Aug 7, 2026
27da68f
opti(web-requests): pre-size PartialDownloadHandler and grow geometri…
eordano Aug 7, 2026
844d3ee
opti(comms-profiles): skip the remove-intentions lock on empty frames
eordano Aug 7, 2026
3c1b9dc
fix: reset stale hand point-at on teleport and scene reload (#9578)
eordano Aug 12, 2026
36c528f
opti(avatar-rendering): hoist frustum-plane extraction out of the out…
eordano Aug 12, 2026
99b03d7
opti(avatar-animation): skip redundant point-at/rotation layer weight…
eordano Aug 12, 2026
22d33ae
refactor(landscape): replace init flag with nullable tuple in RenderG…
NickKhalow Aug 12, 2026
16d4815
docs(landscape): explain why persistent ground containers never need …
NickKhalow Aug 12, 2026
b7151c9
Merge branch 'dev' into opti/perf-hunt/08-render-ground-container-reuse
NickKhalow Aug 12, 2026
5e515e2
Fix formatting (no code changes)
NickKhalow Aug 12, 2026
af8e263
Merge branch 'dev' into opti/multiplayer/06-remove-intentions-fast-path
NickKhalow Aug 12, 2026
f0f4096
changed how reload/teleport reset is managed
lorenzo-ranciaffi Aug 12, 2026
1463233
opti(avatar-animation): finish indexed animator-layer API and drop st…
NickKhalow Aug 12, 2026
24bb9de
Merge branch 'dev' into fix/9578
lorenzo-ranciaffi Aug 12, 2026
26f96cc
Update Explorer/Assets/DCL/Multiplayer/Profiles/RemoveIntentions/IRem…
NickKhalow Aug 12, 2026
7100384
docs(transforms): explain direct world cache write in SetWorldTransform
NickKhalow Aug 12, 2026
7851375
expected length "no double calculation" and comments
NickKhalow Aug 12, 2026
0efdc6e
refactor(avatar-rendering): drop camera param from IsVisibleInCamera,…
NickKhalow Aug 12, 2026
ea776d8
Merge remote-tracking branch 'origin/fix/9578' into opti/perf-hunt/ro…
NickKhalow Aug 12, 2026
b41c866
Merge remote-tracking branch 'origin/opti/perf-hunt/08-render-ground-…
NickKhalow Aug 12, 2026
0c97103
Merge remote-tracking branch 'origin/opti/multiplayer/06-remove-inten…
NickKhalow Aug 12, 2026
34ef78e
Merge remote-tracking branch 'origin/opti/chat/07-avatar-layer-weight…
NickKhalow Aug 12, 2026
3ddb0a1
Merge remote-tracking branch 'origin/opti/perf-hunt/07-set-world-tran…
NickKhalow Aug 12, 2026
49dd614
Merge remote-tracking branch 'origin/opti/perf-hunt/16-partial-downlo…
NickKhalow Aug 12, 2026
823f38c
Merge remote-tracking branch 'origin/opti/perf-hunt/04-bone-matrix-ca…
NickKhalow Aug 12, 2026
74cdef8
Merge remote-tracking branch 'origin/opti/perf-hunt/03-avatar-outline…
NickKhalow Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ public void RegisterAvatar(AvatarBase avatarBase, ref AvatarTransformMatrixCompo
remoteAvatars.Register(avatarBase, ref transformMatrixComponent);
}

/// <summary>
/// Pushes the avatar's authoritative bone count (AvatarCustomSkinningComponent.BoneCount — the
/// exact number of matrices ComputeSkinning uploads) into the matching pipeline so the
/// calculation job produces precisely that range. Must be called every frame before
/// ScheduleBoneMatrixCalculation; unregistered avatars (invalid index) are ignored.
/// </summary>
public void SetBoneCount(ref AvatarTransformMatrixComponent transformMatrixComponent, int boneCount)
{
if (transformMatrixComponent.IndexInGlobalJobArray.TryGetValue(out int validIndex) == false)
return;

if (transformMatrixComponent.IsMainPlayer)
mainPlayerAvatar.SetBoneCount(boneCount);
else
remoteAvatars.SetBoneCount(validIndex, boneCount);
}

public void Dispose()
{
// Leak the resouces. Managed dispose of TransformAccessArray takes very much time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ internal class MainPlayerPipeline : IDisposable
private NativeArray<float4x4> avatarMatrix;
private NativeArray<bool> updateFlag;

private NativeArray<int> perAvatarBoneCount;

public BoneMatrixCalculationJob Job;

internal MainPlayerPipeline(int bonesArrayLength)
Expand All @@ -34,9 +36,19 @@ internal MainPlayerPipeline(int bonesArrayLength)
bonesCombined = new NativeArray<float4x4>(bonesArrayLength, Allocator.Persistent);
avatarMatrix = new NativeArray<float4x4>(1, Allocator.Persistent);
updateFlag = new NativeArray<bool>(1, Allocator.Persistent);
perAvatarBoneCount = new NativeArray<int>(1, Allocator.Persistent) { [0] = bonesArrayLength };
Job = new BoneMatrixCalculationJob(bonesArrayLength, bonesArrayLength, bonesCombined);
}

/// <summary>
/// Refreshes the matrix count for the main player from the authoritative
/// AvatarCustomSkinningComponent.BoneCount. Called every frame before ScheduleAndComplete.
/// </summary>
public void SetBoneCount(int boneCount)
{
perAvatarBoneCount[0] = boneCount;
}

public void Register(Transform rootTransform, BoneArray bones, Transform dummyTransform)
{
updateFlag[0] = true;
Expand Down Expand Up @@ -76,6 +88,7 @@ public void ScheduleAndComplete()

Job.AvatarTransform = avatarMatrix;
Job.UpdateAvatar = updateFlag;
Job.PerAvatarBoneCount = perAvatarBoneCount;
var calcHandle = Job.Schedule(1, 1, gatherHandle);
calcHandle.Complete(); // Fast — 1 avatar, 62 bones. Unlocks main player transforms.
}
Expand All @@ -85,6 +98,7 @@ public void Dispose()
bonesCombined.Dispose();
avatarMatrix.Dispose();
updateFlag.Dispose();
perAvatarBoneCount.Dispose();
Job.Dispose();

if (bonesTA.isCreated) bonesTA.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ internal class RemoteAvatarPipeline : IDisposable
private QuickArray<bool> updateAvatar;
private QuickArray<float4x4> bonesCombined;

private QuickArray<int> perAvatarBoneCount;

private Transform[] flatBones;
private Transform[] flatRoots;

Expand Down Expand Up @@ -61,6 +63,9 @@ internal RemoteAvatarPipeline(int initialCapacity, int bonesArrayLength, int bon
matrixFromAllAvatars = new QuickArray<float4x4>(initialCapacity);
updateAvatar = new QuickArray<bool>(initialCapacity);

perAvatarBoneCount = new QuickArray<int>(initialCapacity);
FillWithStride(perAvatarBoneCount, 0, initialCapacity, bonesArrayLength);

flatBones = new Transform[initialCapacity * bonesArrayLength];
flatRoots = new Transform[initialCapacity];
FillWithDummy(flatBones, 0, flatBones.Length, dummyTransform);
Expand Down Expand Up @@ -152,6 +157,19 @@ public void Release(ref AvatarTransformMatrixComponent avatarTransformMatrixComp
avatarTransformMatrixComponent.IndexInGlobalJobArray = GlobalJobArrayIndex.Unassign();
}

/// <summary>
/// Refreshes the per-avatar matrix count for a live slot from the authoritative
/// AvatarCustomSkinningComponent.BoneCount. Called every frame (before Schedule) so a
/// wearable re-equip that changes the bone count is reflected without needing a re-register.
/// </summary>
public void SetBoneCount(int validIndex, int boneCount)
{
if (validIndex < 0 || validIndex >= perAvatarBoneCount.Length)
return;

perAvatarBoneCount[validIndex] = boneCount;
}

public void Schedule(int batchCount)
{
if (avatarIndex == 0)
Expand All @@ -173,6 +191,7 @@ public void Schedule(int batchCount)

Job.AvatarTransform = matrixFromAllAvatars.InnerNativeArray();
Job.UpdateAvatar = updateAvatar.InnerNativeArray();
Job.PerAvatarBoneCount = perAvatarBoneCount.InnerNativeArray();
handle = Job.Schedule(avatarIndex, batchCount, combinedGatherHandle);
}

Expand Down Expand Up @@ -203,6 +222,10 @@ private void ResizeArrays()
matrixFromAllAvatars.ReAlloc(newCapacity);
updateAvatar.ReAlloc(newCapacity);

int oldCapacity = currentAvatarAmountSupported;
perAvatarBoneCount.ReAlloc(newCapacity);
FillWithStride(perAvatarBoneCount, oldCapacity, newCapacity - oldCapacity, bonesArrayLength);

int oldBonesLength = flatBones.Length;
int oldRootsLength = flatRoots.Length;

Expand All @@ -225,6 +248,12 @@ private static void FillWithDummy(Transform[] array, int startIndex, int count,
array[i] = dummy;
}

private static void FillWithStride(QuickArray<int> array, int startIndex, int count, int value)
{
for (int i = startIndex; i < startIndex + count; i++)
array[i] = value;
}

public void Dispose()
{
// Leak the resouces. Managed dispose of TransformAccessArray takes very much time.
Expand All @@ -244,6 +273,9 @@ public void Dispose()
updateAvatar.Dispose();
stopwatch.LogStep("updateAvatar.Dispose");

perAvatarBoneCount.Dispose();
stopwatch.LogStep("perAvatarBoneCount.Dispose");

Job.Dispose();
stopwatch.LogStep("job.Dispose");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace DCL.AvatarRendering.AvatarShape.ComputeShader
[BurstCompile]
public struct BoneMatrixCalculationJob : IJobParallelFor, IDisposable
{
private readonly int boneCount;
private readonly int boneStride;

[NativeDisableParallelForRestriction]
private NativeArray<float4x4> bonesMatricesResult;
Expand All @@ -20,14 +20,17 @@ public struct BoneMatrixCalculationJob : IJobParallelFor, IDisposable

[NativeDisableParallelForRestriction] public NativeArray<bool> UpdateAvatar;

[ReadOnly] [NativeDisableParallelForRestriction] public NativeArray<int> PerAvatarBoneCount;

public NativeArray<float4x4> BonesMatricesResult => bonesMatricesResult;

public BoneMatrixCalculationJob(int boneCount, int bonesPerAvatarLength, NativeArray<float4x4> boneWorldMatrixArray)
public BoneMatrixCalculationJob(int boneStride, int bonesPerAvatarLength, NativeArray<float4x4> boneWorldMatrixArray)
{
this.boneCount = boneCount;
this.boneStride = boneStride;
bonesMatricesResult = new NativeArray<float4x4>(bonesPerAvatarLength, Allocator.Persistent);
AvatarTransform = default;
UpdateAvatar = default;
PerAvatarBoneCount = default;

this.boneWorldMatrixArray = boneWorldMatrixArray;
}
Expand All @@ -45,9 +48,10 @@ public void Execute(int avatarIdx)
return;

float4x4 avatarMatrix = AvatarTransform[avatarIdx];
int offset = avatarIdx * boneCount;
int offset = avatarIdx * boneStride;
int count = math.min(PerAvatarBoneCount[avatarIdx], boneStride);

for (int b = 0; b < boneCount; b++)
for (int b = 0; b < count; b++)
bonesMatricesResult[offset + b] = math.mul(avatarMatrix, boneWorldMatrixArray[offset + b]);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ namespace DCL.AvatarRendering.AvatarShape
public partial class AvatarShapeVisibilitySystem : BaseUnityLoopSystem
{
private readonly RendererFeature_AvatarOutline? outlineFeature;
// Reused frustum-plane scratch buffer: rewritten once per tick in Update (CalculateFrustumPlanes) before the
// outline query reads it. Owned by the single-threaded ECS Update — NOT safe for concurrent callers.
private readonly Plane[] planes;
private readonly float startFadeDithering;
private readonly float endFadeDithering;
Expand Down Expand Up @@ -60,15 +62,25 @@ protected override void Update(float t)
BanAvatarsQuery(World);
UpdateAvatarsVisibilityStateQuery(World);
UpdateMainPlayerAvatarVisibilityStateQuery(World, camera.GetCameraComponent(World));
GetAvatarsVisibleWithOutlineQuery(World);

if (outlineFeature != null && outlineFeature.isActive)
{
CameraComponent cameraComponent = camera.GetCameraComponent(World);
CalculateFrustumPlanes(cameraComponent.Camera);
GetAvatarsVisibleWithOutlineQuery(World, cameraComponent);
}
}

public bool IsVisibleInCamera(Camera camera, Bounds bounds)
internal void CalculateFrustumPlanes(Camera camera)
{
GeometryUtility.CalculateFrustumPlanes(camera, planes);
return GeometryUtility.TestPlanesAABB(planes, bounds);
}

// Tests the AABB against the frustum planes cached by the most recent CalculateFrustumPlanes call.
// Extraction runs once per tick in Update (not per avatar), so this does not recompute the planes.
internal bool IsVisibleInCamera(Bounds bounds) =>
GeometryUtility.TestPlanesAABB(planes, bounds);

public bool IsWithinCameraDistance(Camera camera, Transform objectTransform, float maxDistancesquared)
{
var diff = camera.transform.position - objectTransform.position;
Expand All @@ -77,9 +89,9 @@ public bool IsWithinCameraDistance(Camera camera, Transform objectTransform, flo
}

[Query]
private void GetAvatarsVisibleWithOutline(in AvatarBase avatarBase, ref AvatarShapeComponent avatarShape)
private void GetAvatarsVisibleWithOutline([Data] in CameraComponent cameraComponent, in AvatarBase avatarBase, ref AvatarShapeComponent avatarShape)
{
if (outlineFeature != null && outlineFeature.isActive && (avatarShape.IsPreview || IsWithinCameraDistance(camera.GetCameraComponent(World).Camera, avatarBase.HeadAnchorPoint, 64.0f) && IsVisibleInCamera(camera.GetCameraComponent(World).Camera, avatarBase.AvatarSkinnedMeshRenderer.bounds)))
if (avatarShape.IsPreview || (IsWithinCameraDistance(cameraComponent.Camera, avatarBase.HeadAnchorPoint, 64.0f) && IsVisibleInCamera(avatarBase.AvatarSkinnedMeshRenderer.bounds)))
{
RendererFeature_AvatarOutline.m_AvatarOutlineRenderers.AddRange(avatarShape.OutlineCompatibleRenderers);
}
Expand Down Expand Up @@ -159,18 +171,21 @@ private void BanAvatars(in Entity entity, ref AvatarShapeComponent avatarShapeCo
SetHiddenComponent(entity, isBanned, HiddenPlayerComponent.HiddenReason.Banned);
}

private void SetHiddenComponent(Entity entity, bool hiddenValue, HiddenPlayerComponent.HiddenReason hiddenReason)
// Bitwise test in place of Enum.HasFlag, which boxes receiver and argument on Mono/IL2CPP (no
// intrinsic elision) — two heap allocations per test on this per-frame tick. Relies on callers
// passing a single flag: "& != 0" means ANY bit set, whereas HasFlag means ALL bits set.
internal void SetHiddenComponent(Entity entity, bool hiddenValue, HiddenPlayerComponent.HiddenReason hiddenReason)
{
Comment thread
NickKhalow marked this conversation as resolved.
ref HiddenPlayerComponent attachedHiddenComponent = ref World.TryGetRef<HiddenPlayerComponent>(entity, out bool isHiddenComponentAttached);

if (hiddenValue && (!isHiddenComponentAttached || (isHiddenComponentAttached && !attachedHiddenComponent.Reason.HasFlag(hiddenReason))))
if (hiddenValue && (!isHiddenComponentAttached || (isHiddenComponentAttached && (attachedHiddenComponent.Reason & hiddenReason) == 0)))
{
if (!isHiddenComponentAttached)
World.Add(entity, new HiddenPlayerComponent { Reason = hiddenReason } );
else
attachedHiddenComponent.Reason |= hiddenReason;
}
else if (!hiddenValue && isHiddenComponentAttached && attachedHiddenComponent.Reason.HasFlag(hiddenReason))
else if (!hiddenValue && isHiddenComponentAttached && (attachedHiddenComponent.Reason & hiddenReason) != 0)
{
attachedHiddenComponent.Reason &= ~hiddenReason;
if (attachedHiddenComponent.Reason == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ protected override void Update(float t)
{
RegisterMainPlayerQuery(World);
RegisterRemoteAvatarsQuery(World);

RefreshBoneCountsQuery(World);

avatarTransformMatrixBatchJob.ScheduleBoneMatrixCalculation();
}

Expand All @@ -54,5 +57,12 @@ private void RegisterRemoteAvatars(ref AvatarBase avatarBase, ref AvatarTransfor

avatarTransformMatrixBatchJob.RegisterAvatar(avatarBase, ref transformMatrixComponent);
}

[Query]
[None(typeof(DeleteEntityIntention))]
private void RefreshBoneCounts(ref AvatarTransformMatrixComponent transformMatrixComponent, ref AvatarCustomSkinningComponent skinningComponent)
{
avatarTransformMatrixBatchJob.SetBoneCount(ref transformMatrixComponent, skinningComponent.BoneCount);
}
}
}
Loading
Loading