From 737db89ad022c580c5cbc4f00f8a39cbca6cf2a7 Mon Sep 17 00:00:00 2001 From: Brendan Punsky Date: Fri, 26 Jun 2026 09:24:52 -0400 Subject: [PATCH] rexcode/ir/spirv: complete the encoder -- types, constants, globals, function bodies The second half of encode(): the side tables (Module.type_ids / global_ids / function_ids -- SPIR-V's flat id space, which ir.Type/Global/ Function don't carry) plus the lowering: emit_types ir.Type -> OpTypeXxx (void/int/float/vector/pointer/struct/ function; INT signedness + POINTER storage class ride in aux) emit_constants OpConstant / OpConstantComposite / true/false/null emit_globals OpVariable (storage class from the pointer type) emit_operation generic table-driven op emit: INSTRUCTION_INDEX gives the result-type/result-id prefix, the rest stream from op.operands emit_functions OpFunction / OpLabel / body / OpFunctionEnd Validated: a complete void compute main module encodes to byte-exact-correct SPIR-V (29 words, all checked). Known gaps: OpFunctionParameter, ARRAY/bool types, explicit enum-parameter operands, computed bound. Decoder next. --- core/rexcode/ir/spirv/encoder.odin | 140 +++++++++++++++++++++++++++-- core/rexcode/ir/spirv/module.odin | 10 +++ 2 files changed, 144 insertions(+), 6 deletions(-) diff --git a/core/rexcode/ir/spirv/encoder.odin b/core/rexcode/ir/spirv/encoder.odin index a524e7927..530d0dd82 100644 --- a/core/rexcode/ir/spirv/encoder.odin +++ b/core/rexcode/ir/spirv/encoder.odin @@ -155,15 +155,140 @@ emit_annotations :: proc "contextless" (w: ^Writer, m: ^Module) { } } +// ----------------------------------------------------------------------------- +// Types / constants / globals (the -defining body, before functions) +// ----------------------------------------------------------------------------- + +// Type_Ref -> the type's wire , via the side table. +@(private="file") +tid :: #force_inline proc "contextless" (m: ^Module, t: Type_Ref) -> Id { + i := u32(t) + return i < u32(len(m.type_ids)) ? m.type_ids[i] : ID_NONE +} + +// Lower ir.Type -> OpTypeXxx. INT signedness and POINTER storage class ride in +// 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 .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 .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 .ARRAY, .OPAQUE, .REF: + w.pos = s // rewind the placeholder; not yet lowered + continue + } + 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 + } + 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) + } +} + +// ----------------------------------------------------------------------------- +// Function bodies (the generic, table-driven operation emit) +// ----------------------------------------------------------------------------- + +// Emit one ir.Operand by its kind. Type refs resolve through the type-id table; +// entity refs and literals are emitted as-is. +@(private="file") +emit_operand :: #force_inline proc "contextless" (w: ^Writer, m: ^Module, o: Operand) { + switch o.kind { + case .NONE: + case .LIT_INT, .LIT_FLOAT, .ATTRIBUTE: w_word(w, u32(o.imm)) + case .REF: w_id(w, operand_id(o)) + case .TYPE: w_id(w, tid(m, operand_type(o))) + } +} + +// Emit one Operation. The opcode's layout (INSTRUCTION_INDEX) supplies the +// leading IdResultType/IdResult from `result`; the remaining operands are +// `op.operands` in order (the producer built them correctly, so no per-operand +// spec match is needed -- only whether a result type/id prefix exists). +@(private="file") +emit_operation :: proc "contextless" (w: ^Writer, m: ^Module, op: ^Operation) { + run: Spec_Run + if int(op.opcode) < len(INSTRUCTION_INDEX) { run = INSTRUCTION_INDEX[op.opcode] } + s := inst_begin(w) + si := 0 + if si < int(run.count) && INSTRUCTION_SPECS[int(run.start) + si].kind == .IdResultType { + w_id(w, tid(m, op.result.type)); si += 1 + } + if si < int(run.count) && INSTRUCTION_SPECS[int(run.start) + si].kind == .IdResult { + w_id(w, op.result.id); si += 1 + } + for o in op.operands { emit_operand(w, m, o) } + inst_end(w, s, Opcode(op.opcode)) +} + +@(private="file") +emit_functions :: proc "contextless" (w: ^Writer, m: ^Module) { + for fn, fi in m.functions { + sig := m.types[u32(fn.signature)] // a FUNCTION type: fields = params ++ [result] + s := inst_begin(w) + w_id(w, tid(m, sig.fields[sig.count])) // result = return type + w_id(w, fi < len(m.function_ids) ? m.function_ids[fi] : ID_NONE) + w_word(w, 0) // FunctionControl (none) + w_id(w, tid(m, fn.signature)) + inst_end(w, s, .OpFunction) + // (OpFunctionParameter not yet modelled in ir.Function) + for blk in fn.blocks { + sl := inst_begin(w); w_id(w, blk.id); inst_end(w, sl, .OpLabel) + for &op in blk.ops { emit_operation(w, m, &op) } + } + se := inst_begin(w); inst_end(w, se, .OpFunctionEnd) + } +} + // ----------------------------------------------------------------------------- // Entry point // ----------------------------------------------------------------------------- -// encode: serialize `m` into `code`, returning the byte count written. -// -// (Types / constants / globals / function bodies are not yet emitted -- that -// half needs the assignment + ir.Type -> OpTypeXxx lowering, which lands -// next. The header + preamble / debug / annotation sections are complete.) +// encode: serialize `m` into `code` in spec layout order, returning the byte +// count written. `m.bound` must be the exclusive upper bound on all s. encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (byte_count: u32, ok: bool) { m := m w := Writer{code = code, ok = true} @@ -171,6 +296,9 @@ encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dy emit_preamble(&w, &m) emit_debug(&w, &m) emit_annotations(&w, &m) - // TODO(codec): emit_types_constants_globals + emit_functions (the lowered body). + emit_types(&w, &m) + emit_constants(&w, &m) + emit_globals(&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 f51c80871..abde4ea6b 100644 --- a/core/rexcode/ir/spirv/module.odin +++ b/core/rexcode/ir/spirv/module.odin @@ -44,6 +44,16 @@ Module :: struct { // --- Debug + annotations --- debug: Debug, decorations: []Decoration_Inst, + + // --- side tables --- + // SPIR-V has one flat space (types, constants, globals, functions, and + // SSA results all draw from it), but ir.Type/Global/Function carry no id of + // their own. These parallel the ir core arrays and hold each entity's wire + // , so decode->encode preserves them. (Results carry their own id in + // Result.id / Constant.result.id; only these three need a side table.) + type_ids: []Id, // parallel to base.types + global_ids: []Id, // parallel to base.globals + function_ids: []Id, // parallel to base.functions } // Member index sentinel: a whole-target decoration / name (OpDecorate / OpName)