|
## MI vs ECS Benchmark v3 — Grouped SoA + Archetype ECS |
|
## ==================================================== |
|
## Addresses v2 feedback: fragmented single-field columns waste cache. |
|
## |
|
## Four approaches: |
|
## 1. ECS-Frag — 8 separate SoA columns (baseline, from v2) |
|
## 2. ECS-Grouped — 5 grouped columns based on system access patterns |
|
## 3. ECS-Archetype — archetype buckets with DenseVec columns (from paste) |
|
## 4. MI-AoS — per-archetype value arrays (baseline winner) |
|
## |
|
## Grouping rationale (by system access, not conceptual meaning): |
|
## SpatialC { pos, vel } → move system touches 1 column |
|
## ViewC { worldPos, sprite } → cull system touches 1 column |
|
## CombatC { hp, targetIdx } → combat system: hp+target in one cache line |
|
## LinkC { parentIdx } → hierarchy system |
|
## LifeC { lifetime } → projectile expiry |
|
|
|
import std/[times, monotimes, strformat, strutils] |
|
from std/typetraits import supportsCopyMem |
|
|
|
const |
|
NBuildings = 15_000 |
|
NUnits = 25_000 |
|
NProjectiles = 10_000 |
|
N = NBuildings + NUnits + NProjectiles |
|
NParented = NUnits div 2 |
|
Frames = 500 |
|
SpawnBatch = 1_000 |
|
SpawnIters = 100 |
|
Dt = 0.016'f32 |
|
ScreenW = 1920'f32 |
|
ScreenH = 1080'f32 |
|
CombatRangeSq = 10_000.0'f32 |
|
CombatDamage = 5.0'f32 |
|
ProjLifetime = 2.0'f32 |
|
|
|
var sink: float32 = 0.0 |
|
|
|
var rngState = 42'u32 |
|
proc rng(): int = |
|
rngState = rngState * 1664525'u32 + 1013904223'u32 |
|
result = int(rngState shr 16) |
|
|
|
type |
|
Vec2f = tuple[x, y: float32] |
|
SpriteC = tuple[color: uint32, size: float32] |
|
HpC = tuple[cur, maxv: float32] |
|
|
|
template v2(x, y: float32): Vec2f = (x, y) |
|
|
|
# ================================================================ |
|
# 1. ECS-Fragmented — 8 separate SoA columns (baseline) |
|
# ================================================================ |
|
|
|
type |
|
Flag = enum fPos, fWorldPos, fVel, fHp, fSprite, fParent, fTarget, fLifetime |
|
|
|
FragWorld = object |
|
count: int |
|
sig: seq[set[Flag]] |
|
pos: seq[Vec2f] |
|
worldPos: seq[Vec2f] |
|
vel: seq[Vec2f] |
|
hp: seq[HpC] |
|
sprite: seq[SpriteC] |
|
parent: seq[int32] |
|
target: seq[int32] |
|
lifetime: seq[float32] |
|
|
|
proc initFrag(cap: int): FragWorld = |
|
result.count = 0 |
|
result.sig = newSeq[set[Flag]](cap) |
|
result.pos = newSeq[Vec2f](cap) |
|
result.worldPos = newSeq[Vec2f](cap) |
|
result.vel = newSeq[Vec2f](cap) |
|
result.hp = newSeq[HpC](cap) |
|
result.sprite = newSeq[SpriteC](cap) |
|
result.parent = newSeq[int32](cap) |
|
result.target = newSeq[int32](cap) |
|
result.lifetime = newSeq[float32](cap) |
|
for i in 0..<cap: |
|
result.parent[i] = -1 |
|
result.target[i] = -1 |
|
|
|
proc populate(w: var FragWorld) = |
|
rngState = 42'u32 |
|
for i in 0..<NBuildings: |
|
let e = w.count; inc w.count |
|
w.sig[e] = {fPos, fSprite} |
|
w.pos[e] = v2(float32(rng() mod 1920), float32(rng() mod 1080)) |
|
w.sprite[e] = (0xFF0000FF'u32, 10'f32) |
|
let unitStart = w.count |
|
for i in 0..<NUnits: |
|
let e = w.count; inc w.count |
|
w.sig[e] = {fPos, fWorldPos, fVel, fHp, fSprite, fTarget} |
|
w.pos[e] = v2(float32(rng() mod 1920), float32(rng() mod 1080)) |
|
w.vel[e] = v2(float32(rng() mod 10) * 0.1'f32, float32(rng() mod 10) * 0.1'f32) |
|
w.hp[e] = (100'f32, 100'f32) |
|
w.sprite[e] = (0x00FF00FF'u32, 20'f32) |
|
w.target[e] = int32(unitStart + rng() mod NUnits) |
|
for i in 0..<NParented: |
|
let e = unitStart + i |
|
w.sig[e].incl(fParent) |
|
w.parent[e] = int32(rng() mod NBuildings) |
|
for i in 0..<NProjectiles: |
|
let e = w.count; inc w.count |
|
w.sig[e] = {fPos, fVel, fLifetime, fTarget} |
|
w.pos[e] = v2(0'f32, 0'f32) |
|
w.vel[e] = v2(2'f32, 1'f32) |
|
w.lifetime[e] = ProjLifetime |
|
w.target[e] = int32(unitStart + rng() mod NUnits) |
|
|
|
proc fragMove(w: var FragWorld, dt: float32) = |
|
for i in 0..<w.count: |
|
if fVel in w.sig[i]: |
|
w.pos[i].x += w.vel[i].x * dt # pos & vel in separate columns |
|
w.pos[i].y += w.vel[i].y * dt |
|
|
|
proc fragHierarchy(w: var FragWorld) = |
|
for i in 0..<w.count: |
|
if fWorldPos in w.sig[i]: |
|
if fParent in w.sig[i]: |
|
let p = w.parent[i] |
|
if p >= 0: |
|
w.worldPos[i].x = w.pos[p].x + w.pos[i].x |
|
w.worldPos[i].y = w.pos[p].y + w.pos[i].y |
|
else: |
|
w.worldPos[i] = w.pos[i] |
|
|
|
proc fragCombat(w: var FragWorld) = |
|
for i in 0..<w.count: |
|
if {fTarget, fHp} <= w.sig[i]: |
|
let t = w.target[i] |
|
if t >= 0: |
|
let dx = w.pos[i].x - w.pos[t].x |
|
let dy = w.pos[i].y - w.pos[t].y |
|
if dx*dx + dy*dy < CombatRangeSq: |
|
w.hp[t].cur -= CombatDamage |
|
if w.hp[t].cur < 0'f32: w.hp[t].cur = 0'f32 |
|
|
|
proc fragCull(w: var FragWorld) = |
|
for i in 0..<w.count: |
|
if fSprite in w.sig[i]: |
|
let p = if fWorldPos in w.sig[i]: w.worldPos[i] else: w.pos[i] |
|
if p.x >= 0 and p.x <= ScreenW and p.y >= 0 and p.y <= ScreenH: |
|
sink += w.sprite[i].size |
|
|
|
proc fragSpawn(w: var FragWorld, n: int, unitStart: int) = |
|
for j in 0..<n: |
|
let e = w.count; inc w.count |
|
w.sig[e] = {fPos, fVel, fLifetime, fTarget} |
|
w.pos[e] = v2(0'f32, 0'f32) |
|
w.vel[e] = v2(2'f32, 1'f32) |
|
w.lifetime[e] = ProjLifetime |
|
w.target[e] = int32(unitStart + rng() mod NUnits) |
|
|
|
proc fragDespawn(w: var FragWorld, n: int, projStart: int) = |
|
for j in 0..<n: |
|
if w.count > projStart: |
|
let idx = projStart + rng() mod (w.count - projStart) |
|
let last = w.count - 1 |
|
if idx != last: |
|
w.sig[idx] = w.sig[last] |
|
w.pos[idx] = w.pos[last] |
|
w.worldPos[idx] = w.worldPos[last] |
|
w.vel[idx] = w.vel[last] |
|
w.hp[idx] = w.hp[last] |
|
w.sprite[idx] = w.sprite[last] |
|
w.parent[idx] = w.parent[last] |
|
w.target[idx] = w.target[last] |
|
w.lifetime[idx] = w.lifetime[last] |
|
dec w.count |
|
|
|
# ================================================================ |
|
# 2. ECS-Grouped — 5 grouped columns based on system access |
|
# ================================================================ |
|
|
|
type |
|
SpatialG = object # move system: pos+vel together |
|
pos: Vec2f |
|
vel: Vec2f |
|
ViewG = object # cull system: worldPos+sprite together |
|
worldPos: Vec2f |
|
sprite: SpriteC |
|
CombatG = object # combat system: hp+target together |
|
hp: HpC |
|
targetIdx: int32 |
|
LinkG = object # hierarchy: parent reference |
|
parentIdx: int32 |
|
LifeG = object # projectile lifetime |
|
lifetime: float32 |
|
|
|
GroupedWorld = object |
|
count: int |
|
sig: seq[set[Flag]] |
|
spatial: seq[SpatialG] |
|
view: seq[ViewG] |
|
combat: seq[CombatG] |
|
link: seq[LinkG] |
|
life: seq[LifeG] |
|
|
|
proc initGrouped(cap: int): GroupedWorld = |
|
result.count = 0 |
|
result.sig = newSeq[set[Flag]](cap) |
|
result.spatial = newSeq[SpatialG](cap) |
|
result.view = newSeq[ViewG](cap) |
|
result.combat = newSeq[CombatG](cap) |
|
result.link = newSeq[LinkG](cap) |
|
result.life = newSeq[LifeG](cap) |
|
for i in 0..<cap: |
|
result.link[i].parentIdx = -1 |
|
result.combat[i].targetIdx = -1 |
|
|
|
proc populate(w: var GroupedWorld) = |
|
rngState = 42'u32 |
|
for i in 0..<NBuildings: |
|
let e = w.count; inc w.count |
|
w.sig[e] = {fPos, fSprite} |
|
w.spatial[e].pos = v2(float32(rng() mod 1920), float32(rng() mod 1080)) |
|
w.view[e].sprite = (0xFF0000FF'u32, 10'f32) |
|
let unitStart = w.count |
|
for i in 0..<NUnits: |
|
let e = w.count; inc w.count |
|
w.sig[e] = {fPos, fWorldPos, fVel, fHp, fSprite, fTarget} |
|
w.spatial[e].pos = v2(float32(rng() mod 1920), float32(rng() mod 1080)) |
|
w.spatial[e].vel = v2(float32(rng() mod 10) * 0.1'f32, float32(rng() mod 10) * 0.1'f32) |
|
w.combat[e].hp = (100'f32, 100'f32) |
|
w.view[e].sprite = (0x00FF00FF'u32, 20'f32) |
|
w.combat[e].targetIdx = int32(unitStart + rng() mod NUnits) |
|
for i in 0..<NParented: |
|
let e = unitStart + i |
|
w.sig[e].incl(fParent) |
|
w.link[e].parentIdx = int32(rng() mod NBuildings) |
|
for i in 0..<NProjectiles: |
|
let e = w.count; inc w.count |
|
w.sig[e] = {fPos, fVel, fLifetime, fTarget} |
|
w.spatial[e].pos = v2(0'f32, 0'f32) |
|
w.spatial[e].vel = v2(2'f32, 1'f32) |
|
w.life[e].lifetime = ProjLifetime |
|
w.combat[e].targetIdx = int32(unitStart + rng() mod NUnits) |
|
|
|
proc groupedMove(w: var GroupedWorld, dt: float32) = |
|
for i in 0..<w.count: |
|
if fVel in w.sig[i]: |
|
w.spatial[i].pos.x += w.spatial[i].vel.x * dt # pos+vel in same struct! |
|
w.spatial[i].pos.y += w.spatial[i].vel.y * dt |
|
|
|
proc groupedHierarchy(w: var GroupedWorld) = |
|
for i in 0..<w.count: |
|
if fWorldPos in w.sig[i]: |
|
if fParent in w.sig[i]: |
|
let p = w.link[i].parentIdx |
|
if p >= 0: |
|
w.view[i].worldPos.x = w.spatial[p].pos.x + w.spatial[i].pos.x |
|
w.view[i].worldPos.y = w.spatial[p].pos.y + w.spatial[i].pos.y |
|
else: |
|
w.view[i].worldPos = w.spatial[i].pos |
|
|
|
proc groupedCombat(w: var GroupedWorld) = |
|
for i in 0..<w.count: |
|
if {fTarget, fHp} <= w.sig[i]: |
|
let t = w.combat[i].targetIdx # hp+target in same struct! |
|
if t >= 0: |
|
let dx = w.spatial[i].pos.x - w.spatial[t].pos.x |
|
let dy = w.spatial[i].pos.y - w.spatial[t].pos.y |
|
if dx*dx + dy*dy < CombatRangeSq: |
|
w.combat[t].hp.cur -= CombatDamage # target's hp in same struct as target idx |
|
if w.combat[t].hp.cur < 0'f32: w.combat[t].hp.cur = 0'f32 |
|
|
|
proc groupedCull(w: var GroupedWorld) = |
|
for i in 0..<w.count: |
|
if fSprite in w.sig[i]: |
|
let p = if fWorldPos in w.sig[i]: w.view[i].worldPos else: w.spatial[i].pos |
|
if p.x >= 0 and p.x <= ScreenW and p.y >= 0 and p.y <= ScreenH: |
|
sink += w.view[i].sprite.size # worldPos+sprite in same struct! |
|
|
|
proc groupedSpawn(w: var GroupedWorld, n: int, unitStart: int) = |
|
for j in 0..<n: |
|
let e = w.count; inc w.count |
|
w.sig[e] = {fPos, fVel, fLifetime, fTarget} |
|
w.spatial[e] = SpatialG(pos: v2(0'f32, 0'f32), vel: v2(2'f32, 1'f32)) |
|
w.life[e] = LifeG(lifetime: ProjLifetime) |
|
w.combat[e].targetIdx = int32(unitStart + rng() mod NUnits) |
|
|
|
proc groupedDespawn(w: var GroupedWorld, n: int, projStart: int) = |
|
for j in 0..<n: |
|
if w.count > projStart: |
|
let idx = projStart + rng() mod (w.count - projStart) |
|
let last = w.count - 1 |
|
if idx != last: |
|
w.sig[idx] = w.sig[last] |
|
w.spatial[idx] = w.spatial[last] # 1 struct copy vs 2 in fragmented |
|
w.view[idx] = w.view[last] |
|
w.combat[idx] = w.combat[last] |
|
w.link[idx] = w.link[last] |
|
w.life[idx] = w.life[last] |
|
dec w.count |
|
|
|
# ================================================================ |
|
# 3. ECS-Archetype — archetype buckets with DenseVec columns |
|
# ================================================================ |
|
|
|
type |
|
ComponentKind = enum |
|
ckTransform, ckVelocity, ckHealth, ckTarget, ckSprite, ckParent, ckLifetime |
|
|
|
ArchetypeKind = enum |
|
BuildingArch, UnitArch, ProjectileArch |
|
|
|
# Component value types for archetype columns |
|
TransformC = object |
|
pos: Vec2f |
|
worldPos: Vec2f |
|
VelocityC = object |
|
vel: Vec2f |
|
HealthC = object |
|
hp: HpC |
|
TargetC = object |
|
targetIdx: int32 |
|
SpriteCol = object |
|
sprite: SpriteC |
|
ParentC = object |
|
parentIdx: int32 |
|
LifetimeC = object |
|
lifetime: float32 |
|
|
|
Archetype = object |
|
mask: set[ComponentKind] |
|
columns: array[ComponentKind, pointer] |
|
|
|
ArchWorld = object |
|
archetypes: array[ArchetypeKind, Archetype] |
|
|
|
# --- Archetype column helpers --- |
|
proc newColumn[T](): pointer = |
|
result = create(seq[T]) |
|
|
|
proc destroyColumn[T](column: pointer) = |
|
if column != nil: |
|
`=destroy`(cast[ptr seq[T]](column)[]) |
|
dealloc(column) |
|
|
|
proc `=destroy`(x: var Archetype) = |
|
if ckTransform in x.mask: destroyColumn[TransformC](x.columns[ckTransform]) |
|
if ckVelocity in x.mask: destroyColumn[VelocityC](x.columns[ckVelocity]) |
|
if ckHealth in x.mask: destroyColumn[HealthC](x.columns[ckHealth]) |
|
if ckTarget in x.mask: destroyColumn[TargetC](x.columns[ckTarget]) |
|
if ckSprite in x.mask: destroyColumn[SpriteCol](x.columns[ckSprite]) |
|
if ckParent in x.mask: destroyColumn[ParentC](x.columns[ckParent]) |
|
if ckLifetime in x.mask: destroyColumn[LifetimeC](x.columns[ckLifetime]) |
|
|
|
proc `=wasMoved`(x: var Archetype) = |
|
x.mask = {} |
|
for ck in ComponentKind: x.columns[ck] = nil |
|
|
|
proc `=copy`(dest: var Archetype; src: Archetype) {.error.} |
|
proc `=dup`(src: Archetype): Archetype {.error.} |
|
|
|
proc `=destroy`(x: var ArchWorld) = |
|
for kind in ArchetypeKind: `=destroy`(x.archetypes[kind]) |
|
|
|
proc `=wasMoved`(x: var ArchWorld) = |
|
for kind in ArchetypeKind: `=wasMoved`(x.archetypes[kind]) |
|
|
|
proc `=copy`(dest: var ArchWorld; src: ArchWorld) {.error.} |
|
proc `=dup`(src: ArchWorld): ArchWorld {.error.} |
|
|
|
# --- Column access templates --- |
|
template col(w, kind, ck, T): untyped = |
|
cast[ptr seq[T]](w.archetypes[kind].columns[ck])[] |
|
|
|
template colData(w, kind, ck, T): untyped = |
|
cast[ptr UncheckedArray[T]](addr col(w, kind, ck, T)[0]) |
|
|
|
template colLen(w, kind, ck, T): untyped = |
|
col(w, kind, ck, T).len |
|
|
|
# --- Archetype initialization --- |
|
proc initArchetype(mask: set[ComponentKind]): Archetype = |
|
result = Archetype(mask: mask) |
|
if ckTransform in mask: result.columns[ckTransform] = newColumn[TransformC]() |
|
if ckVelocity in mask: result.columns[ckVelocity] = newColumn[VelocityC]() |
|
if ckHealth in mask: result.columns[ckHealth] = newColumn[HealthC]() |
|
if ckTarget in mask: result.columns[ckTarget] = newColumn[TargetC]() |
|
if ckSprite in mask: result.columns[ckSprite] = newColumn[SpriteCol]() |
|
if ckParent in mask: result.columns[ckParent] = newColumn[ParentC]() |
|
if ckLifetime in mask: result.columns[ckLifetime] = newColumn[LifetimeC]() |
|
|
|
proc reserve(w: var ArchWorld; kind: ArchetypeKind; cap: int) = |
|
template reserveCol(ck, T): untyped = |
|
if ck in w.archetypes[kind].mask: |
|
cast[ptr seq[T]](w.archetypes[kind].columns[ck])[] = newSeqOfCap[T](cap) |
|
reserveCol(ckTransform, TransformC) |
|
reserveCol(ckVelocity, VelocityC) |
|
reserveCol(ckHealth, HealthC) |
|
reserveCol(ckTarget, TargetC) |
|
reserveCol(ckSprite, SpriteCol) |
|
reserveCol(ckParent, ParentC) |
|
reserveCol(ckLifetime, LifetimeC) |
|
|
|
proc initArchWorld(capBuild, capUnit, capProj: int): ArchWorld = |
|
result.archetypes[BuildingArch] = initArchetype({ckTransform, ckSprite}) |
|
result.archetypes[UnitArch] = initArchetype({ckTransform, ckVelocity, ckHealth, ckTarget, ckSprite, ckParent}) |
|
result.archetypes[ProjectileArch] = initArchetype({ckTransform, ckVelocity, ckTarget, ckLifetime}) |
|
result.reserve(BuildingArch, capBuild) |
|
result.reserve(UnitArch, capUnit) |
|
result.reserve(ProjectileArch, capProj) |
|
|
|
proc populate(w: var ArchWorld) = |
|
rngState = 42'u32 |
|
# Buildings |
|
for i in 0..<NBuildings: |
|
col(w, BuildingArch, ckTransform, TransformC).add( |
|
TransformC(pos: v2(float32(rng() mod 1920), float32(rng() mod 1080)), |
|
worldPos: v2(0'f32, 0'f32))) |
|
col(w, BuildingArch, ckSprite, SpriteCol).add( |
|
SpriteCol(sprite: (0xFF0000FF'u32, 10'f32))) |
|
# Units |
|
for i in 0..<NUnits: |
|
col(w, UnitArch, ckTransform, TransformC).add( |
|
TransformC(pos: v2(float32(rng() mod 1920), float32(rng() mod 1080)), |
|
worldPos: v2(0'f32, 0'f32))) |
|
col(w, UnitArch, ckVelocity, VelocityC).add( |
|
VelocityC(vel: v2(float32(rng() mod 10) * 0.1'f32, float32(rng() mod 10) * 0.1'f32))) |
|
col(w, UnitArch, ckHealth, HealthC).add(HealthC(hp: (100'f32, 100'f32))) |
|
col(w, UnitArch, ckTarget, TargetC).add( |
|
TargetC(targetIdx: int32(rng() mod NUnits))) |
|
col(w, UnitArch, ckSprite, SpriteCol).add( |
|
SpriteCol(sprite: (0x00FF00FF'u32, 20'f32))) |
|
col(w, UnitArch, ckParent, ParentC).add( |
|
ParentC(parentIdx: if i < NParented: int32(rng() mod NBuildings) else: -1'i32)) |
|
# Projectiles |
|
for i in 0..<NProjectiles: |
|
col(w, ProjectileArch, ckTransform, TransformC).add( |
|
TransformC(pos: v2(0'f32, 0'f32), worldPos: v2(0'f32, 0'f32))) |
|
col(w, ProjectileArch, ckVelocity, VelocityC).add(VelocityC(vel: v2(2'f32, 1'f32))) |
|
col(w, ProjectileArch, ckTarget, TargetC).add( |
|
TargetC(targetIdx: int32(rng() mod NUnits))) |
|
col(w, ProjectileArch, ckLifetime, LifetimeC).add( |
|
LifetimeC(lifetime: ProjLifetime)) |
|
|
|
proc archMove(w: var ArchWorld, dt: float32) = |
|
# Units — no signature check, iterate exactly the right entities |
|
let n = colLen(w, UnitArch, ckTransform, TransformC) |
|
let transforms = colData(w, UnitArch, ckTransform, TransformC) |
|
let velocities = colData(w, UnitArch, ckVelocity, VelocityC) |
|
for i in 0..<n: |
|
transforms[i].pos.x += velocities[i].vel.x * dt |
|
transforms[i].pos.y += velocities[i].vel.y * dt |
|
# Projectiles |
|
let pn = colLen(w, ProjectileArch, ckTransform, TransformC) |
|
let ptransforms = colData(w, ProjectileArch, ckTransform, TransformC) |
|
let pvelocities = colData(w, ProjectileArch, ckVelocity, VelocityC) |
|
for i in 0..<pn: |
|
ptransforms[i].pos.x += pvelocities[i].vel.x * dt |
|
ptransforms[i].pos.y += pvelocities[i].vel.y * dt |
|
|
|
proc archHierarchy(w: var ArchWorld) = |
|
let n = colLen(w, UnitArch, ckTransform, TransformC) |
|
let transforms = colData(w, UnitArch, ckTransform, TransformC) |
|
let parents = colData(w, UnitArch, ckParent, ParentC) |
|
let buildingTransforms = colData(w, BuildingArch, ckTransform, TransformC) |
|
for i in 0..<n: |
|
let pi = parents[i].parentIdx |
|
if pi >= 0: |
|
transforms[i].worldPos.x = buildingTransforms[pi].pos.x + transforms[i].pos.x |
|
transforms[i].worldPos.y = buildingTransforms[pi].pos.y + transforms[i].pos.y |
|
else: |
|
transforms[i].worldPos = transforms[i].pos |
|
|
|
proc archCombat(w: var ArchWorld) = |
|
let n = colLen(w, UnitArch, ckTransform, TransformC) |
|
let transforms = colData(w, UnitArch, ckTransform, TransformC) |
|
let targets = colData(w, UnitArch, ckTarget, TargetC) |
|
let healths = colData(w, UnitArch, ckHealth, HealthC) |
|
for i in 0..<n: |
|
let ti = targets[i].targetIdx |
|
if ti >= 0: |
|
let dx = transforms[i].pos.x - transforms[ti].pos.x |
|
let dy = transforms[i].pos.y - transforms[ti].pos.y |
|
if dx*dx + dy*dy < CombatRangeSq: |
|
healths[ti].hp.cur -= CombatDamage |
|
if healths[ti].hp.cur < 0'f32: healths[ti].hp.cur = 0'f32 |
|
|
|
proc archCull(w: var ArchWorld) = |
|
# Buildings |
|
let bn = colLen(w, BuildingArch, ckTransform, TransformC) |
|
let btransforms = colData(w, BuildingArch, ckTransform, TransformC) |
|
let bsprites = colData(w, BuildingArch, ckSprite, SpriteCol) |
|
for i in 0..<bn: |
|
let p = btransforms[i].pos |
|
if p.x >= 0 and p.x <= ScreenW and p.y >= 0 and p.y <= ScreenH: |
|
sink += bsprites[i].sprite.size |
|
# Units |
|
let un = colLen(w, UnitArch, ckTransform, TransformC) |
|
let utransforms = colData(w, UnitArch, ckTransform, TransformC) |
|
let usprites = colData(w, UnitArch, ckSprite, SpriteCol) |
|
for i in 0..<un: |
|
let p = utransforms[i].worldPos |
|
if p.x >= 0 and p.x <= ScreenW and p.y >= 0 and p.y <= ScreenH: |
|
sink += usprites[i].sprite.size |
|
|
|
proc archSpawn(w: var ArchWorld, n: int) = |
|
for j in 0..<n: |
|
col(w, ProjectileArch, ckTransform, TransformC).add( |
|
TransformC(pos: v2(0'f32, 0'f32), worldPos: v2(0'f32, 0'f32))) |
|
col(w, ProjectileArch, ckVelocity, VelocityC).add(VelocityC(vel: v2(2'f32, 1'f32))) |
|
col(w, ProjectileArch, ckTarget, TargetC).add( |
|
TargetC(targetIdx: int32(rng() mod NUnits))) |
|
col(w, ProjectileArch, ckLifetime, LifetimeC).add( |
|
LifetimeC(lifetime: ProjLifetime)) |
|
|
|
proc archDespawn(w: var ArchWorld, n: int) = |
|
let ptf = addr col(w, ProjectileArch, ckTransform, TransformC) |
|
for j in 0..<n: |
|
let len = ptf[].len |
|
if len > 0: |
|
let idx = rng() mod len |
|
let last = len - 1 |
|
if idx != last: |
|
col(w, ProjectileArch, ckTransform, TransformC)[idx] = col(w, ProjectileArch, ckTransform, TransformC)[last] |
|
col(w, ProjectileArch, ckVelocity, VelocityC)[idx] = col(w, ProjectileArch, ckVelocity, VelocityC)[last] |
|
col(w, ProjectileArch, ckTarget, TargetC)[idx] = col(w, ProjectileArch, ckTarget, TargetC)[last] |
|
col(w, ProjectileArch, ckLifetime, LifetimeC)[idx] = col(w, ProjectileArch, ckLifetime, LifetimeC)[last] |
|
col(w, ProjectileArch, ckTransform, TransformC).setLen(last) |
|
col(w, ProjectileArch, ckVelocity, VelocityC).setLen(last) |
|
col(w, ProjectileArch, ckTarget, TargetC).setLen(last) |
|
col(w, ProjectileArch, ckLifetime, LifetimeC).setLen(last) |
|
|
|
# ================================================================ |
|
# 4. MI-AoS — per-archetype value arrays |
|
# ================================================================ |
|
|
|
type |
|
Building = object |
|
pos: Vec2f |
|
sprite: SpriteC |
|
Unit = object |
|
pos: Vec2f |
|
worldPos: Vec2f |
|
vel: Vec2f |
|
hp: HpC |
|
sprite: SpriteC |
|
parentIdx: int32 |
|
targetIdx: int32 |
|
Projectile = object |
|
pos: Vec2f |
|
vel: Vec2f |
|
lifetime: float32 |
|
targetIdx: int32 |
|
AoSWorld = object |
|
buildings: seq[Building] |
|
units: seq[Unit] |
|
projectiles: seq[Projectile] |
|
|
|
proc populate(w: var AoSWorld) = |
|
rngState = 42'u32 |
|
w.buildings = newSeq[Building](NBuildings) |
|
for i in 0..<NBuildings: |
|
w.buildings[i] = Building( |
|
pos: v2(float32(rng() mod 1920), float32(rng() mod 1080)), |
|
sprite: (0xFF0000FF'u32, 10'f32)) |
|
w.units = newSeq[Unit](NUnits) |
|
for i in 0..<NUnits: |
|
w.units[i] = Unit( |
|
pos: v2(float32(rng() mod 1920), float32(rng() mod 1080)), |
|
worldPos: v2(0'f32, 0'f32), |
|
vel: v2(float32(rng() mod 10) * 0.1'f32, float32(rng() mod 10) * 0.1'f32), |
|
hp: (100'f32, 100'f32), |
|
sprite: (0x00FF00FF'u32, 20'f32), |
|
parentIdx: if i < NParented: int32(rng() mod NBuildings) else: -1'i32, |
|
targetIdx: int32(rng() mod NUnits)) |
|
w.projectiles = newSeq[Projectile](NProjectiles) |
|
for i in 0..<NProjectiles: |
|
w.projectiles[i] = Projectile( |
|
pos: v2(0'f32, 0'f32), vel: v2(2'f32, 1'f32), |
|
lifetime: ProjLifetime, targetIdx: int32(rng() mod NUnits)) |
|
|
|
proc aosMove(w: var AoSWorld, dt: float32) = |
|
for i in 0..<w.units.len: |
|
w.units[i].pos.x += w.units[i].vel.x * dt |
|
w.units[i].pos.y += w.units[i].vel.y * dt |
|
for i in 0..<w.projectiles.len: |
|
w.projectiles[i].pos.x += w.projectiles[i].vel.x * dt |
|
w.projectiles[i].pos.y += w.projectiles[i].vel.y * dt |
|
|
|
proc aosHierarchy(w: var AoSWorld) = |
|
for i in 0..<w.units.len: |
|
let pi = w.units[i].parentIdx |
|
if pi >= 0: |
|
w.units[i].worldPos.x = w.buildings[pi].pos.x + w.units[i].pos.x |
|
w.units[i].worldPos.y = w.buildings[pi].pos.y + w.units[i].pos.y |
|
else: |
|
w.units[i].worldPos = w.units[i].pos |
|
|
|
proc aosCombat(w: var AoSWorld) = |
|
for i in 0..<w.units.len: |
|
let ti = w.units[i].targetIdx |
|
if ti >= 0: |
|
let dx = w.units[i].pos.x - w.units[ti].pos.x |
|
let dy = w.units[i].pos.y - w.units[ti].pos.y |
|
if dx*dx + dy*dy < CombatRangeSq: |
|
w.units[ti].hp.cur -= CombatDamage |
|
if w.units[ti].hp.cur < 0'f32: w.units[ti].hp.cur = 0'f32 |
|
|
|
proc aosCull(w: var AoSWorld) = |
|
for i in 0..<w.buildings.len: |
|
let p = w.buildings[i].pos |
|
if p.x >= 0 and p.x <= ScreenW and p.y >= 0 and p.y <= ScreenH: |
|
sink += w.buildings[i].sprite.size |
|
for i in 0..<w.units.len: |
|
let p = w.units[i].worldPos |
|
if p.x >= 0 and p.x <= ScreenW and p.y >= 0 and p.y <= ScreenH: |
|
sink += w.units[i].sprite.size |
|
|
|
proc aosSpawn(w: var AoSWorld, n: int) = |
|
for j in 0..<n: |
|
w.projectiles.add(Projectile( |
|
pos: v2(0'f32, 0'f32), vel: v2(2'f32, 1'f32), |
|
lifetime: ProjLifetime, targetIdx: int32(rng() mod NUnits))) |
|
|
|
proc aosDespawn(w: var AoSWorld, n: int) = |
|
for j in 0..<n: |
|
if w.projectiles.len > 0: |
|
let idx = rng() mod w.projectiles.len |
|
w.projectiles[idx] = w.projectiles[w.projectiles.high] |
|
w.projectiles.setLen(w.projectiles.high) |
|
|
|
# ================================================================ |
|
# Benchmark harness |
|
# ================================================================ |
|
|
|
template bench(title: string, body: untyped) = |
|
for _ in 0..<10: body |
|
let t0 = getMonoTime() |
|
for _ in 0..<Frames: body |
|
let t1 = getMonoTime() |
|
let dur = t1 - t0 |
|
let ns = dur.inNanoseconds.float64 / Frames.float64 / N.float64 |
|
let ms = dur.inMilliseconds.float64 |
|
echo title.align(12) & " " & ns.formatFloat(ffDecimal, 3).align(9) & |
|
" ns/e " & ms.formatFloat(ffDecimal, 1).align(8) & " ms" |
|
|
|
template benchLifeCycle(spawnBody, despawnBody: untyped) = |
|
for _ in 0..<3: |
|
spawnBody |
|
despawnBody |
|
var totalSpawn = 0'i64 |
|
var totalDespawn = 0'i64 |
|
for _ in 0..<SpawnIters: |
|
let s0 = getMonoTime(); spawnBody; let s1 = getMonoTime() |
|
totalSpawn += (s1 - s0).inNanoseconds |
|
let d0 = getMonoTime(); despawnBody; let d1 = getMonoTime() |
|
totalDespawn += (d1 - d0).inNanoseconds |
|
let spMs = totalSpawn.float64 / SpawnIters.float64 / 1e6 |
|
let dpMs = totalDespawn.float64 / SpawnIters.float64 / 1e6 |
|
echo "spawn " & spMs.formatFloat(ffDecimal, 3).align(9) & " ms/batch" |
|
echo "despawn " & dpMs.formatFloat(ffDecimal, 3).align(9) & " ms/batch" |
|
|
|
when isMainModule: |
|
echo "MI vs ECS Benchmark v3 — Grouped + Archetype" |
|
echo "============================================" |
|
echo fmt"Buildings: {NBuildings} Units: {NUnits} Projectiles: {NProjectiles} Total: {N}" |
|
echo fmt"Frames: {Frames} Spawn/despawn batch: {SpawnBatch} x {SpawnIters}" |
|
echo "" |
|
|
|
# ---- ECS-Fragmented ---- |
|
var frag = initFrag(N + SpawnBatch + 16) |
|
frag.populate() |
|
let fragUnitStart = NBuildings |
|
let fragProjStart = NBuildings + NUnits |
|
echo "--- ECS-Fragmented (8 separate SoA columns) ---" |
|
bench("move", frag.fragMove(Dt)) |
|
bench("hierarchy", frag.fragHierarchy()) |
|
bench("combat", frag.fragCombat()) |
|
bench("cull", frag.fragCull()) |
|
rngState = 999'u32 |
|
echo " spawn/despawn:" |
|
benchLifeCycle(frag.fragSpawn(SpawnBatch, fragUnitStart), |
|
frag.fragDespawn(SpawnBatch, fragProjStart)) |
|
echo "" |
|
|
|
# ---- ECS-Grouped ---- |
|
var grp = initGrouped(N + SpawnBatch + 16) |
|
grp.populate() |
|
let grpUnitStart = NBuildings |
|
let grpProjStart = NBuildings + NUnits |
|
echo "--- ECS-Grouped (5 grouped columns by system access) ---" |
|
bench("move", grp.groupedMove(Dt)) |
|
bench("hierarchy", grp.groupedHierarchy()) |
|
bench("combat", grp.groupedCombat()) |
|
bench("cull", grp.groupedCull()) |
|
rngState = 999'u32 |
|
echo " spawn/despawn:" |
|
benchLifeCycle(grp.groupedSpawn(SpawnBatch, grpUnitStart), |
|
grp.groupedDespawn(SpawnBatch, grpProjStart)) |
|
echo "" |
|
|
|
# ---- ECS-Archetype ---- |
|
var arch = initArchWorld(NBuildings.int32, NUnits.int32, |
|
(NProjectiles + SpawnBatch + 16).int32) |
|
arch.populate() |
|
echo "--- ECS-Archetype (DenseVec columns per archetype) ---" |
|
bench("move", arch.archMove(Dt)) |
|
bench("hierarchy", arch.archHierarchy()) |
|
bench("combat", arch.archCombat()) |
|
bench("cull", arch.archCull()) |
|
rngState = 999'u32 |
|
echo " spawn/despawn:" |
|
benchLifeCycle(arch.archSpawn(SpawnBatch), arch.archDespawn(SpawnBatch)) |
|
echo "" |
|
|
|
# ---- MI-AoS ---- |
|
var aos: AoSWorld |
|
aos.projectiles = newSeqOfCap[Projectile](NProjectiles + SpawnBatch + 16) |
|
aos.populate() |
|
echo "--- MI-AoS (per-archetype value arrays) ---" |
|
bench("move", aos.aosMove(Dt)) |
|
bench("hierarchy", aos.aosHierarchy()) |
|
bench("combat", aos.aosCombat()) |
|
bench("cull", aos.aosCull()) |
|
rngState = 999'u32 |
|
echo " spawn/despawn:" |
|
benchLifeCycle(aos.aosSpawn(SpawnBatch), aos.aosDespawn(SpawnBatch)) |
|
echo "" |
|
|
|
# ---- Memory ---- |
|
echo "--- Memory footprint (data only) ---" |
|
let fragMem = N * (1 + sizeof(Vec2f)*2 + sizeof(Vec2f) + sizeof(HpC) + |
|
sizeof(SpriteC) + 4 + 4 + 4) |
|
let grpMem = N * (sizeof(SpatialG) + sizeof(ViewG) + sizeof(CombatG) + |
|
sizeof(LinkG) + sizeof(LifeG)) |
|
let archMem = NBuildings * (sizeof(TransformC) + sizeof(SpriteCol)) + |
|
NUnits * (sizeof(TransformC) + sizeof(VelocityC) + sizeof(HealthC) + |
|
sizeof(TargetC) + sizeof(SpriteCol) + sizeof(ParentC)) + |
|
NProjectiles * (sizeof(TransformC) + sizeof(VelocityC) + |
|
sizeof(TargetC) + sizeof(LifetimeC)) |
|
let aosMem = NBuildings * sizeof(Building) + NUnits * sizeof(Unit) + |
|
NProjectiles * sizeof(Projectile) |
|
echo fmt" ECS-Frag: {fragMem.float64 / 1e6:6.1f} MB (8 columns x {N})" |
|
echo fmt" ECS-Grouped: {grpMem.float64 / 1e6:6.1f} MB (5 grouped columns x {N})" |
|
echo fmt" ECS-Archetype: {archMem.float64 / 1e6:6.1f} MB (only needed comps per archetype)" |
|
echo fmt" MI-AoS: {aosMem.float64 / 1e6:6.1f} MB (struct arrays per archetype)" |
|
echo "" |
|
echo fmt"sink: {sink}" |