892 lines
32 KiB
C++
892 lines
32 KiB
C++
#include "common/ECS.h"
|
|
#include "common/ScriptError.h"
|
|
|
|
#include <simplesquirrel/simplesquirrel.hpp>
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
namespace eve {
|
|
namespace {
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Script component registry (name → Squirrel class)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct ScriptComponentReg {
|
|
HSQUIRRELVM vm = nullptr;
|
|
HSQOBJECT cls;
|
|
ScriptComponentReg() { sq_resetobject(&cls); }
|
|
ScriptComponentReg(HSQUIRRELVM v, HSQOBJECT o) : vm(v), cls(o) {}
|
|
};
|
|
|
|
std::unordered_map<std::string, ScriptComponentReg>& scriptComponents() {
|
|
static std::unordered_map<std::string, ScriptComponentReg> m;
|
|
return m;
|
|
}
|
|
|
|
int& nextEntityId() {
|
|
static int id = 1;
|
|
return id;
|
|
}
|
|
|
|
int allocEntityId() { return nextEntityId()++; }
|
|
|
|
// 当前 VM 的 Squirrel 类对象 → 脚本 view() 收集函数。
|
|
std::unordered_map<const void*, CppEntityViewFn>& cppEntityViews() {
|
|
static std::unordered_map<const void*, CppEntityViewFn> views;
|
|
return views;
|
|
}
|
|
|
|
std::vector<std::function<void(ssq::Table&)>>& postEcsHooks() {
|
|
static std::vector<std::function<void(ssq::Table&)>> hooks;
|
|
return hooks;
|
|
}
|
|
|
|
bool& ecsScriptInjected() {
|
|
static bool injected = false;
|
|
return injected;
|
|
}
|
|
|
|
size_t& postEcsHooksFlushed() {
|
|
static size_t n = 0;
|
|
return n;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Class helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
bool isSubclassOf(HSQUIRRELVM vm, HSQOBJECT child, HSQOBJECT base) {
|
|
if (child._type != OT_CLASS || base._type != OT_CLASS) return false;
|
|
const SQInteger top = sq_gettop(vm);
|
|
sq_pushobject(vm, child);
|
|
for (int guard = 0; guard < 64; ++guard) {
|
|
if (sq_gettype(vm, -1) != OT_CLASS) break;
|
|
HSQOBJECT cur;
|
|
sq_getstackobj(vm, -1, &cur);
|
|
if (cur._unVal.pClass == base._unVal.pClass) {
|
|
sq_settop(vm, top);
|
|
return true;
|
|
}
|
|
if (SQ_FAILED(sq_getbase(vm, -1))) break;
|
|
sq_remove(vm, -2); // drop current, keep base
|
|
if (sq_gettype(vm, -1) != OT_CLASS) break;
|
|
}
|
|
sq_settop(vm, top);
|
|
return false;
|
|
}
|
|
|
|
/** Collect non-method members of a class into `out` (field defaults / statics). */
|
|
void collectClassFields(HSQUIRRELVM vm, HSQOBJECT clsObj, HSQOBJECT outTable) {
|
|
const SQInteger top = sq_gettop(vm);
|
|
sq_pushobject(vm, outTable); // out
|
|
sq_pushobject(vm, clsObj); // out, cls
|
|
sq_pushnull(vm); // out, cls, iter
|
|
while (SQ_SUCCEEDED(sq_next(vm, -2))) {
|
|
// out, cls, iter, key, value
|
|
if (sq_gettype(vm, -2) == OT_STRING) {
|
|
const SQChar* key = nullptr;
|
|
sq_getstring(vm, -2, &key);
|
|
if (key && key[0] != '_') {
|
|
const SQObjectType vt = sq_gettype(vm, -1);
|
|
if (vt != OT_CLOSURE && vt != OT_NATIVECLOSURE && vt != OT_GENERATOR) {
|
|
sq_push(vm, -5); // out
|
|
sq_push(vm, -3); // key
|
|
sq_push(vm, -3); // value
|
|
sq_rawset(vm, -3);
|
|
sq_pop(vm, 1); // pop out
|
|
}
|
|
}
|
|
}
|
|
sq_pop(vm, 2); // key, value
|
|
}
|
|
sq_settop(vm, top);
|
|
}
|
|
|
|
ssq::Table inspectClassFields(ssq::Object clsObj) {
|
|
HSQUIRRELVM vm = clsObj.getHandle();
|
|
ssq::Table out(vm);
|
|
if (clsObj.getType() != ssq::Type::CLASS) return out;
|
|
collectClassFields(vm, clsObj.getRaw(), out.getRaw());
|
|
return out;
|
|
}
|
|
|
|
bool scriptIsSubclass(ssq::Object child, ssq::Object base) {
|
|
if (child.getType() != ssq::Type::CLASS || base.getType() != ssq::Type::CLASS)
|
|
return false;
|
|
return isSubclassOf(child.getHandle(), child.getRaw(), base.getRaw());
|
|
}
|
|
|
|
ssq::Object getClassBase(ssq::Object cls) {
|
|
HSQUIRRELVM vm = cls.getHandle();
|
|
ssq::Object ret(vm);
|
|
if (cls.getType() != ssq::Type::CLASS) return ret;
|
|
const SQInteger top = sq_gettop(vm);
|
|
sq_pushobject(vm, cls.getRaw());
|
|
if (SQ_FAILED(sq_getbase(vm, -1)) || sq_gettype(vm, -1) != OT_CLASS) {
|
|
sq_settop(vm, top);
|
|
return ret;
|
|
}
|
|
sq_getstackobj(vm, -1, &ret.getRaw());
|
|
sq_addref(vm, &ret.getRaw());
|
|
sq_settop(vm, top);
|
|
return ret;
|
|
}
|
|
|
|
void registerScriptComponent(const std::string& name, ssq::Object cls) {
|
|
if (cls.getType() != ssq::Type::CLASS) return;
|
|
HSQUIRRELVM vm = cls.getHandle();
|
|
HSQOBJECT o = cls.getRaw();
|
|
sq_addref(vm, &o);
|
|
auto& m = scriptComponents();
|
|
auto it = m.find(name);
|
|
if (it != m.end()) {
|
|
if (it->second.vm) sq_release(it->second.vm, &it->second.cls);
|
|
it->second = ScriptComponentReg(vm, o);
|
|
} else {
|
|
m.emplace(name, ScriptComponentReg(vm, o));
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Injected Squirrel runtime
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const char* kEcsScript = R"SQ(
|
|
// Script ECS: define Component / Entity classes, create instances, view & System.
|
|
// Mirrors ECS.hpp's class + component model for Squirrel game logic.
|
|
|
|
if (!("Number" in eve)) eve.Number <- "number"
|
|
if (!("Boolean" in eve)) eve.Boolean <- "boolean"
|
|
if (!("String" in eve)) eve.String <- "string"
|
|
|
|
eve._ecsTypes <- {} // EntityClass -> { cls, instances }
|
|
|
|
// --- 性能缓存(万级实体稳态零分配) ---
|
|
// view 结果缓存:create/destroy 时按类链失效;C++ 桥接类(_cppHasView=true)不缓存。
|
|
eve._ecsViewCache <- {} // EntityClass -> array
|
|
eve._ecsSlotsCache <- {} // EntityClass -> { fieldName = ComponentClass }
|
|
eve._ecsCompDefaults <- {} // ComponentClass -> { fieldName = resolvedDefault }
|
|
eve._ecsRevision <- 0 // increments on script entity structural changes
|
|
|
|
function eve::_ecsResolveMarker(v) {
|
|
if (v == this.Number || v == "number") return 0.0
|
|
if (v == this.Boolean || v == "boolean") return false
|
|
if (v == this.String || v == "string") return ""
|
|
return v
|
|
}
|
|
|
|
function eve::_ecsIsComponentClass(obj) {
|
|
if (typeof obj != "class") return false
|
|
return this.isSubclass(obj, this.Component)
|
|
}
|
|
|
|
function eve::_ecsInstantiateComponent(compClass) {
|
|
local c = compClass()
|
|
if (compClass in this._ecsCompDefaults) {
|
|
foreach (name, val in this._ecsCompDefaults[compClass]) c[name] = val
|
|
} else {
|
|
local fields = this.inspectClassFields(compClass)
|
|
local resolved = {}
|
|
foreach (name, val in fields) {
|
|
local cur = null
|
|
try { cur = c[name] } catch (e) { cur = null }
|
|
if (cur == this.Number || cur == this.Boolean || cur == this.String
|
|
|| cur == "number" || cur == "boolean" || cur == "string") {
|
|
c[name] = this._ecsResolveMarker(cur)
|
|
resolved[name] <- c[name]
|
|
}
|
|
}
|
|
this._ecsCompDefaults[compClass] <- resolved
|
|
}
|
|
return c
|
|
}
|
|
|
|
function eve::_ecsCollectSlots(cls) {
|
|
if (cls in this._ecsSlotsCache) return this._ecsSlotsCache[cls]
|
|
// { fieldName = ComponentClass } walking base classes (Entity → … → cls).
|
|
local slots = {}
|
|
local cur = cls
|
|
local guard = 0
|
|
while (cur != null && guard < 64) {
|
|
if (!this.isSubclass(cur, this.Entity)) break
|
|
|
|
local declared = null
|
|
try { declared = cur.components } catch (e) { declared = null }
|
|
if (typeof declared == "table") {
|
|
foreach (k, v in declared) {
|
|
if (!(k in slots) && this._ecsIsComponentClass(v))
|
|
slots[k] <- v
|
|
}
|
|
}
|
|
|
|
local fields = this.inspectClassFields(cur)
|
|
foreach (k, v in fields) {
|
|
if (k == "components") continue
|
|
if (!(k in slots) && this._ecsIsComponentClass(v))
|
|
slots[k] <- v
|
|
}
|
|
|
|
cur = this.getClassBase(cur)
|
|
guard += 1
|
|
}
|
|
this._ecsSlotsCache[cls] <- slots
|
|
return slots
|
|
}
|
|
|
|
function eve::_ecsInvalidateViews(cls) {
|
|
local cur = cls
|
|
local guard = 0
|
|
while (cur != null && guard < 64) {
|
|
if (cur in this._ecsViewCache) delete this._ecsViewCache[cur]
|
|
if (!this.isSubclass(cur, this.Entity)) break
|
|
cur = this.getClassBase(cur)
|
|
guard += 1
|
|
}
|
|
}
|
|
|
|
function eve::_ecsEnsureType(cls) {
|
|
if (cls in this._ecsTypes) return this._ecsTypes[cls]
|
|
local info = { cls = cls, instances = [] }
|
|
this._ecsTypes[cls] <- info
|
|
return info
|
|
}
|
|
|
|
function eve::_ecsRegisterInstance(entity, cls) {
|
|
this._ecsEnsureType(cls).instances.push(entity)
|
|
this._ecsRevision += 1
|
|
this._ecsInvalidateViews(cls)
|
|
}
|
|
|
|
function eve::_ecsUnregisterInstance(entity, cls) {
|
|
if (!(cls in this._ecsTypes)) return
|
|
local arr = this._ecsTypes[cls].instances
|
|
for (local i = arr.len() - 1; i >= 0; --i) {
|
|
if (arr[i] == entity) {
|
|
// O(1) swap-remove(顺序不保证);视图缓存随后失效重建
|
|
local last = arr.len() - 1
|
|
if (i != last) arr[i] = arr[last]
|
|
arr.pop()
|
|
this._ecsRevision += 1
|
|
break
|
|
}
|
|
}
|
|
this._ecsInvalidateViews(cls)
|
|
}
|
|
|
|
function eve::_ecsCollectInstances(cls, out) {
|
|
foreach (key, info in this._ecsTypes) {
|
|
if (info.cls == cls || this.isSubclass(info.cls, cls)) {
|
|
foreach (e in info.instances) {
|
|
if (e != null && e.isAlive()) out.push(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
eve.Component <- class {
|
|
constructor() {}
|
|
}
|
|
|
|
eve.Entity <- class {
|
|
_eid = 0
|
|
_alive = false
|
|
_etype = null
|
|
_slots = null
|
|
_comps = null
|
|
|
|
constructor() {
|
|
_eid = 0
|
|
_alive = false
|
|
_etype = null
|
|
_slots = null
|
|
_comps = null
|
|
}
|
|
|
|
static function create() {
|
|
local e = this()
|
|
e._etype = this
|
|
e._eid = eve.allocEntityId()
|
|
e._alive = true
|
|
e._slots = eve._ecsCollectSlots(this)
|
|
e._comps = {}
|
|
foreach (name, compClass in e._slots) {
|
|
local c = eve._ecsInstantiateComponent(compClass)
|
|
e._comps[name] <- c
|
|
try { e[name] = c } catch (ex) {}
|
|
}
|
|
eve._ecsRegisterInstance(e, this)
|
|
return e
|
|
}
|
|
|
|
// Declare a component slot on this entity class (optional sugar).
|
|
static function component(name, compClass) {
|
|
if (!("components" in this) || typeof this.components != "table")
|
|
this.components <- {}
|
|
this.components[name] <- compClass
|
|
return this
|
|
}
|
|
|
|
function getId() { return _eid }
|
|
function isAlive() { return _alive }
|
|
|
|
function getComponent(compClass) {
|
|
if (_slots == null || _comps == null) return null
|
|
foreach (name, cls in _slots) {
|
|
if (cls == compClass) {
|
|
if (name in _comps) return _comps[name]
|
|
return null
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
function hasComponent(compClass) {
|
|
return getComponent(compClass) != null
|
|
}
|
|
|
|
function destroy() {
|
|
if (!_alive) return
|
|
_alive = false
|
|
if (_etype != null) eve._ecsUnregisterInstance(this, _etype)
|
|
}
|
|
}
|
|
|
|
// Historical name from early game examples
|
|
eve.EntityContainer <- eve.Entity
|
|
|
|
eve.System <- class {
|
|
_query = null
|
|
|
|
constructor(query = null) {
|
|
_query = query
|
|
}
|
|
|
|
function setQuery(query) { _query = query }
|
|
|
|
function entities() {
|
|
if (_query == null) return []
|
|
if (typeof _query == "array") {
|
|
local out = []
|
|
foreach (q in _query) {
|
|
foreach (e in eve.view(q)) out.push(e)
|
|
}
|
|
return out
|
|
}
|
|
return eve.view(_query)
|
|
}
|
|
|
|
function update(dt) {}
|
|
}
|
|
|
|
// GPU-backed System: pack float component fields → SSBO → compute shader → unpack.
|
|
// Requires eve.Gpgpu / eve.EcsShaderSystem (gpgpu module). Push constants:
|
|
// pc.data[0] = dt, pc.data[1] = entityCount. Each bindFields() maps one binding.
|
|
eve.ShaderSystem <- class extends eve.System {
|
|
_gpu = null
|
|
_backend = null
|
|
_bindings = null
|
|
_readback = true
|
|
_localSize = 64
|
|
|
|
constructor(query = null, gpu = null, glsl = null, localSize = 64) {
|
|
base.constructor(query)
|
|
_bindings = []
|
|
_readback = true
|
|
_localSize = localSize
|
|
if (gpu != null) setGpgpu(gpu)
|
|
if (gpu != null && glsl != null) setShaderSource(glsl)
|
|
}
|
|
|
|
function setGpgpu(gpu) {
|
|
_gpu = gpu
|
|
if (_backend == null) {
|
|
if (!("EcsShaderSystem" in eve))
|
|
throw "eve.ShaderSystem requires gpgpu module (eve.EcsShaderSystem)"
|
|
_backend = eve.EcsShaderSystem()
|
|
}
|
|
_backend.setGpgpu(gpu)
|
|
_backend.setLocalSize(_localSize)
|
|
return this
|
|
}
|
|
|
|
function setShaderSource(glsl) {
|
|
if (_backend == null) throw "eve.ShaderSystem.setShaderSource: call setGpgpu first"
|
|
_backend.setShaderSource(glsl)
|
|
return this
|
|
}
|
|
|
|
function setLocalSize(n) {
|
|
_localSize = n
|
|
if (_backend != null) _backend.setLocalSize(n)
|
|
return this
|
|
}
|
|
|
|
function setReadback(enabled) {
|
|
_readback = enabled
|
|
return this
|
|
}
|
|
|
|
// binding: SSBO index; slot: entity component field name; fields: ["x","y",...]
|
|
// uploadEachUpdate=false keeps the buffer GPU-resident after its initial upload.
|
|
// readback=false leaves results resident until requestReadback() is called.
|
|
function bindFields(binding, slot, fields, uploadEachUpdate = true, readback = null) {
|
|
_bindings.push({ binding = binding, slot = slot, fields = fields,
|
|
uploadEachUpdate = uploadEachUpdate,
|
|
readback = readback, uploadedRevision = -1,
|
|
uploadRequested = false, readbackRequested = false,
|
|
uploadFirst = 0, uploadCount = 0,
|
|
readbackFirst = 0, readbackCount = 0,
|
|
source = null, sourceBinding = -1,
|
|
initializeFromCpu = true, schema = null })
|
|
return this
|
|
}
|
|
|
|
// Typed binding schema: { slot, fields, scalar="f32", access="read_write" }.
|
|
// write-only bindings allocate storage without an unnecessary CPU upload.
|
|
function bindSchema(binding, schema, uploadEachUpdate = false, readback = false) {
|
|
if (typeof schema != "table" || !("slot" in schema) || !("fields" in schema))
|
|
throw "eve.ShaderSystem.bindSchema: schema requires slot and fields"
|
|
if (typeof schema.slot != "string" || typeof schema.fields != "array" ||
|
|
schema.fields.len() <= 0)
|
|
throw "eve.ShaderSystem.bindSchema: invalid slot or fields"
|
|
foreach (field in schema.fields)
|
|
if (typeof field != "string")
|
|
throw "eve.ShaderSystem.bindSchema: every field must be a string"
|
|
local scalar = "scalar" in schema ? schema.scalar : "f32"
|
|
if (scalar != "f32")
|
|
throw "eve.ShaderSystem.bindSchema: only f32 is currently supported"
|
|
local access = "access" in schema ? schema.access : "read_write"
|
|
if (access != "read" && access != "write" && access != "read_write")
|
|
throw "eve.ShaderSystem.bindSchema: access must be read, write, or read_write"
|
|
local normalized = { slot = schema.slot, fields = schema.fields,
|
|
scalar = scalar, access = access,
|
|
strideFloats = schema.fields.len() }
|
|
_bindings.push({ binding = binding, slot = schema.slot, fields = schema.fields,
|
|
uploadEachUpdate = access == "write" ? false : uploadEachUpdate,
|
|
readback = readback, uploadedRevision = -1,
|
|
uploadRequested = false, readbackRequested = false,
|
|
uploadFirst = 0, uploadCount = 0,
|
|
readbackFirst = 0, readbackCount = 0,
|
|
source = null, sourceBinding = -1,
|
|
initializeFromCpu = access != "write", schema = normalized })
|
|
return this
|
|
}
|
|
|
|
function getBindingSchema(binding) {
|
|
foreach (b in _bindings) if (b.binding == binding) return b.schema
|
|
return null
|
|
}
|
|
|
|
// Generate backend declaration boilerplate; the kernel body remains explicit.
|
|
function shaderDeclarations(language = "glsl") {
|
|
local out = ""
|
|
foreach (b in _bindings) {
|
|
if (b.schema == null) continue
|
|
if (language == "glsl") {
|
|
local qualifier = b.schema.access == "read" ? "readonly " :
|
|
b.schema.access == "write" ? "writeonly " : ""
|
|
out += "layout(set = 0, binding = " + b.binding + ") " + qualifier +
|
|
"buffer EcsBinding" + b.binding + " { float data[]; } ecs" +
|
|
b.binding + ";\n"
|
|
} else if (language == "wgsl") {
|
|
local access = b.schema.access == "read" ? "read" : "read_write"
|
|
out += "@group(0) @binding(" + b.binding + ") var<storage, " + access +
|
|
"> ecs" + b.binding + " : array<f32>;\n"
|
|
} else {
|
|
throw "eve.ShaderSystem.shaderDeclarations: language must be glsl or wgsl"
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Reuse another ShaderSystem's resident buffer without a CPU round trip.
|
|
// The source system is retained and must be prepared before this consumer.
|
|
function bindSharedFields(binding, sourceSystem, sourceBinding, slot, fields,
|
|
readback = false) {
|
|
if (sourceSystem == null)
|
|
throw "eve.ShaderSystem.bindSharedFields: source system is null"
|
|
_bindings.push({ binding = binding, slot = slot, fields = fields,
|
|
uploadEachUpdate = false, readback = readback,
|
|
uploadedRevision = -1, uploadRequested = false,
|
|
readbackRequested = false, uploadFirst = 0, uploadCount = 0,
|
|
readbackFirst = 0, readbackCount = 0,
|
|
source = sourceSystem, sourceBinding = sourceBinding,
|
|
initializeFromCpu = false, schema = null })
|
|
return this
|
|
}
|
|
|
|
function setBindingUpload(binding, enabled) {
|
|
foreach (b in _bindings) if (b.binding == binding) b.uploadEachUpdate = enabled
|
|
return this
|
|
}
|
|
|
|
function setBindingReadback(binding, enabled) {
|
|
foreach (b in _bindings) if (b.binding == binding) b.readback = enabled
|
|
return this
|
|
}
|
|
|
|
// Re-upload CPU component values on the next update. binding < 0 selects all.
|
|
function requestUpload(binding = -1) {
|
|
foreach (b in _bindings) {
|
|
if ((binding < 0 || b.binding == binding) && b.source == null) {
|
|
b.uploadRequested = true
|
|
b.uploadFirst = 0
|
|
b.uploadCount = -1
|
|
}
|
|
}
|
|
return this
|
|
}
|
|
|
|
// Re-upload a stable contiguous entity range. Repeated requests are coalesced.
|
|
function requestUploadRange(binding, firstEntity, entityCount) {
|
|
if (firstEntity < 0 || entityCount <= 0)
|
|
throw "eve.ShaderSystem.requestUploadRange: invalid entity range"
|
|
foreach (b in _bindings) {
|
|
if (b.binding != binding) continue
|
|
if (b.source != null)
|
|
throw "eve.ShaderSystem.requestUploadRange: shared binding is GPU-owned"
|
|
if (!b.uploadRequested || b.uploadCount == 0) {
|
|
b.uploadFirst = firstEntity
|
|
b.uploadCount = entityCount
|
|
} else if (b.uploadCount >= 0) {
|
|
local oldEnd = b.uploadFirst + b.uploadCount
|
|
local newEnd = firstEntity + entityCount
|
|
local end = oldEnd > newEnd ? oldEnd : newEnd
|
|
b.uploadFirst = b.uploadFirst < firstEntity ? b.uploadFirst : firstEntity
|
|
b.uploadCount = end - b.uploadFirst
|
|
}
|
|
b.uploadRequested = true
|
|
}
|
|
return this
|
|
}
|
|
|
|
// Read GPU values back after the next update. binding < 0 selects all.
|
|
function requestReadback(binding = -1) {
|
|
foreach (b in _bindings) {
|
|
if (binding < 0 || b.binding == binding) {
|
|
b.readbackRequested = true
|
|
b.readbackFirst = 0
|
|
b.readbackCount = -1
|
|
}
|
|
}
|
|
return this
|
|
}
|
|
|
|
// Read back a stable contiguous entity range. Repeated requests are coalesced.
|
|
function requestReadbackRange(binding, firstEntity, entityCount) {
|
|
if (firstEntity < 0 || entityCount <= 0)
|
|
throw "eve.ShaderSystem.requestReadbackRange: invalid entity range"
|
|
foreach (b in _bindings) {
|
|
if (b.binding != binding) continue
|
|
if (!b.readbackRequested || b.readbackCount == 0) {
|
|
b.readbackFirst = firstEntity
|
|
b.readbackCount = entityCount
|
|
} else if (b.readbackCount >= 0) {
|
|
local oldEnd = b.readbackFirst + b.readbackCount
|
|
local newEnd = firstEntity + entityCount
|
|
local end = oldEnd > newEnd ? oldEnd : newEnd
|
|
b.readbackFirst = b.readbackFirst < firstEntity ? b.readbackFirst : firstEntity
|
|
b.readbackCount = end - b.readbackFirst
|
|
}
|
|
b.readbackRequested = true
|
|
}
|
|
return this
|
|
}
|
|
|
|
function setFloat(index, value) {
|
|
if (_backend != null) _backend.setFloat(index, value)
|
|
return this
|
|
}
|
|
|
|
function getBackend() { return _backend }
|
|
|
|
// Register renderer resources and submit a standard 80-byte GpuInstance binding
|
|
// without copying the instance stream back through the CPU.
|
|
function setGpuDrivenEnabled(enabled) {
|
|
if (_gpu == null) throw "eve.ShaderSystem.setGpuDrivenEnabled: call setGpgpu first"
|
|
_gpu.setGpuDrivenEnabled(enabled)
|
|
return this
|
|
}
|
|
|
|
function gpuDrivenMeshRecord(mesh) {
|
|
if (_gpu == null) throw "eve.ShaderSystem.gpuDrivenMeshRecord: call setGpgpu first"
|
|
return _gpu.gpuDrivenMeshRecord(mesh)
|
|
}
|
|
|
|
function gpuDrivenMaterialRecord(material) {
|
|
if (_gpu == null)
|
|
throw "eve.ShaderSystem.gpuDrivenMaterialRecord: call setGpgpu first"
|
|
return _gpu.gpuDrivenMaterialRecord(material)
|
|
}
|
|
|
|
function submitResidentInstances(binding, buckets, instanceCount = -1,
|
|
offsetBytes = 0) {
|
|
if (_gpu == null || _backend == null)
|
|
throw "eve.ShaderSystem.submitResidentInstances: call setGpgpu first"
|
|
local buffer = _backend.getBuffer(binding)
|
|
if (buffer == null)
|
|
throw "eve.ShaderSystem.submitResidentInstances: binding has no buffer"
|
|
if (instanceCount < 0) instanceCount = entities().len()
|
|
return _gpu.submitResidentInstances(buffer, buckets, instanceCount, offsetBytes)
|
|
}
|
|
|
|
function _prepareGpuBatch() {
|
|
if (_backend == null || _gpu == null) return
|
|
if (!("packEcsFloats" in eve) || !("unpackEcsFloats" in eve)) return
|
|
|
|
local ents = entities()
|
|
local n = ents.len()
|
|
if (n <= 0) return null
|
|
|
|
foreach (b in _bindings) {
|
|
local floatsPer = b.fields.len()
|
|
if (floatsPer <= 0) continue
|
|
if (b.source != null) {
|
|
local sourceBackend = b.source.getBackend()
|
|
local sourceBuffer = sourceBackend == null ? null :
|
|
sourceBackend.getBuffer(b.sourceBinding)
|
|
if (sourceBuffer == null)
|
|
throw "eve.ShaderSystem shared source must be prepared before consumer"
|
|
if (sourceBuffer.getSize() < n * floatsPer * 4)
|
|
throw "eve.ShaderSystem shared buffer is smaller than consumer view"
|
|
_backend.attachBuffer(b.binding, sourceBuffer)
|
|
continue
|
|
}
|
|
local buf = _backend.ensureBuffer(b.binding, n * floatsPer)
|
|
local structureChanged = b.uploadedRevision != eve._ecsRevision
|
|
local fullUpload = (structureChanged && (b.initializeFromCpu || b.uploadRequested)) ||
|
|
b.uploadEachUpdate ||
|
|
(b.uploadRequested && b.uploadCount < 0)
|
|
if (fullUpload) {
|
|
eve.packEcsFloats(ents, b.slot, b.fields, buf)
|
|
b.uploadedRevision = eve._ecsRevision
|
|
b.uploadRequested = false
|
|
b.uploadCount = 0
|
|
} else if (structureChanged) {
|
|
// Output-only schema: capacity/order changed, but the shader is
|
|
// authoritative and promises to initialize every accessed record.
|
|
b.uploadedRevision = eve._ecsRevision
|
|
} else if (b.uploadRequested) {
|
|
if (!("packEcsFloatsRange" in eve))
|
|
throw "eve.ShaderSystem.requestUploadRange requires gpgpu range packing"
|
|
eve.packEcsFloatsRange(ents, b.slot, b.fields, buf,
|
|
b.uploadFirst, b.uploadCount)
|
|
b.uploadRequested = false
|
|
b.uploadCount = 0
|
|
}
|
|
}
|
|
return { entities = ents, count = n }
|
|
}
|
|
|
|
function _completeGpuBatch(batch) {
|
|
if (batch == null) return
|
|
local ents = batch.entities
|
|
local n = batch.count
|
|
foreach (b in _bindings) {
|
|
local shouldReadback = b.readback == null ? _readback : b.readback
|
|
if (shouldReadback || (b.readbackRequested && b.readbackCount < 0)) {
|
|
local floatsPer = b.fields.len()
|
|
if (floatsPer <= 0) continue
|
|
local buf = _backend.getBuffer(b.binding)
|
|
eve.unpackEcsFloats(ents, b.slot, b.fields, buf, n)
|
|
b.readbackRequested = false
|
|
b.readbackCount = 0
|
|
} else if (b.readbackRequested) {
|
|
if (!("unpackEcsFloatsRange" in eve))
|
|
throw "eve.ShaderSystem.requestReadbackRange requires gpgpu range unpacking"
|
|
local buf = _backend.getBuffer(b.binding)
|
|
eve.unpackEcsFloatsRange(ents, b.slot, b.fields, buf,
|
|
b.readbackFirst, b.readbackCount)
|
|
b.readbackRequested = false
|
|
b.readbackCount = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// Record into a shared GpuSequence. Call completeRecorded(batch) after submit().
|
|
function record(sequence, dt) {
|
|
if (sequence == null) throw "eve.ShaderSystem.record: sequence is null"
|
|
local batch = _prepareGpuBatch()
|
|
if (batch != null) _backend.recordDispatch(sequence, batch.count, dt)
|
|
return batch
|
|
}
|
|
|
|
function completeRecorded(batch) {
|
|
_completeGpuBatch(batch)
|
|
return this
|
|
}
|
|
|
|
function update(dt) {
|
|
local batch = _prepareGpuBatch()
|
|
if (batch == null) return
|
|
_backend.dispatch(batch.count, dt)
|
|
_completeGpuBatch(batch)
|
|
}
|
|
}
|
|
|
|
function eve::view(entityClass) {
|
|
if (entityClass == null) return []
|
|
// C++ 桥接类的实体在脚本外增删(无法通知脚本),不缓存,每次新鲜收集
|
|
local cppBacked = false
|
|
if ("_cppHasView" in this) cppBacked = this._cppHasView(entityClass)
|
|
if (!cppBacked && (entityClass in this._ecsViewCache)) return this._ecsViewCache[entityClass]
|
|
local out = []
|
|
this._ecsCollectInstances(entityClass, out)
|
|
if ("_cppCollect" in this) this._cppCollect(entityClass, out)
|
|
if (!cppBacked) this._ecsViewCache[entityClass] <- out
|
|
return out
|
|
}
|
|
|
|
function eve::ecsReady() {
|
|
// This function is installed only after the complete ECS script has run.
|
|
// Avoid probing the delegated eve table here: Squirrel's `in` lookup may
|
|
// invoke its lazy _get handler and turn a readiness query into an error.
|
|
return true
|
|
}
|
|
)SQ";
|
|
|
|
void injectEcsScript(ssq::Table& eveTable) {
|
|
HSQUIRRELVM vm = eveTable.getHandle();
|
|
const SQInteger top = sq_gettop(vm);
|
|
if (SQ_FAILED(sq_compilebuffer(vm, kEcsScript,
|
|
static_cast<SQInteger>(std::strlen(kEcsScript)),
|
|
"ecs.nut", SQTrue))) {
|
|
sq_settop(vm, top);
|
|
const script::ScriptErrorContext ctx = script::captureCompileError(vm);
|
|
std::fprintf(stderr, "EVEngine: ECS bootstrap script failed to compile: %s\n",
|
|
ctx.empty() ? "unknown error" : ctx.message.c_str());
|
|
return;
|
|
}
|
|
sq_pushroottable(vm);
|
|
if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) {
|
|
const script::ScriptErrorContext ctx = script::takeLastScriptError(vm);
|
|
std::fprintf(stderr, "EVEngine: ECS bootstrap script failed to run: %s\n",
|
|
ctx.empty() ? "unknown error"
|
|
: script::formatScriptError(ctx).c_str());
|
|
}
|
|
sq_settop(vm, top);
|
|
}
|
|
|
|
/**
|
|
* Script hook for eve.view(): walk the script class chain (cls → base → …) and,
|
|
* at the first class registered via registerCppEntityView(), invoke its collector
|
|
* to append the matching C++ entities to `out`.
|
|
*/
|
|
void cppCollect(ssq::Class cls, ssq::Array out) {
|
|
HSQUIRRELVM vm = cls.getHandle();
|
|
const SQInteger top = sq_gettop(vm);
|
|
sq_pushobject(vm, cls.getRaw());
|
|
for (int guard = 0; guard < 64; ++guard) {
|
|
if (sq_gettype(vm, -1) != OT_CLASS)
|
|
break;
|
|
HSQOBJECT cur;
|
|
sq_getstackobj(vm, -1, &cur);
|
|
auto it = cppEntityViews().find(cur._unVal.pClass);
|
|
if (it != cppEntityViews().end()) {
|
|
sq_settop(vm, top);
|
|
it->second(out);
|
|
return;
|
|
}
|
|
if (SQ_FAILED(sq_getbase(vm, -1)))
|
|
break;
|
|
sq_remove(vm, -2); // drop current class, keep base
|
|
}
|
|
sq_settop(vm, top);
|
|
}
|
|
|
|
/**
|
|
* Script hook for view-cache policy: returns true when any class on the chain
|
|
* (cls → base → …) is registered via registerCppEntityView(). C++ entities can
|
|
* appear/disappear outside script knowledge, so such views must not be cached.
|
|
*/
|
|
bool cppHasView(ssq::Object cls) {
|
|
if (cls.getType() != ssq::Type::CLASS) return false;
|
|
HSQUIRRELVM vm = cls.getHandle();
|
|
const SQInteger top = sq_gettop(vm);
|
|
sq_pushobject(vm, cls.getRaw());
|
|
for (int guard = 0; guard < 64; ++guard) {
|
|
if (sq_gettype(vm, -1) != OT_CLASS) break;
|
|
HSQOBJECT cur;
|
|
sq_getstackobj(vm, -1, &cur);
|
|
if (cppEntityViews().find(cur._unVal.pClass) != cppEntityViews().end()) {
|
|
sq_settop(vm, top);
|
|
return true;
|
|
}
|
|
if (SQ_FAILED(sq_getbase(vm, -1))) break;
|
|
sq_remove(vm, -2); // drop current class, keep base
|
|
}
|
|
sq_settop(vm, top);
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void exposeECS(ssq::Table& table) {
|
|
table.set("Number", std::string("number"));
|
|
table.set("Boolean", std::string("boolean"));
|
|
table.set("String", std::string("string"));
|
|
// ecsReady / view / Entity / Component are provided by the injected script.
|
|
|
|
table.addFunc("inspectClassFields", inspectClassFields);
|
|
table.addFunc("isSubclass", scriptIsSubclass);
|
|
table.addFunc("getClassBase", getClassBase);
|
|
table.addFunc("allocEntityId", allocEntityId);
|
|
table.addFunc("_cppHasView", cppHasView);
|
|
table.addFunc("component", [](std::string name, ssq::Object cls) {
|
|
registerScriptComponent(name, cls);
|
|
});
|
|
table.addFunc("_cppCollect", std::function<void(ssq::Class, ssq::Array)>(cppCollect));
|
|
|
|
injectEcsScript(table);
|
|
|
|
ecsScriptInjected() = true;
|
|
// A new VM re-runs every hook (SceneEntity class, …) against this table.
|
|
postEcsHooksFlushed() = 0;
|
|
flushPostEcsHooks(table);
|
|
}
|
|
|
|
void prepareEcsScriptInjection() {
|
|
// Class identities belong to one VM. Clear them when a new eve table is
|
|
// prepared, before any lazy/native exposer registers the next VM's classes.
|
|
cppEntityViews().clear();
|
|
ecsScriptInjected() = false;
|
|
postEcsHooksFlushed() = 0;
|
|
}
|
|
|
|
void registerCppEntityView(const ssq::Class& cls, CppEntityViewFn fn) {
|
|
cppEntityViews()[cls.getRaw()._unVal.pClass] = std::move(fn);
|
|
}
|
|
|
|
void registerPostEcsHook(PostEcsHook fn) {
|
|
postEcsHooks().push_back(std::move(fn));
|
|
}
|
|
|
|
void flushPostEcsHooks(ssq::Table& table) {
|
|
if (!ecsScriptInjected()) return;
|
|
auto& hooks = postEcsHooks();
|
|
auto& n = postEcsHooksFlushed();
|
|
while (n < hooks.size()) {
|
|
try {
|
|
hooks[n](table);
|
|
} catch (...) {
|
|
// A failing module hook must not break VM exposure.
|
|
}
|
|
++n;
|
|
}
|
|
}
|
|
|
|
void exposeECSToVM(ssq::VM& vm) {
|
|
try {
|
|
ssq::Table eveTable(vm.find("eve"));
|
|
exposeECS(eveTable);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
} // namespace eve
|