This commit is contained in:
Gaël committed 2026-10-19 20:59:15 +02:00
commit 87bff164f1
416 files changed
+279512

No files matched your search

+456
View File
@@ -0,0 +1,456 @@
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "lua/gpu_lua.h"
#include "lua/gpu_lua_profiles.h"
#include "gpu_lua_sources.h"
#include <shaderc/shaderc.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iomanip>
#include <map>
#include <memory>
#include <sstream>
#include <stdexcept>
// This target deliberately uses the exact vendored LuaJIT compiler/bytecode.
// No source is evaluated: luaL_loadbuffer only parses and creates a prototype.
extern "C" {
#include "lauxlib.h"
#include "lua.h"
}
#include "lua/gpu_lua_proto.h"
namespace ng::gpu_lua {
uint64_t fingerprint(const unsigned char* data, std::size_t size) {
uint64_t h=14695981039346656037ull;
for(std::size_t i=0;i<size;++i)if(data[i]!=13)h=(h^data[i])*1099511628211ull;
return h;
}
namespace {
const char* const library="data/scripts/lib/utilities.lua";
const std::map<std::string,unsigned> apis={
{"GetUpdatedEntityID",1},{"EntityGetTransform",2},{"EntityGetFirstComponent",3},
{"ComponentGetValue2",4},{"ComponentSetValue2",5},{"dofile_once",6},{"dofile",7},
{"RaytraceSurfaces",8},{"EntityApplyTransform",9},
{"ComponentGetValueVector2",4},{"ComponentSetValueVector2",5}};
const std::map<std::string,unsigned> math_apis={
{"abs",10},{"sqrt",11},{"atan2",12},{"cos",13},{"sin",14},{"max",15}};
using GCproto=NgLuaProto;
using BCIns=NgLuaInstruction;
static NgLuaOp bc_op(BCIns i){return i.op;}
static unsigned bc_a(BCIns i){return i.a;}
static unsigned bc_b(BCIns i){return i.b;}
static unsigned bc_c(BCIns i){return i.c;}
static unsigned bc_d(BCIns i){return i.d;}
static int bc_j(BCIns i){return i.jump;}
struct Instructions {GCproto* p;BCIns operator[](unsigned i){return ng_lua_instruction(p,i);}};
static Instructions proto_bc(GCproto* p){return {p};}
std::string number(double n) {
const float f=static_cast<float>(n);uint32_t bits;std::memcpy(&bits,&f,4);
std::ostringstream out;out<<"uintBitsToFloat(0x"<<std::hex<<bits<<"u)";return out.str();
}
struct Parsed {
std::unique_ptr<lua_State,decltype(&lua_close)> state{luaL_newstate(),lua_close};
GCproto view{};GCproto* pt=&view;
explicit Parsed(const std::string& source,const std::string& path) {
if(!state)throw std::runtime_error("Lua GPU: cannot allocate parser");
if(source.size()>1024*1024)throw std::runtime_error(path+": Lua GPU source limit exceeded");
if(!source.empty()&&static_cast<unsigned char>(source[0])==0x1b)throw std::runtime_error(path+": Lua source required, bytecode input rejected");
if(luaL_loadbuffer(state.get(),source.data(),source.size(),path.c_str())!=0)
throw std::runtime_error(lua_tostring(state.get(),-1));
// Check the linked VM's frame ABI before dereferencing private structures.
std::vector<unsigned char> dump;
lua_dump(state.get(),[](lua_State*,const void* p,size_t n,void* dst)->int {
auto& b=*static_cast<std::vector<unsigned char>*>(dst);
b.insert(b.end(),static_cast<const unsigned char*>(p),static_cast<const unsigned char*>(p)+n);return 0;
},&dump);
if(dump.size()<5||dump[3]!=2||bool(dump[4]&8)!=bool(ng_lua_frame_shift()))throw std::runtime_error("Lua GPU: linked LuaJIT bytecode ABI mismatch");
if(!ng_lua_view(state.get(),pt))throw std::runtime_error("Lua GPU: expected Lua prototype");
}
};
uint64_t compiled_fingerprint(Parsed& parsed) {
uint64_t digest=14695981039346656037ull;
const int status=ng_lua_dump_stripped(parsed.state.get(),parsed.pt,
[](lua_State*,const void* bytes,size_t size,void* context)->int {
auto& hash=*static_cast<uint64_t*>(context);
const auto* data=static_cast<const unsigned char*>(bytes);
// Bytecode is binary: CR bytes must participate in this digest.
for(size_t i=0;i<size;++i)hash=(hash^data[i])*1099511628211ull;
return 0;
},&digest);
if(status)throw std::runtime_error("Lua GPU: cannot fingerprint compiled library");
return digest;
}
unsigned line(GCproto* pt,unsigned ip){return ng_lua_instruction(pt,ip).line;}
std::string str(GCproto* pt,unsigned d) {
size_t length=0;const char* bytes=ng_lua_string(pt,d,&length);
if(!bytes)throw std::runtime_error("Lua GPU: invalid string constant");
std::string result(bytes,length);
if(std::any_of(result.begin(),result.end(),[](unsigned char c){return c>=128;}))throw std::runtime_error("Lua GPU: ASCII strings required");
return result;
}
double numeric(GCproto* pt,unsigned d) {
if(d>=pt->sizekn)throw std::runtime_error("Lua GPU: invalid numeric constant");
return ng_lua_number(pt,d);
}
struct Generator {
std::string path;
Parsed root;
std::map<std::string,unsigned> symbols;
std::map<std::string,std::string> exports;
std::map<std::string,unsigned> globals;
uint64_t library_semantic = 0;
explicit Generator(const std::string& source,const std::string& file):path(file),root(source,file){}
unsigned intern(const std::string& s) {
auto [it,added]=symbols.emplace(s,static_cast<unsigned>(symbols.size()+1));return it->second;
}
std::string literal(GCproto* p,BCIns ins) {
const auto op=bc_op(ins);const unsigned d=bc_d(ins);
if(op==NG_BC_KSHORT)return "ngp_num("+number(static_cast<int16_t>(d))+")";
if(op==NG_BC_KNUM)return "ngp_num("+number(numeric(p,d))+")";
if(op==NG_BC_KSTR)return "ngp_string("+std::to_string(intern(str(p,d)))+"u)";
if(op==NG_BC_KPRI&&d<=2)return d==0?"ngp_nil()":d==1?"ngp_bool(false)":"ngp_bool(true)";
throw std::runtime_error("Lua GPU: unsupported literal");
}
void read_exports(const std::string& source) {
// Reviewed utilities: top-level constants precede function/table values.
// Those unread inert definitions are omitted; function bodies never run.
Parsed lib(source,library);library_semantic=compiled_fingerprint(lib);
// Both reviewed distributions have the same constants / edit_component
// behavior. Steam additionally defines unused debug/vector/color/texture
// helpers. Comments, source paths and line information are not behavior.
if(library_semantic!=6066579123591393726ull && library_semantic!=13023294535491161211ull)
throw std::runtime_error(std::string(library)+": unreviewed compiled library (fingerprint "+std::to_string(library_semantic)+")");
std::map<unsigned,std::string> values;
for(unsigned i=1;i<lib.pt->sizebc;++i){
BCIns ins=proto_bc(lib.pt)[i];auto op=bc_op(ins);unsigned a=bc_a(ins),d=bc_d(ins);
if(op==NG_BC_FNEW)break;
if(op==NG_BC_KSHORT||op==NG_BC_KNUM||op==NG_BC_KPRI||op==NG_BC_KSTR)values[a]=literal(lib.pt,ins);
else if(op==NG_BC_GSET&&values.count(a))exports[str(lib.pt,d)]=values.at(a);
else throw std::runtime_error(std::string(library)+": unsupported library initializer");
}
}
std::string body() {
GCproto* p=root.pt;
if(p->sizebc>1024||p->framesize>64||p->sizeuv)throw std::runtime_error("Lua GPU: target limit exceeded (1024 instructions, 64 registers, no upvalues)");
auto reg=[&](unsigned r){if(r>=p->framesize)throw std::runtime_error("Lua GPU: register range exceeded");return "ngp_r["+std::to_string(r)+"]";};
std::ostringstream cases;
for(unsigned ip=1;ip<p->sizebc;++ip){
const BCIns ins=proto_bc(p)[ip];const auto op=bc_op(ins);
const unsigned a=bc_a(ins),b=bc_b(ins),c=bc_c(ins),d=bc_d(ins);
auto fail=[&](const std::string& why){throw std::runtime_error(path+":"+std::to_string(line(p,ip))+": "+why+" ("+ins.name+")");};
cases<<"case "<<ip<<": {ngp_line="<<line(p,ip)<<"u;ngp_ip="<<ip+1<<";";
auto test=[&](const std::string& cond){
if(ip+1>=p->sizebc||bc_op(proto_bc(p)[ip+1])!=NG_BC_JMP)fail("expected conditional jump");
cases<<"if(!("<<cond<<"))ngp_ip++;";
};
if(op==NG_BC_KSHORT||op==NG_BC_KNUM||op==NG_BC_KPRI||op==NG_BC_KSTR)cases<<reg(a)<<"="<<literal(p,ins)<<";";
else if(op==NG_BC_MOV)cases<<reg(a)<<"="<<reg(d)<<";";
else if(op==NG_BC_KNIL){reg(d);for(unsigned k=a;k<=d;k++)cases<<reg(k)<<"=ngp_nil();";}
else if(op==NG_BC_NOT)cases<<reg(a)<<"=ngp_bool(!ngp_truth("<<reg(d)<<"));";
else if(op==NG_BC_UNM)cases<<reg(a)<<"=ngp_num(-ngp_number("<<reg(d)<<"));";
else if(op==NG_BC_GGET){
const std::string name=str(p,d);const auto api=apis.find(name);
if(api!=apis.end())cases<<reg(a)<<"=NgpValue(4u,0.0,"<<api->second<<"u);";
else if(name=="math")cases<<reg(a)<<"=NgpValue(5u,0.0,1u);";
else if(exports.count(name)){
auto [it,added]=globals.emplace(name,static_cast<unsigned>(globals.size()));
cases<<reg(a)<<"=ngp_g["<<it->second<<"];";
}else fail("unsupported global/API '"+name+"'");
}else if(op==NG_BC_TGETS){
const auto field=str(p,c);const auto api=math_apis.find(field);
if(api==math_apis.end())fail("unsupported math field '"+field+"'");
cases<<"if("<<reg(b)<<".tag!=5u)ngp_fault(3u);"<<reg(a)<<"=NgpValue(4u,0.0,"<<api->second<<"u);";
}else if(op==NG_BC_CALL){
if(b==0||c==0||b>9||c>9)fail("only fixed calls with up to eight arguments/results are supported");
cases<<"NgpTuple args=ngp_empty();args.count="<<c-1<<";";
for(unsigned k=0;k<c-1;k++)cases<<"args.v["<<k<<"]="<<reg(a+1+ng_lua_frame_shift()+k)<<";";
cases<<"NgpTuple result=ngp_call("<<reg(a)<<",args);";
for(unsigned k=0;k<b-1;k++)cases<<reg(a+k)<<"=result.v["<<k<<"];";
}else if(op==NG_BC_JMP||op==NG_BC_UCLO){
const int target=static_cast<int>(ip)+1+static_cast<int>(bc_j(ins));
if(target<1||target>=static_cast<int>(p->sizebc))fail("jump target out of range");
cases<<"ngp_ip="<<target<<";";
}else if(op==NG_BC_RET0||op==NG_BC_RET1||op==NG_BC_RET)cases<<"projectile=ngp_proj;return ngp_error;";
else if(op==NG_BC_LOOP){} // Marker only; backedges are ordinary JMP.
else if(op>=NG_BC_ISLT&&op<=NG_BC_ISGT){
std::string cond="ngp_less("+reg(a)+","+reg(d)+","+((op==NG_BC_ISLE||op==NG_BC_ISGT)?"true":"false")+")";
// LuaJIT GE/GT are the complement tests, including unordered NaN.
if(op==NG_BC_ISGE||op==NG_BC_ISGT)cond="!"+cond;
test(cond);
}else if(op>=NG_BC_ISEQV&&op<=NG_BC_ISNEP){
std::string rhs;
if(op<=NG_BC_ISNEV)rhs=reg(d);
else if(op<=NG_BC_ISNES)rhs="ngp_string("+std::to_string(intern(str(p,d)))+"u)";
else if(op<=NG_BC_ISNEN)rhs="ngp_num("+number(numeric(p,d))+")";
else {if(d>2)fail("invalid primitive");rhs=d==0?"ngp_nil()":d==1?"ngp_bool(false)":"ngp_bool(true)";}
const bool unequal=(static_cast<int>(op)-NG_BC_ISEQV)%2!=0;
test(std::string(unequal?"!":"")+"ngp_equal("+reg(a)+","+rhs+")");
}else if(op==NG_BC_IST||op==NG_BC_ISF||op==NG_BC_ISTC||op==NG_BC_ISFC){
const bool truth=op==NG_BC_IST||op==NG_BC_ISTC;
std::string cond=std::string(truth?"":"!")+"ngp_truth("+reg(d)+")";
if(op==NG_BC_ISTC||op==NG_BC_ISFC)cases<<"if("<<cond<<")"<<reg(a)<<"="<<reg(d)<<";";
test(cond);
}else if(op>=NG_BC_ADDVN&&op<=NG_BC_POW){
const int index=static_cast<int>(op)-NG_BC_ADDVN;
const int arithmetic=op==NG_BC_POW?5:index%5;
std::string left,right;
if(index<5){left=reg(b);right="ngp_num("+number(numeric(p,c))+")";}
else if(index<10){left="ngp_num("+number(numeric(p,c))+")";right=reg(b);}
else {left=reg(b);right=reg(c);}
cases<<reg(a)<<"=ngp_arithmetic("<<arithmetic<<","<<left<<","<<right<<");";
}else fail("unsupported Lua GPU instruction");
cases<<"break;}\n";
}
std::ostringstream out;
out<<"NgpValue ngp_g["<<std::max<std::size_t>(1,globals.size())<<"];\nvoid ngp_module_init(){";
for(const auto& [name,index]:globals)out<<"ngp_g["<<index<<"]="<<exports.at(name)<<";";
out<<"}\nuvec4 ngp_execute(inout FProj projectile,vec3 owner,float owner_id){\n"
"ngp_proj=projectile;ngp_owner=owner;ngp_owner_id=owner_id;ngp_failed=false;ngp_loaded=false;ngp_error=uvec4(0u);ngp_line=0u;\n"
"for(int i=0;i<ngp_g.length();i++)ngp_g[i]=ngp_nil();\nNgpValue ngp_r["<<unsigned(p->framesize)<<"];\n"
"for(int i=0;i<ngp_r.length();i++)ngp_r[i]=ngp_nil();int ngp_ip=1;\n"
"for(int budget=0;budget<4096&&!ngp_failed;budget++){switch(ngp_ip){\n"<<cases.str()<<
"default:ngp_fault(3u);break;}}\nif(!ngp_failed)ngp_fault(4u);projectile=ngp_proj;return ngp_error;}\n";
return out.str();
}
std::string preamble() {
std::ostringstream out;
for(const auto& [label,value]:std::map<std::string,std::string>{{"PROJECTILE","ProjectileComponent"},{"VELOCITY","VelocityComponent"},{"WHO","mWhoShot"},{"GRAVITY","gravity_y"},{"MOTION","mVelocity"},{"UTIL",library}})
out<<"const uint NGP_"<<label<<"="<<intern(value)<<"u;\n";
out<<"uint ngp_rank(uint s){switch(s){";
unsigned rank=0;for(const auto& [s,id]:symbols)out<<"case "<<id<<"u:return "<<rank++<<"u;";
out<<"default:return 0u;}}\nbool ngp_parse_number(uint s,out float n){n=0.0;switch(s){";
for(const auto& [s,id]:symbols){
lua_pushlstring(root.state.get(),s.data(),s.size());
if(lua_isnumber(root.state.get(),-1))out<<"case "<<id<<"u:n="<<number(lua_tonumber(root.state.get(),-1))<<";return true;";
lua_pop(root.state.get(),1);
}
out<<"default:return false;}}\nvoid ngp_module_init();\n";return out.str();
}
std::string scalar_body() {
// Typed, acyclic SSA lowering for fused additive neighbor bodies. There
// is no register interpreter or dynamic API dispatch in the inner loop.
struct Value {int kind=0;std::string expr;unsigned api=0;double constant=NAN;};
enum {Number=1,Self,Velocity,Function,String,Math};
auto* p=root.pt;std::vector<Value> r(p->framesize);std::ostringstream out;
out<<"uvec4 ngp_execute(inout FProj projectile,vec3 owner,float owner_id){\n";
auto fail=[&](unsigned ip,const std::string& why){throw std::runtime_error(path+":"+std::to_string(line(p,ip))+": scalar neighbor lowering: "+why);};
auto numeric_value=[&](const Value& v,unsigned ip){if(v.kind!=Number)fail(ip,"numeric value required");return v.expr;};
auto temp=[&](unsigned reg,unsigned ip,const std::string& expr){
const auto name="v"+std::to_string(ip)+"_"+std::to_string(reg);
out<<"float "<<name<<"="<<expr<<";\n";r.at(reg)={Number,name};
};
for(unsigned ip=1;ip<p->sizebc;++ip) {
const auto ins=proto_bc(p)[ip];const auto op=bc_op(ins);unsigned a=bc_a(ins),b=bc_b(ins),c=bc_c(ins),d=bc_d(ins);
if(op==NG_BC_KNUM||op==NG_BC_KSHORT){double n=op==NG_BC_KNUM?numeric(p,d):static_cast<int16_t>(d);r.at(a)={Number,number(n),0,n};}
else if(op==NG_BC_KSTR)r.at(a)={String,str(p,d)};
else if(op==NG_BC_MOV)r.at(a)=r.at(d);
else if(op==NG_BC_GGET) {
auto name=str(p,d);auto api=apis.find(name);
if(name=="math")r.at(a)={Math};
else if(api!=apis.end())r.at(a)={Function,"",api->second};
else fail(ip,"unknown global "+name);
}else if(op==NG_BC_TGETS) {
auto api=math_apis.find(str(p,c));
if(r.at(b).kind!=Math||api==math_apis.end())fail(ip,"unsupported table access");
r.at(a)={Function,"",api->second};
}else if(op>=NG_BC_ADDVN&&op<=NG_BC_POW) {
int index=int(op)-NG_BC_ADDVN, arithmetic=op==NG_BC_POW?5:index%5;
Value left,right;
if(index<5){left=r.at(b);double n=numeric(p,c);right={Number,number(n),0,n};}
else if(index<10){double n=numeric(p,c);left={Number,number(n),0,n};right=r.at(b);}
else{left=r.at(b);right=r.at(c);}
auto x=numeric_value(left,ip),y=numeric_value(right,ip);
if(arithmetic==5){if(right.constant!=2.0)fail(ip,"only square powers are reviewed");temp(a,ip,"("+x+"*"+x+")");}
else if(arithmetic<4)temp(a,ip,"("+x+std::string(1,"+-*/"[arithmetic])+y+")");
else fail(ip,"unreviewed arithmetic");
}else if(op==NG_BC_UNM)temp(a,ip,"(-"+numeric_value(r.at(d),ip)+")");
else if(op>=NG_BC_ISLT&&op<=NG_BC_ISGT) {
if(ip+1>=p->sizebc||bc_op(proto_bc(p)[ip+1])!=NG_BC_JMP)fail(ip,"conditional jump required");
auto jump=proto_bc(p)[ip+1];int target=int(ip)+2+bc_j(jump);
if(target<=int(ip)||target>=int(p->sizebc)||bc_op(proto_bc(p)[target])!=NG_BC_RET0)fail(ip,"only terminal forward guards are reviewed");
auto x=numeric_value(r.at(a),ip),y=numeric_value(r.at(d),ip);
std::string condition="("+x+((op==NG_BC_ISLE||op==NG_BC_ISGT)?"<=":"<")+y+")";
if(op==NG_BC_ISGE||op==NG_BC_ISGT)condition="!"+condition;
out<<"if("<<condition<<")return uvec4(0u);\n";++ip;
}else if(op==NG_BC_CALL) {
if(r.at(a).kind!=Function||b==0||c==0)fail(ip,"fixed native call required");
std::vector<Value> args;for(unsigned k=0;k<c-1;++k)args.push_back(r.at(a+1+ng_lua_frame_shift()+k));
auto count=[&](unsigned n){if(args.size()!=n)fail(ip,"argument count mismatch");};
auto num=[&](unsigned n){return numeric_value(args.at(n),ip);};
auto api=r.at(a).api;
if(api==1){count(0);r.at(a)={Self};}
else if(api==2){
count(1);std::string source;
if(args[0].kind==Self)source="projectile.a";
else if(args[0].kind==Number&&args[0].constant==1.0)source="owner";
else fail(ip,"unreviewed entity handle");
if(b!=3)fail(ip,"two transform results required");
temp(a,ip,source+".x");temp(a+1,ip,source+".y");
}else if(api==3){count(2);if(args[0].kind!=Self||args[1].kind!=String||args[1].expr!="VelocityComponent")fail(ip,"velocity component required");r.at(a)={Velocity};}
else if(api==4||api==5){
count(api==4?2:4);
if(args[0].kind!=Velocity||args[1].kind!=String||args[1].expr!="mVelocity")fail(ip,"velocity vector required");
if(api==4){if(b!=3)fail(ip,"two velocity results required");temp(a,ip,"projectile.a.z*60.0");temp(a+1,ip,"projectile.a.w*60.0");}
else {if(b!=1)fail(ip,"write must have no results");out<<"projectile.a.zw=vec2("<<num(2)<<","<<num(3)<<")/60.0;\n";}
}else if(api>=10&&api<=15){
count(api==12||api==15?2:1);if(b!=2)fail(ip,"one math result required");
const char* names[]={"abs","sqrt","ngp_scalar_atan","cos","sin","max"};
temp(a,ip,std::string(names[api-10])+"("+num(0)+(args.size()==2?","+num(1):"")+")");
}else fail(ip,"unreviewed native API");
}else if(op==NG_BC_RET0)out<<"return uvec4(0u);\n";
else fail(ip,std::string("unsupported instruction ")+ins.name);
}
out<<"}\n";
return "float ngp_scalar_atan(float y,float x){float a;if(y==0.0)a=(floatBitsToUint(x)&0x80000000u)!=0u?3.141592653589793:0.0;else if(x==0.0)a=1.5707963267948966;else a=atan(abs(y),x);return (floatBitsToUint(y)&0x80000000u)!=0u?-a:a;}\n"+out.str();
}
};
}
uint64_t library_fingerprint(const std::string& source) {
Parsed parsed(source,library);return compiled_fingerprint(parsed);
}
bool dependency_matches(const Dependency& dep,const unsigned char* data,std::size_t size) {
const auto raw=fingerprint(data,size);
if(raw==dep.fingerprint||(dep.semantic!=0&&raw==dep.validated_fingerprint))return true;
if(dep.semantic==0)return false;
try {
if(library_fingerprint(std::string(reinterpret_cast<const char*>(data),size))!=dep.semantic)return false;
dep.validated_fingerprint=raw;return true;
}catch(const std::exception&){return false;}
}
Program translate(const std::string& source,const Reader& read,const std::string& path) {
Generator gen(source,path);const std::string lib=read(library);gen.read_exports(lib);
Program p;
if(program_id(path)>=first_neighbor_program)p.function=gen.scalar_body();
else {
p.function=gen.body(); // Discover symbols/globals before constructing helpers.
p.function=gen.preamble()+sources::runtime+p.function;
}
p.shader=std::string(sources::layout)+sources::terrain+p.function+
"uvec4 ngp_dispatch(uint program,inout FProj p,vec3 owner,float owner_id){if(program!=1u)return uvec4(14u,0u,0u,0u);return ngp_execute(p,owner,owner_id);}\n"
"uvec4 ngp_neighbor_dispatch(uint program,inout FProj p,vec3 owner){return uvec4(14u,0u,0u,0u);}\n"+sources::dispatch;
p.instructions=gen.root.pt->sizebc-1;p.registers=gen.root.pt->framesize;
for(const auto& [path,bytes]:std::map<std::string,std::string>{{path,source},{library,lib}})
p.dependencies.push_back({path,fingerprint(reinterpret_cast<const unsigned char*>(bytes.data()),bytes.size())});
for(auto& dep:p.dependencies)if(dep.path==library)dep.semantic=gen.library_semantic;
return p;
}
Program translate_reviewed(const Reader& read) {
Program bundle;std::map<std::string,Dependency> dependencies;
std::ostringstream dispatch,neighbors;
dispatch<<"uvec4 ngp_dispatch(uint program,inout FProj p,vec3 owner,float owner_id){switch(program){";
neighbors<<"uvec4 ngp_neighbor_dispatch(uint program,inout FProj p,vec3 owner){switch(program){";
unsigned id=0;
for(const auto& profile:profiles) {
++id;const auto source=read(profile.path);
if(fingerprint(reinterpret_cast<const unsigned char*>(source.data()),source.size())!=profile.fingerprint)
throw std::runtime_error(std::string(profile.path)+": modified projectile needs engine binding review");
std::string lowered=source;
if(id>=first_neighbor_program) {
// Fuse this reviewed additive neighbor loop into a per-target kernel.
// Preserve the original arithmetic, both distance guards and constants.
// Query/iteration/edit_component plumbing becomes GPU gather dispatch.
std::vector<std::string> lines;std::istringstream input(source);std::string line;
while(std::getline(input,line)) {
line.erase(std::remove(line.begin(),line.end(),'\r'),line.end());
auto start=line.find_first_not_of(" \t");
if(start!=std::string::npos)lines.push_back(line.substr(start));
}
auto find=[&](const std::string& prefix) {
std::string result;unsigned count=0;
for(const auto& candidate:lines)if(candidate.rfind(prefix,0)==0){result=candidate;++count;}
if(count!=1)throw std::runtime_error(std::string(profile.path)+": neighbor lowering shape changed: "+prefix);
return result;
};
std::ostringstream pair;
if(std::string(profile.path)=="data/scripts/projectiles/effect_slow_bullets.lua") {
pair<<"local comp=EntityGetFirstComponent(GetUpdatedEntityID(), 'VelocityComponent')\n"
"local vel_x,vel_y=ComponentGetValueVector2(comp,'mVelocity')\n"
<<find("vel_x = vel_x *")<<'\n'<<find("vel_y = vel_y *")<<'\n'
<<"ComponentSetValueVector2(comp,'mVelocity',vel_x,vel_y)\n";
} else {
pair<<"local px,py=EntityGetTransform((GetUpdatedEntityID()))\nlocal x,y=EntityGetTransform(1)\n"
<<find("local distance =")<<'\n'<<find("local distance_full =")<<'\n';
auto diamond=find("if ( distance < distance_full *");auto end=diamond.find(" and ");
if(end==std::string::npos)throw std::runtime_error("neighbor diamond guard missing");
pair<<diamond.substr(0,end)<<" then\n"<<find("distance = math.sqrt")<<"\nlocal "<<find("direction =")<<'\n'
<<find("if ( distance < distance_full )")<<'\n'
<<find("local gravity_percent =")<<'\n'<<find("local gravity_coeff =")<<'\n'
<<"local comp=EntityGetFirstComponent(GetUpdatedEntityID(), 'VelocityComponent')\n"
"local vel_x,vel_y=ComponentGetValueVector2(comp,'mVelocity')\n"
<<find("local offset_x =")<<'\n'<<find("local offset_y =")<<'\n'
<<find("vel_x = vel_x +")<<'\n'<<find("vel_y = vel_y +")<<'\n'
<<"ComponentSetValueVector2(comp,'mVelocity',vel_x,vel_y)\nend\nend\n";
}
lowered=pair.str();
}
auto part=translate(lowered,read,profile.path);
// The dependency is always the original asset, never the fused fragment.
for(auto& dep:part.dependencies)if(dep.path==profile.path)dep.fingerprint=profile.fingerprint;
// Each module owns its constants, registers and dofile_once globals.
// Only the terrain query is shared; it carries no invocation state.
for(const std::string prefix:{"ngp_","NGP_"}) {
const std::string replacement=prefix+std::to_string(id)+"_";
size_t at=0;
while((at=part.function.find(prefix,at))!=std::string::npos) {
if(part.function.compare(at,11,"ngp_surface")==0){at+=11;continue;}
part.function.replace(at,prefix.size(),replacement);at+=replacement.size();
}
}
for(const std::string type:{"NgpValue","NgpTuple"}) {
size_t at=0;const auto replacement=type+std::to_string(id);
while((at=part.function.find(type,at))!=std::string::npos) {
part.function.replace(at,type.size(),replacement);at+=replacement.size();
}
}
bundle.function+=part.function;
bundle.instructions+=part.instructions;bundle.registers=std::max(bundle.registers,part.registers);
for(const auto& dep:part.dependencies)dependencies[dep.path]=dep;
dispatch<<"case "<<id<<"u:return ngp_"<<id<<"_execute(p,owner,owner_id);";
if(id>=first_neighbor_program)neighbors<<"case "<<id<<"u:return ngp_"<<id<<"_execute(p,owner,1.0);";
}
dispatch<<"default:return uvec4(14u,0u,0u,0u);}}\n";
neighbors<<"default:return uvec4(14u,0u,0u,0u);}}\n";
bundle.function+=dispatch.str()+neighbors.str();
bundle.shader=std::string(sources::layout)+sources::terrain+bundle.function+sources::dispatch;
for(const auto& [path,dep]:dependencies)bundle.dependencies.push_back(dep);
return bundle;
}
std::vector<uint32_t> compile_spirv(const std::string& source,const std::filesystem::path& compiler_dll) {
struct Dll {HMODULE module;~Dll(){if(module)FreeLibrary(module);}} dll{
LoadLibraryExW(std::filesystem::absolute(compiler_dll).c_str(),nullptr,LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR|LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)};
if(!dll.module)throw std::runtime_error("Lua GPU: cannot load native compiler "+compiler_dll.string()+" (Windows error "+std::to_string(GetLastError())+")");
auto get=[&](const char* name){auto f=GetProcAddress(dll.module,name);if(!f)throw std::runtime_error(std::string("Lua GPU: missing compiler symbol ")+name);return f;};
#define NG_SHADERC(name) const auto name##_fn=reinterpret_cast<decltype(&name)>(get(#name))
NG_SHADERC(shaderc_compiler_initialize);NG_SHADERC(shaderc_compiler_release);
NG_SHADERC(shaderc_compile_options_initialize);NG_SHADERC(shaderc_compile_options_release);
NG_SHADERC(shaderc_compile_options_set_target_env);NG_SHADERC(shaderc_compile_options_set_optimization_level);
NG_SHADERC(shaderc_compile_into_spv);NG_SHADERC(shaderc_result_release);
NG_SHADERC(shaderc_result_get_compilation_status);NG_SHADERC(shaderc_result_get_error_message);
NG_SHADERC(shaderc_result_get_length);NG_SHADERC(shaderc_result_get_bytes);
#undef NG_SHADERC
std::unique_ptr<shaderc_compiler,decltype(shaderc_compiler_release_fn)> compiler(shaderc_compiler_initialize_fn(),shaderc_compiler_release_fn);
std::unique_ptr<shaderc_compile_options,decltype(shaderc_compile_options_release_fn)> options(shaderc_compile_options_initialize_fn(),shaderc_compile_options_release_fn);
if(!compiler||!options)throw std::runtime_error("Lua GPU: native compiler allocation failed");
shaderc_compile_options_set_target_env_fn(options.get(),shaderc_target_env_vulkan,shaderc_env_version_vulkan_1_1);
// Keep optimization in the Vulkan driver. The SDK optimizer expands this
// bounded-register control flow heavily and crashes for larger VM bundles.
// Straight-line neighbor kernels avoid the VM entirely.
shaderc_compile_options_set_optimization_level_fn(options.get(),shaderc_optimization_level_zero);
std::unique_ptr<shaderc_compilation_result,decltype(shaderc_result_release_fn)> result(
shaderc_compile_into_spv_fn(compiler.get(),source.data(),source.size(),shaderc_compute_shader,script,"main",options.get()),shaderc_result_release_fn);
if(!result)throw std::runtime_error("Lua GPU: shader compiler returned no result");
if(shaderc_result_get_compilation_status_fn(result.get())!=shaderc_compilation_status_success)
throw std::runtime_error(std::string("Lua GPU GLSL: ")+shaderc_result_get_error_message_fn(result.get()));
const auto bytes=shaderc_result_get_length_fn(result.get());
if(bytes%4||bytes<20)throw std::runtime_error("Lua GPU: invalid SPIR-V output");
std::vector<uint32_t> code(bytes/4);std::memcpy(code.data(),shaderc_result_get_bytes_fn(result.get()),bytes);return code;
}
}