code wiki / (root) / nx_shader_ir_front_candidate_t217.nx

nx_shader_ir_front_candidate_t217.nx source

↩ module page · 599 lines · 35417 B

1// nx_shader_ir.nx -- THE SHADER IR: ONE source of truth, N dialect backends. 2// 3// WHY THIS EXISTS (measured 2026-08-23). The estate's shaders are hand-written GLSL string 4// literals inside nx_game_page_emit. Drawing characters on the WebGPU tier therefore demanded a 5// SECOND HAND COPY in WGSL -- ~600 lines (dual-quaternion skinning off a joint texture, instance 6// uniform arrays, the voxel occlusion march, the procedural anatomy/eye/hair/skin paint) of the 7// most actively-edited shader in the estate: three separate feature edits landed in it on the day 8// this was written. Two artifacts that must agree, kept in agreement by discipline, is the exact 9// shape the banked law forbids. This module is the construction that makes disagreement 10// IMPOSSIBLE: neither dialect is authored, both are EMITTED from this representation. 11// 12// HOUSE SHAPE. This mirrors the NishiLang compiler itself -- lex_source -> parse_module -> 13// opt_run -> {nx_compile_x86, nx_compile_wat}: one front-end representation, many backends. 14// nx_glsl.nx and nx_wgsl.nx are backends over this IR, not transpilers bolted to a call site. 15// 16// THE SOURCE OF TRUTH IS NISHILANG (operator, 2026-08-23: "we dont want python or glsl or 17// javascript or all that it should be nishi lang and nishi ecosystem from the first byte up each 18// rung gaining all these capabilities"). GLSL IS A TARGET, NOT AN INPUT. This is NOT a GLSL->WGSL 19// transpiler: translating would enshrine GLSL as the canonical language and make a foreign dialect 20// the artifact we maintain. A shader is authored ONCE in NishiLang; GLSL and WGSL are both OUTPUTS, 21// exactly as x86_64 and WAT are outputs of the same NishiLang front-end. The ~600 lines of GLSL in 22// MVS/MFS are therefore a MIGRATION TARGET, not the source: each rung expresses more of them in 23// NishiLang and PROVES the emitted GLSL equivalent to the hand-written GLSL that ships today. 24// 25// LADDER (each rung widens the covered subset; rung 1 is what this lane ships): 26// rung 1 the fullscreen-triangle vertex stage -- authored in NishiLang, emitting BOTH the GLSL 27// VSH and the WGSL vs that are hand-written and SHIPPING today, proven against them. 28// rung 2 lower the real NishiLang AST (nx_tokenizer/nx_parse) into this IR, so a shader is an 29// ordinary NishiLang function rather than builder calls -- the full 'target, not 30// translator' shape, sharing the front-end with nx_compile_x86 and the WAT lane. 31// rung 3 widen the subset to the MFS paint set; rung 4 the MVS skinning set; then the character 32// pass becomes a compile target and the WebGPU abstention flips from owed to shipped. 33// 34// THE SOURCE OF TRUTH IS NISHILANG (operator, 2026-08-23: "we dont want python or glsl or 35// javascript or all that it should be nishi lang and nishi ecosystem from the first byte up each 36// rung gaining all these capabilities"). GLSL IS A TARGET, NOT AN INPUT. This is NOT a GLSL->WGSL 37// transpiler: translating would enshrine GLSL as the canonical language and make a foreign dialect 38// the artifact we maintain. A shader is authored ONCE in NishiLang; GLSL and WGSL are both OUTPUTS, 39// exactly as x86_64 and WAT are outputs of the same NishiLang front-end. The ~600 lines of GLSL in 40// MVS/MFS are therefore a MIGRATION TARGET, not the source: each rung expresses more of them in 41// NishiLang and PROVES the emitted GLSL equivalent to the hand-written GLSL that ships today. 42// 43// LADDER (each rung widens the covered subset; rung 1 is what this lane ships): 44// rung 1 the fullscreen-triangle vertex stage -- authored in NishiLang, emitting BOTH the GLSL 45// VSH and the WGSL vs that are hand-written and SHIPPING today, proven against them. 46// rung 2 lower the real NishiLang AST (nx_tokenizer/nx_parse) into this IR, so a shader is an 47// ordinary NishiLang function rather than builder calls -- the full "target, not 48// translator" shape, sharing the front-end with nx_compile_x86 and the WAT lane. 49// rung 3 widen the subset to the MFS paint set; rung 4 the MVS skinning set; then the pass 50// itself becomes a compile target and the WebGPU character abstention flips to shipped. 51// 52// SCOPE IS DECLARED AND ENFORCED, NEVER SILENT. The node kinds below cover what MVS/MFS actually 53// use. A backend meeting anything outside the subset must call sir_refuse and return -1. 54// A NAMED REFUSAL IS A CONTRACT; A SILENT MISTRANSLATION IS THE WORST POSSIBLE FAILURE IN A 55// SHADER BACKEND -- it compiles, it runs, and it draws the wrong thing. 56// 57// NO FLOATING POINT IS USED OR NEEDED: the NishiLang toolchain carries no f32, and a shader 58// emitter never computes with literals -- it TRANSCRIBES them. Numeric literals live in the 59// string pool in canonical form ("0.0", never "0."), which is also what WGSL requires. 60 61import "nx_syscalls.nx" 62 63// ---- fixed-width node arena ------------------------------------------------------------- 64// Flat i64 slots rather than structs: the walker stays trivial and the encoding is auditable 65// by eye. Slot 7 is the intrusive NEXT link, so every list is a chain of node ids. 66const SIR_SLOTS: i64 = 8 67// S12c-6 (2026-09-05): the node ARENA. Measured need: the cast vertex stage is 1,394 nodes and its fragment stage is 2.35x 68// its canonical-token count, i.e. ~3,300 nodes -- under 4,096 by a margin no future slice could keep. sir_node OVERFLOW IS 69// SILENT (returns 0, sets M_OVER, and every builder ignores a 0 child), so the arena is sized at 4x the measured vertex+fragment 70// sum and every verb that builds a real module PRINTS M_OVER beside its node count; the flag, not the size, is the guard. 71const SIR_MAXNODE: i64 = 16384 72const SIR_POOLCAP: i64 = 65536 73 74const SIR_KIND: i64 = 0 75const SIR_TY: i64 = 1 76const SIR_A: i64 = 2 77const SIR_B: i64 = 3 78const SIR_C: i64 = 4 79const SIR_NAME: i64 = 5 80const SIR_D: i64 = 6 81const SIR_NEXT: i64 = 7 82 83// module header words 84const M_NODES: i64 = 0 85const M_NCOUNT: i64 = 1 86const M_POOL: i64 = 2 87const M_POOLUSED: i64 = 3 88const M_DECLH: i64 = 4 89const M_DECLT: i64 = 5 90const M_FUNCH: i64 = 6 91const M_FUNCT: i64 = 7 92const M_REFUSE: i64 = 8 93const M_OVER: i64 = 9 94// S2 (2026-09-04): THE EMISSION CONTEXT. The K_FUNC whose body a backend is emitting right now (0 between 95// functions). An E_IDENT carries only a name, and what that name resolves to -- a parameter or local of 96// THIS function, or a module-scope declaration -- decides how a dialect spells it (WGSL reads a uniform as 97// `u.<name>`). Both backends set it; the resolver reads it. Word 10 of the 16-word header sir_new maps. 98const M_CURFN: i64 = 10 99// S5 (2026-09-04): THE FRAGMENT OUTPUT BEING EMITTED. The K_FRAGOUT declaration whose value the entry 100// point currently returns (0 everywhere else -- between functions, inside a helper, outside a fragment 101// stage). GLSL has no value-returning fragment entry: `void main()` ASSIGNS a declared `out` variable, 102// while WGSL RETURNS one. Both backends walk ONE S_RETURN node, so the shape that differs has to be 103// readable from somewhere; this word is that somewhere, and it is set ONLY around the entry body so a 104// plain helper in the same module still emits an ordinary `return`. Word 11 of the 16 sir_new maps. 105const M_FRAGOUT: i64 = 11 106// S7 (2026-09-05): THE INTERSTAGE-STRUCT MODE. 0 = no varyings in play; 1 = inside the vertex entry that 107// RETURNS struct VIO (varying writes and the position assignment land in vo.<name>); 2 = inside the fragment 108// entry that TAKES vin:VIO (varying and position reads come from vin.<name>). Set and cleared by the WGSL 109// entry emitter; wg_expr / wg_stmts read it. GLSL never sets it. Word 12 of the 16 sir_new maps. 110const M_VIOMODE: i64 = 12 111// S12a (2026-09-05): DOES THIS MODULE READ THE INSTANCE INDEX? Set by sir_builtin the moment an instance_index node is 112// built, read by the WGSL vertex-entry emitter, which binds @builtin(instance_index) ii:u32 ONLY then -- so every module 113// that never asks for it (the fullscreen triangle, every S0..S9 fixture) keeps a byte-identical signature. GLSL never 114// reads it: gl_InstanceID needs no declaration. Word 13 of the 16 sir_new maps. 115const M_USESII: i64 = 13 116// S12c-5 (2026-09-05): THE WGSL FRAGMENT OUTPUT VARIABLE. The hand cast fragment ASSIGNS its output (`fc=...;`) and exits 117// early with a void `return;` -- valid GLSL (fc is an `out` variable) and the shape the token ruler must see. WGSL RETURNS 118// its output, so the WGSL backend opens such an entry with `var fc:vec4f;`, lowers every void return inside it to 119// `return fc;` and appends one after the body when the last statement is not a return. This word holds the K_FRAGOUT 120// declaration ONLY while the WGSL backend is inside a fragment entry whose body assigns it (0 everywhere else) -- the 121// WGSL twin of M_FRAGOUT, which the GLSL backend uses for the opposite lowering (value return -> assignment). Word 14. 122const M_WGFRAGOUT: i64 = 14 123// S12 step 2 (2026-09-06): THE BIND GROUP a module's WGSL resources are spelled in (@group(N)). 0 for every module until 124// now; the cast FRAGMENT module is emitted at 1, because its struct U and its textures differ from the vertex module's and 125// WebGPU binds ONE resource per (group,binding) across both stages of a pipeline -- two structs named U at group 0 binding 0 126// would read one buffer through two layouts, and vertex uJT (2d f32) against fragment uVm (3d u32) at binding 1 is a 127// validation refusal. GLSL never reads this word. Word 15 of the 16 sir_new maps (the last free slot). 128const M_BINDGROUP: i64 = 15 129// Source clip depth is explicit: backends translate it to their device convention. 130// Existing modules retain their native convention until they declare otherwise. 131const M_CLIP_DEPTH: i64 = 16 132const SIR_MODULE_WORDS: i64 = 17 133const SIR_CLIP_NATIVE: i64 = 0 134const SIR_CLIP_NEGATIVE_ONE_TO_ONE: i64 = 1 135// S12c-5: an E_LIT flag (SIR_A) -- the unsigned literal keeps its `u` suffix in GLSL too (see sir_lit_u). 136const SIR_LIT_USUFFIX: i64 = 1 137 138// ---- node kinds (the DECLARED SUBSET) ---------------------------------------------------- 139const K_UNIFORM: i64 = 1 140const K_ATTRIB: i64 = 2 141const K_VARY: i64 = 3 142const K_FUNC: i64 = 4 143const K_PARAM: i64 = 5 144// RUNG GE43 -- COMPUTE (gameengine board, 2026-09-02). A storage buffer is the one declaration a 145// rasterizer KERNEL cannot do without: the depth buffer, the tile bins and the framebuffer all live 146// in read_write storage and are resolved with atomics. GLSL ES 3.00 (WebGL2) has NEITHER storage 147// buffers nor atomics nor a compute stage, so the GLSL backend REFUSES this kind BY NAME and the 148// WGSL door is the only third-party-browser path for it -- exactly the split GE42-GE45 declare: 149// ONE kernel authored here, one door per host (WebGPU as a submission pipe; dxg on NishiOS). 150// SIR_TY = element type SIR_A = binding index SIR_B = 1 read_write / 0 read SIR_C = 1 atomic<> 151const K_STORAGE: i64 = 6 152// S3 (2026-09-04): a TEXTURE is its own declaration kind. It is a BOUND RESOURCE in both dialects -- a 153// sampler uniform (`uniform highp usampler3D`) in GLSL, a `@group/@binding var` OUTSIDE struct U in WGSL -- 154// and it was never a uniform: a texture-typed K_UNIFORM passed the WGSL type check and landed INSIDE 155// struct U (WGSL that does not compile, no refusal) until S1 made that a named refusal in both backends. 156// SIR_TY = the texture type (T_TEX3U is the covered one; every other texture type refuses BY NAME) 157// No binding slot is stored: GLSL has no bindings and the WGSL backend DERIVES one from declaration order. 158const K_TEXTURE: i64 = 7 159// S5 (2026-09-04): THE FRAGMENT OUTPUT is its own declaration kind, for the same reason K_TEXTURE is. 160// It was not modelled at all, and the cost was silent: a fragment stage emitted `void main(){ return 161// vec4(...); }` -- a value returned from a void function, against no declared output -- which is invalid 162// GLSL ES 3.00 on BOTH counts and which no gate could see, because nothing pinned the GLSL fragment 163// bytes. The WGSL side had the mirror defect wearing the opposite face: it spelled the whole signature 164// tail as the LITERAL ")->@location(0) vec4f{", so the location and the type were unreadable constants 165// rather than data -- exactly the defect S4 removed from the parameter half of the same line. 166// ONE declaration now feeds both: GLSL emits `out <ty> <name>;` and lowers the entry's return to 167// `<name>=<expr>;`, WGSL emits no declaration and DERIVES `->@location(<loc>) <ty>`. 168// SIR_TY = the output type SIR_A = the @location index 169const K_FRAGOUT: i64 = 8 170// R-D (2026-09-04, GE44, B1 of the hand-shader diff): A STRUCT is a module-scope declaration whose members are 171// K_PARAM nodes chained under it EXACTLY as a function's parameters are (sir_member IS sir_param), so member 172// spelling shares the parameter machinery and cannot drift from it. A struct TYPE is named by its declaration: 173// sir_struct_ty(st) = SIR_MAXNODE + st, unique by construction and above every T_ scalar, so a type speller that 174// meets one looks the declaration up (sir_struct_of) instead of refusing. Member ACCESS needs no new expression 175// kind -- both dialects spell it base.member, the bytes E_SWZ already emits -- and CONSTRUCTION is an E_CALL 176// named after the struct (both dialects spell Hit(a,b)). SIR_A = member chain head, SIR_C = member tail. 177const K_STRUCT: i64 = 9 178const S_RETURN: i64 = 10 179const S_ASSIGN: i64 = 11 180const S_VAR: i64 = 12 181const S_IF: i64 = 13 182const S_DISCARD: i64 = 14 183// RUNG 2b-GENERAL: structured LOOP + BREAK. A back edge cannot be emitted as a goto -- neither 184// GLSL ES 3.00 nor WGSL has one -- so the recovered form is an infinite loop whose exit is an 185// explicit break. That is exactly the Relooper's Loop block, whose Inner block "will appear 186// inside the loop, i.e., when execution reaches the end of that block, flow will return to the 187// beginning" and which "will contain a conditional break defining where it is exited" 188// (Zakai, Emscripten, OOPSLA 2011 -- READ from the mirrored PDF banked as graphics.refs 189// key relooper-zakai11, not recalled from memory). 190const S_LOOP: i64 = 15 191const S_BREAK: i64 = 16 192// GE43: an EXPRESSION STATEMENT -- an atomic read-modify-write whose old value is discarded is a 193// statement in both dialects (`atomicMin(&d[i],z);`), and neither S_ASSIGN nor S_VAR can carry it 194// without inventing a dead local. SIR_A = the expression. 195const S_EXPR: i64 = 17 196const E_LIT: i64 = 20 197const E_IDENT: i64 = 21 198const E_BIN: i64 = 22 199const E_CALL: i64 = 23 200const E_SWZ: i64 = 24 201const E_INDEX: i64 = 25 202const E_TEXLOAD: i64 = 26 203const E_TEXSAMPLE: i64 = 27 204const E_CTOR: i64 = 28 205// rung-1 additions: a numeric CAST (GLSL float(x) / WGSL f32(x)) and a stage BUILTIN 206// (GLSL gl_VertexID / WGSL @builtin(vertex_index)). Both are pure dialect-SPELLING differences 207// over one IR node -- the thesis of this module in miniature. 208const E_CAST: i64 = 29 209const E_BUILTIN: i64 = 30 210// GE43: an ATOMIC read-modify-write. SIR_NAME = the op spelling (atomicMin / atomicMax / atomicAdd, 211// identical in WGSL and in the dxg-lane HLSL family so the IR carries the WGSL spelling), SIR_A = 212// the TARGET lvalue (an E_INDEX into a K_STORAGE), SIR_B = the value. WGSL takes the ADDRESS of the 213// target (`&d[i]`); that `&` is dialect knowledge the backend owns, the author never writes it. 214const E_ATOMIC: i64 = 31 215// R-A (2026-09-04, the GE44 world-shader contract): a SELECT. SIR_A = the condition, SIR_B = the value when 216// true, SIR_C = the value when false. GLSL spells it as the ternary (cond?then:else), WGSL as select(f,t,cond) 217// -- note WGSL's argument order puts the FALSE value first; that ordering is dialect knowledge the backend 218// owns. The condition is a scalar bool by construction of this IR (there is no vector-bool type), so the 219// ternary's scalar-condition rule holds wherever this node appears. 220const E_SELECT: i64 = 32 221// R-G0 (2026-09-04): unary negation. SIR_A = the operand; both dialects spell (-x). The world shader writes 222// -vec3f(...), -u.yp.x and -1.0 as expressions; an E_BIN(0 - x) would be semantically equal but would spell a 223// subtraction the hand shader never wrote, so negation is its own node with one arm per dialect. 224const E_NEG: i64 = 33 225// K_PRIVATE (GE55, 2026-09-05): MODULE-SCOPE PRIVATE STATE -- `var<private> name:T;` in WGSL, a bare global `T name;` 226// in GLSL. Written by one function and read bare by another, never qualified as u.<name>, invisible to the uniform 227// layout. It exists so a shader can derive every float it needs from RAW uniform words in ONE function (upk) while 228// every other function body stays byte-identical: the sim hands the GPU integers, the kernel does the math. Numbered 229// after the last expression kind because node kinds share one space and nothing range-tests them (measured). 230const K_PRIVATE: i64 = 34 231// S12c-4 (2026-09-05): LOGICAL NOT. SIR_A = the operand (a bool); both dialects spell (!x). The cast vertex stage gates a 232// garment instance on `!(gbm==4||gbm==5||gbm==8||gbm==9)` and `!wr9`; the world shader never needed it (its exits are 233// spelled as the inverted comparison, see sir_for_until), so it lands here as its own node kind rather than a rewrite the 234// hand text does not contain -- the token ruler would name that rewrite as a divergence. Numbered after K_PRIVATE for the 235// same reason K_PRIVATE is numbered after E_NEG: one kind space, nothing range-tests it. 236const E_NOT: i64 = 35 237 238// ---- types ------------------------------------------------------------------------------- 239const T_VOID: i64 = 0 240const T_F32: i64 = 1 241const T_I32: i64 = 2 242const T_U32: i64 = 3 243const T_BOOL: i64 = 4 244const T_V2F: i64 = 5 245const T_V3F: i64 = 6 246const T_V4F: i64 = 7 247const T_V3I: i64 = 8 248const T_TEX3U: i64 = 9 249const T_TEX2F: i64 = 10 250// GE43: vec3<u32> -- the type of @builtin(global_invocation_id); GLSL ES 3.10 would spell it uvec3. 251const T_V3U: i64 = 11 252// S8 (2026-09-05): vec2<i32> -- the INTEGER texel coordinate of a 2D fetch (the cast reads its joint table and 253// garment table with texelFetch(uJT, ivec2(...), 0)); WGSL spells it vec2i, GLSL ivec2. 254const T_V2I: i64 = 12 255// S9 (2026-09-05): mat3x3<f32> -- the cast's TBN / joint-rotation matrix. WGSL spells it mat3x3f, GLSL mat3; the 256// constructor takes three column vectors and mat*vec is plain E_BIN '*' in both dialects, so the type NAME is the only 257// dialect fact and it lives in wg_ty/gl_ty like every other type. 258const T_M3F: i64 = 13 259const T_MAX: i64 = 13 260 261func sir_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 262 263func sir_new() -> *i64 { 264 let m: *i64 = sys_mmap(SIR_MODULE_WORDS*8) as *i64 265 m[M_NODES] = sys_mmap(SIR_MAXNODE*SIR_SLOTS*8) as i64 266 m[M_NCOUNT] = 1 267 m[M_POOL] = sys_mmap(SIR_POOLCAP) as i64 268 m[M_POOLUSED] = 1 269 m[M_DECLH] = 0 270 m[M_DECLT] = 0 271 m[M_FUNCH] = 0 272 m[M_FUNCT] = 0 273 m[M_REFUSE] = 0 274 m[M_OVER] = 0 275 m[M_CURFN] = 0 276 m[M_FRAGOUT] = 0 277 return m 278} 279 280func sir_get(m: *i64, id: i64, k: i64) -> i64 { 281 let base: *i64 = m[M_NODES] as *i64 282 return base[id*SIR_SLOTS + k] 283} 284 285func sir_set(m: *i64, id: i64, k: i64, v: i64) -> i64 { 286 let base: *i64 = m[M_NODES] as *i64 287 base[id*SIR_SLOTS + k] = v 288 return 0 289} 290 291// intern a string; returns its pool offset (0 means none). Offsets start at 1 so 0 is a 292// usable NULL for every name slot. 293func sir_str(m: *i64, s: *u8) -> i64 { 294 let pool: *u8 = m[M_POOL] as *u8 295 let n: i64 = sir_slen(s) 296 var u: i64 = m[M_POOLUSED] 297 if u + n + 1 >= SIR_POOLCAP { m[M_OVER] = 1; return 0 } 298 let at: i64 = u 299 var i: i64 = 0 300 while i < n { pool[u] = s[i]; u = u + 1; i = i + 1 } 301 pool[u] = 0 as u8 302 u = u + 1 303 m[M_POOLUSED] = u 304 return at 305} 306 307func sir_cstr(m: *i64, off: i64) -> *u8 { 308 return ((m[M_POOL] + off) as *u8) 309} 310 311func sir_node(m: *i64, kind: i64, ty: i64, a: i64, b: i64, c: i64, nameoff: i64) -> i64 { 312 var id: i64 = m[M_NCOUNT] 313 if id >= SIR_MAXNODE { m[M_OVER] = 1; return 0 } 314 m[M_NCOUNT] = id + 1 315 sir_set(m, id, SIR_KIND, kind) 316 sir_set(m, id, SIR_TY, ty) 317 sir_set(m, id, SIR_A, a) 318 sir_set(m, id, SIR_B, b) 319 sir_set(m, id, SIR_C, c) 320 sir_set(m, id, SIR_NAME, nameoff) 321 sir_set(m, id, SIR_D, 0) 322 sir_set(m, id, SIR_NEXT, 0) 323 return id 324} 325 326// append onto an intrusive chain whose head/tail live in two module words 327func sir_chain(m: *i64, hw: i64, tw: i64, id: i64) -> i64 { 328 if id == 0 { return 0 } 329 if m[hw] == 0 { m[hw] = id; m[tw] = id; return id } 330 sir_set(m, m[tw], SIR_NEXT, id) 331 m[tw] = id 332 return id 333} 334 335// ---- declaration builders ---------------------------------------------------------------- 336// arraylen 0 = scalar. location -1 = unlocated. flat 1 = no interpolation. 337func sir_uniform(m: *i64, name: *u8, ty: i64, arraylen: i64) -> i64 { 338 let id: i64 = sir_node(m, K_UNIFORM, ty, arraylen, 0, 0, sir_str(m, name)) 339 return sir_chain(m, M_DECLH, M_DECLT, id) 340} 341 342// module-scope private state (K_PRIVATE): arraylen 0 = scalar, N = array<ty,N> / ty name[N] 343func sir_private(m: *i64, name: *u8, ty: i64, arraylen: i64) -> i64 { 344 let id: i64 = sir_node(m, K_PRIVATE, ty, arraylen, 0, 0, sir_str(m, name)) 345 return sir_chain(m, M_DECLH, M_DECLT, id) 346} 347 348func sir_attrib(m: *i64, name: *u8, ty: i64, location: i64) -> i64 { 349 let id: i64 = sir_node(m, K_ATTRIB, ty, location, 0, 0, sir_str(m, name)) 350 return sir_chain(m, M_DECLH, M_DECLT, id) 351} 352 353func sir_vary(m: *i64, name: *u8, ty: i64, location: i64, flat: i64) -> i64 { 354 let id: i64 = sir_node(m, K_VARY, ty, location, flat, 0, sir_str(m, name)) 355 return sir_chain(m, M_DECLH, M_DECLT, id) 356} 357 358// GE43: a storage buffer `name: array<elemty>` at @group(0)@binding(binding). readwrite 1 = var<storage,read_write>, 359// 0 = var<storage,read>. atomic 1 wraps the element as atomic<elemty> so E_ATOMIC may target it. 360func sir_storage(m: *i64, name: *u8, elemty: i64, binding: i64, readwrite: i64, atomic: i64) -> i64 { 361 let id: i64 = sir_node(m, K_STORAGE, elemty, binding, readwrite, atomic, sir_str(m, name)) 362 return sir_chain(m, M_DECLH, M_DECLT, id) 363} 364 365// S3: a texture declaration `name: <ty>` (see K_TEXTURE). Read with sir_texload / sir_texsample. 366func sir_texture(m: *i64, name: *u8, ty: i64) -> i64 { 367 let id: i64 = sir_node(m, K_TEXTURE, ty, 0, 0, 0, sir_str(m, name)) 368 return sir_chain(m, M_DECLH, M_DECLT, id) 369} 370 371// S5: the fragment stage's colour output (see K_FRAGOUT). Declared ONCE; each backend owns how its 372// dialect carries it. A shader that only discards declares none, and neither backend requires one. 373func sir_fragout(m: *i64, name: *u8, ty: i64, location: i64) -> i64 { 374 let id: i64 = sir_node(m, K_FRAGOUT, ty, location, 0, 0, sir_str(m, name)) 375 return sir_chain(m, M_DECLH, M_DECLT, id) 376} 377 378// R-D (2026-09-04): struct builders. sir_member IS sir_param -- the same K_PARAM node under the same A/C chain 379// -- so there is exactly one member/parameter shape in the IR. sir_struct_ty derives the type id from the 380// declaration (never picked, never colliding with a T_ scalar); sir_struct_of is the ONE inverse both backends 381// spell a struct type through. sir_member_of delegates to E_SWZ because no dialect distinguishes a member 382// read from a swizzle in its bytes -- a second kind would be two arms that could disagree about `.`. 383func sir_struct(m: *i64, name: *u8) -> i64 { 384 let id: i64 = sir_node(m, K_STRUCT, T_VOID, 0, 0, 0, sir_str(m, name)) 385 return sir_chain(m, M_DECLH, M_DECLT, id) 386} 387func sir_member(m: *i64, st: i64, name: *u8, ty: i64) -> i64 { return sir_param(m, st, name, ty) } 388func sir_struct_ty(st: i64) -> i64 { return SIR_MAXNODE + st } 389// the K_STRUCT declaration a type id names, or 0 for every scalar, vector and resource type 390func sir_struct_of(m: *i64, ty: i64) -> i64 { 391 if ty <= SIR_MAXNODE { return 0 } 392 let st: i64 = ty - SIR_MAXNODE 393 if st >= SIR_MAXNODE { return 0 } 394 if sir_get(m, st, SIR_KIND) != K_STRUCT { return 0 } 395 return st 396} 397func sir_member_of(m: *i64, base: i64, name: *u8, ty: i64) -> i64 { return sir_swz(m, base, name, ty) } 398 399// a function: A = param chain head, B = body chain head, C = param tail (build scratch) 400func sir_func(m: *i64, name: *u8, rettype: i64) -> i64 { 401 let id: i64 = sir_node(m, K_FUNC, rettype, 0, 0, 0, sir_str(m, name)) 402 return sir_chain(m, M_FUNCH, M_FUNCT, id) 403} 404 405// S4 (2026-09-04): HOW A PARAMETER IS BOUND. SIR_A of a K_PARAM names its binding: P_PLAIN (an ordinary 406// function parameter -- the only kind before S4, and sir_node's zero default) or P_BUILTIN_POSITION (the 407// fragment stage's position input: `@builtin(position) name:vec4f` in WGSL, `gl_FragCoord` in GLSL). The IR 408// says WHAT the input is; each backend owns how its dialect spells it, and an entry-point parameter with a 409// binding a backend cannot derive REFUSES by name rather than being dropped from the signature. 410const P_PLAIN: i64 = 0 411const P_BUILTIN_POSITION: i64 = 1 412const P_BUILTIN_FRONT_FACING: i64 = 2 413 414// Rasterizer orientation is a fragment boolean input, never a view-normal estimate. 415func sir_param_front_facing(m:*i64,fn:i64,name:*u8)->i64{ 416 let id:i64=sir_param(m,fn,name,T_BOOL) 417 sir_set(m,id,SIR_A,P_BUILTIN_FRONT_FACING) 418 return id 419} 420 421func sir_param(m: *i64, fn: i64, name: *u8, ty: i64) -> i64 { 422 let id: i64 = sir_node(m, K_PARAM, ty, P_PLAIN, 0, 0, sir_str(m, name)) 423 if sir_get(m, fn, SIR_A) == 0 { sir_set(m, fn, SIR_A, id); sir_set(m, fn, SIR_C, id); return id } 424 sir_set(m, sir_get(m, fn, SIR_C), SIR_NEXT, id) 425 sir_set(m, fn, SIR_C, id) 426 return id 427} 428 429// S4: a parameter bound to the fragment stage's @builtin(position). Typed vec4f by the builtin itself. 430func sir_param_position(m: *i64, fn: i64, name: *u8) -> i64 { 431 let id: i64 = sir_param(m, fn, name, T_V4F) 432 sir_set(m, id, SIR_A, P_BUILTIN_POSITION) 433 return id 434} 435 436// append a statement to a function body (D holds the body tail while building) 437func sir_stmt(m: *i64, fn: i64, st: i64) -> i64 { 438 if st == 0 { return 0 } 439 if sir_get(m, fn, SIR_B) == 0 { sir_set(m, fn, SIR_B, st); sir_set(m, fn, SIR_D, st); return st } 440 sir_set(m, sir_get(m, fn, SIR_D), SIR_NEXT, st) 441 sir_set(m, fn, SIR_D, st) 442 return st 443} 444 445// ---- statement builders ------------------------------------------------------------------ 446func sir_return(m: *i64, e: i64) -> i64 { return sir_node(m, S_RETURN, T_VOID, e, 0, 0, 0) } 447// Fragment depth is window depth [0,1] in both emitted dialects. S_RETURN.B 448// carries it alongside the color expression; ordinary returns leave B zero. 449func sir_return_depth(m: *i64, color: i64, depth: i64) -> i64 { 450 return sir_node(m, S_RETURN, T_VOID, color, depth, 0, 0) 451} 452 453func sir_assign(m: *i64, lhs: i64, rhs: i64) -> i64 { return sir_node(m, S_ASSIGN, T_VOID, lhs, rhs, 0, 0) } 454func sir_var(m: *i64, name: *u8, ty: i64, init: i64) -> i64 { return sir_node(m, S_VAR, ty, init, 0, 0, sir_str(m, name)) } 455func sir_if(m: *i64, cond: i64, thenh: i64, elseh: i64) -> i64 { return sir_node(m, S_IF, T_VOID, cond, thenh, elseh, 0) } 456func sir_discard(m: *i64) -> i64 { return sir_node(m, S_DISCARD, T_VOID, 0, 0, 0, 0) } 457// SIR_A of an S_LOOP is the body chain. The loop is UNCONDITIONAL by construction; every exit is 458// an explicit S_BREAK inside the body, so no dialect needs a loop-condition slot and the two 459// backends cannot disagree about where the test lives. 460func sir_loop(m: *i64, body: i64) -> i64 { return sir_node(m, S_LOOP, T_VOID, body, 0, 0, 0) } 461func sir_break(m: *i64) -> i64 { return sir_node(m, S_BREAK, T_VOID, 0, 0, 0, 0) } 462// GE43: an expression evaluated for its effect (an atomic whose old value is not kept) 463func sir_expr(m: *i64, e: i64) -> i64 { return sir_node(m, S_EXPR, T_VOID, e, 0, 0, 0) } 464// R-B (2026-09-04, GE44): A COUNTED LOOP IS BUILDER SUGAR, NOT A NODE KIND. The whole-population diff of the 465// two hand world shaders named the counted for-loop as a missing IR kind; it lands as a composition of the 466// incumbent S_LOOP/S_BREAK shape -- init; loop { if until { break } body; step } -- appended to fn in that 467// order. Both dialects therefore lower it through arms they already share, so they cannot disagree about 468// where the test lives (the argument sir_loop makes for itself) and the node trace is dialect-independent by 469// construction -- no second visitation order to get wrong. `until` is the EXIT test, spelled by the caller: 470// the IR carries no unary not, and inverting a comparison on the author's behalf would silently change its 471// NaN semantics. Returns the S_LOOP node; init and the loop are already appended to fn. 472func sir_for_until(m: *i64, fn: i64, init: i64, until: i64, step: i64, body: i64) -> i64 { 473 sir_stmt(m, fn, init) 474 let ex: i64 = sir_if(m, until, sir_break(m), 0) 475 let lp: i64 = sir_loop(m, sir_seq(m, sir_seq(m, ex, body), step)) 476 sir_stmt(m, fn, lp) 477 return lp 478} 479 480// chain two statements (for if-branch bodies) 481func sir_seq(m: *i64, a: i64, b: i64) -> i64 { 482 if a == 0 { return b } 483 var t: i64 = a 484 while sir_get(m, t, SIR_NEXT) != 0 { t = sir_get(m, t, SIR_NEXT) } 485 sir_set(m, t, SIR_NEXT, b) 486 return a 487} 488 489// ---- expression builders ----------------------------------------------------------------- 490func sir_lit(m: *i64, text: *u8, ty: i64) -> i64 { return sir_node(m, E_LIT, ty, 0, 0, 0, sir_str(m, text)) } 491// S12c-5: an UNSIGNED literal spelled with its u suffix in BOTH dialects (`0u`). A plain sir_lit(.., T_U32) keeps GLSL bare 492// (the fullscreen triangle's shift amount ships that way); a uint COMPARE in GLSL ES 3.00 needs the suffix, so the cast 493// fragment builds its block ids with this. 494func sir_lit_u(m: *i64, text: *u8) -> i64 { 495 let id: i64 = sir_lit(m, text, T_U32) 496 sir_set(m, id, SIR_A, SIR_LIT_USUFFIX) 497 return id 498} 499// S12c-5: a CONSTANT local (`const float K=1.2;` in GLSL, `const K:f32=1.2;` in WGSL) -- SIR_D = SIR_VAR_CONST on a plain 500// sir_var node, one flavour beside sir_let, so both backends spell it from the same word. 501const SIR_VAR_CONST: i64 = 2 502func sir_const(m: *i64, name: *u8, ty: i64, init: i64) -> i64 { 503 let id: i64 = sir_var(m, name, ty, init) 504 sir_set(m, id, SIR_D, SIR_VAR_CONST) 505 return id 506} 507func sir_ident(m: *i64, name: *u8, ty: i64) -> i64 { return sir_node(m, E_IDENT, ty, 0, 0, 0, sir_str(m, name)) } 508func sir_bin(m: *i64, op: *u8, l: i64, r: i64, ty: i64) -> i64 { return sir_node(m, E_BIN, ty, l, r, 0, sir_str(m, op)) } 509func sir_swz(m: *i64, base: i64, sel: *u8, ty: i64) -> i64 { return sir_node(m, E_SWZ, ty, base, 0, 0, sir_str(m, sel)) } 510func sir_index(m: *i64, base: i64, idx: i64, ty: i64) -> i64 { return sir_node(m, E_INDEX, ty, base, idx, 0, 0) } 511// S8 (2026-09-05): the load's result type FOLLOWS THE TEXTURE -- texture_2d<f32> returns vec4f (the cast uses the 512// fetched joint-table row WHOLE); texture_3d<u32> keeps the T_U32 the vox fixture swizzles .x from, so every 513// existing 3D read is byte-identical. `tex` is the texture's E_IDENT and its SIR_TY names the texture type. 514func sir_texload(m: *i64, tex: i64, coord: i64, lod: i64) -> i64 { 515 var rt: i64 = T_U32 516 if sir_get(m, tex, SIR_TY) == T_TEX2F { rt = T_V4F } 517 return sir_node(m, E_TEXLOAD, rt, tex, coord, lod, 0) 518} 519func sir_texsample(m: *i64, tex: i64, coord: i64) -> i64 { return sir_node(m, E_TEXSAMPLE, T_V4F, tex, coord, 0, 0) } 520 521// call/ctor argument chains are built with sir_arg 522func sir_call(m: *i64, name: *u8, ty: i64) -> i64 { return sir_node(m, E_CALL, ty, 0, 0, 0, sir_str(m, name)) } 523func sir_ctor(m: *i64, ty: i64) -> i64 { return sir_node(m, E_CTOR, ty, 0, 0, 0, 0) } 524func sir_cast(m: *i64, e: i64, ty: i64) -> i64 { return sir_node(m, E_CAST, ty, e, 0, 0, 0) } 525func sir_builtin(m: *i64, which: *u8, ty: i64) -> i64 { 526 // S12a: an instance_index read marks the module so the WGSL vertex entry binds the builtin (M_USESII). 527 if which[0] == (105 as u8) { m[M_USESII] = 1 } 528 return sir_node(m, E_BUILTIN, ty, 0, 0, 0, sir_str(m, which)) 529} 530// GE43: op is the WGSL spelling (atomicMin/atomicMax/atomicAdd); target is an E_INDEX into a storage buffer 531func sir_atomic(m: *i64, op: *u8, target: i64, val: i64, ty: i64) -> i64 { return sir_node(m, E_ATOMIC, ty, target, val, 0, sir_str(m, op)) } 532// R-A: select -- (cond, then, else) in IR order; each backend spells its own argument order. 533func sir_select(m: *i64, cond: i64, tval: i64, fval: i64, ty: i64) -> i64 { return sir_node(m, E_SELECT, ty, cond, tval, fval, 0) } 534// R-G0: unary negation of e, typed ty. 535func sir_neg(m: *i64, e: i64, ty: i64) -> i64 { return sir_node(m, E_NEG, ty, e, 0, 0, 0) } 536// S12c-4: logical not of a bool e (see E_NOT). 537func sir_not(m: *i64, e: i64) -> i64 { return sir_node(m, E_NOT, T_BOOL, e, 0, 0, 0) } 538// R-C: an IMMUTABLE local. Same node as sir_var with SIR_D = 1; WGSL spells it `let`, GLSL has no such 539// spelling for a non-constant initialiser and emits the plain local -- one IR node, two shapes. 540const SIR_VAR_LET: i64 = 1 541func sir_let(m: *i64, name: *u8, ty: i64, init: i64) -> i64 { 542 let id: i64 = sir_var(m, name, ty, init) 543 sir_set(m, id, SIR_D, SIR_VAR_LET) 544 return id 545} 546 547func sir_arg(m: *i64, callid: i64, e: i64) -> i64 { 548 if e == 0 { return 0 } 549 // List linkage belongs to an argument occurrence, never to its reusable expression. 550 // Child expressions remain shared; only the intrusive sibling link needs ownership. 551 let arg:i64=sir_node(m,sir_get(m,e,SIR_KIND),sir_get(m,e,SIR_TY), 552 sir_get(m,e,SIR_A),sir_get(m,e,SIR_B),sir_get(m,e,SIR_C),sir_get(m,e,SIR_NAME)) 553 if arg==0 { return 0 } 554 sir_set(m,arg,SIR_D,sir_get(m,e,SIR_D)) 555 if sir_get(m, callid, SIR_A) == 0 { sir_set(m, callid, SIR_A, arg); sir_set(m, callid, SIR_C, arg); return e } 556 sir_set(m, sir_get(m, callid, SIR_C), SIR_NEXT, arg) 557 sir_set(m, callid, SIR_C, arg) 558 return e 559} 560 561// ---- refusal ----------------------------------------------------------------------------- 562// A backend that meets a construct outside its declared subset records the construct BY NAME 563// and returns -1. Never emit a guess. 564func sir_refuse(m: *i64, what: *u8) -> i64 { 565 if m[M_REFUSE] == 0 { m[M_REFUSE] = sir_str(m, what) } 566 return 0 - 1 567} 568func sir_refused(m: *i64) -> i64 { return m[M_REFUSE] } 569 570// ---- shared emit buffer ------------------------------------------------------------------ 571func eb_put(out: *u8, pos: i64, cap: i64, s: *u8) -> i64 { 572 var p: i64 = pos 573 var i: i64 = 0 574 let n: i64 = sir_slen(s) 575 while i < n { if p + 1 < cap { out[p] = s[i]; p = p + 1 } i = i + 1 } 576 return p 577} 578 579func eb_num(out: *u8, pos: i64, cap: i64, v: i64) -> i64 { 580 var p: i64 = pos 581 var x: i64 = v 582 if x < 0 { p = eb_put(out, p, cap, "-" as *u8); x = 0 - x } 583 let tmp: *u8 = sys_mmap(32) 584 var k: i64 = 0 585 if x == 0 { tmp[0] = 48 as u8; k = 1 } 586 while x > 0 { tmp[k] = ((48 + x - (x/10)*10) as u8); x = x/10; k = k + 1 } 587 while k > 0 { k = k - 1; if p + 1 < cap { out[p] = tmp[k]; p = p + 1 } } 588 return p 589} 590 591// ---- backend trace ------------------------------------------------------------------------ 592// EVERY backend appends each node id as it emits it. Two backends that consumed the SAME ir 593// COMPLETELY produce IDENTICAL traces -- that identity is the equivalence oracle available on a 594// box with no GPU, and it is not vacuous: a backend that silently skips a statement produces a 595// SHORTER trace, and one that visits in a different order produces a DIFFERENT one. 596func tr_add(trace: *i64, tcap: i64, tn: *i64, id: i64) -> i64 { 597 if tn[0] < tcap { trace[tn[0]] = id; tn[0] = tn[0] + 1 } 598 return 0 599}