Skip to content

Commit 74cdef8

Browse files
committed
Merge remote-tracking branch 'origin/opti/perf-hunt/03-avatar-outline-frustum-hoist' into opti/perf-hunt/rollup
2 parents 823f38c + 0efdc6e commit 74cdef8

5 files changed

Lines changed: 254 additions & 34 deletions

File tree

Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarShapeVisibilitySystem.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ namespace DCL.AvatarRendering.AvatarShape
2222
public partial class AvatarShapeVisibilitySystem : BaseUnityLoopSystem
2323
{
2424
private readonly RendererFeature_AvatarOutline? outlineFeature;
25+
// Reused frustum-plane scratch buffer: rewritten once per tick in Update (CalculateFrustumPlanes) before the
26+
// outline query reads it. Owned by the single-threaded ECS Update — NOT safe for concurrent callers.
2527
private readonly Plane[] planes;
2628
private readonly float startFadeDithering;
2729
private readonly float endFadeDithering;
@@ -60,15 +62,25 @@ protected override void Update(float t)
6062
BanAvatarsQuery(World);
6163
UpdateAvatarsVisibilityStateQuery(World);
6264
UpdateMainPlayerAvatarVisibilityStateQuery(World, camera.GetCameraComponent(World));
63-
GetAvatarsVisibleWithOutlineQuery(World);
65+
66+
if (outlineFeature != null && outlineFeature.isActive)
67+
{
68+
CameraComponent cameraComponent = camera.GetCameraComponent(World);
69+
CalculateFrustumPlanes(cameraComponent.Camera);
70+
GetAvatarsVisibleWithOutlineQuery(World, cameraComponent);
71+
}
6472
}
6573

66-
public bool IsVisibleInCamera(Camera camera, Bounds bounds)
74+
internal void CalculateFrustumPlanes(Camera camera)
6775
{
6876
GeometryUtility.CalculateFrustumPlanes(camera, planes);
69-
return GeometryUtility.TestPlanesAABB(planes, bounds);
7077
}
7178

