From 0cd5ed84b2fe12cf0815d0739983e0706c8f3775 Mon Sep 17 00:00:00 2001 From: Brendan Punsky Date: Fri, 26 Jun 2026 12:04:27 -0400 Subject: [PATCH] rexcode/ir/spirv: dependency-ordered definitions + runtime arrays - Definition order: Module.defs records the exact type/constant/global interleaving (SPIR-V's single 'types, constants, global variables' section must be dependency-ordered -- a length constant before the array type that uses it, etc.). decode records it; encode replays it for byte-exact, spec-valid output (empty defs falls back to all-types, then -constants, then -globals). emit_one_type/constant/global factored out for the replay; no encoder alloc. - OpTypeRuntimeArray: ARRAY with len_ref == ID_NONE (vs OpTypeArray's length). Tests: bool_and_array sets defs so OpConstant precedes OpTypeArray (verified in disasm); runtime_array added -> 7 passed. --- core/rexcode/ir/spirv/decoder.odin | 17 ++- core/rexcode/ir/spirv/encoder.odin | 127 ++++++++++-------- core/rexcode/ir/spirv/module.odin | 16 +++ .../ir/spirv/tests/roundtrip_test.odin | 14 ++ 4 files changed, 117 insertions(+), 57 deletions(-) diff --git a/core/rexcode/ir/spirv/decoder.odin b/core/rexcode/ir/spirv/decoder.odin index 0fc7f4df2..bb3e56b13 100644 --- a/core/rexcode/ir/spirv/decoder.odin +++ b/core/rexcode/ir/spirv/decoder.odin @@ -49,6 +49,7 @@ Decoder :: struct { global_ids: [dynamic]Id, functions: [dynamic]Function, function_ids: [dynamic]Id, + defs: [dynamic]Def, // type/constant/global definition order // in-flight function / block in_fn: bool, @@ -107,10 +108,17 @@ tref :: proc(d: ^Decoder, id: u32) -> Type_Ref { @(private="file") add_type :: proc(d: ^Decoder, id: Id, t: Type) { d.id_to_type[id] = Type_Ref(len(d.types)) + append(&d.defs, Def{.TYPE, u32(len(d.types))}) append(&d.types, t) append(&d.type_ids, id) } +@(private="file") +add_const :: proc(d: ^Decoder, c: Constant) { + append(&d.defs, Def{.CONSTANT, u32(len(d.constants))}) + append(&d.constants, c) +} + // Decode a function-body operation generically, by its operand layout: the // result-type/result-id prefix from the leading specs, then one operand per // remaining spec (Id specs -> entity/type refs, the rest -> integer literals). @@ -218,6 +226,7 @@ lower :: proc(d: ^Decoder, opcode: Opcode, w: []u32) { case .OpTypeInt: add_type(d, Id(w[0]), Type{kind = .INT, bits = u16(w[1]), aux = u16(w[2] & 1)}) case .OpTypeFloat: add_type(d, Id(w[0]), Type{kind = .FLOAT, bits = u16(w[1])}) case .OpTypeVector: add_type(d, Id(w[0]), Type{kind = .VECTOR, elem = tref(d, w[1]), count = w[2]}) + case .OpTypeRuntimeArray: add_type(d, Id(w[0]), Type{kind = .ARRAY, elem = tref(d, w[1]), len_ref = ID_NONE}) case .OpTypePointer: add_type(d, Id(w[0]), Type{kind = .POINTER, aux = u16(w[1]), elem = tref(d, w[2])}) case .OpTypeStruct: fields := make([]Type_Ref, len(w) - 1) @@ -233,18 +242,19 @@ lower :: proc(d: ^Decoder, opcode: Opcode, w: []u32) { case .OpConstant: c := Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode, value = u64(w[2])} if len(w) > 3 { c.value |= u64(w[3]) << 32 } - append(&d.constants, c) + add_const(d, c) case .OpConstantTrue, .OpConstantFalse, .OpConstantNull: - append(&d.constants, Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode}) + add_const(d, Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode}) case .OpConstantComposite: elems := make([]Id, len(w) - 2) for j in 2 ..< len(w) { elems[j - 2] = Id(w[j]) } - append(&d.constants, Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode, elements = elems}) + add_const(d, Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode, elements = elems}) case .OpVariable: if d.in_fn { if d.have_blk { append(&d.blk_ops, decode_operation(d, opcode, w)) } } else { + append(&d.defs, Def{.GLOBAL, u32(len(d.globals))}) append(&d.globals, Global{type = tref(d, w[0]), init = len(w) > 3 ? Id(w[3]) : ID_NONE}) append(&d.global_ids, Id(w[1])) } @@ -337,6 +347,7 @@ decode :: proc(data: []u8, m: ^Module, errors: ^[dynamic]Error, allocator := con m.global_ids = d.global_ids[:] m.functions = d.functions[:] m.function_ids = d.function_ids[:] + m.defs = d.defs[:] return u32(nwords * 4), true } diff --git a/core/rexcode/ir/spirv/encoder.odin b/core/rexcode/ir/spirv/encoder.odin index 996d75918..fd1efbcd0 100644 --- a/core/rexcode/ir/spirv/encoder.odin +++ b/core/rexcode/ir/spirv/encoder.odin @@ -170,64 +170,85 @@ tid :: #force_inline proc "contextless" (m: ^Module, t: Type_Ref) -> Id { // Type.aux. (ARRAY/OPAQUE/REF need a length constant / extra modelling and are // skipped for now.) @(private="file") -emit_types :: proc "contextless" (w: ^Writer, m: ^Module) { - for t, i in m.types { - s := inst_begin(w) - w_id(w, i < len(m.type_ids) ? m.type_ids[i] : ID_NONE) - op: Opcode - switch t.kind { - case .VOID: op = .OpTypeVoid - case .BOOL: op = .OpTypeBool - case .INT: w_word(w, u32(t.bits)); w_word(w, u32(t.aux & 1)); op = .OpTypeInt - case .FLOAT: w_word(w, u32(t.bits)); op = .OpTypeFloat - case .VECTOR: w_id(w, tid(m, t.elem)); w_word(w, t.count); op = .OpTypeVector - case .ARRAY: w_id(w, tid(m, t.elem)); w_id(w, t.len_ref); op = .OpTypeArray // length: a constant - case .POINTER: w_word(w, u32(t.aux)); w_id(w, tid(m, t.elem)); op = .OpTypePointer - case .STRUCT: - for f in t.fields { w_id(w, tid(m, f)) } - op = .OpTypeStruct - case .FUNCTION: - w_id(w, tid(m, t.fields[t.count])) // return type - for pi in 0 ..< int(t.count) { w_id(w, tid(m, t.fields[pi])) } - op = .OpTypeFunction - case .OPAQUE, .REF: - w.pos = s // rewind the placeholder; not yet lowered - continue - } - inst_end(w, s, op) +emit_one_type :: proc "contextless" (w: ^Writer, m: ^Module, i: int) { + t := m.types[i] + s := inst_begin(w) + w_id(w, i < len(m.type_ids) ? m.type_ids[i] : ID_NONE) + op: Opcode + switch t.kind { + case .VOID: op = .OpTypeVoid + case .BOOL: op = .OpTypeBool + case .INT: w_word(w, u32(t.bits)); w_word(w, u32(t.aux & 1)); op = .OpTypeInt + case .FLOAT: w_word(w, u32(t.bits)); op = .OpTypeFloat + case .VECTOR: w_id(w, tid(m, t.elem)); w_word(w, t.count); op = .OpTypeVector + case .ARRAY: + // length: an (OpTypeArray) or absent (OpTypeRuntimeArray). + w_id(w, tid(m, t.elem)) + if t.len_ref != ID_NONE { w_id(w, t.len_ref); op = .OpTypeArray } + else { op = .OpTypeRuntimeArray } + case .POINTER: w_word(w, u32(t.aux)); w_id(w, tid(m, t.elem)); op = .OpTypePointer + case .STRUCT: + for f in t.fields { w_id(w, tid(m, f)) } + op = .OpTypeStruct + case .FUNCTION: + w_id(w, tid(m, t.fields[t.count])) // return type + for pi in 0 ..< int(t.count) { w_id(w, tid(m, t.fields[pi])) } + op = .OpTypeFunction + case .OPAQUE, .REF: + w.pos = s // rewind the placeholder; not yet lowered + return } + inst_end(w, s, op) } @(private="file") -emit_constants :: proc "contextless" (w: ^Writer, m: ^Module) { - for c in m.constants { - s := inst_begin(w) - w_id(w, tid(m, c.result.type)) - w_id(w, c.result.id) - #partial switch c.opcode { - case .OpConstant: - t := m.types[u32(c.result.type)] - w_word(w, u32(c.value)) - if (t.kind == .INT || t.kind == .FLOAT) && t.bits > 32 { - w_word(w, u32(c.value >> 32)) // context-dependent number, second word +emit_one_constant :: proc "contextless" (w: ^Writer, m: ^Module, i: int) { + c := m.constants[i] + s := inst_begin(w) + w_id(w, tid(m, c.result.type)) + w_id(w, c.result.id) + #partial switch c.opcode { + case .OpConstant: + t := m.types[u32(c.result.type)] + w_word(w, u32(c.value)) + if (t.kind == .INT || t.kind == .FLOAT) && t.bits > 32 { + w_word(w, u32(c.value >> 32)) // context-dependent number, second word + } + case .OpConstantComposite: + for e in c.elements { w_id(w, e) } + } + inst_end(w, s, c.opcode) +} + +@(private="file") +emit_one_global :: proc "contextless" (w: ^Writer, m: ^Module, i: int) { + g := m.globals[i] + s := inst_begin(w) + w_id(w, tid(m, g.type)) // a pointer type + w_id(w, i < len(m.global_ids) ? m.global_ids[i] : ID_NONE) + w_word(w, u32(m.types[u32(g.type)].aux)) // storage class = the pointer's address space + if g.init != ID_NONE { w_id(w, g.init) } + inst_end(w, s, .OpVariable) +} + +// The "types, constants, global variables" section. When Module.defs records the +// stream order (from decode), replay it -- byte-exact and dependency-correct. +// Otherwise fall back to all-types, then all-constants, then all-globals. +@(private="file") +emit_definitions :: proc "contextless" (w: ^Writer, m: ^Module) { + if len(m.defs) > 0 { + for d in m.defs { + switch d.kind { + case .TYPE: emit_one_type(w, m, int(d.index)) + case .CONSTANT: emit_one_constant(w, m, int(d.index)) + case .GLOBAL: emit_one_global(w, m, int(d.index)) } - case .OpConstantComposite: - for e in c.elements { w_id(w, e) } } - inst_end(w, s, c.opcode) - } -} - -@(private="file") -emit_globals :: proc "contextless" (w: ^Writer, m: ^Module) { - for g, gi in m.globals { - s := inst_begin(w) - w_id(w, tid(m, g.type)) // a pointer type - w_id(w, gi < len(m.global_ids) ? m.global_ids[gi] : ID_NONE) - w_word(w, u32(m.types[u32(g.type)].aux)) // storage class = the pointer's address space - if g.init != ID_NONE { w_id(w, g.init) } - inst_end(w, s, .OpVariable) + return } + for i in 0 ..< len(m.types) { emit_one_type(w, m, i) } + for i in 0 ..< len(m.constants) { emit_one_constant(w, m, i) } + for i in 0 ..< len(m.globals) { emit_one_global(w, m, i) } } // ----------------------------------------------------------------------------- @@ -331,9 +352,7 @@ encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dy emit_preamble(&w, &m) emit_debug(&w, &m) emit_annotations(&w, &m) - emit_types(&w, &m) - emit_constants(&w, &m) - emit_globals(&w, &m) + emit_definitions(&w, &m) emit_functions(&w, &m) return w.pos, w.ok } diff --git a/core/rexcode/ir/spirv/module.odin b/core/rexcode/ir/spirv/module.odin index abde4ea6b..2aeffac99 100644 --- a/core/rexcode/ir/spirv/module.odin +++ b/core/rexcode/ir/spirv/module.odin @@ -54,6 +54,22 @@ Module :: struct { type_ids: []Id, // parallel to base.types global_ids: []Id, // parallel to base.globals function_ids: []Id, // parallel to base.functions + + // --- Definition order --- + // The interleaving of types / constants / globals as they appear in the stream. + // SPIR-V has one "types, constants, global variables" section that must be in + // dependency order (an array's length constant before the array type, a + // constant's result type before the constant, ...). decode records the order; + // encode replays it for byte-exact, spec-valid output. Empty => encode falls + // back to all-types, then all-constants, then all-globals. + defs: []Def, +} + +// A node in Module.defs: which of the three definition arrays, and its index. +Def_Kind :: enum u8 { TYPE, CONSTANT, GLOBAL } +Def :: struct { + kind: Def_Kind, + index: u32, } // Member index sentinel: a whole-target decoration / name (OpDecorate / OpName) diff --git a/core/rexcode/ir/spirv/tests/roundtrip_test.odin b/core/rexcode/ir/spirv/tests/roundtrip_test.odin index 923d83d50..07e3e1145 100644 --- a/core/rexcode/ir/spirv/tests/roundtrip_test.odin +++ b/core/rexcode/ir/spirv/tests/roundtrip_test.odin @@ -218,9 +218,23 @@ main :: proc() { } m.type_ids = {spirv.Id(1), spirv.Id(2), spirv.Id(3)} m.constants = {{result = {spirv.Id(4), spirv.Type_Ref(1)}, opcode = .OpConstant, value = 4}} + // definition order: the length constant before the array type (spec-valid). + m.defs = {{.TYPE, 0}, {.TYPE, 1}, {.CONSTANT, 0}, {.TYPE, 2}} roundtrip("bool_and_array", m) } + // (7) a runtime array (OpTypeRuntimeArray): ARRAY with len_ref == ID_NONE. + { + m := spirv.make_module() + m.capabilities = {.Shader} + m.types = { + {kind = .INT, bits = 32, aux = 1}, + {kind = .ARRAY, elem = spirv.Type_Ref(0), len_ref = spirv.ID_NONE}, // int32[] + } + m.type_ids = {spirv.Id(1), spirv.Id(2)} + roundtrip("runtime_array", m) + } + fmt.printf("\n%d passed, %d failed\n", ok_count, fail_count) if fail_count > 0 { os.exit(1) } }