diff --git a/src/Arch.Tests/SparseJaggedArrayBugTest.cs b/src/Arch.Tests/SparseJaggedArrayBugTest.cs
new file mode 100644
index 00000000..ed32c070
--- /dev/null
+++ b/src/Arch.Tests/SparseJaggedArrayBugTest.cs
@@ -0,0 +1,226 @@
+using System.Text;
+using Arch.Core;
+using Arch.Core.Extensions;
+using Arch.Core.Utils;
+using static NUnit.Framework.Assert;
+
+namespace Arch.Tests;
+
+///
+/// Test class for reproducing the SparseJaggedArray memory corruption bug.
+/// This bug occurs during high-load operations with frequent archetype transitions.
+///
+[TestFixture]
+public sealed class SparseJaggedArrayBugTest
+{
+ ///
+ /// Reproduces the System.AccessViolationException that occurs when adding components
+ /// with reference types (strings) during high-load archetype transitions.
+ /// This test should fail with the current implementation due to memory corruption.
+ ///
+ [Test]
+ public void HighLoad_RenderComponent_Addition_Reproduces_Memory_Corruption_Bug()
+ {
+ // Reproduction steps from bug report:
+ // Create a test that adds 10,000+ entities with multiple components
+ // Include components with reference types (strings) like this struct:
+ // Add components in a way that triggers archetype transitions
+
+ const int entityCount = 10000;
+
+ using var world = World.Create();
+
+ // Create entities with base components first
+ var entities = new Entity[entityCount];
+ for (int i = 0; i < entityCount; i++)
+ {
+ entities[i] = world.Create(new Transform { X = i, Y = i });
+ }
+
+ // Add render components (this triggers archetype transitions and the crash)
+ // This is where the SparseJaggedArray.get_Capacity() crash occurs
+ for (int i = 0; i < entityCount; i++)
+ {
+ var meshId = $"mesh_{i}";
+ var materialId = $"material_{i}";
+ var renderComponent = RenderComponent.Default(meshId, materialId);
+
+ // This Add operation should trigger archetype edge checking via HasAddEdge
+ // which calls SparseJaggedArray.ContainsKey() -> get_Capacity()
+ world.Add(entities[i], renderComponent);
+ }
+
+ // Verify all entities have both components (if no crash)
+ var query = new QueryDescription().WithAll();
+ var count = world.CountEntities(query);
+
+ That(count, Is.EqualTo(entityCount), "All entities should have both Transform and RenderComponent");
+
+ // Additional verification that the components are correctly set
+ world.Query(query, (Entity entity, ref Transform transform, ref RenderComponent render) =>
+ {
+ var expectedId = (int)transform.X;
+ That(transform.X, Is.EqualTo(expectedId));
+ That(transform.Y, Is.EqualTo(expectedId));
+ That(render.MeshId, Is.EqualTo($"mesh_{expectedId}"));
+ That(render.MaterialId, Is.EqualTo($"material_{expectedId}"));
+ That(render.IsVisible, Is.True);
+ });
+ }
+
+ ///
+ /// Test with fewer entities to verify the operation works at smaller scale.
+ /// This should pass and helps verify the test setup is correct.
+ ///
+ [Test]
+ public void LowLoad_RenderComponent_Addition_Works()
+ {
+ const int entityCount = 100;
+
+ using var world = World.Create();
+
+ // Create entities with base components first
+ var entities = new Entity[entityCount];
+ for (int i = 0; i < entityCount; i++)
+ {
+ entities[i] = world.Create(new Transform { X = i, Y = i });
+ }
+
+ // Add render components
+ for (int i = 0; i < entityCount; i++)
+ {
+ var meshId = $"mesh_{i}";
+ var materialId = $"material_{i}";
+ var renderComponent = RenderComponent.Default(meshId, materialId);
+
+ world.Add(entities[i], renderComponent);
+ }
+
+ // Verify all entities have both components
+ var query = new QueryDescription().WithAll();
+ var count = world.CountEntities(query);
+
+ That(count, Is.EqualTo(entityCount), "All entities should have both Transform and RenderComponent");
+ }
+
+ ///
+ /// Test that specifically exercises archetype edge checking during high load.
+ /// This isolates the SparseJaggedArray.ContainsKey() operation that triggers the bug.
+ ///
+ [Test]
+ public void HighLoad_ArchetypeEdgeChecking_StressTest()
+ {
+ const int entityCount = 10000;
+
+ using var world = World.Create();
+
+ // Create entities and immediately add render components to trigger edge creation
+ for (int i = 0; i < entityCount; i++)
+ {
+ var entity = world.Create(new Transform { X = i, Y = i });
+ var meshId = $"mesh_{i}";
+ var materialId = $"material_{i}";
+ var renderComponent = RenderComponent.Default(meshId, materialId);
+
+ // This triggers HasAddEdge() calls which stress the SparseJaggedArray
+ world.Add(entity, renderComponent);
+ }
+
+ // Create additional entities with the same archetype to stress capacity
+ for (int i = 0; i < entityCount; i++)
+ {
+ var entity = world.Create(new Transform { X = i + entityCount, Y = i + entityCount });
+ var meshId = $"mesh_duplicate_{i}";
+ var materialId = $"material_duplicate_{i}";
+ var renderComponent = RenderComponent.Default(meshId, materialId);
+
+ // This re-uses existing archetype edges
+ world.Add(entity, renderComponent);
+ }
+
+ // Verify total count
+ var query = new QueryDescription().WithAll();
+ var count = world.CountEntities(query);
+
+ That(count, Is.EqualTo(entityCount * 2), "All entities should have both components");
+ }
+
+ ///
+ /// Tests that our bounds checking safely handles any component ID, even those
+ /// that far exceed the SparseJaggedArray capacity, preventing memory corruption crashes.
+ ///
+ [Test]
+ public void BoundsChecking_Handles_Any_ComponentId_Safely()
+ {
+ using var world = World.Create();
+
+ // Create an entity to generate an archetype
+ var entity = world.Create(new Transform { X = 1, Y = 1 });
+ var archetype = world.Archetypes[0];
+
+ // Test with component IDs that are much higher than any realistic scenario
+ // These IDs would definitely exceed SparseJaggedArray capacity and cause crashes without bounds checking
+ var extremeHighIds = new[] { 1000, 10000, 100000, 1000000, int.MaxValue };
+
+ foreach (var extremeId in extremeHighIds)
+ {
+ // These calls should return false safely instead of crashing
+ // Without bounds checking, these would cause System.AccessViolationException
+ var hasAddEdge = archetype.HasAddEdge(extremeId);
+ var hasRemoveEdge = archetype.HasRemoveEdge(extremeId);
+
+ That(hasAddEdge, Is.False, $"HasAddEdge should return false safely for extreme ID {extremeId}");
+ That(hasRemoveEdge, Is.False, $"HasRemoveEdge should return false safely for extreme ID {extremeId}");
+
+ // Get calls should return null safely
+ var getAddEdge = archetype.GetAddEdge(extremeId);
+ var getRemoveEdge = archetype.GetRemoveEdge(extremeId);
+
+ That(getAddEdge, Is.Null, $"GetAddEdge should return null safely for extreme ID {extremeId}");
+ That(getRemoveEdge, Is.Null, $"GetRemoveEdge should return null safely for extreme ID {extremeId}");
+ }
+ }
+
+ ///
+ /// Simple test to verify that HasAddEdge returns false for high component IDs
+ /// that exceed SparseJaggedArray capacity, preventing crashes.
+ ///
+ [Test]
+ public void HasAddEdge_ReturnsFalse_ForHighComponentIds()
+ {
+ using var world = World.Create();
+
+ // Create an entity to generate an archetype
+ var entity = world.Create(new Transform { X = 1, Y = 1 });
+ var archetype = world.Archetypes[0]; // Get the archetype with Transform
+
+ // Test with component IDs that would exceed SparseJaggedArray capacity
+ // These IDs are way beyond the BucketSize of 64 we set
+ const int veryHighId = 10000;
+
+ // This should return false safely instead of crashing
+ var result = archetype.HasAddEdge(veryHighId);
+ That(result, Is.False, "HasAddEdge should return false for IDs exceeding capacity");
+ }
+
+ ///
+ /// Test adding a component that triggers archetype edge operations
+ /// without causing memory corruption.
+ ///
+ [Test]
+ public void AddComponent_DoesNotCrash_WithBasicTypes()
+ {
+ using var world = World.Create();
+
+ var entity = world.Create(new Transform { X = 1, Y = 1 });
+
+ // Add render component - this should create archetype edges
+ var renderComponent = RenderComponent.Default("test_mesh", "test_material");
+ world.Add(entity, renderComponent);
+
+ // Verify it worked
+ var query = new QueryDescription().WithAll();
+ var count = world.CountEntities(query);
+ That(count, Is.EqualTo(1));
+ }
+}
diff --git a/src/Arch.Tests/Utils/Structs.cs b/src/Arch.Tests/Utils/Structs.cs
index 3d48be9a..5ce31f85 100644
--- a/src/Arch.Tests/Utils/Structs.cs
+++ b/src/Arch.Tests/Utils/Structs.cs
@@ -12,3 +12,25 @@ public struct Rotation
public struct Ai { }
+public struct RenderComponent
+{
+ public string MeshId;
+ public string MaterialId;
+ public bool IsVisible;
+ public int RenderLayer;
+ public bool CastsShadows;
+ public bool ReceivesShadows;
+
+ public static RenderComponent Default(string meshId, string materialId)
+ {
+ return new RenderComponent
+ {
+ MeshId = meshId,
+ MaterialId = materialId,
+ IsVisible = true,
+ RenderLayer = 0,
+ CastsShadows = true,
+ ReceivesShadows = true
+ };
+ }
+}
diff --git a/src/Arch/Core/Edges/Archetype.Edges.cs b/src/Arch/Core/Edges/Archetype.Edges.cs
index 9155b4c6..d8df66eb 100644
--- a/src/Arch/Core/Edges/Archetype.Edges.cs
+++ b/src/Arch/Core/Edges/Archetype.Edges.cs
@@ -10,7 +10,7 @@ public partial class Archetype
///
/// The bucket size of each bucket inside the .
///
- private const int BucketSize = 16;
+ private const int BucketSize = 64;
///
/// Caches other s indexed by the
@@ -58,6 +58,10 @@ internal void AddRemoveEdge(int index, Archetype archetype)
internal bool HasAddEdge(int index)
{
+ // Bounds check to prevent SparseJaggedArray capacity overflow crashes
+ if (index < 0) return false;
+ var bucketIndex = index / BucketSize;
+ if (bucketIndex >= _addEdges.Buckets) return false;
return _addEdges.ContainsKey(index);
}
@@ -69,6 +73,10 @@ internal bool HasAddEdge(int index)
internal bool HasRemoveEdge(int index)
{
+ // Bounds check to prevent SparseJaggedArray capacity overflow crashes
+ if (index < 0) return false;
+ var bucketIndex = index / BucketSize;
+ if (bucketIndex >= _removeEdges.Buckets) return false;
return _removeEdges.ContainsKey(index);
}
@@ -83,6 +91,10 @@ internal bool HasRemoveEdge(int index)
internal Archetype GetAddEdge(int index)
{
+ // Bounds check to prevent SparseJaggedArray access violations
+ if (index < 0) return null!;
+ var bucketIndex = index / BucketSize;
+ if (bucketIndex >= _addEdges.Buckets) return null!;
return _addEdges[index];
}
@@ -97,6 +109,10 @@ internal Archetype GetAddEdge(int index)
internal Archetype GetRemoveEdge(int index)
{
+ // Bounds check to prevent SparseJaggedArray access violations
+ if (index < 0) return null!;
+ var bucketIndex = index / BucketSize;
+ if (bucketIndex >= _removeEdges.Buckets) return null!;
return _removeEdges[index];
}