From 18f9f141e42302593ec703c8d6d3961ca671267c Mon Sep 17 00:00:00 2001 From: Brendan Punsky Date: Fri, 26 Jun 2026 07:33:01 -0400 Subject: [PATCH] rexcode/ir/spirv: encoder foundation -- header + preamble/debug/annotation sections The Module -> word-stream encoder's first half: a fast single-pass word writer (one host-endian store per word; instruction headers written as a placeholder and backpatched, so variable-length instructions need no measure pass) plus the header and the preamble / debug / annotation sections in spec layout order (capabilities, extensions, ext-inst imports, memory model, entry points, execution modes, OpString/OpSource/OpName, decorations). Validated: a minimal module encodes to byte-exact-correct SPIR-V (magic, v1.5, OpCapability Shader, OpMemoryModel Logical GLSL450, OpEntryPoint GLCompute %main, NUL-terminated 'main' string, correct backpatched word counts). Types/constants/globals/function bodies (the + type lowering) are next. --- core/rexcode/ir/spirv/encoder.odin | 176 +++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 core/rexcode/ir/spirv/encoder.odin diff --git a/core/rexcode/ir/spirv/encoder.odin b/core/rexcode/ir/spirv/encoder.odin new file mode 100644 index 000000000..a524e7927 --- /dev/null +++ b/core/rexcode/ir/spirv/encoder.odin @@ -0,0 +1,176 @@ +// rexcode · Brendan Punsky (dotbmp@github), original author + +package rexcode_spirv + +import "base:intrinsics" + +// ============================================================================= +// SECTION: Encoder (Module -> SPIR-V word stream) +// ============================================================================= +// +// Emits the module header followed by the instruction stream in the spec's +// required section order (SPIR-V spec §2.4 Logical Layout): capabilities, +// extensions, ext-inst imports, memory model, entry points, execution modes, +// debug, annotations, then types/constants/globals and function definitions. +// +// Fast and single-pass: each word is one (host-endian) store into the caller's +// buffer; an instruction's header word is written as a placeholder, the operands +// streamed after it, and the word count backpatched at the end -- so variable- +// length instructions (strings, variadic operands) need no pre-measure pass. +// SPIR-V's endianness is self-describing via the magic word, so host-endian +// output is conformant. + +// ----------------------------------------------------------------------------- +// Word writer +// ----------------------------------------------------------------------------- + +Writer :: struct { + code: []u8, + pos: u32, // byte offset; always a multiple of 4 + ok: bool, +} + +@(private="file") +w_word :: #force_inline proc "contextless" (w: ^Writer, word: u32) { + if int(w.pos) + 4 > len(w.code) { + w.ok = false + return + } + intrinsics.unaligned_store(cast(^u32)&w.code[w.pos], word) + w.pos += 4 +} + +@(private="file") w_id :: #force_inline proc "contextless" (w: ^Writer, id: Id) { w_word(w, u32(id)) } + +// SPIR-V LiteralString: the UTF-8 bytes, NUL-terminated, packed little-endian +// into words and zero-padded to a word boundary. (len+4)/4 words: always at +// least the terminator + padding, even for an empty / word-multiple string. +@(private="file") +w_string :: proc "contextless" (w: ^Writer, s: string) { + nwords := (len(s) + 4) / 4 + for wi in 0 ..< nwords { + word: u32 = 0 + for b in 0 ..< 4 { + idx := wi * 4 + b + if idx < len(s) { + word |= u32(s[idx]) << uint(b * 8) + } + } + w_word(w, word) + } +} + +// Reserve the instruction header word; pair with inst_end to backpatch its count. +@(private="file") +inst_begin :: #force_inline proc "contextless" (w: ^Writer) -> u32 { + p := w.pos + w_word(w, 0) + return p +} + +@(private="file") +inst_end :: #force_inline proc "contextless" (w: ^Writer, start: u32, opcode: Opcode) { + if !w.ok { return } + count := (w.pos - start) / 4 + intrinsics.unaligned_store(cast(^u32)&w.code[start], inst_head(count, opcode)) +} + +// ----------------------------------------------------------------------------- +// Header + preamble sections +// ----------------------------------------------------------------------------- + +@(private="file") +emit_header :: proc "contextless" (w: ^Writer, m: ^Module) { + w_word(w, MAGIC) + w_word(w, m.version != 0 ? m.version : VERSION_1_5) + w_word(w, m.generator) + w_word(w, m.bound) // exclusive upper bound on s (caller-set / lowered) + w_word(w, 0) // schema +} + +@(private="file") +emit_preamble :: proc "contextless" (w: ^Writer, m: ^Module) { + for cap in m.capabilities { + s := inst_begin(w); w_word(w, u32(cap)); inst_end(w, s, .OpCapability) + } + for ext in m.extensions { + s := inst_begin(w); w_string(w, ext); inst_end(w, s, .OpExtension) + } + for ei in m.ext_imports { + s := inst_begin(w); w_id(w, ei.result); w_string(w, ei.name); inst_end(w, s, .OpExtInstImport) + } + { + s := inst_begin(w); w_word(w, u32(m.addressing)); w_word(w, u32(m.memory)); inst_end(w, s, .OpMemoryModel) + } + for ep in m.entry_points { + s := inst_begin(w) + w_word(w, u32(ep.model)); w_id(w, ep.function); w_string(w, ep.name) + for iface in ep.interface { w_id(w, iface) } + inst_end(w, s, .OpEntryPoint) + } + for em in m.exec_modes { + s := inst_begin(w) + w_id(w, em.entry); w_word(w, u32(em.mode)) + for op in em.operands { w_word(w, op) } + inst_end(w, s, em.is_id ? .OpExecutionModeId : .OpExecutionMode) + } +} + +@(private="file") +emit_debug :: proc "contextless" (w: ^Writer, m: ^Module) { + for str in m.debug.strings { + s := inst_begin(w); w_id(w, str.result); w_string(w, str.text); inst_end(w, s, .OpString) + } + if m.debug.source_language != 0 { + s := inst_begin(w) + w_word(w, m.debug.source_language); w_word(w, m.debug.source_version) + if m.debug.source_file != ID_NONE { w_id(w, m.debug.source_file) } + inst_end(w, s, .OpSource) + } + for nm in m.debug.names { + s := inst_begin(w) + w_id(w, nm.target) + if nm.member != MEMBER_NONE { + w_word(w, nm.member); w_string(w, nm.text); inst_end(w, s, .OpMemberName) + } else { + w_string(w, nm.text); inst_end(w, s, .OpName) + } + } +} + +@(private="file") +emit_annotations :: proc "contextless" (w: ^Writer, m: ^Module) { + for d in m.decorations { + s := inst_begin(w) + w_id(w, d.target) + if d.member != MEMBER_NONE { + w_word(w, d.member); w_word(w, u32(d.decoration)) + for op in d.operands { w_word(w, op) } + inst_end(w, s, .OpMemberDecorate) + } else { + w_word(w, u32(d.decoration)) + for op in d.operands { w_word(w, op) } + inst_end(w, s, .OpDecorate) + } + } +} + +// ----------------------------------------------------------------------------- +// 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 :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (byte_count: u32, ok: bool) { + m := m + w := Writer{code = code, ok = true} + emit_header(&w, &m) + emit_preamble(&w, &m) + emit_debug(&w, &m) + emit_annotations(&w, &m) + // TODO(codec): emit_types_constants_globals + emit_functions (the lowered body). + return w.pos, w.ok +}