Skip to content

Instantly share code, notes, and snippets.

@fearofcode
Last active October 29, 2025 03:11
Show Gist options
  • Save fearofcode/0fb8bec972e7d860493400624eb8cb14 to your computer and use it in GitHub Desktop.
Save fearofcode/0fb8bec972e7d860493400624eb8cb14 to your computer and use it in GitHub Desktop.
wgpu Uniform buffers in Odin
package vendor_wgpu_example_triangle
import "core:time"
import "base:runtime"
import "core:fmt"
import "core:math"
import "vendor:wgpu"
import "vendor:glfw"
import "vendor:wgpu/glfwglue"
import glm "core:math/linalg/glsl"
state: struct {
ctx: runtime.Context,
window: glfw.WindowHandle,
instance: wgpu.Instance,
surface: wgpu.Surface,
adapter: wgpu.Adapter,
device: wgpu.Device,
config: wgpu.SurfaceConfiguration,
queue: wgpu.Queue,
module: wgpu.ShaderModule,
pipeline_layout: wgpu.PipelineLayout,
pipeline: wgpu.RenderPipeline,
uniform_buffer: wgpu.Buffer,
bind_group_layout: wgpu.BindGroupLayout,
bind_group: wgpu.BindGroup,
}
Uniforms :: struct {
scale: glm.vec2,
offset: glm.vec2
}
start_tick: time.Tick
init_glfw :: proc() {
if !glfw.Init() {
panic("[glfw] init failure")
}
glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API)
state.window = glfw.CreateWindow(800, 600, "WGPU Native Triangle", nil, nil)
glfw.SetFramebufferSizeCallback(state.window, size_callback)
glfw.SwapInterval(1)
glfw.MakeContextCurrent(state.window)
}
get_framebuffer_size :: proc() -> (width, height: u32) {
iw, ih := glfw.GetFramebufferSize(state.window)
return u32(iw), u32(ih)
}
size_callback :: proc "c" (window: glfw.WindowHandle, width, height: i32) {
resize()
}
main :: proc() {
start_tick = time.tick_now()
state.ctx = context
init_glfw()
state.instance = wgpu.CreateInstance(nil)
if state.instance == nil {
panic("WebGPU is not supported")
}
state.surface = glfwglue.GetSurface(state.instance, state.window)
wgpu.InstanceRequestAdapter(state.instance, &{ compatibleSurface = state.surface }, { callback = on_adapter })
on_adapter :: proc "c" (status: wgpu.RequestAdapterStatus, adapter: wgpu.Adapter, message: string, userdata1: rawptr, userdata2: rawptr) {
context = state.ctx
if status != .Success || adapter == nil {
fmt.panicf("request adapter failure: [%v] %s", status, message)
}
state.adapter = adapter
wgpu.AdapterRequestDevice(adapter, nil, { callback = on_device })
}
on_device :: proc "c" (status: wgpu.RequestDeviceStatus, device: wgpu.Device, message: string, userdata1: rawptr, userdata2: rawptr) {
context = state.ctx
if status != .Success || device == nil {
fmt.panicf("request device failure: [%v] %s", status, message)
}
state.device = device
width, height := get_framebuffer_size()
state.config = wgpu.SurfaceConfiguration {
device = state.device,
usage = { .RenderAttachment },
format = .BGRA8UnormSrgb,
width = width,
height = height,
presentMode = .Fifo,
alphaMode = .Opaque,
}
wgpu.SurfaceConfigure(state.surface, &state.config)
state.queue = wgpu.DeviceGetQueue(state.device)
shader :: string(#load("shader.wgsl"))
state.module = wgpu.DeviceCreateShaderModule(state.device, &{
nextInChain = &wgpu.ShaderSourceWGSL{
sType = .ShaderSourceWGSL,
code = shader,
},
})
bind_group_layout_entry := wgpu.BindGroupLayoutEntry {
binding = 0,
visibility = { .Vertex }, // This uniform is only used in the vertex shader
buffer = wgpu.BufferBindingLayout {
type = .Uniform,
},
}
state.bind_group_layout = wgpu.DeviceCreateBindGroupLayout(state.device, &{
label = "Uniform Bind Group Layout",
entryCount = 1,
entries = &bind_group_layout_entry,
})
state.pipeline_layout = wgpu.DeviceCreatePipelineLayout(state.device, &{
bindGroupLayoutCount = 1,
bindGroupLayouts = &state.bind_group_layout,
})
state.pipeline = wgpu.DeviceCreateRenderPipeline(state.device, &{
layout = state.pipeline_layout,
vertex = {
module = state.module,
entryPoint = "vs_main",
},
fragment = &{
module = state.module,
entryPoint = "fs_main",
targetCount = 1,
targets = &wgpu.ColorTargetState{
format = .BGRA8UnormSrgb,
writeMask = wgpu.ColorWriteMaskFlags_All,
},
},
primitive = {
topology = .TriangleList,
},
multisample = {
count = 1,
mask = 0xFFFFFFFF,
},
})
uniform_buffer_size := size_of(Uniforms)
state.uniform_buffer = wgpu.DeviceCreateBuffer(state.device, &{
usage = { .Uniform, .CopyDst },
size = u64(uniform_buffer_size),
})
state.bind_group = wgpu.DeviceCreateBindGroup(device, &{
layout = state.bind_group_layout,
entryCount = 1,
entries = &wgpu.BindGroupEntry{
binding = 0,
buffer = state.uniform_buffer,
size = u64(size_of(Uniforms)),
},
})
main_loop()
}
}
main_loop :: proc() {
dt: f32
elapsed := time.duration_milliseconds(time.tick_since(start_tick))
fmt.println("Application run time: ", elapsed, " ms")
for !glfw.WindowShouldClose(state.window) {
elapsed = time.duration_milliseconds(time.tick_since(start_tick))
start := time.tick_now()
glfw.PollEvents()
context = state.ctx
total_seconds := f32(time.duration_seconds(time.tick_since(start_tick)))
offset_t := f32(math.sin(total_seconds))*0.1
uniform_values := Uniforms {
scale = glm.vec2{ 1.0, 1.0},
offset = glm.vec2{ offset_t, -0.2},
}
wgpu.QueueWriteBuffer(
queue=state.queue,
buffer=state.uniform_buffer,
bufferOffset=0,
data=&uniform_values,
size=size_of(uniform_values),
)
surface_texture := wgpu.SurfaceGetCurrentTexture(state.surface)
switch surface_texture.status {
case .SuccessOptimal, .SuccessSuboptimal:
// All good, could handle suboptimal here.
case .Timeout, .Outdated, .Lost:
// Skip this frame, and re-configure surface.
if surface_texture.texture != nil {
wgpu.TextureRelease(surface_texture.texture)
}
resize()
return
case .OutOfMemory, .DeviceLost, .Error:
// Fatal error
fmt.panicf("get_current_texture status=%v", surface_texture.status)
}
defer wgpu.TextureRelease(surface_texture.texture)
frame := wgpu.TextureCreateView(surface_texture.texture, nil)
defer wgpu.TextureViewRelease(frame)
command_encoder := wgpu.DeviceCreateCommandEncoder(state.device, nil)
defer wgpu.CommandEncoderRelease(command_encoder)
render_pass_encoder := wgpu.CommandEncoderBeginRenderPass(
command_encoder, &{
colorAttachmentCount = 1,
colorAttachments = &wgpu.RenderPassColorAttachment{
view = frame,
loadOp = .Clear,
storeOp = .Store,
depthSlice = wgpu.DEPTH_SLICE_UNDEFINED,
clearValue = { 0, 0, 0, 1 },
},
},
)
wgpu.RenderPassEncoderSetPipeline(render_pass_encoder, state.pipeline)
wgpu.RenderPassEncoderSetBindGroup(render_pass_encoder, 0, state.bind_group)
wgpu.RenderPassEncoderDraw(render_pass_encoder, vertexCount=3, instanceCount=1, firstVertex=0, firstInstance=0)
wgpu.RenderPassEncoderEnd(render_pass_encoder)
wgpu.RenderPassEncoderRelease(render_pass_encoder)
command_buffer := wgpu.CommandEncoderFinish(command_encoder, nil)
defer wgpu.CommandBufferRelease(command_buffer)
wgpu.QueueSubmit(state.queue, { command_buffer })
wgpu.SurfacePresent(state.surface)
dt = f32(time.duration_seconds(time.tick_since(start)))
}
finish()
glfw.DestroyWindow(state.window)
glfw.Terminate()
}
resize :: proc "c" () {
context = state.ctx
state.config.width, state.config.height = get_framebuffer_size()
wgpu.SurfaceConfigure(state.surface, &state.config)
}
finish :: proc() {
wgpu.BindGroupLayoutRelease(state.bind_group_layout)
wgpu.BindGroupRelease(state.bind_group)
wgpu.BufferRelease(state.uniform_buffer)
wgpu.RenderPipelineRelease(state.pipeline)
wgpu.PipelineLayoutRelease(state.pipeline_layout)
wgpu.ShaderModuleRelease(state.module)
wgpu.QueueRelease(state.queue)
wgpu.DeviceRelease(state.device)
wgpu.AdapterRelease(state.adapter)
wgpu.SurfaceRelease(state.surface)
wgpu.InstanceRelease(state.instance)
}
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
struct Uniforms {
scale: vec2f,
offset: vec2f
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> OurVertexShaderOutput {
let pos = array(
vec2f( 0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
let color = array<vec4f, 3>(
vec4f(1, 0, 0, 1), // red
vec4f(0, 0, 1, 1), // blue
vec4f(0, 1, 0, 1), // green
);
var vsOutput: OurVertexShaderOutput;
vsOutput.position = vec4f(pos[vertexIndex]*uniforms.scale + uniforms.offset, 0.0, 1.0);
vsOutput.color = color[vertexIndex];
return vsOutput;
}
@fragment
fn fs_main(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
return fsInput.color;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment