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 <id> + type lowering) are next.
This commit is contained in:
Brendan Punsky
2026-06-26 07:33:01 -04:00
committed by Flāvius
parent d027fbf938
commit 18f9f141e4

View File

@@ -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 <id>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 <id> 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
}