From 163aa3052bdbf3e0868a27656d691bbb4f10fd94 Mon Sep 17 00:00:00 2001 From: Riyyi Date: Mon, 26 Oct 2026 22:06:30 +0100 Subject: [PATCH] Add basic reflection to wgpu struct and vertex attributes --- src/platform/wgpu.odin | 70 ++++----- src/platform/wgpu_reflect.odin | 276 +++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+), 39 deletions(-) create mode 100644 src/platform/wgpu_reflect.odin diff --git a/src/platform/wgpu.odin b/src/platform/wgpu.odin index 33ab4ce..3d70735 100644 --- a/src/platform/wgpu.odin +++ b/src/platform/wgpu.odin @@ -316,40 +316,48 @@ on_device :: proc "c" ( // ---------------------------------------- // Compile shader - shader :: ` - struct Uniforms { - mvp: mat4x4, - }; + uniforms_struct := to_wgsl_struct(Camera_Uniform, "Uniforms") + defer delete(uniforms_struct) + vertex_input_struct := to_wgsl_struct( + Vertex, + "VertexInput", + locations = true, + ) + defer delete(vertex_input_struct) + + shader := fmt.tprintf( + ` + %s @group(0) @binding(0) var u: Uniforms; - @group(0) @binding(1) var t_diffuse: texture_2d; - @group(0) @binding(2) var s_diffuse: sampler; + @group(0) @binding(1) var t_diffuse: texture_2d; + @group(0) @binding(2) var s_diffuse: sampler; - struct VertexInput { - @location(0) pos: vec3, - @location(1) col: vec3, - @location(2) uv: vec2, - }; + %s - struct VertexOutput { + struct VertexOutput {{ @builtin(position) position: vec4, @location(0) col: vec3, @location(1) uv: vec2, - }; + }}; @vertex - fn vs_main(in: VertexInput) -> VertexOutput { + fn vs_main(in: VertexInput) -> VertexOutput {{ var out: VertexOutput; out.position = u.mvp * vec4(in.pos, 1.0); out.col = in.col; out.uv = in.uv; return out; - } + }} @fragment - fn fs_main(in: VertexOutput) -> @location(0) vec4 { + fn fs_main(in: VertexOutput) -> @location(0) vec4 {{ return textureSample(t_diffuse, s_diffuse, in.uv) * vec4(in.col, 1.0); - }` + }} + `, + uniforms_struct, + vertex_input_struct, + ) state.module = wgpu.DeviceCreateShaderModule( state.device, @@ -415,30 +423,14 @@ on_device :: proc "c" ( // ---------------------------------------- // Buffer shape - vertex_attributes := [?]wgpu.VertexAttribute { - { - format = .Float32x3, - offset = cast(u64)offset_of(Vertex, pos), - shaderLocation = 0, - }, - { - format = .Float32x3, - offset = cast(u64)offset_of(Vertex, col), - shaderLocation = 1, - }, - { - format = .Float32x2, - offset = cast(u64)offset_of(Vertex, uv), - shaderLocation = 2, - }, - } + vertex_attributes := to_vertex_attributes(Vertex) vertex_layout := wgpu.VertexBufferLayout { - stepMode = .Vertex, - arrayStride = size_of(Vertex), - attributeCount = len(vertex_attributes), - attributes = raw_data(vertex_attributes[:]), - } + stepMode = .Vertex, + arrayStride = size_of(Vertex), + attributeCount = len(vertex_attributes), + attributes = raw_data(vertex_attributes), + } // ---------------------------------------- // Render pipeline diff --git a/src/platform/wgpu_reflect.odin b/src/platform/wgpu_reflect.odin new file mode 100644 index 0000000..bd600d0 --- /dev/null +++ b/src/platform/wgpu_reflect.odin @@ -0,0 +1,276 @@ +package platform + +import "core:fmt" +import "core:reflect" +import "core:strings" + +import "wgpu:wgpu" + +// ----------------------------------------- +// Types + +// ----------------------------------------- +// Public functions + +// Generate a WGSL struct declaration from an Odin struct type. +// The caller owns the returned string (allocated on context.allocator). +// +// If locations is true, each member is prefixed with @location(n). This is +// required for vertex input structs; uniform structs should use false. +to_wgsl_struct :: proc( + $T: typeid, + name: string, + locations := false, +) -> string { + ti := base_type_info(type_info_of(T)) + s, ok := ti.variant.(reflect.Type_Info_Struct) + if !ok do fmt.panicf("[wgpu_reflect] %s is not a struct", typeid_of(T)) + + b: strings.Builder + strings.write_string(&b, "struct ") + strings.write_string(&b, name) + strings.write_string(&b, " {\n") + + names := s.names[:s.field_count] + types := s.types[:s.field_count] + for i in 0 ..< s.field_count { + wgsl_type, type_ok := to_wgsl_type(types[i]) + if !type_ok { + fmt.panicf("[wgpu_reflect] cannot map field %s to WGSL", names[i]) + } + if locations { + fmt.sbprintf( + &b, + "\t@location(%d) %s: %s,\n", + i, + names[i], + wgsl_type, + ) + } else { + fmt.sbprintf(&b, "\t%s: %s,\n", names[i], wgsl_type) + } + } + + strings.write_string(&b, "};") + return strings.to_string(b) +} + +// Generate a []wgpu.VertexAttribute array that matches the fields of struct T. +// The caller owns the returned slice (allocated on context.allocator) +to_vertex_attributes :: proc($T: typeid) -> []wgpu.VertexAttribute { + ti := base_type_info(type_info_of(T)) + s, ok := ti.variant.(reflect.Type_Info_Struct) + if !ok do fmt.panicf("[wgpu_reflect] %s is not a struct", typeid_of(T)) + + attrs := make([]wgpu.VertexAttribute, s.field_count) + names := s.names[:s.field_count] + types := s.types[:s.field_count] + offsets := s.offsets[:s.field_count] + + for i in 0 ..< s.field_count { + format, format_ok := to_vertex_format(types[i]) + if !format_ok { + fmt.panicf( + "[wgpu_reflect] cannot map field %s to a vertex format", + names[i], + ) + } + + attrs[i] = wgpu.VertexAttribute { + format = format, + offset = cast(u64)offsets[i], + shaderLocation = u32(i), + } + } + + return attrs +} + +// ----------------------------------------- +// Private functions + +// Strips named-type wrappers so we can inspect the underlying type +@(private = "file") +base_type_info :: proc(type_info: ^reflect.Type_Info) -> ^reflect.Type_Info { + #partial switch v in type_info.variant { + case reflect.Type_Info_Named: + return base_type_info(v.base) + } + return type_info +} + +// Map an Odin type to a WGSL scalar/vector/matrix type name +@(private = "file") +to_wgsl_type :: proc(type_info: ^reflect.Type_Info) -> (string, bool) { + ti := base_type_info(type_info) + + #partial switch v in ti.variant { + case reflect.Type_Info_Float: + switch ti.size { + case 4: + return "f32", true + case 8: + return "f64", true + } + + case reflect.Type_Info_Integer: + prefix := v.signed ? "i" : "u" + switch ti.size { + case 1: + return fmt.tprintf("%s8", prefix), true + case 2: + return fmt.tprintf("%s16", prefix), true + case 4: + return fmt.tprintf("%s32", prefix), true + case 8: + return fmt.tprintf("%s64", prefix), true + } + + case reflect.Type_Info_Array: + elem_type, elem_ok := to_wgsl_type(v.elem) + if !elem_ok do return "", false + switch v.count { + case 2: + return fmt.tprintf("vec2<%s>", elem_type), true + case 3: + return fmt.tprintf("vec3<%s>", elem_type), true + case 4: + return fmt.tprintf("vec4<%s>", elem_type), true + } + + case reflect.Type_Info_Matrix: + elem_type, elem_ok := to_wgsl_type(v.elem) + if !elem_ok do return "", false + return fmt.tprintf( + "mat%dx%d<%s>", + v.column_count, + v.row_count, + elem_type, + ), + true + + case: + fmt.panicf("[wgpu_reflect] unimplemented type translation: %v", ti) + } + + return "", false +} + +// Map an Odin type to a wgpu.VertexFormat. +// Only handles the scalar/vector numeric formats commonly used as vertex attributes +@(private = "file") +to_vertex_format :: proc( + type_info: ^reflect.Type_Info, +) -> ( + wgpu.VertexFormat, + bool, +) { + ti := base_type_info(type_info) + + #partial switch v in ti.variant { + case reflect.Type_Info_Float: + if ti.size == 2 do return .Float16, true + else if ti.size == 4 do return .Float32, true + + case reflect.Type_Info_Integer: + if v.signed { + switch ti.size { + case 1: + return .Sint8, true + case 2: + return .Sint16, true + case 4: + return .Sint32, true + } + } else { + switch ti.size { + case 1: + return .Uint8, true + case 2: + return .Uint16, true + case 4: + return .Uint32, true + } + } + + case reflect.Type_Info_Array: + elem := base_type_info(v.elem) + #partial switch ev in elem.variant { + case reflect.Type_Info_Float: + if elem.size == 2 { + switch v.count { + case 2: + return .Float16x2, true + case 4: + return .Float16x4, true + } + } else if elem.size == 4 { + switch v.count { + case 2: + return .Float32x2, true + case 3: + return .Float32x3, true + case 4: + return .Float32x4, true + } + } + + case reflect.Type_Info_Integer: + if ev.signed { + switch elem.size { + case 1: + switch v.count { + case 2: + return .Sint8x2, true + case 4: + return .Sint8x4, true + } + case 2: + switch v.count { + case 2: + return .Sint16x2, true + case 4: + return .Sint16x4, true + } + case 4: + switch v.count { + case 2: + return .Sint32x2, true + case 3: + return .Sint32x3, true + case 4: + return .Sint32x4, true + } + } + } else { + switch elem.size { + case 1: + switch v.count { + case 2: + return .Uint8x2, true + case 4: + return .Uint8x4, true + } + case 2: + switch (v.count) { + case 2: + return .Uint16x2, true + case 4: + return .Uint16x4, true + } + case 4: + switch (v.count) { + case 2: + return .Uint32x2, true + case 3: + return .Uint32x3, true + case 4: + return .Uint32x4, true + } + } + } + } + } + + return .Float32, false +}