yank sdl3-triangle example

- https://github.com/odin-lang/examples/tree/master/wgpu/sdl3-triangle
This commit is contained in:
2025-11-09 21:21:30 +01:00
parent d582848582
commit 490bf9dd33
5 changed files with 374 additions and 0 deletions

20
mimr_fractal/src/build_web.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euxo pipefail
# NOTE: changing this requires changing the same values in the `web/index.html`.
INITIAL_MEMORY_PAGES=2000
MAX_MEMORY_PAGES=65536
INITIAL_MEMORY_BYTES=$(expr $INITIAL_MEMORY_PAGES \* $MAX_MEMORY_PAGES)
MAX_MEMORY_BYTES=$(expr $MAX_MEMORY_PAGES \* $MAX_MEMORY_PAGES)
ODIN_ROOT=$(odin root)
ODIN_JS="$ODIN_ROOT/core/sys/wasm/js/odin.js"
WGPU_JS="$ODIN_ROOT/vendor/wgpu/wgpu.js"
odin build . -target:js_wasm32 -out:web/triangle.wasm -o:size \
-extra-linker-flags:"--export-table --import-memory --initial-memory=$INITIAL_MEMORY_BYTES --max-memory=$MAX_MEMORY_BYTES"
cp $ODIN_JS web/odin.js
cp $WGPU_JS web/wgpu.js

199
mimr_fractal/src/main.odin Normal file
View File

@@ -0,0 +1,199 @@
package vendor_wgpu_example_triangle
import "base:runtime"
import "core:fmt"
import "vendor:wgpu"
state: struct {
ctx: runtime.Context,
os: OS,
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,
}
main :: proc() {
state.ctx = context
os_init()
state.instance = wgpu.CreateInstance(nil)
if state.instance == nil {
panic("WebGPU is not supported")
}
state.surface = os_get_surface(state.instance)
wgpu.InstanceRequestAdapter(
state.instance,
&{compatibleSurface = state.surface},
{callback = on_adapter},
)
on_adapter :: proc "c" (
status: wgpu.RequestAdapterStatus,
adapter: wgpu.Adapter,
message: string,
userdata1, 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, userdata2: rawptr,
) {
context = state.ctx
if status != .Success || device == nil {
fmt.panicf("request device failure: [%v] %s", status, message)
}
state.device = device
width, height := os_get_framebuffer_size()
state.config = wgpu.SurfaceConfiguration {
device = state.device,
usage = {.RenderAttachment},
format = .BGRA8Unorm,
width = width,
height = height,
presentMode = .Fifo,
alphaMode = .Opaque,
}
wgpu.SurfaceConfigure(state.surface, &state.config)
state.queue = wgpu.DeviceGetQueue(state.device)
shader :: `
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
let x = f32(i32(in_vertex_index) - 1);
let y = f32(i32(in_vertex_index & 1u) * 2 - 1);
return vec4<f32>(x, y, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}`
state.module = wgpu.DeviceCreateShaderModule(
state.device,
&{nextInChain = &wgpu.ShaderSourceWGSL{sType = .ShaderSourceWGSL, code = shader}},
)
state.pipeline_layout = wgpu.DeviceCreatePipelineLayout(state.device, &{})
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 = .BGRA8Unorm,
writeMask = wgpu.ColorWriteMaskFlags_All,
},
},
primitive = {topology = .TriangleList},
multisample = {count = 1, mask = 0xFFFFFFFF},
},
)
os_run()
}
}
resize :: proc "c" () {
context = state.ctx
state.config.width, state.config.height = os_get_framebuffer_size()
wgpu.SurfaceConfigure(state.surface, &state.config)
}
frame :: proc "c" (dt: f32) {
context = state.ctx
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("[triangle] 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, 1, 0, 1},
},
},
)
wgpu.RenderPassEncoderSetPipeline(render_pass_encoder, state.pipeline)
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)
}
finish :: proc() {
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)
}

View File

