Created
October 31, 2025 04:43
-
-
Save fearofcode/d2dc09058da3bca125484f56bda92f71 to your computer and use it in GitHub Desktop.
WebGPU Odin simple texture example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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