diff --git a/mimr_fractal/src/build_web.sh b/mimr_fractal/src/build_web.sh new file mode 100755 index 0000000..90d5fd8 --- /dev/null +++ b/mimr_fractal/src/build_web.sh @@ -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 \ No newline at end of file diff --git a/mimr_fractal/src/main.odin b/mimr_fractal/src/main.odin new file mode 100644 index 0000000..ba91549 --- /dev/null +++ b/mimr_fractal/src/main.odin @@ -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 { + let x = f32(i32(in_vertex_index) - 1); + let y = f32(i32(in_vertex_index & 1u) * 2 - 1); + return vec4(x, y, 0.0, 1.0); + } + + @fragment + fn fs_main() -> @location(0) vec4 { + return vec4(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) +} diff --git a/mimr_fractal/src/os_js.odin b/mimr_fractal/src/os_js.odin new file mode 100644 index 0000000..ab2437a --- /dev/null +++ b/mimr_fractal/src/os_js.odin @@ -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() +} diff --git a/mimr_fractal/src/os_sdl3.odin b/mimr_fractal/src/os_sdl3.odin new file mode 100644 index 0000000..25fc7af --- /dev/null +++ b/mimr_fractal/src/os_sdl3.odin @@ -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) +} diff --git a/mimr_fractal/src/web/index.html b/mimr_fractal/src/web/index.html new file mode 100644 index 0000000..3b804a1 --- /dev/null +++ b/mimr_fractal/src/web/index.html @@ -0,0 +1,26 @@ + + + + + + + WGPU WASM Triangle + + + + + + + + + + + \ No newline at end of file