From d1c5309beecf1d603af8d0c3f51f8a0dfd7b3e6c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:04:15 +0300
Subject: [PATCH 01/13] Add SpanList utility
---
Source.Common/Utilities/SpanList.cs | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 Source.Common/Utilities/SpanList.cs
diff --git a/Source.Common/Utilities/SpanList.cs b/Source.Common/Utilities/SpanList.cs
new file mode 100644
index 000000000..e02057597
--- /dev/null
+++ b/Source.Common/Utilities/SpanList.cs
@@ -0,0 +1,26 @@
+namespace Source.Common.Utilities;
+
+///
+/// List style of operations on fixed/pre-allocated arrays.
+///
+public ref struct SpanList(Span items)
+{
+ readonly Span Items = items;
+ int Used;
+
+ public readonly int Count => Used;
+ public readonly int Capacity => Items.Length;
+
+ public readonly ref T this[int index] => ref Items[..Used][index];
+
+ public void Add(in T value) => Items[Used++] = value;
+
+ public void RemoveAt(int index) {
+ Items[(index + 1)..Used].CopyTo(Items[index..]);
+ Used--;
+ }
+
+ public void Clear() => Used = 0;
+
+ public readonly Span AsSpan() => Items[..Used];
+}
From 68f22fadf35ded151b99713656ea505a8baf96ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:05:11 +0300
Subject: [PATCH 02/13] Store collision map data in arrays instead of lists
---
Source.Engine/CModel_Private.cs | 8 +--
Source.Engine/CollisionModelSubsystem.cs | 77 ++++++++++++------------
Source.Engine/DebugLeafVis.cs | 2 +-
Source.Engine/DispMapload.cs | 6 +-
Source.Engine/ModelLoader.cs | 8 +--
5 files changed, 52 insertions(+), 49 deletions(-)
diff --git a/Source.Engine/CModel_Private.cs b/Source.Engine/CModel_Private.cs
index 1d6576f63..1f0f59456 100644
--- a/Source.Engine/CModel_Private.cs
+++ b/Source.Engine/CModel_Private.cs
@@ -1269,15 +1269,15 @@ internal static void TraceToLeaf(bool IS_POINT, TraceInfo traceInfo, int ndxLeaf
// trace ray/box sweep against all brushes in this leaf
int numleafbrushes = leaf.NumLeafBrushes;
int lastleafbrush = leaf.FirstLeafBrush + numleafbrushes;
- List map_leafbrushes = traceInfo.BSPData!.MapLeafBrushes;
- List map_brushes = traceInfo.BSPData!.MapBrushes;
+ ushort[] map_leafbrushes = traceInfo.BSPData!.MapLeafBrushes;
+ CollisionBrush[] map_brushes = traceInfo.BSPData!.MapBrushes;
Span counters = traceInfo.GetBrushCounters();
TraceCounter_t count = traceInfo.GetCount();
for (int ndxLeafBrush = leaf.FirstLeafBrush; ndxLeafBrush < lastleafbrush; ndxLeafBrush++) {
// get the current brush
- int ndxBrush = map_leafbrushes.AsSpan()[ndxLeafBrush];
+ int ndxBrush = map_leafbrushes[ndxLeafBrush];
- ref CollisionBrush brush = ref map_brushes.AsSpan()[ndxBrush];
+ ref CollisionBrush brush = ref map_brushes[ndxBrush];
// make sure we only check this brush once per trace/stab
if (!traceInfo.Visit(ref brush, ndxBrush, count, counters))
diff --git a/Source.Engine/CollisionModelSubsystem.cs b/Source.Engine/CollisionModelSubsystem.cs
index c53d8d8ad..ca23efcf1 100644
--- a/Source.Engine/CollisionModelSubsystem.cs
+++ b/Source.Engine/CollisionModelSubsystem.cs
@@ -8,6 +8,7 @@
using Source.Common.GUI;
using Source.Common.MaterialSystem;
using Source.Common.Mathematics;
+using Source.Common.Utilities;
using System.Drawing;
using System.Numerics;
@@ -27,16 +28,16 @@ public class CollisionBSPData
{
public string? MapName;
public string? MapNullName;
- public readonly List MapCollisionModels = [];
- public readonly List MapSurfaces = [];
- public readonly List MapPlanes = [];
- public readonly List MapBrushes = [];
- public readonly List MapBrushSides = [];
- public readonly List MapBoxBrushes = [];
- public readonly List MapNodes = [];
- public readonly List MapLeafs = [];
- public readonly List MapDispList = [];
- public readonly List MapLeafBrushes = [];
+ public CollisionModel[] MapCollisionModels = [];
+ public CollisionSurface[] MapSurfaces = [];
+ public CollisionPlane[] MapPlanes = [];
+ public CollisionBrush[] MapBrushes = [];
+ public CollisionBrushSide[] MapBrushSides = [];
+ public CollisionBoxBrush[] MapBoxBrushes = [];
+ public CollisionNode[] MapNodes = [];
+ public CollisionLeaf[] MapLeafs = [];
+ public ushort[] MapDispList = [];
+ public ushort[] MapLeafBrushes = [];
public readonly List TextureNames = [];
public static readonly CollisionSurface NullSurface = new() { Name = "**empty**", Flags = 0, SurfaceProps = 0 };
public string? MapEntityString;
@@ -61,9 +62,9 @@ public class CollisionBSPData
// More of these should just be explicit Count's into their respective lists, honestly
public int NumSurfaces;
public int NumLeafs;
- public int NumBrushes => MapBrushes.Count;
+ public int NumBrushes => MapBrushes.Length;
public int NumAreas;
- public int NumPlanes => MapPlanes.Count;
+ public int NumPlanes => MapPlanes.Length;
public int NumClusters;
public int NumNodes;
public int NumTextures;
@@ -90,7 +91,7 @@ internal void PreLoad() {
Init();
}
internal void Destroy() {
- for (int i = 0; i < MapCollisionModels.Count; i++)
+ for (int i = 0; i < MapCollisionModels.Length; i++)
physcollision.VCollideUnload(MapCollisionModels[i].VCollisionData);
// DispCollTrees_FreeLeafList(this);
@@ -100,20 +101,21 @@ internal void Destroy() {
// CM.g_DispBounds = null;
CM.g_DispCollTreeCount = 0;
- MapPlanes.Clear();
+ MapPlanes = [];
TextureNames.Clear();
- MapSurfaces.Clear();
+ MapSurfaces = [];
MapAreaPortals = [];
PortalOpen = [];
MapAreas = [];
MapEntityString = null;
- MapBrushes.Clear();
- MapDispList.Clear();
- MapCollisionModels.Clear();
- MapLeafBrushes.Clear();
- MapLeafs.Clear();
- MapNodes.Clear();
- MapBrushSides.Clear();
+ MapBrushes = [];
+ MapDispList = [];
+ MapCollisionModels = [];
+ MapLeafBrushes = [];
+ MapLeafs = [];
+ MapNodes = [];
+ MapBrushSides = [];
+ MapBoxBrushes = [];
MapVis = null;
NumBrushSides = 0;
@@ -139,7 +141,8 @@ internal void LoadTextures() {
BSPDTexData[] inData = lh.LoadLumpData(throwIfNoElements: true, maxElements: BSPFileCommon.MAX_MAP_TEXDATA, sysErrorIfOOB: true);
IMaterial? material;
- MapSurfaces.Clear(); MapSurfaces.EnsureCapacity(inData.Length);
+ MapSurfaces = new CollisionSurface[inData.Length];
+ SpanList surfaces = new(MapSurfaces);
TextureNames.Clear(); TextureNames.EnsureCapacity(inData.Length);
int lastNull = -1;
for (int i = 0; i < stringData.Length; i++) {
@@ -158,7 +161,7 @@ internal void LoadTextures() {
int index = _in.NameStringTableID;
- MapSurfaces.Add(new CollisionSurface() {
+ surfaces.Add(new CollisionSurface() {
Name = TextureNames[index]!,
SurfaceProps = 0,
Flags = 0
@@ -211,7 +214,7 @@ internal void LoadLeafs() {
private void CollisionBSPData_LoadLeafs_Version_1(MapLoadHelper lh) {
BSPDLeaf[] inData = lh.LoadLumpData(throwIfNoElements: true, BSPFileCommon.MAX_MAP_LEAFS, sysErrorIfOOB: true);
int count = inData.Length;
- MapLeafs.Clear(); MapLeafs.EnsureCount(count + 1);
+ MapLeafs = new CollisionLeaf[count + 1];
NumLeafs = count;
NumClusters = 0;
@@ -247,7 +250,7 @@ private void CollisionBSPData_LoadLeafs_Version_1(MapLoadHelper lh) {
private void CollisionBSPData_LoadLeafs_Version_0(MapLoadHelper lh) {
BSPDLeafVersion0[] inData = lh.LoadLumpData(throwIfNoElements: true, BSPFileCommon.MAX_MAP_PLANES, sysErrorIfOOB: true);
int count = inData.Length;
- MapLeafs.Clear(); MapLeafs.EnsureCount(count + 1);
+ MapLeafs = new CollisionLeaf[count + 1];
NumLeafs = count;
NumClusters = 0;
@@ -284,13 +287,13 @@ internal void LoadLeafBrushes() {
MapLoadHelper lh = new MapLoadHelper(LumpIndex.LeafBrushes);
ushort[] inData = lh.LoadLumpData(throwIfNoElements: true, BSPFileCommon.MAX_MAP_LEAFBRUSHES, sysErrorIfOOB: true);
- MapLeafBrushes.Clear(); MapLeafBrushes.AddRange(inData);
+ MapLeafBrushes = inData;
}
internal void LoadPlanes() {
MapLoadHelper lh = new MapLoadHelper(LumpIndex.Planes);
BSPDPlane[] inData = lh.LoadLumpData(throwIfNoElements: true, BSPFileCommon.MAX_MAP_PLANES, sysErrorIfOOB: true);
- MapPlanes.Clear(); MapPlanes.EnsureCount(inData.Length + 1);
+ MapPlanes = new CollisionPlane[inData.Length + 1];
Span planes = MapPlanes.AsSpan();
int count = inData.Length;
@@ -313,7 +316,7 @@ internal void LoadBrushes() {
MapLoadHelper lh = new MapLoadHelper(LumpIndex.Brushes);
BSPDBrush[] inData = lh.LoadLumpData(throwIfNoElements: true, BSPFileCommon.MAX_MAP_BRUSHES, sysErrorIfOOB: true);
- MapBrushes.EnsureCountNew(inData.Length);
+ MapBrushes = new CollisionBrush[inData.Length];
Span outData = MapBrushes.AsSpan();
for (int i = 0; i < inData.Length; i++) {
ref CollisionBrush outBrush = ref outData[i];
@@ -354,8 +357,8 @@ internal void LoadBrushSides(List map_texinfo) {
}
}
- MapBrushSides.EnsureCountNew(brushSideCount);
- MapBoxBrushes.EnsureCountNew(boxBrushCount);
+ MapBrushSides = new CollisionBrushSide[brushSideCount];
+ MapBoxBrushes = new CollisionBoxBrush[boxBrushCount];
Span brushsides = MapBrushSides.AsSpan();
Span boxbrushes = MapBoxBrushes.AsSpan();
@@ -377,7 +380,7 @@ internal void LoadBrushSides(List map_texinfo) {
for (int j = 0; j < brush.NumSides; j++) {
ref CollisionBrushSide side = ref brushsides[outBrushSide];
ref BSPDBrushSide inputSide = ref inData[firstInputSide + j];
- side.SetPlanePointer(MapPlanes.Base(), inputSide.PlaneNum);
+ side.SetPlanePointer(MapPlanes, inputSide.PlaneNum);
int t = inputSide.TexInfo;
if (t >= map_texinfo.Count)
Sys.Error("Bad brushside texinfo");
@@ -435,10 +438,10 @@ internal void LoadSubmodels() {
MapLoadHelper lh = new MapLoadHelper(LumpIndex.Models);
BSPDModel[] inData = lh.LoadLumpData(throwIfNoElements: true, BSPFileCommon.MAX_MAP_MODELS, sysErrorIfOOB: true);
- MapCollisionModels.EnsureCountNew(inData.Length);
+ MapCollisionModels = new CollisionModel[inData.Length];
for (int i = 0; i < inData.Length; i++) {
- CollisionModel outModel = MapCollisionModels[i];
+ CollisionModel outModel = MapCollisionModels[i] = new();
ref BSPDModel inModel = ref inData[i];
for (int j = 0; j < 3; j++) { // spread the mins / maxs by a pixel
@@ -453,7 +456,7 @@ internal void LoadNodes() {
MapLoadHelper lh = new MapLoadHelper(LumpIndex.Nodes);
BSPDNode[] inData = lh.LoadLumpData(throwIfNoElements: true, BSPFileCommon.MAX_MAP_NODES, sysErrorIfOOB: true);
int count = inData.Length;
- MapNodes.Clear(); MapNodes.EnsureCount(count + 6);
+ MapNodes = new CollisionNode[count + 6];
NumNodes = count;
MapRootNode = 0;
@@ -704,7 +707,7 @@ private void CM_DispTreeLeafnum() {
}
// Write the compact per-leaf reference lists.
- MapDispList.SetCount(totalRefs);
+ MapDispList = new ushort[totalRefs];
Span outList = MapDispList.AsSpan();
for (int i = 0; i < treeCount; i++) {
foreach (int leafIndex in perDispLeaves[i]) {
@@ -895,7 +898,7 @@ private static void DispTreeLeafnum(CollisionBSPData bspData) {
internal static CollisionModel? InlineModelNumber(int index) {
CollisionBSPData bspData = GetCollisionBSPData();
- if ((index < 0) || (index >= bspData.MapCollisionModels.Count))
+ if ((index < 0) || (index >= bspData.MapCollisionModels.Length))
return null;
return (bspData.MapCollisionModels[index]);
diff --git a/Source.Engine/DebugLeafVis.cs b/Source.Engine/DebugLeafVis.cs
index 94d8d1cfe..4ee863ec5 100644
--- a/Source.Engine/DebugLeafVis.cs
+++ b/Source.Engine/DebugLeafVis.cs
@@ -322,7 +322,7 @@ static void RecomputeClipbrushes(bool bEnabled) {
int lastBrush = pBSP.NumBrushes;
- if (pBSP.MapCollisionModels.Count > 1)
+ if (pBSP.MapCollisionModels.Length > 1)
lastBrush = FindMinBrush(pBSP, pBSP.MapCollisionModels[1].HeadNode, lastBrush);
for (int i = 0; i < lastBrush; i++) {
diff --git a/Source.Engine/DispMapload.cs b/Source.Engine/DispMapload.cs
index 729a0cf76..a81f295ab 100644
--- a/Source.Engine/DispMapload.cs
+++ b/Source.Engine/DispMapload.cs
@@ -391,10 +391,10 @@ public static bool DispInfo_LoadDisplacements(Model world, MaterialSystem_SortIn
int curVert = 0;
int curTri = 0;
- List coreDisps = [];
+ CoreDispInfo[] coreDisps = new CoreDispInfo[numDisplacements];
int disp = 0;
for (disp = 0; disp < numDisplacements; disp++)
- coreDisps.Add(new());
+ coreDisps[disp] = new();
for (disp = 0; disp < numDisplacements; ++disp) {
ref BSPDDispInfo mapDisp = ref tempDisps[disp];
@@ -413,7 +413,7 @@ public static bool DispInfo_LoadDisplacements(Model world, MaterialSystem_SortIn
return false;
}
- SmoothDispSurfNormals(coreDisps.Base(), numDisplacements);
+ SmoothDispSurfNormals(coreDisps, numDisplacements);
for (disp = 0; disp < numDisplacements; ++disp) {
DispInfo_CreateStaticBuffersAndTags(world, disp, coreDisps[disp], tempVerts);
diff --git a/Source.Engine/ModelLoader.cs b/Source.Engine/ModelLoader.cs
index 446bed500..9ce0935a9 100644
--- a/Source.Engine/ModelLoader.cs
+++ b/Source.Engine/ModelLoader.cs
@@ -1604,8 +1604,8 @@ private void Mod_LoadLeafs() {
pMap.Leafs[i].DispCount = pCLeaf[i].DispCount;
pMap.Leafs[i].DispListStart = pCLeaf[i].DispListStart;
}
- pMap.DispInfoReferences = GetCollisionBSPData().MapDispList.Base();
- pMap.NumDispInfoReferences = GetCollisionBSPData().MapDispList.Count;
+ pMap.DispInfoReferences = GetCollisionBSPData().MapDispList;
+ pMap.NumDispInfoReferences = GetCollisionBSPData().MapDispList.Length;
}
private void Mod_LoadMarksurfaces() {
@@ -2120,7 +2120,7 @@ private void Mod_LoadVertNormalIndices() {
}
private void Mod_LoadTexdata() {
MapLoadHelper.Map!.NumTexData = GetCollisionBSPData().NumSurfaces;
- MapLoadHelper.Map!.TexData = GetCollisionBSPData().MapSurfaces.Base();
+ MapLoadHelper.Map!.TexData = GetCollisionBSPData().MapSurfaces;
}
private void Mod_LoadTexinfo() {
@@ -2166,7 +2166,7 @@ private void Mod_LoadTexinfo() {
}
private void Mod_LoadPlanes() {
- MapLoadHelper.Map!.Planes = GetCollisionBSPData().MapPlanes.Base();
+ MapLoadHelper.Map!.Planes = GetCollisionBSPData().MapPlanes;
MapLoadHelper.Map!.NumPlanes = GetCollisionBSPData().NumPlanes;
}
From 04db398d67cf17352ec5a09208c5d47d1ddefb09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:11:08 +0300
Subject: [PATCH 03/13] Store displacement vertices and indices in arrays
---
Source.Engine/DispInfo.cs | 10 +++++-----
Source.Engine/DispMapload.cs | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/Source.Engine/DispInfo.cs b/Source.Engine/DispInfo.cs
index 9c7024779..3311fb97d 100644
--- a/Source.Engine/DispInfo.cs
+++ b/Source.Engine/DispInfo.cs
@@ -25,8 +25,8 @@ public class DispInfo : DispUtilsHelper, IDispInfo
public readonly MeshReader MeshReader = new();
- public readonly List Indices = [];
- public readonly List Verts = [];
+ public ushort[] Indices = [];
+ public DispRenderVert[] Verts = [];
public MaxDispVertsBitSet ActiveVerts;
public MaxDispVertsBitSet AllowedVerts;
@@ -259,7 +259,7 @@ public void CopyMapDispData(in BSPDDispInfo buildDisp) {
PowerInfo = PowerInfo.GetPowerInfo(Power);
int size = GetSideLength();
- Indices.SetSize(6 * (size - 1) * (size - 1));
+ Array.Resize(ref Indices, 6 * (size - 1) * (size - 1));
NodeInfo = new DispNodeInfo[PowerInfo.NodeCount];
}
@@ -311,7 +311,7 @@ internal void CopyCoreDispVertData(CoreDispInfo coreDisp, float bumpSTexCoordOff
if (NumLightmaps() <= 1)
bumpSTexCoordOffset = 0.0f;
- Verts.SetSizeInitialized(PowerInfo!.MaxVerts);
+ Array.Resize(ref Verts, PowerInfo!.MaxVerts);
BumpSTexCoordOffset = bumpSTexCoordOffset;
for (int i = 0; i < NumVerts(); i++) {
ref DispRenderVert vert = ref GetVertex(i);
@@ -426,7 +426,7 @@ public void InitializeActiveVerts() {
public ref DispRenderVert GetVertex(int i) {
Assert(i < NumVerts());
- return ref Verts.AsSpan()[i];
+ return ref Verts[i];
}
// public void ComputeLightmapAndTextureCoordinate(in RayDispOutput output, Vector2[]? luv, Vector2[]? tuv) => throw new NotImplementedException();
diff --git a/Source.Engine/DispMapload.cs b/Source.Engine/DispMapload.cs
index a81f295ab..1c80655ee 100644
--- a/Source.Engine/DispMapload.cs
+++ b/Source.Engine/DispMapload.cs
@@ -340,7 +340,7 @@ static unsafe void SetupMeshReaders(Model world, nint numDisplacements) {
desc.Vertex.TangentSSize = sizeof(DispRenderVert);
desc.Vertex.TangentTSize = sizeof(DispRenderVert);
- DispRenderVert[] pBaseVert = pDisp.Verts.Base();
+ DispRenderVert[] pBaseVert = pDisp.Verts;
desc.Index.IndexSize = 1;
From a96719cd630626c18b0ff3dfcaf9ec1f8aef1bf7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:18:19 +0300
Subject: [PATCH 04/13] Store cached bone data in an array
---
Game.Client/C_BaseAnimating.cs | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/Game.Client/C_BaseAnimating.cs b/Game.Client/C_BaseAnimating.cs
index 1fcfc6408..fc66f0dda 100644
--- a/Game.Client/C_BaseAnimating.cs
+++ b/Game.Client/C_BaseAnimating.cs
@@ -283,7 +283,7 @@ public void Dispose() {
Vector3 BonePosition;
QAngle BoneAngles;
Handle AttachedTo = new();
- readonly List CachedBoneData = [];
+ Matrix3x4[] CachedBoneData = [];
public void InvalidateBoneCache() {
MostRecentModelBoneCounter = ModelBoneCounter - 1;
@@ -533,7 +533,7 @@ public override bool SetupBones(Span boneToWorldOut, int maxBones, in
AccumulatedBoneMask = 0;
}
- int nBoneCount = CachedBoneData.Count;
+ int nBoneCount = CachedBoneData.Length;
AccumulatedBoneMask |= boneMask;
@@ -597,11 +597,11 @@ public override bool SetupBones(Span boneToWorldOut, int maxBones, in
}
if (!boneToWorldOut.IsEmpty) {
- if (maxBones >= CachedBoneData.Count) {
+ if (maxBones >= CachedBoneData.Length) {
memcpy(boneToWorldOut, CachedBoneData.AsSpan());
}
else {
- Warning($"SetupBones: invalid bone array size ({maxBones} - needs {CachedBoneData.Count})\n");
+ Warning($"SetupBones: invalid bone array size ({maxBones} - needs {CachedBoneData.Length})\n");
return false;
}
}
@@ -992,12 +992,12 @@ private static void RecvProxy_Sequence(ref readonly RecvProxyData data, object i
Studio.DestroyBoneCache(HitboxBoneCacheHandle);
HitboxBoneCacheHandle = 0;
- if (CachedBoneData.Count != hdr.NumBones()) {
- CachedBoneData.SetSize(hdr.NumBones());
+ if (CachedBoneData.Length != hdr.NumBones()) {
+ CachedBoneData = new Matrix3x4[hdr.NumBones()];
for (i = 0; i < hdr.NumBones(); i++)
- MathLib.SetIdentityMatrix(out CachedBoneData.AsSpan()[i]);
+ MathLib.SetIdentityMatrix(out CachedBoneData[i]);
}
- BoneAccessor.Init(CachedBoneData.Base());
+ BoneAccessor.Init(CachedBoneData);
Ik = null;
From b20fb34d2855de4e81ab1b1ebbf3388cfbb069b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:26:14 +0300
Subject: [PATCH 05/13] Store trace visit counters in arrays
---
Source.Engine/CModel_Private.cs | 25 ++++++++++++++-----------
1 file changed, 14 insertions(+), 11 deletions(-)
diff --git a/Source.Engine/CModel_Private.cs b/Source.Engine/CModel_Private.cs
index 1f0f59456..cf8bd4536 100644
--- a/Source.Engine/CModel_Private.cs
+++ b/Source.Engine/CModel_Private.cs
@@ -13,7 +13,7 @@
using System.Runtime.Intrinsics;
using TraceCounter_t = uint;
-using TraceCounterVec = System.Collections.Generic.List;
+using TraceCounterVec = uint[];
namespace Source.Engine;
@@ -54,8 +54,14 @@ public void Reset() { }
public bool CheckPrimary;
public int CheckDepth = -1;
public readonly TraceCounter_t[] Count = new TraceCounter_t[MAX_CHECK_COUNT_DEPTH];
- public readonly TraceCounterVec[] BrushCounters = new TraceCounterVec[MAX_CHECK_COUNT_DEPTH].InstantiateArray();
- public readonly TraceCounterVec[] DispCounters = new TraceCounterVec[MAX_CHECK_COUNT_DEPTH].InstantiateArray();
+ public readonly TraceCounterVec[] BrushCounters = NewCounters();
+ public readonly TraceCounterVec[] DispCounters = NewCounters();
+
+ static TraceCounterVec[] NewCounters() {
+ TraceCounterVec[] counters = new TraceCounterVec[MAX_CHECK_COUNT_DEPTH];
+ Array.Fill(counters, []);
+ return counters;
+ }
public TraceCounter_t GetCount() => Count[CheckDepth];
public Span GetBrushCounters() => BrushCounters[CheckDepth].AsSpan();
@@ -368,16 +374,13 @@ public static void DestroyDispPhysCollide() {
internal static TraceInfo BeginTrace() {
TraceInfo traceInfo = g_TraceInfoPool.Alloc();
- if (traceInfo.BrushCounters[0].Count != GetCollisionBSPData().NumBrushes + 1) {
+ if (traceInfo.BrushCounters[0].Length != GetCollisionBSPData().NumBrushes + 1) {
memreset(traceInfo.Count);
traceInfo.CheckDepth = -1;
for (int i = 0; i < MAX_CHECK_COUNT_DEPTH; i++) {
- traceInfo.BrushCounters[i].SetCount(GetCollisionBSPData().NumBrushes + 1);
- traceInfo.DispCounters[i].SetCount((int)g_DispCollTreeCount);
-
- memreset(traceInfo.BrushCounters[i].AsSpan());
- memreset(traceInfo.DispCounters[i].AsSpan());
+ traceInfo.BrushCounters[i] = new TraceCounter_t[GetCollisionBSPData().NumBrushes + 1];
+ traceInfo.DispCounters[i] = new TraceCounter_t[g_DispCollTreeCount];
}
}
@@ -673,8 +676,8 @@ internal static void PushTraceVisits(TraceInfo traceInfo) {
traceInfo.Count[i]++;
if (traceInfo.Count[i] == 0) {
traceInfo.Count[i]++;
- memreset(traceInfo.BrushCounters[i].Base());
- memreset(traceInfo.DispCounters[i].Base());
+ memreset(traceInfo.BrushCounters[i]);
+ memreset(traceInfo.DispCounters[i]);
}
}
From 6132819a223e12f29911256d7ef6e34e618d663f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:30:14 +0300
Subject: [PATCH 06/13] Fix autoplay list exposing unused list capacity
---
Source.Common/Studio.cs | 2 +-
Source.Common/StudioVirtualModel.cs | 5 +++--
Source.DataCache/MDLCache.cs | 2 +-
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/Source.Common/Studio.cs b/Source.Common/Studio.cs
index ce047d563..c30a61a71 100644
--- a/Source.Common/Studio.cs
+++ b/Source.Common/Studio.cs
@@ -350,7 +350,7 @@ public VirtualGroup AnimGroup(int animation) {
public readonly List Group = [];
public readonly List Node = [];
public readonly List IKLock = [];
- public readonly List AutoplaySequences = [];
+ public short[] AutoplaySequences = [];
}
public enum StudioMeshGroupFlags
diff --git a/Source.Common/StudioVirtualModel.cs b/Source.Common/StudioVirtualModel.cs
index 3a973efb8..252fa0d2b 100644
--- a/Source.Common/StudioVirtualModel.cs
+++ b/Source.Common/StudioVirtualModel.cs
@@ -426,7 +426,8 @@ public void AppendModels(int group, StudioHeader studioHdr) {
}
public void UpdateAutoplaySequences(StudioHeader studioHdr) {
int autoplayCount = studioHdr.CountAutoplaySequences();
- AutoplaySequences.SetCount(autoplayCount);
- studioHdr.CopyAutoplaySequences(AutoplaySequences.Base().AsSpan(), autoplayCount);
+ if (AutoplaySequences.Length != autoplayCount)
+ AutoplaySequences = new short[autoplayCount];
+ studioHdr.CopyAutoplaySequences(AutoplaySequences, autoplayCount);
}
}
diff --git a/Source.DataCache/MDLCache.cs b/Source.DataCache/MDLCache.cs
index 20164c3ee..19b0eb331 100644
--- a/Source.DataCache/MDLCache.cs
+++ b/Source.DataCache/MDLCache.cs
@@ -224,7 +224,7 @@ public Memory GetAutoplayList(MDLHandle_t handle) {
VirtualModel? virtualModel = GetVirtualModel(handle);
if (virtualModel != null)
- return virtualModel.AutoplaySequences.Base();
+ return virtualModel.AutoplaySequences;
StudioData studioData = HandleToMDLDict[handle];
return studioData.AutoplaySequenceList;
From e2d4068b6651b280bc9596b50f8b8e41ebe9cf25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:33:44 +0300
Subject: [PATCH 07/13] Return the autoplay list as a ReadOnlySpan over arrays
---
Source.Common/BoneSetup.cs | 2 +-
Source.Common/DataCache/IMDLCache.cs | 2 +-
Source.Common/Engine/IModelInfo.cs | 2 +-
Source.Common/Studio.cs | 16 ++--------------
Source.DataCache/MDLCache.cs | 8 ++++----
Source.Engine/ModelInfo.cs | 4 ++--
6 files changed, 11 insertions(+), 23 deletions(-)
diff --git a/Source.Common/BoneSetup.cs b/Source.Common/BoneSetup.cs
index d1491285d..58d92a485 100644
--- a/Source.Common/BoneSetup.cs
+++ b/Source.Common/BoneSetup.cs
@@ -1545,7 +1545,7 @@ public void CalcAutoplaySequences(Span pos, Span q, TimeUni
ikContext?.AddAutoplayLocks(pos, q);
- int count = studioHdr.GetAutoplayList(out Span pList);
+ int count = studioHdr.GetAutoplayList(out ReadOnlySpan pList);
for (int i = 0; i < count; i++) {
int sequenceIndex = pList[i];
MStudioSeqDesc seqdesc = studioHdr.Seqdesc(sequenceIndex);
diff --git a/Source.Common/DataCache/IMDLCache.cs b/Source.Common/DataCache/IMDLCache.cs
index 221250d77..66626e4a3 100644
--- a/Source.Common/DataCache/IMDLCache.cs
+++ b/Source.Common/DataCache/IMDLCache.cs
@@ -51,7 +51,7 @@ public interface IMDLCache
VCollide? GetVCollide(MDLHandle_t handle);
Memory GetAnimBlock(MDLHandle_t handle, int block);
VirtualModel? GetVirtualModel(MDLHandle_t handle);
- Memory GetAutoplayList(MDLHandle_t handle);
+ ReadOnlySpan GetAutoplayList(MDLHandle_t handle);
VertexFileHeader? GetVertexData(MDLHandle_t handle);
void TouchAllData(MDLHandle_t handle);
diff --git a/Source.Common/Engine/IModelInfo.cs b/Source.Common/Engine/IModelInfo.cs
index 01177d6b7..aafe256e3 100644
--- a/Source.Common/Engine/IModelInfo.cs
+++ b/Source.Common/Engine/IModelInfo.cs
@@ -61,7 +61,7 @@ public interface IVModelInfo
byte ComputeViewScreenFade(in Vector3 absOrigin, float radius, float fadeScale);
// both client and server
- int GetAutoplayList(StudioHeader studioHdr, out Span autoplayList);
+ int GetAutoplayList(StudioHeader studioHdr, out ReadOnlySpan autoplayList);
// Gets a terrain collision model (creates if necessary)
// NOTE: This may return NULL if the terrain model cannot be ized
diff --git a/Source.Common/Studio.cs b/Source.Common/Studio.cs
index c30a61a71..67b2acf2d 100644
--- a/Source.Common/Studio.cs
+++ b/Source.Common/Studio.cs
@@ -2674,7 +2674,7 @@ internal int RelativeSeq(int baseseq, int relseq) {
return group.MasterSeq[relseq];
}
- public int GetAutoplayList(out Span pList) {
+ public int GetAutoplayList(out ReadOnlySpan pList) {
return studioHdr!.GetAutoplayList(out pList);
}
@@ -3181,7 +3181,7 @@ public ref byte LocalTransition(int i) {
}
- public int GetAutoplayList(out Span pList) {
+ public int GetAutoplayList(out ReadOnlySpan pList) {
return modelinfo.GetAutoplayList(this, out pList);
}
@@ -3221,18 +3221,6 @@ public int CountAutoplaySequences() {
return count;
}
- public void CopyAutoplaySequences(ref Memory autoplaySequenceList, int outCount) {
- int outIndex = 0;
- for (int i = 0; i < GetNumSeq() && outIndex < outCount; i++) {
- MStudioSeqDesc seqdesc = Seqdesc(i);
- if ((seqdesc.Flags & StudioAnimSeqFlags.Autoplay) != 0) {
- autoplaySequenceList.Span[outIndex] = (short)i;
- outIndex++;
- }
- }
- autoplaySequenceList = autoplaySequenceList[..outIndex];
- }
-
public int CopyAutoplaySequences(Span autoplaySequenceList, int outCount) {
int outIndex = 0;
for (int i = 0; i < GetNumSeq() && outIndex < outCount; i++) {
diff --git a/Source.DataCache/MDLCache.cs b/Source.DataCache/MDLCache.cs
index 19b0eb331..0900528d6 100644
--- a/Source.DataCache/MDLCache.cs
+++ b/Source.DataCache/MDLCache.cs
@@ -45,7 +45,7 @@ public class StudioData
public object? UserData;
- public Memory AutoplaySequenceList;
+ public short[] AutoplaySequenceList = [];
public int AnimBlockCount;
public object?[]? AnimBlock; // todo: research what this is
@@ -218,9 +218,9 @@ public bool GetAsyncLoad(MDLCacheDataType type) {
throw new NotImplementedException();
}
- public Memory GetAutoplayList(MDLHandle_t handle) {
+ public ReadOnlySpan GetAutoplayList(MDLHandle_t handle) {
if (handle == MDLHANDLE_INVALID)
- return Array.Empty();
+ return [];
VirtualModel? virtualModel = GetVirtualModel(handle);
if (virtualModel != null)
@@ -415,7 +415,7 @@ private bool ReadMDLFile(MDLHandle_t handle, ReadOnlySpan mdlFileName, Mem
int count = studioHdr.CountAutoplaySequences();
if (count != 0) {
AllocateAutoplaySequences(HandleToMDLDict[handle], count);
- studioHdr.CopyAutoplaySequences(ref HandleToMDLDict[handle].AutoplaySequenceList, count);
+ studioHdr.CopyAutoplaySequences(HandleToMDLDict[handle].AutoplaySequenceList, count);
}
}
diff --git a/Source.Engine/ModelInfo.cs b/Source.Engine/ModelInfo.cs
index e1a5b49b3..464c9d5f0 100644
--- a/Source.Engine/ModelInfo.cs
+++ b/Source.Engine/ModelInfo.cs
@@ -99,9 +99,9 @@ public virtual MDLHandle_t GetCacheHandle(Model model) {
public Memory GetAnimBlock(StudioHeader studioHdr, int block) {
return mdlCache.GetAnimBlock(studioHdr.VirtualModel, block);
}
- public int GetAutoplayList(StudioHeader studioHdr, out Span autoplayList) {
+ public int GetAutoplayList(StudioHeader studioHdr, out ReadOnlySpan autoplayList) {
MDLHandle_t handle = studioHdr.VirtualModel;
- autoplayList = mdlCache.GetAutoplayList(handle).Span;
+ autoplayList = mdlCache.GetAutoplayList(handle);
return autoplayList.Length;
}
From f4e1fe3529e821bbbf62740517274c01562656c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:38:51 +0300
Subject: [PATCH 08/13] Store the studio header group cache in an array
---
Source.Common/Studio.cs | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/Source.Common/Studio.cs b/Source.Common/Studio.cs
index 67b2acf2d..c5fe0e6ba 100644
--- a/Source.Common/Studio.cs
+++ b/Source.Common/Studio.cs
@@ -2038,7 +2038,7 @@ public class StudioHdr
public Span Bones() => studioHdr.Bones();
- readonly List StudioHdrCache = [];
+ StudioHeader?[] StudioHdrCache = [];
public StudioHdrFlags Flags() => studioHdr!.Flags;
public AnonymousSafeFieldPointer FrameUnlockCounterPtr;
@@ -2048,7 +2048,7 @@ public void Init(StudioHeader? studioHdr, IMDLCache mdlcache) {
this.studioHdr = studioHdr;
this.vModel = null;
- StudioHdrCache.Clear();
+ StudioHdrCache = [];
if (this.studioHdr == null)
return;
@@ -2088,10 +2088,7 @@ public bool SequencesAvailable() {
private VirtualModel? ResetVModel(VirtualModel? virtualModel) {
if (virtualModel != null) {
vModel = virtualModel;
- StudioHdrCache!.EnsureCountDefault(vModel.Group.Count());
-
- for (int i = 0; i < StudioHdrCache.Count; i++)
- StudioHdrCache[i] = null!;
+ StudioHdrCache = new StudioHeader?[vModel.Group.Count()];
return vModel;
}
@@ -2120,13 +2117,13 @@ public int RemapSeqBone(int sequence, int localBone) {
lock (FrameUnlockCounterMutex) {
if (FrameUnlockCounterPtr.Get() != FrameUnlockCounter) // i.e., this thread got the mutex
{
- memcreset(StudioHdrCache.Base());
+ memcreset(StudioHdrCache);
FrameUnlockCounter = FrameUnlockCounterPtr.Get();
}
}
}
- if (!StudioHdrCache.IsValidIndex(i)) {
+ if ((uint)i >= (uint)StudioHdrCache.Length) {
ReadOnlySpan name = (this.studioHdr != null) ? this.studioHdr.GetName() : "<>";
Debugger.Break();
return this.studioHdr; // return something known to probably exist, certainly things will be messed up, but hopefully not crash before the warning is noticed
From 50edad0ebfbd9f9d63211230031f4918ee1a3f88 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:47:02 +0300
Subject: [PATCH 09/13] Replace virtual model list Swap with copy-and-assign
like the SDK
---
Source.Common/Exts.cs | 21 ---------------------
Source.Common/Studio.cs | 12 ++++++------
Source.Common/StudioVirtualModel.cs | 24 ++++++++----------------
3 files changed, 14 insertions(+), 43 deletions(-)
diff --git a/Source.Common/Exts.cs b/Source.Common/Exts.cs
index e8ed40f1f..425569b18 100644
--- a/Source.Common/Exts.cs
+++ b/Source.Common/Exts.cs
@@ -674,27 +674,6 @@ static ArrayAccessor() {
}
}
- static class TempSwap
- {
- public static readonly ThreadLocal> List = new(() => []);
- }
-
- public static void Swap(this List a, List b) {
- var swap = TempSwap.List.Value!;
- swap.Clear();
- swap.EnsureCapacity(a.Count);
-
- a.Clear();
- a.EnsureCapacity(b.Count);
- b.Base().AsSpan()[..b.Count].CopyTo(a.Base());
- a.CountForEdit() = b.Count;
-
- b.Clear();
- b.EnsureCapacity(swap.Count);
- swap.Base().AsSpan()[..swap.Count].CopyTo(b.Base());
- b.CountForEdit() = swap.Count;
- }
-
public static nint Find(this List list, T? value) {
for (int i = 0, c = list.Count; i < c; i++) {
T? at = list[i];
diff --git a/Source.Common/Studio.cs b/Source.Common/Studio.cs
index c5fe0e6ba..86762d582 100644
--- a/Source.Common/Studio.cs
+++ b/Source.Common/Studio.cs
@@ -343,13 +343,13 @@ public VirtualGroup AnimGroup(int animation) {
public readonly object Lock = new();
- public readonly List Seq = [];
- public readonly List Anim = [];
- public readonly List Attachment = [];
- public readonly List Pose = [];
+ public List Seq = [];
+ public List Anim = [];
+ public List Attachment = [];
+ public List Pose = [];
public readonly List Group = [];
- public readonly List Node = [];
- public readonly List IKLock = [];
+ public List Node = [];
+ public List IKLock = [];
public short[] AutoplaySequences = [];
}
diff --git a/Source.Common/StudioVirtualModel.cs b/Source.Common/StudioVirtualModel.cs
index 252fa0d2b..cb17adb39 100644
--- a/Source.Common/StudioVirtualModel.cs
+++ b/Source.Common/StudioVirtualModel.cs
@@ -56,16 +56,8 @@ public void Dispose() {
private int LookupIndex;
}
- void BeginTemporaryCopyList(List source, out List dest) {
- dest = ListPool.Shared.Alloc();
- dest.Clear();
- dest.CountForEdit() = source.Count;
- source.AsSpan().CopyTo(dest.AsSpan());
- }
- void EndTemporaryCopyList(List source, List dest) {
- source.Swap(dest);
- ListPool.Shared.Free(dest);
- }
+ void BeginTemporaryCopyList(List source, out List dest) => dest = new List(source);
+ void EndTemporaryCopyList(ref List source, List dest) => source = dest;
public void AppendSequences(int group, StudioHeader studioHdr) {
lock (Lock) {
@@ -123,7 +115,7 @@ public void AppendSequences(int group, StudioHeader studioHdr) {
GetSeqTable()[s1.Hash(invariant: false)] = (short)j;
}
}
- EndTemporaryCopyList(Seq, seq);
+ EndTemporaryCopyList(ref Seq, seq);
}
}
}
@@ -171,7 +163,7 @@ public void AppendAnimations(int group, StudioHeader studioHdr) {
}
}
- EndTemporaryCopyList(Anim, anim);
+ EndTemporaryCopyList(ref Anim, anim);
}
}
}
@@ -224,7 +216,7 @@ public void AppendAttachments(int group, StudioHeader studioHdr) {
Group[group].MasterAttachment[j] = k;
}
- EndTemporaryCopyList(Attachment, attachment);
+ EndTemporaryCopyList(ref Attachment, attachment);
}
}
public void AppendPoseParameters(int group, StudioHeader studioHdr) {
@@ -264,7 +256,7 @@ public void AppendPoseParameters(int group, StudioHeader studioHdr) {
Group[group].MasterPose[j] = k;
}
- EndTemporaryCopyList(Pose, pose);
+ EndTemporaryCopyList(ref Pose, pose);
}
}
public void AppendBonemap(int group, StudioHeader studioHdr) {
@@ -339,7 +331,7 @@ public void AppendNodes(int group, StudioHeader studioHdr) {
Group[group].MasterNode[j] = k;
}
- EndTemporaryCopyList(Node, node);
+ EndTemporaryCopyList(ref Node, node);
}
}
@@ -367,7 +359,7 @@ public void AppendIKLocks(int group, StudioHeader studioHDR) {
}
}
- EndTemporaryCopyList(IKLock, iklock);
+ EndTemporaryCopyList(ref IKLock, iklock);
}
}
From bc93c59394afeb41817396e9d22c101c79db6a65 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 00:48:46 +0300
Subject: [PATCH 10/13] Remove IL-emitted List backing array accessors
---
Source.Common/Exts.cs | 33 +--------------------------------
1 file changed, 1 insertion(+), 32 deletions(-)
diff --git a/Source.Common/Exts.cs b/Source.Common/Exts.cs
index 425569b18..7b8a8b7c1 100644
--- a/Source.Common/Exts.cs
+++ b/Source.Common/Exts.cs
@@ -650,30 +650,7 @@ public static class BitVecExts
public static void Clear(this bool[] bools, int bit) => bools[bit] = false;
}
public static class ListExtensions
-{ // Thanks JamesHoux: https://stackoverflow.com/questions/4972951/listt-to-t-without-copying
- delegate ref TField RefAccessor(TObj obj);
- static class ArrayAccessor
- {
- public static Func, T[]> Getter;
- public static RefAccessor, int> GetCount;
-
- static ArrayAccessor() {
- var dm = new DynamicMethod("get", MethodAttributes.Static | MethodAttributes.Public, CallingConventions.Standard, typeof(T[]), [typeof(List)], typeof(ArrayAccessor), true);
- var il = dm.GetILGenerator();
- il.Emit(OpCodes.Ldarg_0); // Load List argument
- il.Emit(OpCodes.Ldfld, typeof(List).GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)!); // Replace argument by field
- il.Emit(OpCodes.Ret); // Return field
- Getter = (Func, T[]>)dm.CreateDelegate(typeof(Func, T[]>));
-
- dm = new DynamicMethod("getSize", MethodAttributes.Static | MethodAttributes.Public, CallingConventions.Standard, typeof(int).MakeByRefType(), [typeof(List)], typeof(ArrayAccessor), true);
- il = dm.GetILGenerator();
- il.Emit(OpCodes.Ldarg_0);
- il.Emit(OpCodes.Ldflda, typeof(List).GetField("_size", BindingFlags.NonPublic | BindingFlags.Instance)!);
- il.Emit(OpCodes.Ret);
- GetCount = (RefAccessor, int>)dm.CreateDelegate(typeof(RefAccessor, int>));
- }
- }
-
+{
public static nint Find(this List list, T? value) {
for (int i = 0, c = list.Count; i < c; i++) {
T? at = list[i];
@@ -689,14 +666,6 @@ public static nint Find(this List list, T? value) {
}
return -1;
}
-
- public static T[] Base(this List list) {
- return ArrayAccessor.Getter(list);
- }
-
- public static ref int CountForEdit(this List list) {
- return ref ArrayAccessor.GetCount(list);
- }
}
public static class ClassUtils
{
From 60f27a92d05712f440fa02fe9d5b798d1a0d6458 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com>
Date: Sat, 17 Oct 2026 01:03:59 +0300
Subject: [PATCH 11/13] Collect model material lists into a stack-backed
SpanList
---
Source.Common/IStudioRender.cs | 5 +-
Source.Engine/ModelLoader.cs | 87 +++++++++++-----------
Source.StudioRender/StudioRenderContext.cs | 49 +++++-------
3 files changed, 65 insertions(+), 76 deletions(-)
diff --git a/Source.Common/IStudioRender.cs b/Source.Common/IStudioRender.cs
index d7fd8d065..0b3826b3f 100644
--- a/Source.Common/IStudioRender.cs
+++ b/Source.Common/IStudioRender.cs
@@ -2,6 +2,7 @@
using Source.Common.Engine;
using Source.Common.MaterialSystem;
using Source.Common.Mathematics;
+using Source.Common.Utilities;
using System.Numerics;
@@ -100,8 +101,8 @@ public interface IStudioRender {
bool LoadModel(StudioHeader studioHDR, Memory vtxData, StudioHWData hardwareData);
void UnloadModel(StudioHWData hardwareData);
- int GetMaterialList(StudioHeader studioHDR, Span materials);
- int GetMaterialListFromBodyAndSkin(MDLHandle_t studio, int skin, int body, Span outputMaterials);
+ void GetMaterialList(StudioHeader studioHDR, ref SpanList materials);
+ void GetMaterialListFromBodyAndSkin(MDLHandle_t studio, int skin, int body, ref SpanList outputMaterials);
Span LockBoneMatrices(int boneCount);
void UnlockBoneMatrices();
void LockFlexWeights(int weightCount, out Span flexWeights);
diff --git a/Source.Engine/ModelLoader.cs b/Source.Engine/ModelLoader.cs
index 9ce0935a9..5f5e4c40c 100644
--- a/Source.Engine/ModelLoader.cs
+++ b/Source.Engine/ModelLoader.cs
@@ -12,8 +12,8 @@
using Source.Common.MaterialSystem;
using Source.Common.Mathematics;
using Source.Common.ShaderAPI;
+using Source.Common.Utilities;
-using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -219,10 +219,11 @@ private void ComputeModelFlags(Model model, uint handle) {
if ((studioHdr.Flags & StudioHdrFlags.DoNotCastShadows) != 0)
model.Flags |= ModelFlag.DoNotCastShadows;
- Span materials = new IMaterial[128];
- int materialCount = modelLoader.Mod_GetModelMaterials(model, materials);
+ InlineArray128 materialBuffer = default;
+ SpanList materials = new(materialBuffer);
+ modelLoader.Mod_GetModelMaterials(model, ref materials);
- for (int i = 0; i < materialCount; ++i) {
+ for (int i = 0; i < materials.Count; ++i) {
IMaterial material = materials[i];
if (material == null)
continue;
@@ -525,14 +526,13 @@ private void Studio_LoadModel(Model model, bool touchAllData) {
double t1 = Platform.Time;
model.Materials = default;
- IMaterial[] materials = ArrayPool.Shared.Rent(128);
- int nMaterials = Mod_GetModelMaterials(model, materials);
+ InlineArray128 materialBuffer = default;
+ SpanList materials = new(materialBuffer);
+ Mod_GetModelMaterials(model, ref materials);
+ int nMaterials = materials.Count;
- if ((model.LoadFlags & ModelLoaderFlags.Dynamic) != 0) {
- model.Materials = new Memory(new IMaterial[nMaterials]);
- for (int i = 0; i < nMaterials; i++)
- model.Materials.Span[i] = materials[i];
- }
+ if ((model.LoadFlags & ModelLoaderFlags.Dynamic) != 0)
+ model.Materials = materials.AsSpan().ToArray();
if (nMaterials != 0)
model.LoadFlags |= ModelLoaderFlags.TouchedMaterials;
@@ -542,8 +542,6 @@ private void Studio_LoadModel(Model model, bool touchAllData) {
if (touchAllData || preloaded)
Mod_TouchAllData(model, Host.GetServerCount());
-
- ArrayPool.Shared.Return(materials, true);
}
}
@@ -576,34 +574,36 @@ internal void Mod_RecomputeTranslucency(Model mod, int skin, int body, object? c
if ((studioHdr.Flags & StudioHdrFlags.ForceOpaque) != 0)
return;
- IMaterial[] materialsArray = ArrayPool.Shared.Rent(128);
- try {
- Span materials = materialsArray.AsSpan(0, 128);
- int materialCount = StudioRender.GetMaterialListFromBodyAndSkin(mod.Studio, skin, body, materials);
- for (int i = 0; i < materialCount; i++) {
- if (materials[i] != null) {
- using MatRenderContextPtr renderContext = new(SourceDllMain.materials);
- renderContext.Bind(materials[i], clientRenderable);
- bool isTranslucent = materials[i].IsTranslucent();
-
- if (isTranslucent) {
- mod.Flags |= ModelFlag.Translucent;
- break;
- }
+ InlineArray128 materialBuffer = default;
+ SpanList materials = new(materialBuffer);
+ StudioRender.GetMaterialListFromBodyAndSkin(mod.Studio, skin, body, ref materials);
+ for (int i = 0; i < materials.Count; i++) {
+ if (materials[i] != null) {
+ using MatRenderContextPtr renderContext = new(SourceDllMain.materials);
+ renderContext.Bind(materials[i], clientRenderable);
+ bool isTranslucent = materials[i].IsTranslucent();
+
+ if (isTranslucent) {
+ mod.Flags |= ModelFlag.Translucent;
+ break;
}
}
}
- finally {
- ArrayPool.Shared.Return(materialsArray, true);
- }
}
break;
}
}
- internal int Mod_GetModelMaterials(Model model, Span materials) {
+ static bool ContainsMaterial(ReadOnlySpan materials, IMaterial? material) {
+ foreach (IMaterial existing in materials) {
+ if (existing == material)
+ return true;
+ }
+ return false;
+ }
+
+ internal void Mod_GetModelMaterials(Model model, ref SpanList materials) {
StudioHeader studioHdr;
- int found = 0;
int i;
switch (model.Type) {
@@ -615,27 +615,26 @@ internal int Mod_GetModelMaterials(Model model, Span materials) {
IMaterial? material = MSurf_TexInfo(ref surfID, model.Brush.Shared).Material;
- int j = found;
- while (--j >= 0) {
- if (materials[j] == material)
- break;
- }
- if (j < 0)
- materials[found++] = material!;
+ if (!ContainsMaterial(materials.AsSpan(), material))
+ materials.Add(material!);
- if (found >= materials.Length)
- return found;
+ if (materials.Count >= materials.Capacity)
+ return;
}
}
break;
case ModelType.Studio:
if (!model.Materials.IsEmpty) {
- model.Materials.Span.CopyTo(materials);
+ foreach (IMaterial material in model.Materials.Span) {
+ if (materials.Count >= materials.Capacity)
+ break;
+ materials.Add(material);
+ }
}
else {
studioHdr = MDLCache.GetStudioHdr(model.Studio);
- found = StudioRender.GetMaterialList(studioHdr, materials);
+ StudioRender.GetMaterialList(studioHdr, ref materials);
}
break;
@@ -643,8 +642,6 @@ internal int Mod_GetModelMaterials(Model model, Span materials) {
Assert(false);
break;
}
-
- return found;
}
private void Mod_TouchAllData(Model model, int serverCount) {
diff --git a/Source.StudioRender/StudioRenderContext.cs b/Source.StudioRender/StudioRenderContext.cs
index fb9873e47..5b1d72da7 100644
--- a/Source.StudioRender/StudioRenderContext.cs
+++ b/Source.StudioRender/StudioRenderContext.cs
@@ -6,6 +6,7 @@
using Source.Common.Engine;
using Source.Common.MaterialSystem;
using Source.Common.Mathematics;
+using Source.Common.Utilities;
using System;
using System.Collections.Generic;
@@ -58,12 +59,18 @@ public void EndFrame() {
public void UpdateConfig(in StudioRenderConfig config) => RC.Config = config;
- public int GetMaterialListFromBodyAndSkin(MDLHandle_t studio, int skin, int body, Span outputMaterials) {
- int found = 0;
+ static bool ContainsMaterial(ReadOnlySpan materials, IMaterial material) {
+ foreach (IMaterial existing in materials) {
+ if (existing == material)
+ return true;
+ }
+ return false;
+ }
+ public void GetMaterialListFromBodyAndSkin(MDLHandle_t studio, int skin, int body, ref SpanList outputMaterials) {
StudioHWData? studioHWData = mdlcache.GetHardwareData(studio);
if (studioHWData == null)
- return 0;
+ return;
for (int lodID = studioHWData.RootLOD; lodID < studioHWData.NumLODs; lodID++) {
StudioHeader studioHdr = mdlcache.GetStudioHdr(studio)!;
@@ -82,36 +89,28 @@ public int GetMaterialListFromBodyAndSkin(MDLHandle_t studio, int skin, int body
IMaterial material = inputMaterials[skinRef[mesh.Material]];
Assert(material != null);
- int m;
- for (m = 0; m < found; m++) {
- if (outputMaterials[m] == material)
- break;
- }
- if (m >= found) {
- outputMaterials[found++] = material!;
+ if (!ContainsMaterial(outputMaterials.AsSpan(), material!)) {
+ outputMaterials.Add(material!);
- if (found >= outputMaterials.Length)
- return found;
+ if (outputMaterials.Count >= outputMaterials.Capacity)
+ return;
}
}
}
}
-
- return found;
}
- public int GetMaterialList(StudioHeader studioHDR, Span materials) {
+ public void GetMaterialList(StudioHeader studioHDR, ref SpanList materials) {
AssertMsg(studioHDR != null, "Don't ignore this assert! StudioRenderContext.GetMaterialList() has null studioHDR.");
if (studioHDR == null)
- return 0;
+ return;
if (studioHDR.TextureIndex == 0)
- return 0;
+ return;
int i;
int j;
- int found = 0;
Span path = stackalloc char[MAX_PATH];
for (i = 0; i < studioHDR.NumTextures; i++) {
path.Clear();
@@ -140,21 +139,13 @@ public int GetMaterialList(StudioHeader studioHDR, Span materials) {
if (material == null)
continue;
- if (found < materials.Length) {
- int k;
-
- for (k = 0; k < found; k++)
- if (materials[k] == material)
- break;
-
- if (k >= found)
- materials[found++] = material;
+ if (materials.Count < materials.Capacity) {
+ if (!ContainsMaterial(materials.AsSpan(), material))
+ materials.Add(material);
}
else
break;
}
-
- return found;
}
public bool LoadModel(StudioHeader studioHdr, Memory vtxBuffer, StudioHWData studioHWData) {
From 9dd5bc80695ce11abbfb8ae60bc1702c84628512 Mon Sep 17 00:00:00 2001
From: march <106459595+marchc1@users.noreply.github.com>
Date: Fri, 16 Oct 2026 15:54:15 -0700
Subject: [PATCH 12/13] Fix -> {T}
---
Source.Common/Utilities/SpanList.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Source.Common/Utilities/SpanList.cs b/Source.Common/Utilities/SpanList.cs
index e02057597..599699913 100644
--- a/Source.Common/Utilities/SpanList.cs
+++ b/Source.Common/Utilities/SpanList.cs
@@ -1,7 +1,7 @@
namespace Source.Common.Utilities;
///
-/// List style of operations on fixed/pre-allocated arrays.
+/// List{T} style of operations on fixed/pre-allocated arrays.
///
public ref struct SpanList(Span items)
{
From 1ec9986948fc94a2c57d5f3bd33c5a6f206c269a Mon Sep 17 00:00:00 2001
From: march <106459595+marchc1@users.noreply.github.com>
Date: Fri, 16 Oct 2026 15:56:15 -0700
Subject: [PATCH 13/13] sorry, forgot this needed a see tag
---
Source.Common/Utilities/SpanList.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Source.Common/Utilities/SpanList.cs b/Source.Common/Utilities/SpanList.cs
index 599699913..e9cd69e21 100644
--- a/Source.Common/Utilities/SpanList.cs
+++ b/Source.Common/Utilities/SpanList.cs
@@ -1,7 +1,7 @@
namespace Source.Common.Utilities;
///
-/// List{T} style of operations on fixed/pre-allocated arrays.
+/// style of operations on fixed/pre-allocated arrays.
///
public ref struct SpanList(Span items)
{