rexcode/ir/spirv: codec round-trip test suite

A tests/ package (the ISA test convention) that, per module shape, does
encode -> decode -> re-encode and asserts byte-identical output -- exercising the
encoder, decoder, <id> side tables, and the generic operation codec together:

  void_main      header + preamble + a void function (116 B)
  int_constants  i32 type + two scalar OpConstants incl. 0xDEADBEEF (88 B)
  iadd_function  %r = OpIAdd %i32 %a %b ; OpReturn -- generic operand codec
                 with a result + value operands (164 B)

3 passed. Run: odin run core/rexcode/ir/spirv/tests. (build.lua is ISA-centric;
wiring the ir/ packages into the gen/check/test pipeline is a follow-up.)
This commit is contained in:
Brendan Punsky
2026-06-26 09:43:59 -04:00
committed by Flāvius
parent be65df38c1
commit ded7a2dea0

View File

@@ -0,0 +1,130 @@
// rexcode · Brendan Punsky (dotbmp@github), original author
// SPIR-V codec round-trip suite. For each module shape: encode -> decode ->
// re-encode and assert the two encodings are byte-identical. A byte-exact
// round-trip exercises the encoder, the decoder, the <id> side tables, and the
// generic table-driven operation codec all at once: any drift in operand order,
// id mapping, or section layout shows up as a word mismatch.
package rexcode_spirv_tests
import "core:fmt"
import "core:os"
import "core:slice"
import spirv ".."
ok_count: int
fail_count: int
@(private="file")
roundtrip :: proc(name: string, m: spirv.Module) {
buf1 := make([]u8, 64 * 1024, context.temp_allocator)
buf2 := make([]u8, 64 * 1024, context.temp_allocator)
relocs: [dynamic]spirv.Relocation; defer delete(relocs)
errors: [dynamic]spirv.Error; defer delete(errors)
n1, ok1 := spirv.encode(m, buf1, &relocs, &errors)
if !ok1 {
fmt.printf(" [FAIL] %s: encode failed\n", name); fail_count += 1; return
}
m2: spirv.Module
derr: [dynamic]spirv.Error; defer delete(derr)
_, ok2 := spirv.decode(buf1[:n1], &m2, &derr, context.temp_allocator)
if !ok2 {
fmt.printf(" [FAIL] %s: decode failed\n", name); fail_count += 1; return
}
n2, ok3 := spirv.encode(m2, buf2, &relocs, &errors)
if !ok3 {
fmt.printf(" [FAIL] %s: re-encode failed\n", name); fail_count += 1; return
}
if n1 != n2 || !slice.equal(buf1[:n1], buf2[:n2]) {
fmt.printf(" [FAIL] %s: round-trip mismatch (n1=%d n2=%d)\n", name, n1, n2)
for i := u32(0); i < max(n1, n2); i += 4 {
w1 := i < n1 ? word_at(buf1, i) : 0
w2 := i < n2 ? word_at(buf2, i) : 0
if w1 != w2 { fmt.printf(" [%d] %08x != %08x\n", i / 4, w1, w2) }
}
fail_count += 1
return
}
fmt.printf(" [ok] %-22s %d bytes\n", name, n1)
ok_count += 1
}
@(private="file")
word_at :: proc(b: []u8, i: u32) -> u32 {
return u32(b[i]) | u32(b[i + 1]) << 8 | u32(b[i + 2]) << 16 | u32(b[i + 3]) << 24
}
main :: proc() {
fmt.println("==== SPIR-V codec round-trip ====")
// (1) a void compute entry point: header + preamble + a void function.
{
m := spirv.make_module()
m.capabilities = {.Shader}
m.entry_points = {{model = .GLCompute, function = spirv.Id(4), name = "main"}}
m.types = {
{kind = .VOID},
{kind = .FUNCTION, fields = {spirv.Type_Ref(0)}, count = 0},
}
m.type_ids = {spirv.Id(2), spirv.Id(3)}
m.functions = {
{signature = spirv.Type_Ref(1), blocks = {
{id = spirv.Id(5), ops = {{opcode = u16(spirv.Opcode.OpReturn)}}},
}},
}
m.function_ids = {spirv.Id(4)}
m.bound = 6
roundtrip("void_main", m)
}
// (2) an int type + two scalar constants (the constant pool + OpConstant width).
{
m := spirv.make_module()
m.capabilities = {.Shader}
m.types = {{kind = .INT, bits = 32, aux = 1}} // signed i32
m.type_ids = {spirv.Id(1)}
m.constants = {
{result = {spirv.Id(2), spirv.Type_Ref(0)}, opcode = .OpConstant, value = 10},
{result = {spirv.Id(3), spirv.Type_Ref(0)}, opcode = .OpConstant, value = 0xDEAD_BEEF},
}
m.bound = 4
roundtrip("int_constants", m)
}
// (3) a function body with a real operation (the generic operand codec):
// %6 = OpIAdd %i32 %const_a %const_b ; OpReturn
{
m := spirv.make_module()
m.capabilities = {.Shader}
m.types = {
{kind = .VOID},
{kind = .INT, bits = 32, aux = 1},
{kind = .FUNCTION, fields = {spirv.Type_Ref(0)}, count = 0},
}
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 = 10},
{result = {spirv.Id(5), spirv.Type_Ref(1)}, opcode = .OpConstant, value = 20},
}
add := spirv.Operation{
opcode = u16(spirv.Opcode.OpIAdd),
result = {spirv.Id(6), spirv.Type_Ref(1)},
operands = {spirv.op_value(spirv.Id(4)), spirv.op_value(spirv.Id(5))},
}
m.functions = {
{signature = spirv.Type_Ref(2), blocks = {
{id = spirv.Id(8), ops = {add, {opcode = u16(spirv.Opcode.OpReturn)}}},
}},
}
m.function_ids = {spirv.Id(7)}
m.bound = 9
roundtrip("iadd_function", m)
}
fmt.printf("\n%d passed, %d failed\n", ok_count, fail_count)
if fail_count > 0 { os.exit(1) }
}