rexcode/ir/spirv: round-trip opaque types (image/sampler/event/...) verbatim

Type-defining instructions the codec doesn't model structurally -- OpTypeImage,
OpTypeSampler, OpTypeSampledImage, OpTypeMatrix, OpTypeOpaque, OpTypeEvent,
OpTypeDeviceEvent, OpTypeReserveId, OpTypeQueue, OpTypePipe -- decode to
Type{.OPAQUE} plus an Opaque_Info{opcode, words} side entry (the operand words
after the result <id>) and re-emit verbatim. One generic fallback covers every
opaque SPIR-V type byte-exact, with no per-type fields.

Test: image_type (OpTypeImage, 7 operands) round-trips -> 9 passed.
This commit is contained in:
Brendan Punsky
2026-06-26 12:11:25 -04:00
committed by Flāvius
parent ac1d51a0cd
commit c2a5091336
4 changed files with 81 additions and 1 deletions

View File

@@ -50,6 +50,7 @@ Decoder :: struct {
functions: [dynamic]Function,
function_ids: [dynamic]Id,
defs: [dynamic]Def, // type/constant/global definition order
opaque_info: [dynamic]Opaque_Info, // parallel to types; set for OPAQUE
// in-flight function / block
in_fn: bool,
@@ -111,6 +112,17 @@ add_type :: proc(d: ^Decoder, id: Id, t: Type) {
append(&d.defs, Def{.TYPE, u32(len(d.types))})
append(&d.types, t)
append(&d.type_ids, id)
append(&d.opaque_info, Opaque_Info{}) // stay parallel; filled for OPAQUE
}
// A type-defining instruction the codec doesn't model: store it as OPAQUE plus
// its verbatim operand words (after the result <id>) so it re-emits exactly.
@(private="file")
add_opaque :: proc(d: ^Decoder, opcode: Opcode, w: []u32) {
add_type(d, Id(w[0]), Type{kind = .OPAQUE})
words := make([]u32, len(w) - 1)
copy(words, w[1:])
d.opaque_info[len(d.opaque_info) - 1] = Opaque_Info{opcode = opcode, words = words}
}
@(private="file")
@@ -238,6 +250,10 @@ lower :: proc(d: ^Decoder, opcode: Opcode, w: []u32) {
for j in 0 ..< nparam { fields[j] = tref(d, w[2 + j]) }
fields[nparam] = tref(d, w[1]) // return type last: fields = params ++ [result]
add_type(d, Id(w[0]), Type{kind = .FUNCTION, fields = fields, count = u32(nparam)})
case .OpTypeMatrix, .OpTypeImage, .OpTypeSampler, .OpTypeSampledImage,
.OpTypeOpaque, .OpTypeEvent, .OpTypeDeviceEvent, .OpTypeReserveId,
.OpTypeQueue, .OpTypePipe:
add_opaque(d, opcode, w) // captured verbatim -> OPAQUE
case .OpConstant:
c := Constant{result = {Id(w[1]), tref(d, w[0])}, opcode = opcode, value = u64(w[2])}
@@ -348,6 +364,7 @@ decode :: proc(data: []u8, m: ^Module, errors: ^[dynamic]Error, allocator := con
m.functions = d.functions[:]
m.function_ids = d.function_ids[:]
m.defs = d.defs[:]
m.opaque_info = d.opaque_info[:]
return u32(nwords * 4), true
}

View File

@@ -194,7 +194,16 @@ emit_one_type :: proc "contextless" (w: ^Writer, m: ^Module, i: int) {
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:
case .OPAQUE:
// re-emit the captured instruction verbatim (image/sampler/event/...).
info := i < len(m.opaque_info) ? m.opaque_info[i] : Opaque_Info{}
if info.opcode == .OpNop {
w.pos = s // no recorded detail; cannot lower
return
}
for word in info.words { w_word(w, word) }
op = info.opcode
case .REF:
w.pos = s // rewind the placeholder; not yet lowered
return
}

View File

@@ -63,6 +63,14 @@ Module :: struct {
// encode replays it for byte-exact, spec-valid output. Empty => encode falls
// back to all-types, then all-constants, then all-globals.
defs: []Def,
// --- Opaque type detail ---
// Verbatim operand words for type-defining instructions the codec does not
// model structurally (OpTypeImage / Sampler / SampledImage / Matrix / Event /
// Pipe / ...). Parallel to base.types; the entry is meaningful only where
// types[i].kind == OPAQUE. Lets any such type round-trip byte-exact without a
// dedicated field per SPIR-V opaque kind.
opaque_info: []Opaque_Info,
}
// A node in Module.defs: which of the three definition arrays, and its index.
@@ -72,6 +80,13 @@ Def :: struct {
index: u32,
}
// An unmodeled type-defining instruction, captured verbatim (the operand words
// after its result <id>) so it re-emits exactly.
Opaque_Info :: struct {
opcode: Opcode,
words: []u32,
}
// Member index sentinel: a whole-target decoration / name (OpDecorate / OpName)
// rather than a struct member one (OpMemberDecorate / OpMemberName).
MEMBER_NONE :: u32(0xFFFF_FFFF)

View File

@@ -235,6 +235,45 @@ main :: proc() {
roundtrip("runtime_array", m)
}
// (8) OpPhi -- SPIR-V's SSA merge (variadic value/parent-block pairs). The
// generic codec round-trips it with no special handling; non-entry blocks
// use OpPhi, not block params, so this is the whole story.
{
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)}
phi := spirv.Operation{ // %6 = OpPhi %int %7 %8 (value %7 from block %8)
opcode = u16(spirv.Opcode.OpPhi), result = {spirv.Id(6), spirv.Type_Ref(1)},
operands = {spirv.op_value(spirv.Id(7)), spirv.op_value(spirv.Id(8))},
}
m.functions = {
{signature = spirv.Type_Ref(2), blocks = {
{id = spirv.Id(5), ops = {phi, {opcode = u16(spirv.Opcode.OpReturn)}}},
}},
}
m.function_ids = {spirv.Id(4)}
roundtrip("phi", m)
}
// (9) an opaque type captured verbatim: OpTypeImage (sampled type + 6 enum/
// literal operands) round-trips through the Opaque_Info side table, with no
// per-image-field modelling.
{
m := spirv.make_module()
m.capabilities = {.Shader}
m.types = {{kind = .FLOAT, bits = 32}, {kind = .OPAQUE}}
m.type_ids = {spirv.Id(1), spirv.Id(2)}
m.opaque_info = {
{},
{opcode = .OpTypeImage, words = {1, 1, 0, 0, 0, 1, 0}}, // %1, Dim 2D, ..., Unknown
}
roundtrip("image_type", m)
}
fmt.printf("\n%d passed, %d failed\n", ok_count, fail_count)
if fail_count > 0 { os.exit(1) }
}