@@ -0,0 +1,62 @@
package vendor_wgpu_example_triangle
import "base:runtime"
import "core:sys/wasm/js"
import "vendor:wgpu"
OS :: struct {
initialized: bool,
}
os_init :: proc() {
ok := js.add_window_event_listener(.Resize, nil, size_callback)
assert(ok)
}
// NOTE: frame loop is done by the runtime.js repeatedly calling `step`.
os_run :: proc() {
state.os.initialized = true
}
@(private = "file", export)
step :: proc(dt: f32) -> bool {
if !state.os.initialized {
return true
}
frame(dt)
return true
}
os_get_framebuffer_size :: proc() -> (width, height: u32) {
rect := js.get_bounding_client_rect("body")
dpi := js.device_pixel_ratio()
return u32(f64(rect.width) * dpi), u32(f64(rect.height) * dpi)
}
os_get_surface :: proc(instance: wgpu.Instance) -> wgpu.Surface {
return wgpu.InstanceCreateSurface(
instance,
&wgpu.SurfaceDescriptor {
nextInChain = &wgpu.SurfaceSourceCanvasHTMLSelector {
sType = .SurfaceSourceCanvasHTMLSelector,
selector = "#wgpu-canvas",
},
},
)
}
@(private = "file", fini)
os_fini :: proc "contextless" () {
context = runtime.default_context()
js.remove_window_event_listener(.Resize, nil, size_callback)
finish()
}
@(private = "file")
size_callback :: proc(e: js.Event) {
resize()
}

View File

@@ -0,0 +1,67 @@
#+build !js
package vendor_wgpu_example_triangle
import "core:fmt"
import SDL "vendor:sdl3"
import "vendor:wgpu"
import "vendor:wgpu/sdl3glue"
OS :: struct {
window: ^SDL.Window,
}
os_init :: proc() {
if !SDL.Init({.VIDEO}) {
fmt.panicf("SDL.Init error: ", SDL.GetError())
}
state.os.window = SDL.CreateWindow(
"WGPU Native Triangle",
960,
540,
{.RESIZABLE, .HIGH_PIXEL_DENSITY},
)
if state.os.window == nil {
fmt.panicf("SDL.CreateWindow error: ", SDL.GetError())
}
}
os_run :: proc() {
now := SDL.GetPerformanceCounter()
last: u64
dt: f32
main_loop: for {
last = now
now = SDL.GetPerformanceCounter()
dt = f32((now - last) * 1000) / f32(SDL.GetPerformanceFrequency())
e: SDL.Event
for SDL.PollEvent(&e) {
#partial switch (e.type) {
case .QUIT:
break main_loop
case .WINDOW_RESIZED, .WINDOW_PIXEL_SIZE_CHANGED:
resize()
}
}
frame(dt)
}
finish()
SDL.DestroyWindow(state.os.window)
SDL.Quit()
}
os_get_framebuffer_size :: proc() -> (width, height: u32) {
w, h: i32
SDL.GetWindowSizeInPixels(state.os.window, &w, &h)
return u32(w), u32(h)
}
os_get_surface :: proc(instance: wgpu.Instance) -> wgpu.Surface {
return sdl3glue.GetSurface(instance, state.os.window)
}

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en" style="height: 100%;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WGPU WASM Triangle</title>
</head>
<body id="body" style="height: 100%; padding: 0; margin: 0; overflow: hidden;">
<canvas id="wgpu-canvas" style="height: 100%; width: 100%;"></canvas>
<script type="text/javascript" src="odin.js"></script>
<script type="text/javascript" src="wgpu.js"></script>
<script type="text/javascript">
const mem = new WebAssembly.Memory({ initial: 2000, maximum: 65536, shared: false });
const memInterface = new odin.WasmMemoryInterface();
memInterface.setMemory(mem);
const wgpuInterface = new odin.WebGPUInterface(memInterface);
odin.runWasm("triangle.wasm", null, { wgpu: wgpuInterface.getInterface() }, memInterface, /*intSize=8*/);
</script>
</body>
</html>