Skip to content

Instantly share code, notes, and snippets.

@fearofcode
Created October 31, 2025 04:43
Show Gist options
  • Save fearofcode/d2dc09058da3bca125484f56bda92f71 to your computer and use it in GitHub Desktop.
Save fearofcode/d2dc09058da3bca125484f56bda92f71 to your computer and use it in GitHub Desktop.
WebGPU Odin simple texture example
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"
OBJECT_COUNT :: 1
VERTEX_COUNT :: 6
TEXTURE_WIDTH :: 35
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,
bind_group_layout: wgpu.BindGroupLayout,
instance_buffer: wgpu.Buffer,
vertex_buffer: wgpu.Buffer,
bind_group: wgpu.BindGroup,
texture: wgpu.Texture,
texture_view: wgpu.TextureView,
sampler: wgpu.Sampler,
}
Instance :: struct {
scale: glm.vec2,
offset: glm.vec2
}
Vertex :: struct {
position: glm.vec2,
texcoord: 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
// required_features := []wgpu.FeatureName{.Float32Filterable}
// device_descriptor := wgpu.DeviceDescriptor {
// requiredFeatureCount=1,
// requiredFeatures = raw_data(required_features),
// }
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_entries := [3]wgpu.BindGroupLayoutEntry {
{
binding = 0,
visibility = { .Vertex },
buffer = wgpu.BufferBindingLayout {
type = .ReadOnlyStorage,
},
},
{
binding = 1,
visibility = { .Fragment },
sampler = wgpu.SamplerBindingLayout {
type = .Filtering,
},
},
{
binding = 2,
visibility = { .Fragment },
texture = wgpu.TextureBindingLayout {
sampleType = .Float,
viewDimension = ._2D,
},
},
}
state.bind_group_layout = wgpu.DeviceCreateBindGroupLayout(state.device, &{
label = "Instance + Texture Bind Group Layout",
entryCount = 3,
entries = &bind_group_layout_entries[0],
})
state.pipeline_layout = wgpu.DeviceCreatePipelineLayout(state.device, &{
bindGroupLayoutCount = 1,
bindGroupLayouts = &state.bind_group_layout,
})
vertex_attributes: [2]wgpu.VertexAttribute
vertex_attributes[0] = wgpu.VertexAttribute {
format = .Float32x2,
shaderLocation = 0,
offset = 0
}
vertex_attributes[1] = wgpu.VertexAttribute {
format = .Float32x2,
shaderLocation = 1,
offset = 8
}
state.pipeline = wgpu.DeviceCreateRenderPipeline(state.device, &{
layout = state.pipeline_layout,
vertex = {
module = state.module,
entryPoint = "vs_main",
bufferCount = 1,
buffers = &wgpu.VertexBufferLayout {
stepMode = .Vertex,
arrayStride = size_of(Vertex),
attributeCount = 2,
attributes = &vertex_attributes[0]
},
},
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,
},
})
instance_buffer_size := size_of(Instance)
state.instance_buffer = wgpu.DeviceCreateBuffer(state.device, &{
usage = { .Storage, .CopyDst },
size = u64(instance_buffer_size)*OBJECT_COUNT,
})
state.vertex_buffer = wgpu.DeviceCreateBuffer(state.device, &{
usage = { .Vertex, .CopyDst },
size = size_of(Vertex)*VERTEX_COUNT,
})
vertices := [VERTEX_COUNT] Vertex {
// first triangle
Vertex{
position = glm.vec2{-0.5, 0.5}, // top left
texcoord = glm.vec2{0.0, 0.0} // top left (coordinates different from OpenGL)
},
Vertex{
position = glm.vec2{-0.5, -0.5}, // bottom left
texcoord = glm.vec2{0.0, 1.0}
},
Vertex{
position = glm.vec2{0.5, -0.5}, // bottom right
texcoord = glm.vec2{1.0, 1.0}
},
// second triangle
Vertex{
position = glm.vec2{0.5, -0.5}, // bottom right
texcoord = glm.vec2{1.0, 1.0}
},
Vertex{
position = glm.vec2{0.5, 0.5}, // top right
texcoord = glm.vec2{1.0, 0.0}
},
Vertex{
position = glm.vec2{-0.5, 0.5}, // top left
texcoord = glm.vec2{0.0, 0.0}
},
}
wgpu.QueueWriteBuffer(
queue=state.queue,
buffer=state.vertex_buffer,
bufferOffset=0,
data=&vertices,
size=size_of(Vertex)*VERTEX_COUNT,
)
red := [4]u8 {255, 0, 0, 255}
green := [4]u8 {0, 255, 0, 255}
blue := [4]u8 {0, 0, 255, 255}
texture_data := [TEXTURE_WIDTH] [4]u8 {
blue, red, red, red, red,
red, green, green, green, red,
red, green, red, red, red,
red, green, green, red, red,
red, green, red, red, red,
red, green, red, red, red,
red, red, red, red, red,
}
state.texture = wgpu.DeviceCreateTexture(device, &{
size = wgpu.Extent3D { width = 5, height = 7, depthOrArrayLayers=1},
format = .RGBA8UnormSrgb,
usage = {.TextureBinding, .CopyDst},
mipLevelCount=1,
sampleCount=1,
})
state.texture_view = wgpu.TextureCreateView(state.texture)
wgpu.QueueWriteTexture(
queue=state.queue,
destination=&{
texture = state.texture,
},
data=&texture_data,
dataSize=size_of(texture_data),
dataLayout = &{
rowsPerImage=5*7,
bytesPerRow=5*size_of([4]u8),
},
writeSize = &{
width = 5, height = 7, depthOrArrayLayers=1
}
)
state.sampler = wgpu.DeviceCreateSampler(device)
bind_group_entries := [3]wgpu.BindGroupEntry {
{
binding = 0,
buffer = state.instance_buffer,
size = u64(size_of(Instance))*OBJECT_COUNT,
},
{
binding = 1,
sampler = state.sampler,
},
{
binding = 2,
textureView = state.texture_view,
},
}
state.bind_group = wgpu.DeviceCreateBindGroup(device, &{
layout = state.bind_group_layout,
entryCount = 3,
entries = &bind_group_entries[0],
})
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)))
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.RenderPassEncoderSetVertexBuffer(render_pass_encoder, 0, state.vertex_buffer, 0, size_of(Vertex)*VERTEX_COUNT)
instance_values: [OBJECT_COUNT]Instance
for i in 0..<OBJECT_COUNT {
offset_t := f32(math.sin(total_seconds))*0.1 + f32(i)/20.0
y_offset := f32(i)/10.0
instance_values[i] = Instance {
scale = glm.vec2{ 1.0, 1.0},
offset = glm.vec2{ offset_t, -0.2 + y_offset},
}
}
wgpu.QueueWriteBuffer(
queue=state.queue,
buffer=state.instance_buffer,
bufferOffset=0,
data=&instance_values,
size=size_of(Instance)*OBJECT_COUNT,
)
wgpu.RenderPassEncoderSetBindGroup(render_pass_encoder, 0, state.bind_group)
wgpu.RenderPassEncoderDraw(render_pass_encoder, vertexCount=VERTEX_COUNT, instanceCount=OBJECT_COUNT, 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.TextureRelease(state.texture)
wgpu.TextureViewRelease(state.texture_view)
wgpu.SamplerRelease(state.sampler)
wgpu.BindGroupLayoutRelease(state.bind_group_layout)
wgpu.BindGroupRelease(state.bind_group)
wgpu.BufferRelease(state.instance_buffer)
wgpu.BufferRelease(state.vertex_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 Vertex {
@location(0) position: vec2f,
@location(1) texcoord: vec2f
}
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
};
struct InstanceData {
scale: vec2f,
offset: vec2f
};
@group(0) @binding(0) var<storage, read> instances: array<InstanceData>;
@group(0) @binding(1) var texture_sampler: sampler;
@group(0) @binding(2) var texture: texture_2d<f32>;
@vertex
fn vs_main(vert: Vertex, @builtin(instance_index) instanceIndex: u32) -> OurVertexShaderOutput {
var vsOutput: OurVertexShaderOutput;
let instance = instances[instanceIndex];
vsOutput.position = vec4f(vert.position*instance.scale + instance.offset, 0.0, 1.0);
vsOutput.texcoord = vert.texcoord;
return vsOutput;
}
@fragment
fn fs_main(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
return textureSample(texture, texture_sampler, fsInput.texcoord);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment