Files
sindri/src/platform/wgpu.odin
T

686 lines
17 KiB
Odin

package platform
import "base:runtime"
import "core:fmt"
import "core:math"
import "core:math/linalg"
import "core:os"
import "core:slice"
import "wgpu:wgpu"
import "sindri:base"
// -----------------------------------------
// Types
Vec2 :: [2]f32
Vec3 :: [3]f32
Vertex :: struct #align (4) {
pos: Vec3,
col: Vec3,
uv: Vec2, // texture coordinate
}
Camera_Uniform :: struct {
mvp: linalg.Matrix4f32,
}
// -----------------------------------------
// Variables
// Instance -> Surface -> Adapter -> Device -> Queue
state: struct {
ctx: runtime.Context,
os: OS,
// ----------------------------------------
instance: wgpu.Instance, // entry point: creates Adapter, Device and Surface
surface: wgpu.Surface, // handle to a presentable surface (e.g. a window)
adapter: wgpu.Adapter, // handle to physical GPU
device: wgpu.Device, // open connection to a GPU
queue: wgpu.Queue, // handle to command queue on a Device
config: wgpu.SurfaceConfiguration,
module: wgpu.ShaderModule,
pipeline_layout: wgpu.PipelineLayout,
pipeline: wgpu.RenderPipeline,
// ----------------------------------------
capabilities: wgpu.SurfaceCapabilities,
vsync: bool,
// ----------------------------------------
depth_texture: wgpu.Texture,
depth_texture_view: wgpu.TextureView,
// ----------------------------------------
uniform_buffer: wgpu.Buffer,
bind_group_layout: wgpu.BindGroupLayout,
bind_group: wgpu.BindGroup,
// ----------------------------------------
vertex_buffer: wgpu.Buffer,
index_buffer: wgpu.Buffer,
index_count: u32,
// ----------------------------------------
diffuse_texture: wgpu.Texture,
diffuse_texture_view: wgpu.TextureView,
diffuse_sampler: wgpu.Sampler,
}
// -----------------------------------------
// Constructor/destructor
instance_init :: proc(settings: base.Settings) {
state.ctx = context
wgpu.SetLogCallback(log_callback, nil)
wgpu.SetLogLevel(.Warn)
// Instance
state.instance = wgpu.CreateInstance(nil)
if state.instance == nil {
panic("[wgpu] WebGPU is not supported")
}
// Surface
state.surface = os_get_surface(state.instance)
// Adapter
wgpu.InstanceRequestAdapter(
state.instance,
&{compatibleSurface = state.surface},
{callback = on_adapter},
)
// Present modes
get_present_modes()
state.vsync = settings.vsync
// Device
wgpu.AdapterRequestDevice(state.adapter, nil, {callback = on_device})
}
instance_destroy :: proc() {
wgpu.TextureViewRelease(state.depth_texture_view)
wgpu.TextureRelease(state.depth_texture)
wgpu.SamplerRelease(state.diffuse_sampler)
wgpu.TextureViewRelease(state.diffuse_texture_view)
wgpu.TextureRelease(state.diffuse_texture)
wgpu.BufferRelease(state.uniform_buffer)
wgpu.BufferRelease(state.vertex_buffer)
wgpu.BufferRelease(state.index_buffer)
wgpu.BindGroupRelease(state.bind_group)
wgpu.BindGroupLayoutRelease(state.bind_group_layout)
wgpu.RenderPipelineRelease(state.pipeline)
wgpu.PipelineLayoutRelease(state.pipeline_layout)
wgpu.ShaderModuleRelease(state.module)
wgpu.QueueRelease(state.queue)
wgpu.DeviceRelease(state.device)
wgpu.SurfaceCapabilitiesFreeMembers(state.capabilities)
wgpu.AdapterRelease(state.adapter)
wgpu.SurfaceRelease(state.surface)
wgpu.InstanceRelease(state.instance)
}
// -----------------------------------------
// Public functions
log_callback :: proc "c" (
level: wgpu.LogLevel,
message: string,
userdata: rawptr,
) {
context = state.ctx
fmt.eprintfln("[wgpu:%v] %v", level, message)
}
resize :: proc "c" () {
context = state.ctx
if state.surface == nil || state.device == nil do return
state.config.width, state.config.height = os_get_framebuffer_size()
wgpu.SurfaceConfigure(state.surface, &state.config)
create_depth_texture()
// The projection depends on aspect, so rebuild and re-upload the matrix
upload_camera(state.queue, state.uniform_buffer, framebuffer_aspect())
}
frame :: proc "c" () {
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 .Occluded:
// Window is occluded (e.g. minimized), skip this frame.
return
case .Error:
// Fatal error
fmt.panicf(
"[wgpu] 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 = {55 / 255.0, 73 / 255.0, 73 / 255.0, 1},
},
depthStencilAttachment = &wgpu.RenderPassDepthStencilAttachment {
view = state.depth_texture_view,
depthLoadOp = .Clear,
depthStoreOp = .Store,
depthClearValue = 1.0,
},
},
)
wgpu.RenderPassEncoderSetPipeline(render_pass_encoder, state.pipeline)
// The camera's MVP matrix: group 0
wgpu.RenderPassEncoderSetBindGroup(
render_pass_encoder,
0,
state.bind_group,
dynamicOffsets = nil, // reuse bind group af different byte offsets, per binding
)
wgpu.RenderPassEncoderSetVertexBuffer(
render_pass_encoder,
slot = 0,
buffer = state.vertex_buffer,
offset = 0, // offset into buffer to start from
size = wgpu.WHOLE_SIZE, // x bytes of the buffer to use
)
wgpu.RenderPassEncoderSetIndexBuffer(
render_pass_encoder,
buffer = state.index_buffer,
format = .Uint16, // type of each element in the buffer
offset = 0, // offset into buffer to start from
size = wgpu.WHOLE_SIZE, // x bytes of the buffer to use
)
wgpu.RenderPassEncoderDrawIndexed(
render_pass_encoder,
state.index_count,
instanceCount = 1, // draw x instance(s) of the mesh
firstIndex = 0, // offset into buffer to start from, useful for drawing sub-mesh
baseVertex = 0, // value added to index before vertex fetch, useful for multi-mesh vertex buffers
firstInstance = 0, // number to start instance index from
)
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)
}
// Upload arbitrary vertex/index data to GPU buffers
fill_buffers_with_model :: proc(vertices: []Vertex, indices: []u16) {
state.vertex_buffer = wgpu.DeviceCreateBufferWithData(
state.device,
&{label = "model vertices", usage = {.Vertex}},
vertices,
)
state.index_buffer = wgpu.DeviceCreateBufferWithData(
state.device,
&{label = "model indices", usage = {.Index}},
indices,
)
state.index_count = cast(u32)len(indices)
}
// -----------------------------------------
// Private functions
@(private = "file")
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("[wgpu] request adapter failure: [%v] %s", status, message)
}
state.adapter = adapter
}
@(private = "file")
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("[wgpu] request device failure: [%v] %s", status, message)
}
state.device = device
state.queue = wgpu.DeviceGetQueue(state.device)
width, height := os_get_framebuffer_size()
state.config = wgpu.SurfaceConfiguration {
device = state.device,
usage = {.RenderAttachment},
format = .BGRA8Unorm,
width = width,
height = height,
presentMode = pick_present_mode(state.vsync),
alphaMode = .Opaque,
}
wgpu.SurfaceConfigure(state.surface, &state.config)
// ----------------------------------------
// Depth buffer
create_depth_texture()
// ----------------------------------------
// Compile shader
uniforms_struct := to_wgsl_struct(Camera_Uniform, "Uniforms")
defer delete(uniforms_struct)
vertex_input_struct := to_wgsl_struct(
Vertex,
"VertexInput",
locations = true,
)
defer delete(vertex_input_struct)
shader := fmt.tprintf(
`
%s
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var t_diffuse: texture_2d<f32>;
@group(0) @binding(2) var s_diffuse: sampler;
%s
struct VertexOutput {{
@builtin(position) position: vec4<f32>,
@location(0) col: vec3<f32>,
@location(1) uv: vec2<f32>,
}};
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {{
var out: VertexOutput;
out.position = u.mvp * vec4<f32>(in.pos, 1.0);
out.col = in.col;
out.uv = in.uv;
return out;
}}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {{
return textureSample(t_diffuse, s_diffuse, in.uv) * vec4<f32>(in.col, 1.0);
}}
`,
uniforms_struct,
vertex_input_struct,
)
state.module = wgpu.DeviceCreateShaderModule(
state.device,
&{
nextInChain = &wgpu.ShaderSourceWGSL {
sType = .ShaderSourceWGSL,
code = shader,
},
},
)
// ----------------------------------------
// Camera uniform: buffer, then the baked layout it will be read through
state.uniform_buffer = wgpu.DeviceCreateBuffer(
state.device,
&{
label = "camera uniform",
usage = {.Uniform, .CopyDst}, // shader reads, queue writes
size = cast(u64)size_of(Camera_Uniform),
},
)
camera_layout_entries := [?]wgpu.BindGroupLayoutEntry {
{
binding = 0, // slot 0
visibility = {.Vertex},
buffer = {
type = .Uniform,
minBindingSize = cast(u64)size_of(Camera_Uniform),
},
},
{
binding = 1, // slot 1
visibility = {.Fragment},
texture = {
sampleType = .Float,
viewDimension = ._2D,
multisampled = false,
},
},
{
binding = 2, // slot 2
visibility = {.Fragment},
sampler = {type = .Filtering},
},
}
state.bind_group_layout = wgpu.DeviceCreateBindGroupLayout(
state.device,
&{
label = "camera layout",
entryCount = len(camera_layout_entries),
entries = raw_data(camera_layout_entries[:]),
},
)
// ----------------------------------------
// Diffuse texture + sampler
state.diffuse_texture, state.diffuse_texture_view, state.diffuse_sampler =
create_diffuse_texture(state.device, state.queue)
// ----------------------------------------
// Buffer shape
vertex_attributes := to_vertex_attributes(Vertex)
vertex_layout := wgpu.VertexBufferLayout {
stepMode = .Vertex,
arrayStride = size_of(Vertex),
attributeCount = len(vertex_attributes),
attributes = raw_data(vertex_attributes),
}
// ----------------------------------------
// Render pipeline
camera_group_layouts := [1]wgpu.BindGroupLayout{state.bind_group_layout}
state.pipeline_layout = wgpu.DeviceCreatePipelineLayout(
state.device,
&{
bindGroupLayoutCount = len(camera_group_layouts),
bindGroupLayouts = raw_data(camera_group_layouts[:]),
},
)
state.pipeline = wgpu.DeviceCreateRenderPipeline(
state.device,
&{
label = "pipeline",
layout = state.pipeline_layout,
vertex = {
module = state.module,
entryPoint = "vs_main",
bufferCount = 1,
buffers = &vertex_layout,
},
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},
depthStencil = &wgpu.DepthStencilState {
format = .Depth24Plus,
depthWriteEnabled = .True,
depthCompare = .Less,
},
},
)
// ----------------------------------------
// Camera bind group
bind_group_entries := [?]wgpu.BindGroupEntry {
{
binding = 0, // slot 0
buffer = state.uniform_buffer,
offset = 0, // offset into buffer to start from
size = cast(u64)size_of(Camera_Uniform),
},
{binding = 1, textureView = state.diffuse_texture_view},
{binding = 2, sampler = state.diffuse_sampler},
}
state.bind_group = wgpu.DeviceCreateBindGroup(
state.device,
&{
label = "camera bind group",
layout = state.bind_group_layout,
entryCount = len(bind_group_entries),
entries = raw_data(bind_group_entries[:]),
},
)
// Initial upload of the matrix
upload_camera(state.queue, state.uniform_buffer, framebuffer_aspect())
}
@(private = "file")
get_present_modes :: proc() {
capabilities, status := wgpu.SurfaceGetCapabilities(
state.surface,
state.adapter,
)
if status != .Success {
fmt.panicf("[wgpu] surface capabilities failure: [%v]", status)
}
state.capabilities = capabilities
present_modes := capabilities.presentModes[:capabilities.presentModeCount]
fmt.println("[wgpu] supported present modes", present_modes)
}
@(private = "file")
pick_present_mode :: proc(vsync: bool) -> wgpu.PresentMode {
present_modes := state.capabilities.presentModes[:state.capabilities.presentModeCount]
if len(present_modes) == 0 do return .Fifo
// Mimmick wgpu auto Vsync behavior
// https://docs.rs/wgpu/latest/wgpu/enum.PresentMode.html
if vsync {
if slice.contains(present_modes, wgpu.PresentMode.FifoRelaxed) {
return .FifoRelaxed // Adaptive Vsync
}
} else {
if slice.contains(present_modes, wgpu.PresentMode.Immediate) {
return .Immediate // Vsync Off
}
if slice.contains(present_modes, wgpu.PresentMode.Mailbox) {
return .Mailbox // Fast Vsync
}
}
return .Fifo // Vsync On
}
// Depth buffer: same size as the surface, should get recreated on resize
@(private = "file")
create_depth_texture :: proc() {
if state.depth_texture != nil {
wgpu.TextureViewRelease(state.depth_texture_view)
wgpu.TextureRelease(state.depth_texture)
}
state.depth_texture = wgpu.DeviceCreateTexture(
state.device,
&{
label = "depth texture",
usage = {.RenderAttachment},
dimension = ._2D,
size = {
width = state.config.width,
height = state.config.height,
depthOrArrayLayers = 1,
},
format = .Depth24Plus,
mipLevelCount = 1,
sampleCount = 1,
},
)
state.depth_texture_view = wgpu.TextureCreateView(state.depth_texture, nil)
}
@(private = "file")
framebuffer_aspect :: proc() -> f32 {
width, height := os_get_framebuffer_size()
if height == 0 {
return 1
}
return cast(f32)width / cast(f32)height
}
// Perspective projection, WebGPU flavour
@(private = "file")
camera_perspective :: proc(
fovy_degrees, aspect, near, far: f32,
) -> linalg.Matrix4f32 {
gl := linalg.matrix4_perspective(
fovy_degrees * math.PI / 180.0,
aspect,
near,
far,
)
// GL's clip space is [-1, 1] and WebGPU's clip space is [0, 1],
// so fold z with a remap: z' = 1/2 z + 1/2 w
remap := linalg.identity(linalg.Matrix4f32)
remap[2, 2] = 0.5
remap[2, 3] = 0.5
return remap * gl
}
@(private = "file")
camera_mvp :: proc(aspect: f32) -> linalg.Matrix4f32 {
projection := camera_perspective(60, aspect, 0.1, 100)
view := linalg.matrix4_look_at_f32({0, 0.75, 2}, {0, 0, 0}, {0, 1, 0})
// model = identity: the loader already centered the model at the origin
return projection * view
}
@(private = "file")
upload_camera :: proc(queue: wgpu.Queue, buffer: wgpu.Buffer, aspect: f32) {
uniforms := Camera_Uniform {
mvp = camera_mvp(aspect),
}
// bufferOffset = offset into buffer to start from
wgpu.QueueWriteBuffer(queue, buffer, 0, &uniforms, size_of(Camera_Uniform))
}
// Test 256x256 checkerboard texture, uploaded once
@(private = "file")
create_diffuse_texture :: proc(
device: wgpu.Device,
queue: wgpu.Queue,
) -> (
wgpu.Texture,
wgpu.TextureView,
wgpu.Sampler,
) {
width, height: u32 = 256, 256
pixels := make([]u8, width * height * 4)
defer delete(pixels)
for y in 0 ..< height {
for x in 0 ..< width {
checker := ((x / 32) + (y / 32)) % 2 == 0
idx := (y * width + x) * 4
if checker {
pixels[idx + 0] = 240
pixels[idx + 1] = 240
pixels[idx + 2] = 240
pixels[idx + 3] = 255
} else {
pixels[idx + 0] = 64
pixels[idx + 1] = 64
pixels[idx + 2] = 64
pixels[idx + 3] = 255
}
}
}
texture := wgpu.DeviceCreateTexture(
device,
&{
label = "diffuse texture",
usage = {.TextureBinding, .CopyDst}, // shader samples, queue writes
dimension = ._2D,
size = {width = width, height = height, depthOrArrayLayers = 1},
format = .RGBA8Unorm,
mipLevelCount = 1,
sampleCount = 1,
},
)
// Upload texture to GPU
wgpu.QueueWriteTexture(
queue,
&{texture = texture, mipLevel = 0, origin = {0, 0, 0}, aspect = .All}, // destination
raw_data(pixels),
len(pixels),
&{offset = 0, bytesPerRow = width * 4, rowsPerImage = height}, // data layout
&{width = width, height = height, depthOrArrayLayers = 1}, // 3D extent
)
view := wgpu.TextureCreateView(texture, nil)
sampler := wgpu.DeviceCreateSampler(
device,
&{
addressModeU = .Repeat,
addressModeV = .Repeat,
magFilter = .Nearest,
minFilter = .Nearest,
mipmapFilter = .Nearest,
maxAnisotropy = 1,
},
)
return texture, view, sampler
}