package shader import "core:fmt" // SPIR-V 1.5 binary backend — emits from IR_Module // Uses function-scoped OpVariable + OpStore/OpLoad instead of SSA phi nodes. // This produces valid SPIR-V; GPU drivers handle mem2reg. SPIRV_Loop_Context :: struct { continue_label: u32, merge_label: u32, } SPIRV_Builder :: struct { next_id: u32, module: ^IR_Module, current_fn: ^IR_Function, // Section buffers — concatenated in spec order capabilities: [dynamic]u32, extensions: [dynamic]u32, ext_imports: [dynamic]u32, mem_model: [dynamic]u32, entry_points: [dynamic]u32, exec_modes: [dynamic]u32, debug_source: [dynamic]u32, // OpString, OpSource (before OpName) debug_names: [dynamic]u32, // OpName, OpMemberName debug_process: [dynamic]u32, // OpModuleProcessed (after OpName) annotations: [dynamic]u32, type_section: [dynamic]u32, // types + constants + global vars func_section: [dynamic]u32, // Caches type_cache: map[string]u32, // type key string -> type ID const_cache: map[string]u32, // const key string -> const ID ptr_cache: map[string]u32, // "storage_class:type_id" -> pointer type ID wrapped_bindings: map[string]bool, // bindings wrapped in synthetic Block struct (non-struct uniforms) decorated_structs: map[u32]bool, // struct type IDs already decorated with offsets ssa_values: map[IR_Var_Id]u32, // let-bound variables kept as SSA values (not Function variables) // Tracking glsl_ext_id: u32, void_type_id: u32, // Function ID tracking (pre-allocated for forward references) function_ids: map[string]u32, // function name -> function ID // Per-function state value_map: map[IR_Var_Id]u32, // variable id -> pointer ID (function-scoped OpVariable) binding_ids: map[string]u32, // binding name -> variable ID input_ids: map[string]u32, // input io name -> variable ID output_ids: map[string]u32, // output io name -> variable ID builtin_input_ids: map[string]u32, // builtin name -> variable ID builtin_output_ids: map[string]u32, // builtin name -> variable ID shared_var_ids: map[string]u32, // shared var name -> variable ID // Loop context stack for break/continue loop_stack: [dynamic]SPIRV_Loop_Context, // Interface variable IDs for OpEntryPoint interface_ids: [dynamic]u32, // Function-scoped OpVariable buffer (must be emitted first in entry block) var_buffer: [dynamic]u32, // Debug info debug: bool, source_file: string, source_file_id: u32, last_emitted_line: int, diagnostics: [dynamic]Diagnostic, } emit_spirv :: proc(module: ^IR_Module, debug := false, source_file := "", allocator := context.allocator) -> ([]u8, []Diagnostic) { b := SPIRV_Builder{ next_id = 1, module = module, capabilities = make([dynamic]u32, allocator), extensions = make([dynamic]u32, allocator), ext_imports = make([dynamic]u32, allocator), mem_model = make([dynamic]u32, allocator), entry_points = make([dynamic]u32, allocator), exec_modes = make([dynamic]u32, allocator), debug_source = make([dynamic]u32, allocator), debug_names = make([dynamic]u32, allocator), debug_process = make([dynamic]u32, allocator), annotations = make([dynamic]u32, allocator), type_section = make([dynamic]u32, allocator), func_section = make([dynamic]u32, allocator), type_cache = make(map[string]u32, allocator = allocator), const_cache = make(map[string]u32, allocator = allocator), ptr_cache = make(map[string]u32, allocator = allocator), wrapped_bindings = make(map[string]bool, allocator = allocator), function_ids = make(map[string]u32, allocator = allocator), binding_ids = make(map[string]u32, allocator = allocator), input_ids = make(map[string]u32, allocator = allocator), output_ids = make(map[string]u32, allocator = allocator), builtin_input_ids = make(map[string]u32, allocator = allocator), builtin_output_ids = make(map[string]u32, allocator = allocator), shared_var_ids = make(map[string]u32, allocator = allocator), interface_ids = make([dynamic]u32, allocator), diagnostics = make([dynamic]Diagnostic, allocator), } b.debug = debug b.source_file = source_file // Preamble spirv_emit_capability(&b, SpvCapability_Shader) b.glsl_ext_id = spirv_alloc_id(&b) spirv_emit_ext_inst_import(&b, b.glsl_ext_id, "GLSL.std.450") spirv_emit_memory_model(&b, SpvAddressingModel_Logical, SpvMemoryModel_GLSL450) // Debug info (if enabled) if b.debug { // OpString for source filename (goes before OpName) b.source_file_id = spirv_alloc_id(&b) file_str := b.source_file if b.source_file != "" else "" spirv_encode_inst_str(&b.debug_source, SpvOp_String, {b.source_file_id}, file_str) // OpSource SourceLanguage_Unknown version file (goes before OpName) spirv_encode_inst(&b.debug_source, SpvOp_Source, SpvSourceLanguage_Unknown, 100, b.source_file_id) // OpModuleProcessed (goes after OpName/OpMemberName) spirv_encode_inst_str(&b.debug_process, SpvOp_ModuleProcessed, {}, "Luma Compiler v0.1") } // Pre-create void type b.void_type_id = spirv_get_or_create_type(&b, nil) // Emit specialization constants for &sc in module.spec_constants { spirv_emit_spec_constant(&b, &sc) } // Emit global bindings for &bind in module.bindings { spirv_emit_binding(&b, &bind) } // Emit shared variables (workgroup memory) for sv in module.shared_vars { spirv_emit_shared_var(&b, sv) } // Pre-allocate function IDs for forward references in function calls for &fn in module.functions { if !fn.is_entry { b.function_ids[fn.name] = spirv_alloc_id(&b) } } // Emit functions for &fn in module.functions { spirv_emit_function(&b, &fn) } // Assemble final binary result := spirv_assemble(&b) return result, b.diagnostics[:] } // -- ID allocation -- @(private = "file") spirv_alloc_id :: proc(b: ^SPIRV_Builder) -> u32 { id := b.next_id b.next_id += 1 return id } // -- Instruction encoding -- @(private = "file") spirv_encode_inst :: proc(section: ^[dynamic]u32, opcode: u32, operands: ..u32) { word_count := u32(1 + len(operands)) append(section, (word_count << 16) | opcode) for op in operands { append(section, op) } } @(private = "file") spirv_encode_inst_str :: proc(section: ^[dynamic]u32, opcode: u32, pre_operands: []u32, str: string, post_operands: []u32 = {}) { // String is null-terminated and padded to 4-byte boundary str_words := (len(str) + 4) / 4 // includes null terminator word_count := u32(1 + len(pre_operands) + str_words + len(post_operands)) append(section, (word_count << 16) | opcode) for op in pre_operands { append(section, op) } // Encode string as words spirv_encode_string(section, str) for op in post_operands { append(section, op) } } @(private = "file") spirv_encode_string :: proc(section: ^[dynamic]u32, str: string) { bytes := transmute([]u8)str i := 0 for i + 3 < len(bytes) { word := u32(bytes[i]) | (u32(bytes[i+1]) << 8) | (u32(bytes[i+2]) << 16) | (u32(bytes[i+3]) << 24) append(section, word) i += 4 } // Remaining bytes + null terminator word: u32 = 0 shift: uint = 0 for i < len(bytes) { word |= u32(bytes[i]) << shift shift += 8 i += 1 } // null terminator is already 0 in remaining bits append(section, word) } // -- Preamble -- @(private = "file") spirv_emit_capability :: proc(b: ^SPIRV_Builder, cap: u32) { spirv_encode_inst(&b.capabilities, SpvOp_Capability, cap) } @(private = "file") spirv_emit_ext_inst_import :: proc(b: ^SPIRV_Builder, result_id: u32, name: string) { spirv_encode_inst_str(&b.ext_imports, SpvOp_ExtInstImport, {result_id}, name) } @(private = "file") spirv_emit_memory_model :: proc(b: ^SPIRV_Builder, addressing: u32, memory: u32) { spirv_encode_inst(&b.mem_model, SpvOp_MemoryModel, addressing, memory) } // -- Type system -- @(private = "file") spirv_get_or_create_type :: proc(b: ^SPIRV_Builder, t: ^Resolved_Type) -> u32 { key := spirv_type_key(t) if id, ok := b.type_cache[key]; ok { return id } id := spirv_alloc_id(b) b.type_cache[key] = id if t == nil { spirv_encode_inst(&b.type_section, SpvOp_TypeVoid, id) return id } #partial switch v in t^ { case Type_Void: spirv_encode_inst(&b.type_section, SpvOp_TypeVoid, id) case Type_Scalar: switch v.kind { case .Bool: spirv_encode_inst(&b.type_section, SpvOp_TypeBool, id) case .Int: spirv_encode_inst(&b.type_section, SpvOp_TypeInt, id, 32, 1) case .Uint: spirv_encode_inst(&b.type_section, SpvOp_TypeInt, id, 32, 0) case .Float: spirv_encode_inst(&b.type_section, SpvOp_TypeFloat, id, 32) case .Half: spirv_encode_inst(&b.type_section, SpvOp_TypeFloat, id, 16) } case Type_Vector: elem_type := make_type(Type_Scalar{kind = v.elem}) elem_id := spirv_get_or_create_type(b, elem_type) spirv_encode_inst(&b.type_section, SpvOp_TypeVector, id, elem_id, u32(v.size)) case Type_Matrix: col_type := make_type(Type_Vector{elem = v.elem, size = v.rows}) col_id := spirv_get_or_create_type(b, col_type) spirv_encode_inst(&b.type_section, SpvOp_TypeMatrix, id, col_id, u32(v.cols)) case Type_Struct_Resolved: member_ids := make([dynamic]u32) append(&member_ids, id) for f in v.fields { append(&member_ids, spirv_get_or_create_type(b, f.type)) } spirv_encode_inst(&b.type_section, SpvOp_TypeStruct, ..member_ids[:]) // Debug names for struct members spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {id}, v.name) for f, i in v.fields { spirv_encode_inst_str(&b.debug_names, SpvOp_MemberName, {id, u32(i)}, f.name) } case Type_Array_Resolved: elem_id := spirv_get_or_create_type(b, v.elem) if v.size > 0 { // Fixed-size array: needs a constant for the length int_type := spirv_get_or_create_type(b, TYPE_UINT) length_id := spirv_get_or_create_const_int(b, int_type, u32(v.size)) spirv_encode_inst(&b.type_section, SpvOp_TypeArray, id, elem_id, length_id) } else { spirv_encode_inst(&b.type_section, SpvOp_TypeRuntimeArray, id, elem_id) } case Type_Sampler: // Create image type, then sampled image type float_id := spirv_get_or_create_type(b, TYPE_FLOAT) image_id := spirv_alloc_id(b) dim := spirv_sampler_dim(v.kind) depth: u32 = v.kind == .Sampler2DShadow ? 1 : 0 arrayed: u32 = v.kind == .Sampler2DArray ? 1 : 0 // OpTypeImage: result_id, sampled_type, dim, depth, arrayed, multisampled, sampled, format spirv_encode_inst(&b.type_section, SpvOp_TypeImage, image_id, float_id, dim, depth, arrayed, 0, 1, SpvImageFormat_Unknown) spirv_encode_inst(&b.type_section, SpvOp_TypeSampledImage, id, image_id) } return id } @(private = "file") spirv_type_key :: proc(t: ^Resolved_Type) -> string { if t == nil do return "void" #partial switch v in t^ { case Type_Void: return "void" case Type_Scalar: switch v.kind { case .Bool: return "bool" case .Int: return "int32" case .Uint: return "uint32" case .Float: return "float32" case .Half: return "float16" } case Type_Vector: return fmt.aprintf("vec_%v_%d", v.elem, v.size) case Type_Matrix: return fmt.aprintf("mat_%v_%d_%d", v.elem, v.cols, v.rows) case Type_Struct_Resolved: return fmt.aprintf("struct_%s", v.name) case Type_Array_Resolved: return fmt.aprintf("arr_%s_%d", spirv_type_key(v.elem), v.size) case Type_Sampler: return fmt.aprintf("sampler_%v", v.kind) } return "unknown" } @(private = "file") spirv_get_ptr_type :: proc(b: ^SPIRV_Builder, storage_class: u32, pointee_type: u32) -> u32 { key := fmt.aprintf("%d:%d", storage_class, pointee_type) if id, ok := b.ptr_cache[key]; ok { return id } id := spirv_alloc_id(b) b.ptr_cache[key] = id spirv_encode_inst(&b.type_section, SpvOp_TypePointer, id, storage_class, pointee_type) return id } @(private = "file") spirv_sampler_dim :: proc(kind: Sampler_Kind) -> u32 { switch kind { case .Sampler2D, .Sampler2DShadow, .Sampler2DArray: return SpvDim_2D case .Sampler3D: return SpvDim_3D case .SamplerCube: return SpvDim_Cube } return SpvDim_2D } // -- Constants -- @(private = "file") spirv_get_or_create_const_float :: proc(b: ^SPIRV_Builder, type_id: u32, value: f64) -> u32 { key := fmt.aprintf("f:%d:%v", type_id, value) if id, ok := b.const_cache[key]; ok { return id } id := spirv_alloc_id(b) b.const_cache[key] = id bits := transmute(u32)f32(value) spirv_encode_inst(&b.type_section, SpvOp_Constant, type_id, id, bits) return id } @(private = "file") spirv_get_or_create_const_int :: proc(b: ^SPIRV_Builder, type_id: u32, value: u32) -> u32 { key := fmt.aprintf("i:%d:%d", type_id, value) if id, ok := b.const_cache[key]; ok { return id } id := spirv_alloc_id(b) b.const_cache[key] = id spirv_encode_inst(&b.type_section, SpvOp_Constant, type_id, id, value) return id } @(private = "file") spirv_get_or_create_const_bool :: proc(b: ^SPIRV_Builder, type_id: u32, value: bool) -> u32 { key := fmt.aprintf("b:%d:%v", type_id, value) if id, ok := b.const_cache[key]; ok { return id } id := spirv_alloc_id(b) b.const_cache[key] = id op := value ? u32(SpvOp_ConstantTrue) : u32(SpvOp_ConstantFalse) spirv_encode_inst(&b.type_section, op, type_id, id) return id } // -- Decorations -- @(private = "file") spirv_decorate :: proc(b: ^SPIRV_Builder, target: u32, decoration: u32, operands: ..u32) { args := make([dynamic]u32) append(&args, target, decoration) for op in operands { append(&args, op) } spirv_encode_inst(&b.annotations, SpvOp_Decorate, ..args[:]) } @(private = "file") spirv_member_decorate :: proc(b: ^SPIRV_Builder, struct_id: u32, member: u32, decoration: u32, operands: ..u32) { args := make([dynamic]u32) append(&args, struct_id, member, decoration) for op in operands { append(&args, op) } spirv_encode_inst(&b.annotations, SpvOp_MemberDecorate, ..args[:]) } // -- Specialization Constants -- @(private = "file") spirv_emit_spec_constant :: proc(b: ^SPIRV_Builder, sc: ^IR_Spec_Constant) { type_id := spirv_get_or_create_type(b, sc.type) sc_id := spirv_alloc_id(b) switch v in sc.default_value { case bool: if v { spirv_encode_inst(&b.type_section, SpvOp_SpecConstantTrue, type_id, sc_id) } else { spirv_encode_inst(&b.type_section, SpvOp_SpecConstantFalse, type_id, sc_id) } case i64: spirv_encode_inst(&b.type_section, SpvOp_SpecConstant, type_id, sc_id, u32(v)) case f64: bits := transmute(u32)f32(v) spirv_encode_inst(&b.type_section, SpvOp_SpecConstant, type_id, sc_id, bits) case: // Default to 0 spirv_encode_inst(&b.type_section, SpvOp_SpecConstant, type_id, sc_id, 0) } spirv_decorate(b, sc_id, SpvDecoration_SpecId, u32(sc.spec_id)) spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {sc_id}, sc.name) // Store ID so expressions can reference it b.const_cache[fmt.aprintf("spec_%s", sc.name)] = sc_id } // -- Bindings -- @(private = "file") spirv_emit_binding :: proc(b: ^SPIRV_Builder, bind: ^IR_Binding) { switch bind.kind { case .Uniform: if bind.struct_ref != nil { // Struct-backed uniform block struct_type_id := spirv_get_or_create_type(b, make_type(bind.struct_ref^)) spirv_decorate(b, struct_type_id, SpvDecoration_Block) spirv_decorate_struct_offsets(b, struct_type_id, bind.struct_ref) ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_Uniform, struct_type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_Uniform) spirv_decorate(b, var_id, SpvDecoration_DescriptorSet, u32(bind.group)) spirv_decorate(b, var_id, SpvDecoration_Binding, u32(bind.binding_num)) spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {var_id}, bind.name) b.binding_ids[bind.name] = var_id } else { // Non-struct uniform (e.g. uniform light_dir: vec3) — wrap in synthetic Block struct inner_type_id := spirv_get_or_create_type(b, bind.type) wrapper_key := fmt.aprintf("_wrap_%s_%d", bind.name, inner_type_id) wrapper_type_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_TypeStruct, wrapper_type_id, inner_type_id) spirv_decorate(b, wrapper_type_id, SpvDecoration_Block) spirv_member_decorate(b, wrapper_type_id, 0, SpvDecoration_Offset, 0) spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {wrapper_type_id}, wrapper_key) ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_Uniform, wrapper_type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_Uniform) spirv_decorate(b, var_id, SpvDecoration_DescriptorSet, u32(bind.group)) spirv_decorate(b, var_id, SpvDecoration_Binding, u32(bind.binding_num)) spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {var_id}, bind.name) b.binding_ids[bind.name] = var_id b.wrapped_bindings[bind.name] = true } case .Buffer: if bind.struct_ref == nil do return struct_type_id := spirv_get_or_create_type(b, make_type(bind.struct_ref^)) spirv_decorate(b, struct_type_id, SpvDecoration_Block) spirv_decorate_struct_offsets(b, struct_type_id, bind.struct_ref) ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_StorageBuffer, struct_type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_StorageBuffer) spirv_decorate(b, var_id, SpvDecoration_DescriptorSet, u32(bind.group)) spirv_decorate(b, var_id, SpvDecoration_Binding, u32(bind.binding_num)) spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {var_id}, bind.name) b.binding_ids[bind.name] = var_id case .Texture: // Sampled image variable type_id := spirv_get_or_create_type(b, bind.type) ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_UniformConstant, type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_UniformConstant) spirv_decorate(b, var_id, SpvDecoration_DescriptorSet, u32(bind.group)) spirv_decorate(b, var_id, SpvDecoration_Binding, u32(bind.binding_num)) spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {var_id}, bind.name) b.binding_ids[bind.name] = var_id case .Sampler: // Skip — combined with texture in SPIR-V sampled image // The Texture binding's type is already SampledImage return case .Push_Constant: if bind.struct_ref == nil do return struct_type_id := spirv_get_or_create_type(b, make_type(bind.struct_ref^)) spirv_decorate(b, struct_type_id, SpvDecoration_Block) spirv_decorate_struct_offsets(b, struct_type_id, bind.struct_ref) ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_PushConstant, struct_type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_PushConstant) // Push constants have no descriptor set or binding decorations spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {var_id}, bind.name) b.binding_ids[bind.name] = var_id } } @(private = "file") spirv_emit_shared_var :: proc(b: ^SPIRV_Builder, sv: IR_Shared_Var) { type_id := spirv_get_or_create_type(b, sv.type) ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_Workgroup, type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_Workgroup) b.shared_var_ids[sv.name] = var_id spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {var_id}, sv.name) } @(private = "file") spirv_emit_constant_u32 :: proc(b: ^SPIRV_Builder, value: u32) -> u32 { key := fmt.aprintf("u32_%d", value) if cached, ok := b.const_cache[key]; ok { return cached } uint_type_id := spirv_get_or_create_type(b, TYPE_UINT) const_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_Constant, uint_type_id, const_id, value) b.const_cache[key] = const_id return const_id } @(private = "file") spirv_decorate_struct_offsets :: proc(b: ^SPIRV_Builder, struct_type_id: u32, s: ^Type_Struct_Resolved) { if struct_type_id in b.decorated_structs do return b.decorated_structs[struct_type_id] = true offset: u32 = 0 for f, i in s.fields { align := spirv_type_alignment(f.type) offset = (offset + align - 1) & ~(align - 1) spirv_member_decorate(b, struct_type_id, u32(i), SpvDecoration_Offset, offset) // Matrix decorations if f.type != nil { if m, ok := f.type^.(Type_Matrix); ok { spirv_member_decorate(b, struct_type_id, u32(i), SpvDecoration_ColMajor) stride := spirv_type_size_scalar(make_type(Type_Vector{elem = m.elem, size = m.rows})) stride = (stride + 15) & ~u32(15) // round up to vec4 alignment for std140 spirv_member_decorate(b, struct_type_id, u32(i), SpvDecoration_MatrixStride, stride) } // Nested struct: recursively decorate member offsets if nested, ok := f.type^.(Type_Struct_Resolved); ok { nested_type_id := spirv_get_or_create_type(b, f.type) spirv_decorate_struct_offsets(b, nested_type_id, &nested) } // Array stride decoration (required for arrays in Block-decorated structs) if arr, ok := f.type^.(Type_Array_Resolved); ok { arr_type_id := spirv_get_or_create_type(b, f.type) elem_stride := spirv_type_size(arr.elem) // Align to element alignment (std140: round up to 16 for vec/mat, 4 for scalar) elem_align := spirv_type_alignment(arr.elem) elem_stride = (elem_stride + elem_align - 1) & ~(elem_align - 1) spirv_decorate(b, arr_type_id, SpvDecoration_ArrayStride, elem_stride) // Decorate element struct members if array of structs if nested, ok2 := arr.elem^.(Type_Struct_Resolved); ok2 { nested_type_id := spirv_get_or_create_type(b, arr.elem) spirv_decorate_struct_offsets(b, nested_type_id, &nested) } // Matrix decorations for arrays of matrices (e.g. [4]mat4) if m, ok2 := arr.elem^.(Type_Matrix); ok2 { spirv_member_decorate(b, struct_type_id, u32(i), SpvDecoration_ColMajor) stride := spirv_type_size_scalar(make_type(Type_Vector{elem = m.elem, size = m.rows})) stride = (stride + 15) & ~u32(15) spirv_member_decorate(b, struct_type_id, u32(i), SpvDecoration_MatrixStride, stride) } } } offset += spirv_type_size(f.type) } } @(private = "file") spirv_type_alignment :: proc(t: ^Resolved_Type) -> u32 { if t == nil do return 4 #partial switch v in t^ { case Type_Scalar: return 4 case Type_Vector: switch v.size { case 2: return 8 case 3, 4: return 16 } case Type_Matrix: return 16 case Type_Struct_Resolved: return 16 case Type_Array_Resolved: return 16 case: return 4 } return 4 } @(private = "file") spirv_type_size :: proc(t: ^Resolved_Type) -> u32 { if t == nil do return 0 #partial switch v in t^ { case Type_Scalar: if v.kind == .Half do return 2 return 4 case Type_Vector: elem_size: u32 = v.elem == .Half ? 2 : 4 return elem_size * u32(v.size) case Type_Matrix: col_size := spirv_type_size(make_type(Type_Vector{elem = v.elem, size = v.rows})) col_stride := (col_size + 15) & ~u32(15) // std140 column stride return col_stride * u32(v.cols) case Type_Struct_Resolved: total: u32 = 0 for f in v.fields { align := spirv_type_alignment(f.type) total = (total + align - 1) & ~(align - 1) total += spirv_type_size(f.type) } return total case Type_Array_Resolved: elem_sz := spirv_type_size(v.elem) elem_align := spirv_type_alignment(v.elem) elem_stride := (elem_sz + elem_align - 1) & ~(elem_align - 1) return elem_stride * u32(v.size) case: return 4 } return 0 } @(private = "file") spirv_type_size_scalar :: proc(t: ^Resolved_Type) -> u32 { if t == nil do return 0 #partial switch v in t^ { case Type_Scalar: if v.kind == .Half do return 2 return 4 case Type_Vector: return (v.elem == .Half ? 2 : 4) * u32(v.size) case: return spirv_type_size(t) } return 0 } // -- Functions -- @(private = "file") spirv_emit_function :: proc(b: ^SPIRV_Builder, fn: ^IR_Function) { b.current_fn = fn b.value_map = make(map[IR_Var_Id]u32) if fn.is_entry { spirv_emit_entry_function(b, fn) } else { spirv_emit_helper_function(b, fn) } b.current_fn = nil } @(private = "file") spirv_emit_entry_function :: proc(b: ^SPIRV_Builder, fn: ^IR_Function) { // Create I/O variables b.input_ids = make(map[string]u32) b.output_ids = make(map[string]u32) b.builtin_input_ids = make(map[string]u32) b.builtin_output_ids = make(map[string]u32) b.interface_ids = make([dynamic]u32) for io in fn.inputs { type_id := spirv_get_or_create_type(b, io.type) ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_Input, type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_Input) spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {var_id}, io.name) if io.builtin != "" { spirv_decorate(b, var_id, SpvDecoration_BuiltIn, spirv_builtin_id(io.builtin, fn.stage)) b.builtin_input_ids[io.builtin] = var_id } else { spirv_decorate(b, var_id, SpvDecoration_Location, u32(io.location)) b.input_ids[io.name] = var_id } append(&b.interface_ids, var_id) } for io in fn.outputs { type_id := spirv_get_or_create_type(b, io.type) ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_Output, type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.type_section, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_Output) spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {var_id}, io.name) if io.builtin != "" { spirv_decorate(b, var_id, SpvDecoration_BuiltIn, spirv_builtin_id(io.builtin, fn.stage)) b.builtin_output_ids[io.builtin] = var_id } else { spirv_decorate(b, var_id, SpvDecoration_Location, u32(io.location)) b.output_ids[io.name] = var_id } append(&b.interface_ids, var_id) } // Add binding variables to interface list (required by SPIR-V 1.4+) for name, var_id in b.binding_ids { append(&b.interface_ids, var_id) } // Add shared variables to interface list for name, var_id in b.shared_var_ids { append(&b.interface_ids, var_id) } // Function type: void(void) fn_type_id := spirv_get_fn_type(b, b.void_type_id, {}) fn_id := spirv_alloc_id(b) spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {fn_id}, fn.name) // Entry point declaration exec_model := spirv_execution_model(fn.stage) spirv_emit_entry_point_inst(b, exec_model, fn_id, fn.name, b.interface_ids[:]) // Execution mode if fn.stage == .Fragment { spirv_encode_inst(&b.exec_modes, SpvOp_ExecutionMode, fn_id, SpvExecutionMode_OriginUpperLeft) } if fn.stage == .Compute { ws := fn.workgroup_size spirv_encode_inst(&b.exec_modes, SpvOp_ExecutionMode, fn_id, SpvExecutionMode_LocalSize, u32(ws[0] > 0 ? ws[0] : 1), u32(ws[1] > 0 ? ws[1] : 1), u32(ws[2] > 0 ? ws[2] : 1)) } // Function definition spirv_encode_inst(&b.func_section, SpvOp_Function, b.void_type_id, fn_id, SpvFunctionControl_None, fn_type_id) // Entry label entry_label := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Label, entry_label) // Reset var buffer for this function b.var_buffer = make([dynamic]u32) // Save position after label — var_buffer will be inserted here label_end := len(b.func_section) // Emit body spirv_emit_stmts(b, fn.body[:]) // Splice var_buffer right after the label (before body instructions) if len(b.var_buffer) > 0 { spirv_splice_vars(b, label_end) } // Implicit return (skip if body already terminates, e.g. discard/OpKill) if !spirv_block_has_terminator(fn.body[:]) { spirv_encode_inst(&b.func_section, SpvOp_Return) } spirv_encode_inst(&b.func_section, SpvOp_FunctionEnd) } @(private = "file") spirv_emit_entry_point_inst :: proc(b: ^SPIRV_Builder, exec_model: u32, fn_id: u32, name: string, interfaces: []u32) { str_words := (len(name) + 4) / 4 word_count := u32(3 + str_words + len(interfaces)) append(&b.entry_points, (word_count << 16) | u32(SpvOp_EntryPoint)) append(&b.entry_points, exec_model) append(&b.entry_points, fn_id) spirv_encode_string(&b.entry_points, name) for iface in interfaces { append(&b.entry_points, iface) } } @(private = "file") spirv_emit_helper_function :: proc(b: ^SPIRV_Builder, fn: ^IR_Function) { // Build parameter type list param_type_ids := make([dynamic]u32) for p in fn.params { append(¶m_type_ids, spirv_get_or_create_type(b, p.type)) } ret_type_id := spirv_get_or_create_type(b, fn.return_type) fn_type_id := spirv_get_fn_type(b, ret_type_id, param_type_ids[:]) // Use pre-allocated function ID for forward reference support fn_id := b.function_ids[fn.name] spirv_encode_inst_str(&b.debug_names, SpvOp_Name, {fn_id}, fn.name) spirv_encode_inst(&b.func_section, SpvOp_Function, ret_type_id, fn_id, SpvFunctionControl_None, fn_type_id) // Parameters — collect IDs, then copy into function-scoped variables after label Param_Info :: struct { id: u32, type_id: u32, var_id: IR_Var_Id } param_infos := make([dynamic]Param_Info) for p in fn.params { param_id := spirv_alloc_id(b) param_type_id := spirv_get_or_create_type(b, p.type) spirv_encode_inst(&b.func_section, SpvOp_FunctionParameter, param_type_id, param_id) append(¶m_infos, Param_Info{id = param_id, type_id = param_type_id, var_id = p.id}) } // Entry label entry_label := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Label, entry_label) b.var_buffer = make([dynamic]u32) label_end := len(b.func_section) // Copy parameters into function-scoped variables (SPIR-V requires OpLoad from pointers) for pi in param_infos { ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_Function, pi.type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.var_buffer, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_Function) spirv_encode_inst(&b.func_section, SpvOp_Store, var_id, pi.id) b.value_map[pi.var_id] = var_id } spirv_emit_stmts(b, fn.body[:]) if len(b.var_buffer) > 0 { spirv_splice_vars(b, label_end) } if fn.return_type == nil && !spirv_block_has_terminator(fn.body[:]) { spirv_encode_inst(&b.func_section, SpvOp_Return) } spirv_encode_inst(&b.func_section, SpvOp_FunctionEnd) } @(private = "file") spirv_get_fn_type :: proc(b: ^SPIRV_Builder, ret_type: u32, param_types: []u32) -> u32 { key := fmt.aprintf("fn_%d", ret_type) for pt in param_types { key = fmt.aprintf("%s_%d", key, pt) } if id, ok := b.type_cache[key]; ok { return id } id := spirv_alloc_id(b) b.type_cache[key] = id args := make([dynamic]u32) append(&args, id, ret_type) for pt in param_types { append(&args, pt) } spirv_encode_inst(&b.type_section, SpvOp_TypeFunction, ..args[:]) return id } // -- Statements -- @(private = "file") spirv_emit_stmts :: proc(b: ^SPIRV_Builder, stmts: []IR_Stmt) { for stmt in stmts { spirv_emit_stmt(b, stmt) } } @(private = "file") spirv_emit_stmt :: proc(b: ^SPIRV_Builder, stmt: IR_Stmt) { // Emit OpLine debug info if enabled if b.debug && b.source_file_id != 0 { span := ir_stmt_span(stmt) if span.line_start > 0 && span.line_start != b.last_emitted_line { spirv_encode_inst(&b.func_section, SpvOp_Line, b.source_file_id, u32(span.line_start), u32(max(span.col_start - 1, 0))) b.last_emitted_line = span.line_start } } switch s in stmt { case ^IR_Let: val_id := spirv_emit_expr(b, s.value) // Create function-scoped variable (deferred to entry block) and store type_id := spirv_get_or_create_type(b, s.type) if !s.mutable && type_id in b.decorated_structs { // Immutable struct types with Offset decorations can't be used in Function storage. // Keep as SSA value; field accesses will use OpCompositeExtract. b.ssa_values[s.id] = val_id } else { ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_Function, type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.var_buffer, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_Function) spirv_encode_inst(&b.func_section, SpvOp_Store, var_id, val_id) b.value_map[s.id] = var_id } case ^IR_Assign: val_id := spirv_emit_expr(b, s.value) ptr_id := spirv_emit_lvalue_ptr(b, s.target) if ptr_id != 0 { spirv_encode_inst(&b.func_section, SpvOp_Store, ptr_id, val_id) } case ^IR_Return: if s.value != nil { val_id := spirv_emit_expr(b, s.value) spirv_encode_inst(&b.func_section, SpvOp_ReturnValue, val_id) } else { spirv_encode_inst(&b.func_section, SpvOp_Return) } case ^IR_Store_Output: val_id := spirv_emit_expr(b, s.value) fn := b.current_fn if fn != nil && s.io_index >= 0 && s.io_index < len(fn.outputs) { io := fn.outputs[s.io_index] var_id: u32 if io.builtin != "" { var_id = b.builtin_output_ids[io.builtin] } else { var_id = b.output_ids[io.name] } if var_id != 0 { spirv_encode_inst(&b.func_section, SpvOp_Store, var_id, val_id) } } case ^IR_If: spirv_emit_if(b, s) case ^IR_For: spirv_emit_for(b, s) case ^IR_While: spirv_emit_while(b, s) case ^IR_Expr_Stmt: spirv_emit_expr(b, s.expr) // result unused case ^IR_Barrier: // OpControlBarrier execution=Workgroup, memory=Workgroup, semantics=WorkgroupMemory|AcquireRelease scope_wg_id := spirv_emit_constant_u32(b, u32(SpvScope_Workgroup)) semantics_id := spirv_emit_constant_u32(b, u32(SpvMemorySemantics_WorkgroupMemory | SpvMemorySemantics_AcquireRelease)) spirv_encode_inst(&b.func_section, SpvOp_ControlBarrier, scope_wg_id, scope_wg_id, semantics_id) case ^IR_Discard: spirv_encode_inst(&b.func_section, SpvOp_Kill) case ^IR_Break: ctx := b.loop_stack[len(b.loop_stack) - 1] spirv_encode_inst(&b.func_section, SpvOp_Branch, ctx.merge_label) case ^IR_Continue: ctx := b.loop_stack[len(b.loop_stack) - 1] spirv_encode_inst(&b.func_section, SpvOp_Branch, ctx.continue_label) } } // -- Control flow -- @(private = "file") spirv_emit_if :: proc(b: ^SPIRV_Builder, s: ^IR_If) { cond_id := spirv_emit_expr(b, s.condition) then_label := spirv_alloc_id(b) else_label := spirv_alloc_id(b) merge_label := spirv_alloc_id(b) has_else := len(s.else_body) > 0 || len(s.elseif_clauses) > 0 false_label := has_else ? else_label : merge_label spirv_encode_inst(&b.func_section, SpvOp_SelectionMerge, merge_label, SpvSelectionControl_None) spirv_encode_inst(&b.func_section, SpvOp_BranchConditional, cond_id, then_label, false_label) // Then block spirv_encode_inst(&b.func_section, SpvOp_Label, then_label) spirv_emit_stmts(b, s.then_body[:]) if !spirv_block_has_terminator(s.then_body[:]) { spirv_encode_inst(&b.func_section, SpvOp_Branch, merge_label) } // Else block if has_else { spirv_encode_inst(&b.func_section, SpvOp_Label, else_label) // Handle elseif chains as nested ifs if len(s.elseif_clauses) > 0 { spirv_emit_elseif_chain(b, s.elseif_clauses[:], s.else_body[:], merge_label) } else { spirv_emit_stmts(b, s.else_body[:]) } // For elseif chains, the chain handles its own branching; for plain else, check body if len(s.elseif_clauses) == 0 { if !spirv_block_has_terminator(s.else_body[:]) { spirv_encode_inst(&b.func_section, SpvOp_Branch, merge_label) } } } // Merge block spirv_encode_inst(&b.func_section, SpvOp_Label, merge_label) } @(private = "file") spirv_emit_elseif_chain :: proc(b: ^SPIRV_Builder, clauses: []IR_Elseif, else_body: []IR_Stmt, outer_merge: u32) { if len(clauses) == 0 { spirv_emit_stmts(b, else_body[:]) if !spirv_block_has_terminator(else_body[:]) { spirv_encode_inst(&b.func_section, SpvOp_Branch, outer_merge) } return } clause := clauses[0] cond_id := spirv_emit_expr(b, clause.condition) then_label := spirv_alloc_id(b) else_label := spirv_alloc_id(b) merge_label := spirv_alloc_id(b) has_more := len(clauses) > 1 || len(else_body) > 0 spirv_encode_inst(&b.func_section, SpvOp_SelectionMerge, merge_label, SpvSelectionControl_None) spirv_encode_inst(&b.func_section, SpvOp_BranchConditional, cond_id, then_label, has_more ? else_label : merge_label) spirv_encode_inst(&b.func_section, SpvOp_Label, then_label) spirv_emit_stmts(b, clause.body[:]) if !spirv_block_has_terminator(clause.body[:]) { spirv_encode_inst(&b.func_section, SpvOp_Branch, merge_label) } if has_more { spirv_encode_inst(&b.func_section, SpvOp_Label, else_label) spirv_emit_elseif_chain(b, clauses[1:], else_body, merge_label) } // Merge block — branch to the outer merge so control flow propagates up spirv_encode_inst(&b.func_section, SpvOp_Label, merge_label) spirv_encode_inst(&b.func_section, SpvOp_Branch, outer_merge) } @(private = "file") spirv_emit_for :: proc(b: ^SPIRV_Builder, s: ^IR_For) { // Initialize loop variable start_id := spirv_emit_expr(b, s.start) loop_type := s.start != nil ? s.start.type : TYPE_INT is_uint := spirv_is_uint_type(loop_type) int_type_id := spirv_get_or_create_type(b, loop_type if loop_type != nil else TYPE_INT) ptr_type_id := spirv_get_ptr_type(b, SpvStorageClass_Function, int_type_id) var_id := spirv_alloc_id(b) spirv_encode_inst(&b.var_buffer, SpvOp_Variable, ptr_type_id, var_id, SpvStorageClass_Function) spirv_encode_inst(&b.func_section, SpvOp_Store, var_id, start_id) b.value_map[s.var_id] = var_id header_label := spirv_alloc_id(b) cond_label := spirv_alloc_id(b) body_label := spirv_alloc_id(b) continue_label := spirv_alloc_id(b) merge_label := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Branch, header_label) // Header — OpLoopMerge must be second-to-last, followed only by a branch spirv_encode_inst(&b.func_section, SpvOp_Label, header_label) spirv_encode_inst(&b.func_section, SpvOp_LoopMerge, merge_label, continue_label, SpvLoopControl_None) spirv_encode_inst(&b.func_section, SpvOp_Branch, cond_label) // Condition: var <= stop spirv_encode_inst(&b.func_section, SpvOp_Label, cond_label) cur_val := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, int_type_id, cur_val, var_id) stop_id := spirv_emit_expr(b, s.stop) bool_type_id := spirv_get_or_create_type(b, TYPE_BOOL) cond_id := spirv_alloc_id(b) cmp_op := is_uint ? u32(SpvOp_ULessThanEqual) : u32(SpvOp_SLessThanEqual) spirv_encode_inst(&b.func_section, cmp_op, bool_type_id, cond_id, cur_val, stop_id) spirv_encode_inst(&b.func_section, SpvOp_BranchConditional, cond_id, body_label, merge_label) // Body spirv_encode_inst(&b.func_section, SpvOp_Label, body_label) append(&b.loop_stack, SPIRV_Loop_Context{continue_label, merge_label}) spirv_emit_stmts(b, s.body[:]) pop(&b.loop_stack) if !spirv_block_has_terminator(s.body[:]) { spirv_encode_inst(&b.func_section, SpvOp_Branch, continue_label) } // Continue: increment spirv_encode_inst(&b.func_section, SpvOp_Label, continue_label) cur_val2 := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, int_type_id, cur_val2, var_id) step_id: u32 if s.step != nil { step_id = spirv_emit_expr(b, s.step) } else { step_id = spirv_get_or_create_const_int(b, int_type_id, 1) } next_val := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_IAdd, int_type_id, next_val, cur_val2, step_id) spirv_encode_inst(&b.func_section, SpvOp_Store, var_id, next_val) spirv_encode_inst(&b.func_section, SpvOp_Branch, header_label) // Merge spirv_encode_inst(&b.func_section, SpvOp_Label, merge_label) } @(private = "file") spirv_emit_while :: proc(b: ^SPIRV_Builder, s: ^IR_While) { header_label := spirv_alloc_id(b) cond_label := spirv_alloc_id(b) body_label := spirv_alloc_id(b) continue_label := spirv_alloc_id(b) merge_label := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Branch, header_label) // Header — OpLoopMerge must be second-to-last, followed only by a branch spirv_encode_inst(&b.func_section, SpvOp_Label, header_label) spirv_encode_inst(&b.func_section, SpvOp_LoopMerge, merge_label, continue_label, SpvLoopControl_None) spirv_encode_inst(&b.func_section, SpvOp_Branch, cond_label) // Condition spirv_encode_inst(&b.func_section, SpvOp_Label, cond_label) cond_id := spirv_emit_expr(b, s.condition) spirv_encode_inst(&b.func_section, SpvOp_BranchConditional, cond_id, body_label, merge_label) // Body spirv_encode_inst(&b.func_section, SpvOp_Label, body_label) append(&b.loop_stack, SPIRV_Loop_Context{continue_label, merge_label}) spirv_emit_stmts(b, s.body[:]) pop(&b.loop_stack) if !spirv_block_has_terminator(s.body[:]) { spirv_encode_inst(&b.func_section, SpvOp_Branch, continue_label) } // Continue spirv_encode_inst(&b.func_section, SpvOp_Label, continue_label) spirv_encode_inst(&b.func_section, SpvOp_Branch, header_label) // Merge spirv_encode_inst(&b.func_section, SpvOp_Label, merge_label) } // -- Expressions -- @(private = "file") spirv_emit_expr :: proc(b: ^SPIRV_Builder, expr: ^IR_Expr) -> u32 { if expr == nil do return 0 result_type_id := spirv_get_or_create_type(b, expr.type) switch d in expr.derived { case ^IR_Literal: return spirv_emit_literal(b, d, result_type_id) case ^IR_Var_Ref: // Check for spec constant first if sc_id, ok := b.const_cache[fmt.aprintf("spec_%s", d.name)]; ok { return sc_id } // Check for SSA value (let-bound decorated structs) if val_id, ok := b.ssa_values[d.id]; ok { return val_id } if ptr_id, ok := b.value_map[d.id]; ok { // Load from function-scoped variable loaded_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, result_type_id, loaded_id, ptr_id) return loaded_id } return 0 case ^IR_Binary: left_id := spirv_emit_expr(b, d.left) right_id := spirv_emit_expr(b, d.right) result_id := spirv_alloc_id(b) opcode := spirv_binary_opcode(d.op, expr.type, d.left.type, d.right.type) // SPIR-V VectorTimesScalar requires (vector, scalar) order l_id, r_id := left_id, right_id if opcode == SpvOp_VectorTimesScalar || opcode == SpvOp_MatrixTimesScalar { if _, is_scalar := d.left.type^.(Type_Scalar); is_scalar { l_id, r_id = right_id, left_id } } // SPIR-V requires matching types for FDiv/FAdd/FSub etc — splat scalar to vector // Skip for opcodes that natively handle scalar operands if opcode != SpvOp_VectorTimesScalar && opcode != SpvOp_MatrixTimesScalar { if result_vec, is_vec := expr.type^.(Type_Vector); is_vec { if is_scalar(d.left.type) { l_id = spirv_splat_scalar(b, l_id, result_type_id, result_vec.size) } if is_scalar(d.right.type) { r_id = spirv_splat_scalar(b, r_id, result_type_id, result_vec.size) } } } spirv_encode_inst(&b.func_section, opcode, result_type_id, result_id, l_id, r_id) return result_id case ^IR_Unary: operand_id := spirv_emit_expr(b, d.operand) result_id := spirv_alloc_id(b) opcode: u32 if d.op == .Neg { opcode = spirv_is_float_type(expr.type) ? u32(SpvOp_FNegate) : u32(SpvOp_SNegate) } else { opcode = SpvOp_LogicalNot } spirv_encode_inst(&b.func_section, opcode, result_type_id, result_id, operand_id) return result_id case ^IR_Call: return spirv_emit_call(b, d, expr) case ^IR_Field_Access: // Memory path: AccessChain + Load (binding field access) obj_id := spirv_emit_expr_ptr(b, d.object) if obj_id == 0 { // Fallback: treat as composite extract obj_val := spirv_emit_expr(b, d.object) idx := resolve_field_index(d.object.type, d.field_name) result_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_CompositeExtract, result_type_id, result_id, obj_val, u32(idx)) return result_id } idx := resolve_field_index(d.object.type, d.field_name) int_type_id := spirv_get_or_create_type(b, TYPE_UINT) idx_const := spirv_get_or_create_const_int(b, int_type_id, u32(idx)) sc := spirv_storage_class_for_expr(b, d.object) ptr_type_id := spirv_get_ptr_type(b, sc, result_type_id) chain_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_AccessChain, ptr_type_id, chain_id, obj_id, idx_const) loaded_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, result_type_id, loaded_id, chain_id) return loaded_id case ^IR_Swizzle: obj_id := spirv_emit_expr(b, d.object) // Single component -> CompositeExtract if len(d.components) == 1 { idx := spirv_swizzle_index(d.components[0]) result_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_CompositeExtract, result_type_id, result_id, obj_id, u32(idx)) return result_id } // Multi-component -> VectorShuffle result_id := spirv_alloc_id(b) args := make([dynamic]u32) append(&args, result_type_id, result_id, obj_id, obj_id) // two input vectors (same) for ch in d.components { append(&args, u32(spirv_swizzle_index(u8(ch)))) } spirv_encode_inst(&b.func_section, SpvOp_VectorShuffle, ..args[:]) return result_id case ^IR_Composite_Extract: obj_id := spirv_emit_expr(b, d.object) result_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_CompositeExtract, result_type_id, result_id, obj_id, u32(d.index)) return result_id case ^IR_Vector_Shuffle: obj_id := spirv_emit_expr(b, d.object) result_id := spirv_alloc_id(b) args := make([dynamic]u32) append(&args, result_type_id, result_id, obj_id, obj_id) for idx in d.components { append(&args, u32(idx)) } spirv_encode_inst(&b.func_section, SpvOp_VectorShuffle, ..args[:]) return result_id case ^IR_Index: // Check if the object is memory-backed (shared, binding, var) — use AccessChain + Load base_ptr := spirv_emit_expr_ptr(b, d.object) if base_ptr != 0 { idx_id := spirv_emit_expr(b, d.index) sc := spirv_storage_class_for_expr(b, d.object) ptr_type_id := spirv_get_ptr_type(b, sc, result_type_id) chain_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_AccessChain, ptr_type_id, chain_id, base_ptr, idx_id) loaded_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, result_type_id, loaded_id, chain_id) return loaded_id } // Fallback: value-based extraction obj_id := spirv_emit_expr(b, d.object) idx_id := spirv_emit_expr(b, d.index) result_id := spirv_alloc_id(b) // Use VectorExtractDynamic for vector types (supports runtime index) spirv_encode_inst(&b.func_section, SpvOp_VectorExtractDynamic, result_type_id, result_id, obj_id, idx_id) return result_id case ^IR_Construct: result_id := spirv_alloc_id(b) target_type := expr.type^ // Special case: mat3(mat4) — extract first 3 columns and truncate each vec4 to vec3 if target_mat, ok := target_type.(Type_Matrix); ok && len(d.args) == 1 { arg := d.args[0] if src_mat, ok2 := arg.type^.(Type_Matrix); ok2 && src_mat.cols >= target_mat.cols && src_mat.rows >= target_mat.rows && !type_equals(arg.type, expr.type) { src_id := spirv_emit_expr(b, arg) col_type_id := spirv_get_or_create_type(b, make_type(Type_Vector{target_mat.elem, target_mat.rows})) src_col_type_id := spirv_get_or_create_type(b, make_type(Type_Vector{src_mat.elem, src_mat.rows})) col_ids := make([dynamic]u32) for c in 0 ..< target_mat.cols { // Extract column (vec4) from source matrix ext_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_CompositeExtract, src_col_type_id, ext_id, src_id, u32(c)) if src_mat.rows > target_mat.rows { // Truncate vec4 to vec3 via VectorShuffle trunc_id := spirv_alloc_id(b) shuffle_args := make([dynamic]u32) append(&shuffle_args, col_type_id, trunc_id, ext_id, ext_id) // two operands (same) for i in 0 ..< target_mat.rows { append(&shuffle_args, u32(i)) } spirv_encode_inst(&b.func_section, SpvOp_VectorShuffle, ..shuffle_args[:]) append(&col_ids, trunc_id) } else { append(&col_ids, ext_id) } } construct_args := make([dynamic]u32) append(&construct_args, result_type_id, result_id) for id in col_ids { append(&construct_args, id) } spirv_encode_inst(&b.func_section, SpvOp_CompositeConstruct, ..construct_args[:]) return result_id } } // Special case: vecN(scalar) — splat scalar to N components if target_vec, ok := target_type.(Type_Vector); ok && len(d.args) == 1 { arg := d.args[0] if is_scalar(arg.type) { scalar_id := spirv_emit_expr(b, arg) splat_args := make([dynamic]u32) append(&splat_args, result_type_id, result_id) for _ in 0 ..< target_vec.size { append(&splat_args, scalar_id) } spirv_encode_inst(&b.func_section, SpvOp_CompositeConstruct, ..splat_args[:]) return result_id } } // Default: pass args directly to OpCompositeConstruct args := make([dynamic]u32) append(&args, result_type_id) append(&args, result_id) for arg in d.args { append(&args, spirv_emit_expr(b, arg)) } spirv_encode_inst(&b.func_section, SpvOp_CompositeConstruct, ..args[:]) return result_id case ^IR_Type_Cast: val_id := spirv_emit_expr(b, d.value) result_id := spirv_alloc_id(b) opcode := spirv_cast_opcode(d.value.type, expr.type) spirv_encode_inst(&b.func_section, opcode, result_type_id, result_id, val_id) return result_id case ^IR_Load_Binding: // Look up by name or combined_name (for split sampler bindings) var_id: u32 found := false if id, ok := b.binding_ids[d.name]; ok { var_id = id found = true } else { // Search by combined_name (e.g., "material_tex" -> "material_tex_tex") for &bind in b.module.bindings { if bind.combined_name == d.name && bind.kind == .Texture { if id2, ok2 := b.binding_ids[bind.name]; ok2 { var_id = id2 found = true break } } } } if found { // For uniform blocks, return the pointer (field access will use AccessChain) for &bind in b.module.bindings { if bind.name == d.name { if bind.kind == .Uniform || bind.kind == .Buffer || bind.kind == .Push_Constant { // Non-struct uniforms wrapped in synthetic struct: AccessChain to member 0, then Load if b.wrapped_bindings[d.name] { sc := bind.kind == .Buffer ? u32(SpvStorageClass_StorageBuffer) : u32(SpvStorageClass_Uniform) member_ptr_type_id := spirv_get_ptr_type(b, sc, result_type_id) zero_id := spirv_get_or_create_const_int(b, spirv_get_or_create_type(b, make_type(Type_Scalar{kind = .Uint})), 0) chain_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_AccessChain, member_ptr_type_id, chain_id, var_id, zero_id) loaded_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, result_type_id, loaded_id, chain_id) return loaded_id } return var_id // return pointer for AccessChain } break } } loaded_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, result_type_id, loaded_id, var_id) return loaded_id } return 0 case ^IR_Input_Field: if var_id, ok := b.input_ids[d.field_name]; ok { loaded_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, result_type_id, loaded_id, var_id) return loaded_id } return 0 case ^IR_Builtin_Var: ids := d.is_input ? b.builtin_input_ids : b.builtin_output_ids if var_id, ok := ids[d.name]; ok { if !d.is_input { return var_id // return pointer for stores } loaded_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, result_type_id, loaded_id, var_id) return loaded_id } return 0 case ^IR_Shared_Ref: if var_id, ok := b.shared_var_ids[d.name]; ok { loaded_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Load, result_type_id, loaded_id, var_id) return loaded_id } return 0 case ^IR_Select: cond_id := spirv_emit_expr(b, d.condition) true_id := spirv_emit_expr(b, d.true_val) false_id := spirv_emit_expr(b, d.false_val) result_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Select, result_type_id, result_id, cond_id, true_id, false_id) return result_id } return 0 } @(private = "file") spirv_emit_expr_ptr :: proc(b: ^SPIRV_Builder, expr: ^IR_Expr) -> u32 { // Returns a pointer ID for expressions that are memory-backed if expr == nil do return 0 #partial switch d in expr.derived { case ^IR_Load_Binding: if var_id, ok := b.binding_ids[d.name]; ok { return var_id } case ^IR_Var_Ref: if ptr_id, ok := b.value_map[d.id]; ok { return ptr_id } case ^IR_Shared_Ref: if var_id, ok := b.shared_var_ids[d.name]; ok { return var_id } case ^IR_Field_Access: base_ptr := spirv_emit_expr_ptr(b, d.object) if base_ptr == 0 do return 0 idx := resolve_field_index(d.object.type, d.field_name) int_type_id := spirv_get_or_create_type(b, TYPE_UINT) idx_const := spirv_get_or_create_const_int(b, int_type_id, u32(idx)) sc := spirv_storage_class_for_expr(b, d.object) field_type_id := spirv_get_or_create_type(b, expr.type) ptr_type_id := spirv_get_ptr_type(b, sc, field_type_id) chain_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_AccessChain, ptr_type_id, chain_id, base_ptr, idx_const) return chain_id } return 0 } // Returns a pointer ID for an lvalue expression (assignment target). // Handles var refs, shared refs, indexed shared/binding access, and field access on bindings. @(private = "file") spirv_emit_lvalue_ptr :: proc(b: ^SPIRV_Builder, expr: ^IR_Expr) -> u32 { if expr == nil do return 0 #partial switch d in expr.derived { case ^IR_Var_Ref: if ptr_id, ok := b.value_map[d.id]; ok { return ptr_id } case ^IR_Shared_Ref: if var_id, ok := b.shared_var_ids[d.name]; ok { return var_id } case ^IR_Index: // array[idx] — AccessChain from base pointer base_ptr := spirv_emit_lvalue_ptr(b, d.object) if base_ptr == 0 do return 0 idx_id := spirv_emit_expr(b, d.index) sc := spirv_storage_class_for_expr(b, d.object) elem_type_id := spirv_get_or_create_type(b, expr.type) ptr_type_id := spirv_get_ptr_type(b, sc, elem_type_id) chain_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_AccessChain, ptr_type_id, chain_id, base_ptr, idx_id) return chain_id case ^IR_Field_Access: base_ptr := spirv_emit_lvalue_ptr(b, d.object) if base_ptr == 0 do return 0 idx := resolve_field_index(d.object.type, d.field_name) int_type_id := spirv_get_or_create_type(b, TYPE_UINT) idx_const := spirv_get_or_create_const_int(b, int_type_id, u32(idx)) sc := spirv_storage_class_for_expr(b, d.object) field_type_id := spirv_get_or_create_type(b, expr.type) ptr_type_id := spirv_get_ptr_type(b, sc, field_type_id) chain_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_AccessChain, ptr_type_id, chain_id, base_ptr, idx_const) return chain_id case ^IR_Load_Binding: if var_id, ok := b.binding_ids[d.name]; ok { return var_id } } return 0 } // Determine the SPIR-V storage class for a given expression's base. @(private = "file") spirv_storage_class_for_expr :: proc(b: ^SPIRV_Builder, expr: ^IR_Expr) -> u32 { if expr == nil do return SpvStorageClass_Function #partial switch d in expr.derived { case ^IR_Shared_Ref: return SpvStorageClass_Workgroup case ^IR_Load_Binding: for &bind in b.module.bindings { if bind.name == d.name { if bind.kind == .Buffer { return SpvStorageClass_StorageBuffer } if bind.kind == .Push_Constant { return SpvStorageClass_PushConstant } return SpvStorageClass_Uniform } } return SpvStorageClass_Uniform case ^IR_Var_Ref: return SpvStorageClass_Function case ^IR_Field_Access: return spirv_storage_class_for_expr(b, d.object) case ^IR_Index: return spirv_storage_class_for_expr(b, d.object) } return SpvStorageClass_Function } @(private = "file") spirv_emit_literal :: proc(b: ^SPIRV_Builder, lit: ^IR_Literal, type_id: u32) -> u32 { switch v in lit.value { case f64: return spirv_get_or_create_const_float(b, type_id, v) case i64: return spirv_get_or_create_const_int(b, type_id, u32(v)) case bool: return spirv_get_or_create_const_bool(b, type_id, v) } return 0 } @(private = "file") spirv_emit_call :: proc(b: ^SPIRV_Builder, call: ^IR_Call, expr: ^IR_Expr) -> u32 { result_type_id := spirv_get_or_create_type(b, expr.type) // Handle texture sampling if call.is_builtin && call.name == "sample" && len(call.args) >= 2 { // Load the sampled image (combined texture+sampler in SPIR-V) sampled_image_id := spirv_emit_expr(b, call.args[0]) coord_id := spirv_emit_expr(b, call.args[1]) result_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_ImageSampleImplicitLod, result_type_id, result_id, sampled_image_id, coord_id) return result_id } // Handle sample_level — OpImageSampleExplicitLod with Lod operand if call.is_builtin && call.name == "sample_level" && len(call.args) >= 3 { sampled_image_id := spirv_emit_expr(b, call.args[0]) coord_id := spirv_emit_expr(b, call.args[1]) lod_id := spirv_emit_expr(b, call.args[2]) result_id := spirv_alloc_id(b) // ImageOperandsMask: Lod = 0x2 spirv_encode_inst(&b.func_section, SpvOp_ImageSampleExplicitLod, result_type_id, result_id, sampled_image_id, coord_id, 0x2, lod_id) return result_id } // Handle shadow texture sampling (depth comparison) if call.is_builtin && call.name == "sample_shadow" && len(call.args) >= 3 { sampled_image_id := spirv_emit_expr(b, call.args[0]) coord_id := spirv_emit_expr(b, call.args[1]) dref_id := spirv_emit_expr(b, call.args[2]) result_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_ImageSampleDrefImplicitLod, result_type_id, result_id, sampled_image_id, coord_id, dref_id) return result_id } // GLSL.std.450 extended instructions if call.is_builtin { if glsl_inst, ok := spirv_glsl_ext_inst(call.name); ok { // Select integer variants for integer types if len(call.args) > 0 && call.args[0].type != nil { is_uint := spirv_is_uint_type(call.args[0].type) is_int := spirv_is_int_type(call.args[0].type) && !is_uint switch call.name { case "min": if is_uint { glsl_inst = GLSLstd450_UMin } else if is_int { glsl_inst = GLSLstd450_SMin } case "max": if is_uint { glsl_inst = GLSLstd450_UMax } else if is_int { glsl_inst = GLSLstd450_SMax } case "clamp": if is_uint { glsl_inst = GLSLstd450_UClamp } else if is_int { glsl_inst = GLSLstd450_SClamp } case "abs": if is_int { glsl_inst = GLSLstd450_SAbs } case "sign": if is_int { glsl_inst = GLSLstd450_SSign } } } arg_ids := make([dynamic]u32) for arg in call.args { append(&arg_ids, spirv_emit_expr(b, arg)) } // GLSL.std.450 requires all operands to match result type. // Splat scalar args to vectors when result is a vector. if vec, is_vec := expr.type^.(Type_Vector); is_vec { for arg, i in call.args { if arg.type != nil { if _, is_scalar := arg.type^.(Type_Scalar); is_scalar { // Splat: construct vector from repeated scalar splat_args := make([dynamic]u32) append(&splat_args, result_type_id, spirv_alloc_id(b)) for _ in 0 ..< vec.size { append(&splat_args, arg_ids[i]) } splat_id := splat_args[1] spirv_encode_inst(&b.func_section, SpvOp_CompositeConstruct, ..splat_args[:]) arg_ids[i] = splat_id } } } } result_id := spirv_alloc_id(b) // OpExtInst: result_type, result_id, ext_set, instruction, ...args full_args := make([dynamic]u32) append(&full_args, result_type_id, result_id, b.glsl_ext_id, glsl_inst) for aid in arg_ids { append(&full_args, aid) } spirv_encode_inst(&b.func_section, SpvOp_ExtInst, ..full_args[:]) return result_id } // Handle dot product specially — it's a dedicated opcode, not GLSL.std.450 if call.name == "dot" && len(call.args) >= 2 { left_id := spirv_emit_expr(b, call.args[0]) right_id := spirv_emit_expr(b, call.args[1]) result_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Dot, result_type_id, result_id, left_id, right_id) return result_id } // Handle transpose — native SPIR-V opcode, not GLSL.std.450 if call.name == "transpose" && len(call.args) >= 1 { mat_id := spirv_emit_expr(b, call.args[0]) result_id := spirv_alloc_id(b) spirv_encode_inst(&b.func_section, SpvOp_Transpose, result_type_id, result_id, mat_id) return result_id } // Handle derivative ops — native SPIR-V opcodes, not GLSL.std.450 if (call.name == "dfdx" || call.name == "dfdy" || call.name == "fwidth") && len(call.args) >= 1 { arg_id := spirv_emit_expr(b, call.args[0]) result_id := spirv_alloc_id(b) opcode: u32 switch call.name { case "dfdx": opcode = SpvOp_DPdx case "dfdy": opcode = SpvOp_DPdy case "fwidth": opcode = SpvOp_Fwidth } spirv_encode_inst(&b.func_section, opcode, result_type_id, result_id, arg_id) return result_id } } // Regular function call arg_ids := make([dynamic]u32) for arg in call.args { append(&arg_ids, spirv_emit_expr(b, arg)) } result_id := spirv_alloc_id(b) full_args := make([dynamic]u32) append(&full_args, result_type_id, result_id) // Look up pre-allocated function ID fn_id := b.function_ids[call.name] or_else 0 append(&full_args, fn_id) for aid in arg_ids { append(&full_args, aid) } spirv_encode_inst(&b.func_section, SpvOp_FunctionCall, ..full_args[:]) return result_id } // -- Helpers -- @(private = "file") spirv_binary_opcode :: proc(op: IR_Op, result_type: ^Resolved_Type, left_type: ^Resolved_Type, right_type: ^Resolved_Type) -> u32 { is_float := spirv_is_float_type(left_type) is_uint := spirv_is_uint_type(left_type) switch op { case .Add: return is_float ? u32(SpvOp_FAdd) : u32(SpvOp_IAdd) case .Sub: return is_float ? u32(SpvOp_FSub) : u32(SpvOp_ISub) case .Mul: if left_type != nil && right_type != nil { _, l_is_mat := left_type^.(Type_Matrix) _, r_is_mat := right_type^.(Type_Matrix) _, l_is_vec := left_type^.(Type_Vector) _, r_is_vec := right_type^.(Type_Vector) _, r_is_scalar := right_type^.(Type_Scalar) _, l_is_scalar := left_type^.(Type_Scalar) if l_is_mat && r_is_vec { return SpvOp_MatrixTimesVector } if l_is_vec && r_is_mat { return SpvOp_VectorTimesMatrix } if l_is_mat && r_is_mat { return SpvOp_MatrixTimesMatrix } if l_is_mat && r_is_scalar { return SpvOp_MatrixTimesScalar } if l_is_scalar && r_is_mat { return SpvOp_MatrixTimesScalar } if l_is_vec && r_is_scalar { return SpvOp_VectorTimesScalar } if l_is_scalar && r_is_vec { return SpvOp_VectorTimesScalar } } return is_float ? u32(SpvOp_FMul) : u32(SpvOp_IMul) case .Div: return is_float ? u32(SpvOp_FDiv) : (is_uint ? u32(SpvOp_UDiv) : u32(SpvOp_SDiv)) case .Mod: return is_float ? u32(SpvOp_FMod) : (is_uint ? u32(SpvOp_UMod) : u32(SpvOp_SMod)) case .Eq: return is_float ? u32(SpvOp_FOrdEqual) : u32(SpvOp_IEqual) case .Neq: return is_float ? u32(SpvOp_FOrdNotEqual) : u32(SpvOp_INotEqual) case .Lt: return is_float ? u32(SpvOp_FOrdLessThan) : (is_uint ? u32(SpvOp_ULessThan) : u32(SpvOp_SLessThan)) case .Gt: return is_float ? u32(SpvOp_FOrdGreaterThan) : (is_uint ? u32(SpvOp_UGreaterThan) : u32(SpvOp_SGreaterThan)) case .Lte: return is_float ? u32(SpvOp_FOrdLessThanEqual) : (is_uint ? u32(SpvOp_ULessThanEqual) : u32(SpvOp_SLessThanEqual)) case .Gte: return is_float ? u32(SpvOp_FOrdGreaterThanEqual) : (is_uint ? u32(SpvOp_UGreaterThanEqual) : u32(SpvOp_SGreaterThanEqual)) case .And: return SpvOp_LogicalAnd case .Or: return SpvOp_LogicalOr case .Neg: return is_float ? u32(SpvOp_FNegate) : u32(SpvOp_SNegate) case .Not: return SpvOp_LogicalNot } return SpvOp_Nop } @(private = "file") spirv_is_float_type :: proc(t: ^Resolved_Type) -> bool { if t == nil do return false #partial switch v in t^ { case Type_Scalar: return v.kind == .Float || v.kind == .Half case Type_Vector: return v.elem == .Float || v.elem == .Half case Type_Matrix: return true // matrices are always float case: return false } return false } @(private = "file") spirv_splat_scalar :: proc(b: ^SPIRV_Builder, scalar_id: u32, vec_type_id: u32, size: int) -> u32 { splat_args := make([dynamic]u32) splat_id := spirv_alloc_id(b) append(&splat_args, vec_type_id, splat_id) for _ in 0 ..< size { append(&splat_args, scalar_id) } spirv_encode_inst(&b.func_section, SpvOp_CompositeConstruct, ..splat_args[:]) return splat_id } @(private = "file") spirv_is_int_type :: proc(t: ^Resolved_Type) -> bool { if t == nil do return false #partial switch v in t^ { case Type_Scalar: return v.kind == .Int || v.kind == .Uint case Type_Vector: return v.elem == .Int || v.elem == .Uint case: return false } return false } @(private = "file") spirv_is_uint_type :: proc(t: ^Resolved_Type) -> bool { if t == nil do return false #partial switch v in t^ { case Type_Scalar: return v.kind == .Uint case Type_Vector: return v.elem == .Uint case: return false } return false } @(private = "file") spirv_cast_opcode :: proc(from: ^Resolved_Type, to: ^Resolved_Type) -> u32 { from_float := spirv_is_float_type(from) to_float := spirv_is_float_type(to) if from_float && !to_float { // Check if target is signed (scalar or vector of signed int) if to != nil { #partial switch v in to^ { case Type_Scalar: if v.kind == .Int do return SpvOp_ConvertFToS case Type_Vector: if v.elem == .Int do return SpvOp_ConvertFToS } } return SpvOp_ConvertFToU } if !from_float && to_float { if spirv_is_uint_type(from) { return SpvOp_ConvertUToF } return SpvOp_ConvertSToF } if from_float && to_float { return SpvOp_FConvert } return SpvOp_Bitcast } @(private = "file") spirv_swizzle_index :: proc(ch: u8) -> int { switch ch { case 'x', 'r', 's': return 0 case 'y', 'g', 't': return 1 case 'z', 'b', 'p': return 2 case 'w', 'a', 'q': return 3 } return 0 } @(private = "file") spirv_execution_model :: proc(stage: Shader_Stage) -> u32 { #partial switch stage { case .Vertex: return SpvExecutionModel_Vertex case .Fragment: return SpvExecutionModel_Fragment case .Compute: return SpvExecutionModel_GLCompute } return SpvExecutionModel_Vertex } @(private = "file") spirv_builtin_id :: proc(name: string, stage: Shader_Stage = .Vertex) -> u32 { switch name { case "position": return stage == .Fragment ? SpvBuiltIn_FragCoord : SpvBuiltIn_Position case "vertex_id": return SpvBuiltIn_VertexIndex case "instance_id": return SpvBuiltIn_InstanceIndex case "frag_coord": return SpvBuiltIn_FragCoord case "front_facing": return SpvBuiltIn_FrontFacing case "frag_depth": return SpvBuiltIn_FragDepth case "local_invocation_id": return SpvBuiltIn_LocalInvocationId case "local_invocation_index": return SpvBuiltIn_LocalInvocationIndex case "global_invocation_id": return SpvBuiltIn_GlobalInvocationId case "workgroup_id": return SpvBuiltIn_WorkgroupId } return 0 } @(private = "file") spirv_glsl_ext_inst :: proc(name: string) -> (u32, bool) { switch name { case "round": return GLSLstd450_Round, true case "floor": return GLSLstd450_Floor, true case "ceil": return GLSLstd450_Ceil, true case "fract": return GLSLstd450_Fract, true case "abs": return GLSLstd450_FAbs, true case "sign": return GLSLstd450_FSign, true case "sin": return GLSLstd450_Sin, true case "cos": return GLSLstd450_Cos, true case "tan": return GLSLstd450_Tan, true case "asin": return GLSLstd450_Asin, true case "acos": return GLSLstd450_Acos, true case "atan": return GLSLstd450_Atan, true case "atan2": return GLSLstd450_Atan2, true case "pow": return GLSLstd450_Pow, true case "exp": return GLSLstd450_Exp, true case "log": return GLSLstd450_Log, true case "exp2": return GLSLstd450_Exp2, true case "log2": return GLSLstd450_Log2, true case "sqrt": return GLSLstd450_Sqrt, true case "inversesqrt": return GLSLstd450_InverseSqrt, true case "inverse": return GLSLstd450_MatrixInverse, true case "determinant": return GLSLstd450_Determinant, true case "min": return GLSLstd450_FMin, true case "max": return GLSLstd450_FMax, true case "clamp": return GLSLstd450_FClamp, true case "mix": return GLSLstd450_FMix, true case "step": return GLSLstd450_Step, true case "smoothstep": return GLSLstd450_SmoothStep, true case "length": return GLSLstd450_Length, true case "distance": return GLSLstd450_Distance, true case "cross": return GLSLstd450_Cross, true case "normalize": return GLSLstd450_Normalize, true case "reflect": return GLSLstd450_Reflect, true case "refract": return GLSLstd450_Refract, true } return 0, false } // -- Final assembly -- @(private = "file") spirv_splice_vars :: proc(b: ^SPIRV_Builder, insert_pos: int) { // Insert var_buffer contents at insert_pos in func_section // (right after OpLabel, before any other instructions) old_len := len(b.func_section) var_len := len(b.var_buffer) // Extend func_section by var_len resize(&b.func_section, old_len + var_len) // Shift existing instructions after insert_pos to make room copy(b.func_section[insert_pos + var_len:], b.func_section[insert_pos:old_len]) // Copy var_buffer into the gap copy(b.func_section[insert_pos:], b.var_buffer[:]) } // Check if a statement list ends with a block-terminating statement (discard/return). // These produce SPIR-V terminators (OpKill, OpReturn), so we must not emit OpBranch after them. @(private = "file") spirv_block_has_terminator :: proc(stmts: []IR_Stmt) -> bool { if len(stmts) == 0 do return false last := stmts[len(stmts) - 1] #partial switch s in last { case ^IR_Discard: return true case ^IR_Return: return true case ^IR_Break: return true case ^IR_Continue: return true } return false } @(private = "file") ir_stmt_span :: proc(stmt: IR_Stmt) -> Source_Span { switch s in stmt { case ^IR_Let: return s.span case ^IR_Assign: return s.span case ^IR_Return: return s.span case ^IR_If: return s.span case ^IR_For: return s.span case ^IR_While: return s.span case ^IR_Store_Output: return s.span case ^IR_Expr_Stmt: return s.span case ^IR_Barrier: return s.span case ^IR_Discard: return s.span case ^IR_Break: return s.span case ^IR_Continue: return s.span } return {} } @(private = "file") spirv_assemble :: proc(b: ^SPIRV_Builder) -> []u8 { words := make([dynamic]u32) // Header append(&words, SPIRV_MAGIC) append(&words, SPIRV_VERSION) append(&words, SPIRV_GENERATOR) append(&words, b.next_id) // bound append(&words, 0) // schema // Sections in order for w in b.capabilities { append(&words, w) } for w in b.extensions { append(&words, w) } for w in b.ext_imports { append(&words, w) } for w in b.mem_model { append(&words, w) } for w in b.entry_points { append(&words, w) } for w in b.exec_modes { append(&words, w) } for w in b.debug_source { append(&words, w) } // OpString, OpSource for w in b.debug_names { append(&words, w) } // OpName, OpMemberName for w in b.debug_process { append(&words, w) } // OpModuleProcessed for w in b.annotations { append(&words, w) } for w in b.type_section { append(&words, w) } for w in b.func_section { append(&words, w) } // Convert to bytes byte_len := len(words) * 4 bytes := make([]u8, byte_len) for w, i in words { bytes[i*4 + 0] = u8(w) bytes[i*4 + 1] = u8(w >> 8) bytes[i*4 + 2] = u8(w >> 16) bytes[i*4 + 3] = u8(w >> 24) } return bytes }