79+
// Tests the AABB against the frustum planes cached by the most recent CalculateFrustumPlanes call.
80+
// Extraction runs once per tick in Update (not per avatar), so this does not recompute the planes.
81+
internal bool IsVisibleInCamera(Bounds bounds) =>
82+
GeometryUtility.TestPlanesAABB(planes, bounds);
83+
7284
public bool IsWithinCameraDistance(Camera camera, Transform objectTransform, float maxDistancesquared)
7385
{
7486
var diff = camera.transform.position - objectTransform.position;
@@ -77,9 +89,9 @@ public bool IsWithinCameraDistance(Camera camera, Transform objectTransform, flo
7789
}
7890

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

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

166-
if (hiddenValue && (!isHiddenComponentAttached || (isHiddenComponentAttached && !attachedHiddenComponent.Reason.HasFlag(hiddenReason))))
181+
if (hiddenValue && (!isHiddenComponentAttached || (isHiddenComponentAttached && (attachedHiddenComponent.Reason & hiddenReason) == 0)))
167182
{
168183
if (!isHiddenComponentAttached)
169184
World.Add(entity, new HiddenPlayerComponent { Reason = hiddenReason } );
170185
else
171186
attachedHiddenComponent.Reason |= hiddenReason;
172187
}
173-
else if (!hiddenValue && isHiddenComponentAttached && attachedHiddenComponent.Reason.HasFlag(hiddenReason))
188+
else if (!hiddenValue && isHiddenComponentAttached && (attachedHiddenComponent.Reason & hiddenReason) != 0)
174189
{
175190
attachedHiddenComponent.Reason &= ~hiddenReason;
176191
if (attachedHiddenComponent.Reason == 0)

Explorer/Assets/DCL/AvatarRendering/AvatarShape/Tests/EditMode/AvatarShapeVisibilitySystemShould.cs

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ public class AvatarShapeVisibilitySystemShould : UnitySystemTestBase<AvatarShape
2424
private const float START_FADE_DITHERING = 2.0f;
2525
private const float END_FADE_DITHERING = 0.5f;
2626

27-
private IUserBlockingCache userBlockingCache;
28-
private IRendererFeaturesCache rendererFeaturesCache;
27+
private IUserBlockingCache userBlockingCache = null!;
28+
private IRendererFeaturesCache rendererFeaturesCache = null!;
2929

30-
private GameObject cameraGameObject;
31-
private Camera testCamera;
30+
private GameObject cameraGameObject = null!;
31+
private Camera testCamera = null!;
3232
private Entity cameraEntity;
3333

34-
private GameObject avatarGameObject;
35-
private AvatarBase avatarBase;
34+
private GameObject avatarGameObject = null!;
35+
private AvatarBase avatarBase = null!;
3636

3737
private readonly List<GameObject> createdGameObjects = new ();
3838

@@ -115,7 +115,7 @@ private void AddFakeWearableToAvatarShape(ref AvatarShapeComponent avatarShape)
115115
var renderer = wearableGO.AddComponent<MeshRenderer>();
116116

117117
// CachedAttachment requires proper initialization - we use reflection to create one with Renderers list
118-
var attachment = new Loading.Assets.CachedAttachment(null, wearableGO, false, Array.Empty<SpringBoneData>());
118+
var attachment = new Loading.Assets.CachedAttachment(null!, wearableGO, false, Array.Empty<SpringBoneData>());
119119
// The Renderers list is created but empty, we need to add a renderer to it
120120
attachment.Renderers.Add(renderer);
121121

@@ -130,7 +130,8 @@ public void ReturnTrueWhenObjectIsVisibleInCamera()
130130
cameraGameObject.transform.LookAt(bounds.center);
131131

132132
// Act
133-
bool isVisible = system.IsVisibleInCamera(testCamera, bounds);
133+
system!.CalculateFrustumPlanes(testCamera);
134+
bool isVisible = system.IsVisibleInCamera(bounds);
134135

135136
// Assert
136137
Assert.IsTrue(isVisible);
@@ -144,7 +145,8 @@ public void ReturnFalseWhenObjectIsNotVisibleInCamera()
144145
cameraGameObject.transform.rotation = Quaternion.identity; // Looking forward (+Z)
145146

146147
// Act
147-
bool isVisible = system.IsVisibleInCamera(testCamera, bounds);
148+
system!.CalculateFrustumPlanes(testCamera);
149+
bool isVisible = system.IsVisibleInCamera(bounds);
148150

149151
// Assert
150152
Assert.IsFalse(isVisible);
@@ -158,7 +160,7 @@ public void ReturnTrueWhenWithinCameraDistance()
158160
avatarGameObject.transform.position = new Vector3(0, 0, 5); // 5 units away
159161

160162
// Act
161-
bool isWithin = system.IsWithinCameraDistance(testCamera, avatarGameObject.transform, maxDistanceSquared);
163+
bool isWithin = system!.IsWithinCameraDistance(testCamera, avatarGameObject.transform, maxDistanceSquared);
162164

163165
// Assert
164166
Assert.IsTrue(isWithin);
@@ -172,7 +174,7 @@ public void ReturnFalseWhenNotWithinCameraDistance()
172174
avatarGameObject.transform.position = new Vector3(0, 0, 10); // 10 units away
173175

174176
// Act
175-
bool isWithin = system.IsWithinCameraDistance(testCamera, avatarGameObject.transform, maxDistanceSquared);
177+
bool isWithin = system!.IsWithinCameraDistance(testCamera, avatarGameObject.transform, maxDistanceSquared);
176178

177179
// Assert
178180
Assert.IsFalse(isWithin);
@@ -189,7 +191,7 @@ public void AddCachedVisibilityComponentToPlayerAvatar()
189191
Entity playerEntity = world.Create(avatarShape, playerComponent, avatarBase, new CharacterEmoteComponent());
190192

191193
// Act
192-
system.Update(0);
194+
system!.Update(0);
193195

194196
// Assert
195197
Assert.IsTrue(world.Has<AvatarCachedVisibilityComponent>(playerEntity));
@@ -203,7 +205,7 @@ public void AddCachedVisibilityComponentToNonPlayerAvatar()
203205
Entity otherEntity = world.Create(avatarShape, avatarBase, new CharacterEmoteComponent());
204206

205207
// Act
206-
system.Update(0);
208+
system!.Update(0);
207209

208210
// Assert
209211
Assert.IsTrue(world.Has<AvatarCachedVisibilityComponent>(otherEntity));
@@ -225,7 +227,7 @@ public void HidePlayerAvatarWhenInFirstPersonMode()
225227
cameraComponent.Mode = CameraMode.FirstPerson;
226228

227229
// Act - first update adds component, second updates state
228-
system.Update(0);
230+
system!.Update(0);
229231
system.Update(0);
230232

231233
// Assert
@@ -247,7 +249,7 @@ public void ShowPlayerAvatarWhenInThirdPersonMode()
247249
ref var cameraComponent = ref world.Get<CameraComponent>(cameraEntity);
248250
cameraComponent.Mode = CameraMode.FirstPerson;
249251

250-
system.Update(0);
252+
system!.Update(0);
251253

252254
// Verify avatar is hidden in first person
253255
ref var avatarShapeAfterFirstPerson = ref world.Get<AvatarShapeComponent>(playerEntity);
@@ -277,7 +279,7 @@ public void AddHiddenComponentWhenUserIsBlocked()
277279
Entity avatarEntity = world.Create(avatarShape, avatarBase, new CharacterEmoteComponent());
278280

279281
// Act
280-
system.Update(0);
282+
system!.Update(0);
281283

282284
// Assert
283285
Assert.IsTrue(world.Has<HiddenPlayerComponent>(avatarEntity));
@@ -298,7 +300,7 @@ public void RemoveHiddenComponentWhenUserIsUnblocked()
298300
Entity avatarEntity = world.Create(avatarShape, avatarBase, new CharacterEmoteComponent());
299301

300302
// First update - user is blocked
301-
system.Update(0);
303+
system!.Update(0);
302304
Assert.IsTrue(world.Has<HiddenPlayerComponent>(avatarEntity));
303305

304306
// Change blocking status
@@ -353,7 +355,7 @@ public void NotBlockAvatarsWithoutInstantiatedWearables()
353355
Entity avatarEntity = world.Create(avatarShape, avatarBase, new CharacterEmoteComponent());
354356

355357
// Act
356-
system.Update(0);
358+
system!.Update(0);
357359

358360
// Assert
359361
Assert.IsFalse(world.Has<HiddenPlayerComponent>(avatarEntity));
@@ -370,7 +372,7 @@ public void HideAvatarWhenHiddenByModifierArea()
370372
Entity avatarEntity = world.Create(avatarShape, avatarBase, new CharacterEmoteComponent());
371373

372374
// Act
373-
system.Update(0);
375+
system!.Update(0);
374376
system.Update(0);
375377

376378
// Assert
@@ -388,7 +390,7 @@ public void ShowAvatarWhenNotHiddenByModifierArea()
388390
Entity avatarEntity = world.Create(avatarShape, avatarBase, new CharacterEmoteComponent());
389391

390392
// First update - avatar should be hidden due to modifier area
391-
system.Update(0);
393+
system!.Update(0);
392394

393395
ref var avatarShapeAfterHide = ref world.Get<AvatarShapeComponent>(avatarEntity);
394396
Assert.IsFalse(avatarShapeAfterHide.IsVisible, "Avatar should be hidden by modifier area");
@@ -415,15 +417,14 @@ public void ResetDitherStateWhenAvatarShapeIsDirty()
415417
Entity playerEntity = world.Create(avatarShape, playerComponent, avatarBase, new CharacterEmoteComponent());
416418

417419
// Act - first update adds component
418-
system.Update(0);
420+
system!.Update(0);
419421

420422
// Set dirty again
421423
ref var shapeRef = ref world.Get<AvatarShapeComponent>(playerEntity);
422424
shapeRef.IsDirty = true;
423425

424426
// Add skinning component for dither test
425427
var skinningMaterials = new List<AvatarCustomSkinningComponent.MaterialSetup>();
426-
var skinningComponent = new AvatarCustomSkinningComponent();
427428

428429
// Act - This update should trigger ResetDitherState due to IsDirty
429430
system.Update(0);
@@ -445,7 +446,7 @@ public void CombineMultipleHiddenReasons()
445446
Entity avatarEntity = world.Create(avatarShape, avatarBase, new CharacterEmoteComponent());
446447

447448
// First, add blocked reason
448-
system.Update(0);
449+
system!.Update(0);
449450

450451
// Manually add banned reason to test combination
451452
ref var hiddenComponent = ref world.Get<HiddenPlayerComponent>(avatarEntity);
@@ -475,7 +476,7 @@ public void KeepAnimatorDisabledWhileLegacyAnimationIsPlaying()
475476
Entity avatarEntity = world.Create(avatarShape, avatarBase, new CharacterEmoteComponent());
476477

477478
// Prime the cached state — marks the avatar as hidden so the next update's visible-transition is NOT early-returned.
478-
system.Update(0);
479+
system!.Update(0);
479480

480481
// Start a legacy Animation on the avatar — LSD/Builder-preview FullBody emotes run through this component.
481482
Animation legacyAnimation = avatarBase.AddOrGetLegacyAnimation();
@@ -519,7 +520,7 @@ public void HidePlayerAvatarWhenTransitioningToFirstPersonAndCloseToCameraStart(
519520
cameraComponent.IsTransitioningToFirstPerson = true;
520521

521522
// Act
522-
system.Update(0);
523+
system!.Update(0);
523524
system.Update(0);
524525

525526
// Assert
@@ -545,7 +546,7 @@ public void NotHidePlayerAvatarWhenTransitioningToFirstPersonButFarFromCamera()
545546
cameraComponent.IsTransitioningToFirstPerson = true;
546547

547548
// Act
548-
system.Update(0);
549+
system!.Update(0);
549550
system.Update(0);
550551

551552
// Assert

0 commit comments

Comments
 (0)