Files

471 lines
14 KiB
C++

#include "ScriptTest.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
static const char* kScriptEcsContent = R"SQ(
function testEcsReady() {
return eve.ecsReady()
}
function testDefineClassAndComponent() {
class Position extends eve.Component {
x = 0.0
y = 0.0
}
class Velocity extends eve.Component {
x = 1.0
y = 2.0
}
class Moveable extends eve.Entity {
pos = Position
vel = Velocity
}
local e = Moveable.create()
if (e == null) return false
if (!e.isAlive()) return false
if (e.getId() <= 0) return false
if (e.pos.x != 0.0 || e.pos.y != 0.0) return false
if (e.vel.x != 1.0 || e.vel.y != 2.0) return false
e.pos.x = 10.0
e.vel.x = 3.0
if (e.pos.x != 10.0) return false
if (e.getComponent(Position).x != 10.0) return false
if (!e.hasComponent(Velocity)) return false
return true
}
function testViewIncludesSubclass() {
class Health extends eve.Component {
hp = 100
}
class Actor extends eve.Entity {
hp = Health
}
class Enemy extends Actor {
}
local a = Actor.create()
local b = Enemy.create()
a.hp.hp = 50
b.hp.hp = 80
local actors = eve.view(Actor)
if (actors.len() < 2) return false
local enemies = eve.view(Enemy)
if (enemies.len() < 1) return false
local foundEnemy = false
foreach (e in actors) {
if (e == b) foundEnemy = true
}
if (!foundEnemy) return false
return true
}
function testSystemUpdate() {
class Position extends eve.Component {
x = 0.0
}
class Velocity extends eve.Component {
x = 5.0
}
class Mover extends eve.Entity {
pos = Position
vel = Velocity
}
class MoveSys extends eve.System {
constructor() {
base.constructor(Mover)
}
function update(dt) {
foreach (e in entities()) {
e.pos.x += e.vel.x * dt
}
}
}
local e = Mover.create()
local sys = MoveSys()
sys.update(2.0)
if (e.pos.x != 10.0) return false
e.destroy()
if (e.isAlive()) return false
// destroyed entities leave the view
foreach (ent in eve.view(Mover)) {
if (ent == e) return false
}
return true
}
function testSystemDynamicSpawnDespawn() {
class Position extends eve.Component { x = 0.0 }
class Velocity extends eve.Component { x = 1.0 }
class Mover extends eve.Entity {
pos = Position
vel = Velocity
}
class MoveSys extends eve.System {
constructor() { base.constructor(Mover) }
function update(dt) {
local dying = []
foreach (e in entities()) {
e.pos.x += e.vel.x * dt
if (e.pos.x >= 2.0) dying.push(e)
}
foreach (e in dying) e.destroy()
}
}
local sys = MoveSys()
local a = Mover.create()
local b = Mover.create()
a.vel.x = 1.0
b.vel.x = 1.0
if (eve.view(Mover).len() != 2) return false
sys.update(1.0)
if (a.pos.x != 1.0 || b.pos.x != 1.0) return false
if (eve.view(Mover).len() != 2) return false
sys.update(1.0)
if (a.isAlive() || b.isAlive()) return false
if (eve.view(Mover).len() != 0) return false
local c = Mover.create()
c.vel.x = 1.0
sys.update(0.5)
if (!c.isAlive() || c.pos.x != 0.5) return false
if (eve.view(Mover).len() != 1) return false
// A later entities() call in the same update sees newly created instances.
class SpawnSys extends eve.System {
snapshot = 0
constructor() { base.constructor(Mover) }
function update(dt) {
snapshot = entities().len()
local extra = Mover.create()
extra.pos.x = 99.0
if (entities().len() != snapshot + 1) return false
foreach (e in entities()) {
if (e.pos.x >= 2.0) e.destroy()
}
return true
}
}
local spawnSys = SpawnSys()
if (!spawnSys.update(0.0)) return false
if (spawnSys.snapshot != 1) return false
if (eve.view(Mover).len() != 1) return false
foreach (e in eve.view(Mover)) {
if (e != c) return false
e.destroy()
}
return eve.view(Mover).len() == 0
}
function testSystemQueryArrayAndSubclass() {
class Hp extends eve.Component { v = 1 }
class Actor extends eve.Entity { hp = Hp }
class Enemy extends Actor {}
class Prop extends eve.Entity { hp = Hp }
class TickSys extends eve.System {
constructor() { base.constructor([Actor, Prop]) }
function update(dt) {
foreach (e in entities()) e.hp.v += 1
}
}
local actor = Actor.create()
local enemy = Enemy.create()
local prop = Prop.create()
local sys = TickSys()
sys.update(0.0)
if (actor.hp.v != 2 || enemy.hp.v != 2 || prop.hp.v != 2) return false
sys.setQuery(Enemy)
sys.update(0.0)
if (actor.hp.v != 2 || enemy.hp.v != 3 || prop.hp.v != 2) return false
local emptySys = eve.System()
if (emptySys.entities().len() != 0) return false
actor.destroy()
enemy.destroy()
prop.destroy()
return true
}
function testTypeMarkersAndEntityContainer() {
class Status extends eve.Component {
alive = eve.Boolean
name = eve.String
score = eve.Number
}
class Thing extends eve.EntityContainer {
status = Status
}
local t = Thing.create()
if (t.status.alive != false) return false
if (t.status.name != "") return false
if (t.status.score != 0.0) return false
t.status.alive = true
t.status.name = "orb"
t.status.score = 3.5
if (!t.status.alive || t.status.name != "orb" || t.status.score != 3.5) return false
return eve.EntityContainer == eve.Entity
}
function testStaticComponentsTable() {
class Pos extends eve.Component {
x = 1.0
}
class Node extends eve.Entity {
static components = { pos = Pos }
pos = null
}
local n = Node.create()
if (n.pos == null) return false
if (n.pos.x != 1.0) return false
return true
}
function testShaderSystemClassExists() {
if (!("ShaderSystem" in eve)) return false
// Construction without GPU is allowed; update() no-ops until setGpgpu.
class Pos extends eve.Component { x = 0.0; y = 0.0 }
class Vel extends eve.Component { x = 1.0; y = 0.0 }
class Mover extends eve.Entity { pos = Pos; vel = Vel }
local sys = eve.ShaderSystem(Mover)
sys.bindFields(0, "pos", ["x", "y"])
sys.bindFields(1, "vel", ["x", "y"])
sys.update(0.016)
return true
}
function testShaderSystemGpuResidentTransfers() {
class Pos extends eve.Component { x = 0.0 }
class Node extends eve.Entity { pos = Pos }
class FakeBackend {
dispatches = 0
records = 0
function ensureBuffer(binding, count) { return binding }
function getBuffer(binding) { return binding }
function dispatch(count, dt) { dispatches += 1 }
function recordDispatch(sequence, count, dt) { records += 1 }
}
local uploads = 0
local downloads = 0
local uploadRange = null
local downloadRange = null
eve.packEcsFloats <- function(ents, slot, fields, buf) { uploads += 1 }
eve.unpackEcsFloats <- function(ents, slot, fields, buf, count) { downloads += 1 }
eve.packEcsFloatsRange <- function(ents, slot, fields, buf, first, count) {
uploadRange = [first, count]
}
eve.unpackEcsFloatsRange <- function(ents, slot, fields, buf, first, count) {
downloadRange = [first, count]
}
local node = Node.create()
local second = Node.create()
local third = Node.create()
local sys = eve.ShaderSystem(Node)
sys._gpu = true
sys._backend = FakeBackend()
sys.bindFields(0, "pos", ["x"], false, false)
sys.update(0.016)
sys.update(0.016)
if (uploads != 1 || downloads != 0 || sys._backend.dispatches != 2) return false
sys.requestUpload(0).requestReadback(0)
sys.update(0.016)
if (uploads != 2 || downloads != 1) return false
sys.requestUploadRange(0, 1, 1).requestUploadRange(0, 2, 1)
sys.requestReadbackRange(0, 1, 2)
sys.update(0.016)
if (uploadRange == null || uploadRange[0] != 1 || uploadRange[1] != 2) return false
if (downloadRange == null || downloadRange[0] != 1 || downloadRange[1] != 2) return false
if (uploads != 2 || downloads != 1) return false
local batch = sys.record(true, 0.016)
sys.completeRecorded(batch)
if (sys._backend.records != 1) return false
// Typed write-only schemas allocate resident output without packing stale CPU data.
local outputSys = eve.ShaderSystem(Node)
outputSys._gpu = true
outputSys._backend = FakeBackend()
outputSys.bindSchema(0, { slot = "pos", fields = ["x"], scalar = "f32",
access = "write" })
outputSys.update(0.016)
if (uploads != 2) return false
local schema = outputSys.getBindingSchema(0)
if (schema == null || schema.strideFloats != 1 || schema.access != "write") return false
if (outputSys.shaderDeclarations("glsl") !=
"layout(set = 0, binding = 0) writeonly buffer EcsBinding0 { float data[]; } ecs0;\n")
return false
if (outputSys.shaderDeclarations("wgsl") !=
"@group(0) @binding(0) var<storage, read_write> ecs0 : array<f32>;\n")
return false
local rejected = false
try { outputSys.bindSchema(1, { slot = "pos", fields = ["x"], scalar = "i32" }) }
catch (e) { rejected = true }
if (!rejected) return false
// A structural revision invalidates resident packing even at the same capacity.
local fourth = Node.create()
sys.update(0.016)
if (uploads != 3 || downloads != 1) return false
node.destroy()
second.destroy()
third.destroy()
fourth.destroy()
return true
}
function testViewCacheStableAndInvalidated() {
class Pos extends eve.Component { x = 0.0 }
class Node extends eve.Entity { pos = Pos }
class Sub extends Node {}
local a = Node.create()
local b = Sub.create()
// No changes between calls → same cached array identity (zero allocation
// per frame in System.entities()).
local v1 = eve.view(Node)
if (v1.len() != 2) return false
local v2 = eve.view(Node)
if (v1 != v2) return false
local sv1 = eve.view(Sub)
if (sv1.len() != 1) return false
// create() invalidates the cached view (and subclass views via the chain)
local c = Node.create()
if (eve.view(Node).len() != 3) return false
if (eve.view(Sub).len() != 1) return false
// destroy() invalidates too; dead entities leave the view immediately
a.destroy()
b.destroy()
c.destroy()
if (eve.view(Node).len() != 0) return false
return true
}
function testViewCacheBaseClassIncludesSubclasses() {
class Pos extends eve.Component { x = 0.0 }
class Node extends eve.Entity { pos = Pos }
class Sub extends Node {}
local n = Node.create()
local s = Sub.create()
if (eve.view(Node).len() != 2) return false
if (eve.view(eve.Entity).len() < 2) return false
n.destroy()
s.destroy()
return true
}
function testComponentDefaultsAndSlotsCached() {
class C extends eve.Component { v = eve.Number; s = eve.String }
class E extends eve.Entity { c = C }
local a = E.create()
if (a.c.v != 0.0 || a.c.s != "") return false
a.c.v = 5.0
// Second instance gets fresh resolved defaults, not the mutated values.
local b = E.create()
if (b.c.v != 0.0 || b.c.s != "") return false
if (a.c.v != 5.0) return false
if (a.getComponent(C) != a.c) return false
if (!a.hasComponent(C)) return false
a.destroy()
b.destroy()
return true
}
function testGpuDrivenBindingSurface() {
local gpu
try { gpu = eve.Gpgpu() } catch (e) { return false }
local required = ["setGpuDrivenEnabled", "isGpuDrivenEnabled",
"gpuDrivenMeshRecord", "gpuDrivenMaterialRecord",
"gpuDrivenMaterialUsable", "getGpuDrivenInstanceStride",
"getGpuResidentOffsetAlignment", "writeGpuDrivenInstance",
"submitResidentInstances"]
foreach (name in required) if (!(name in gpu)) return false
return gpu.getGpuDrivenInstanceStride() == 80 &&
gpu.getGpuResidentOffsetAlignment() == 256
}
)SQ";
UnitSciptTest(ScriptEcsTest, kScriptEcsContent);
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.ready") {
CHECK(vm.callFunc(vm.findFunc("testEcsReady"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.defineClassAndComponent") {
CHECK(vm.callFunc(vm.findFunc("testDefineClassAndComponent"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewIncludesSubclass") {
CHECK(vm.callFunc(vm.findFunc("testViewIncludesSubclass"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemUpdate") {
CHECK(vm.callFunc(vm.findFunc("testSystemUpdate"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemDynamicSpawnDespawn") {
CHECK(vm.callFunc(vm.findFunc("testSystemDynamicSpawnDespawn"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemQueryArrayAndSubclass") {
CHECK(vm.callFunc(vm.findFunc("testSystemQueryArrayAndSubclass"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.typeMarkersAndEntityContainer") {
CHECK(vm.callFunc(vm.findFunc("testTypeMarkersAndEntityContainer"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.staticComponentsTable") {
CHECK(vm.callFunc(vm.findFunc("testStaticComponentsTable"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.shaderSystemClassExists") {
CHECK(vm.callFunc(vm.findFunc("testShaderSystemClassExists"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.shaderSystemGpuResidentTransfers") {
CHECK(vm.callFunc(vm.findFunc("testShaderSystemGpuResidentTransfers"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewCacheStableAndInvalidated") {
CHECK(vm.callFunc(vm.findFunc("testViewCacheStableAndInvalidated"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewCacheBaseClassIncludesSubclasses") {
CHECK(vm.callFunc(vm.findFunc("testViewCacheBaseClassIncludesSubclasses"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.componentDefaultsAndSlotsCached") {
CHECK(vm.callFunc(vm.findFunc("testComponentDefaultsAndSlotsCached"), vm).toBool());
}
TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.gpuDrivenBindingSurface") {
CHECK(vm.callFunc(vm.findFunc("testGpuDrivenBindingSurface"), vm).toBool());
}