Compare commits

..
4 Commits
Author SHA1 Message Date
Riyyi 2b8488a1e2 Add mouse position input function 2026-09-13 00:43:18 +02:00
Riyyi 7740632541 Free wgpu capabilities memory 2026-09-13 00:23:56 +02:00
Riyyi 5ef1dafed2 Fix debug build warning 2026-09-12 18:15:55 +02:00
Riyyi 86ee8083b5 Apply Vsync setting 2026-09-12 18:13:00 +02:00
6 changed files with 180 additions and 103 deletions
+1 -2
View File
@@ -6,6 +6,7 @@ PROJECT="sindri"
VERSION="dev-$(date -u '+%Y-%m-%d')-$(git rev-parse --short HEAD)"
OPTION="${1:-}"
shift
# ------------------------------------------
@@ -42,8 +43,6 @@ if pgrep -x $PROJECT >/dev/null; then
fi
if [ "$OPTION" = "debug" ]; then
shift
odin build src/ -show-timings \
-collection:sindri=src \
-collection:gram=vendor/gram/src \
+1 -1
View File
@@ -7,7 +7,7 @@ Settings :: struct {
height: u16,
title: string,
mode: WindowMode,
refresh: u16,
refresh: u16, // 0 = unlimited
vsync: bool,
// TODO:
// windowed resizable y/n
+4
View File
@@ -11,5 +11,9 @@ key_state: proc(key: Key) -> Action
// implementation in platform package, to prevent cyclic dependency
mouse_button_state: proc(button: Mouse_Button) -> Action
// Returns the position of the mouse cursor
// implementation in platform package, to prevent cyclic dependency
mouse_position: proc() -> (x_pos: f32, y_pos: f32)
// -----------------------------------------
// Private functions
+1 -1
View File
@@ -26,7 +26,7 @@ main :: proc() {
defer platform.os_destroy()
// Initialize GPU resources
platform.instance_init()
platform.instance_init(settings)
defer platform.instance_destroy()
api.init_once()
+8 -7
View File
@@ -52,11 +52,7 @@ os_init :: proc(settings: core.Settings) {
// Register input functions
input.key_state = key_state
input.mouse_button_state = mouse_button_state
// TODO: Figure out proper vsync, found 3 spots so far
// - glfw.SwapInterval(0) this is only for OpenGL it seems?
// - glfw.SetWindowMonitor(refresh)
// - wgpu presentMode = .Fifo
input.mouse_position = mouse_position
}
os_destroy :: proc() {
@@ -106,8 +102,7 @@ os_set_monitor :: proc(settings: core.Settings) {
)
}
refresh := settings.vsync ? mode.refresh_rate : i32(settings.refresh)
if refresh == 0 do refresh = glfw.DONT_CARE
refresh := settings.refresh == 0 ? glfw.DONT_CARE : i32(settings.refresh)
glfw.SetWindowMonitor(
state.os.window,
@@ -158,6 +153,12 @@ mouse_button_state :: proc(button: input.Mouse_Button) -> input.Action {
return input_action(glfw.GetMouseButton(state.os.window, i32(button)))
}
@(private = "file")
mouse_position :: proc() -> (x_pos: f32, y_pos: f32) {
x_pos_f64, y_pos_64 := glfw.GetCursorPos(state.os.window)
return f32(x_pos_f64), f32(y_pos_64)
}
@(private = "file")
input_action :: proc(action: i32) -> input.Action {
if action == glfw.RELEASE do return input.Action.Release
+165 -92
View File
@@ -2,15 +2,20 @@ package platform
import "base:runtime"
import "core:fmt"
import "core:slice"
import "wgpu:wgpu"
import "sindri:core"
// -----------------------------------------
// 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
@@ -20,115 +25,42 @@ state: struct {
module: wgpu.ShaderModule,
pipeline_layout: wgpu.PipelineLayout,
pipeline: wgpu.RenderPipeline,
// ----------------------------------------
capabilities: wgpu.SurfaceCapabilities,
vsync: bool,
}
// -----------------------------------------
// Constructor/destructor
instance_init :: proc() {
instance_init :: proc(settings: core.Settings) {
state.ctx = context
wgpu.SetLogCallback(log_callback, nil)
wgpu.SetLogLevel(.Warn)
// Instance
state.instance = wgpu.CreateInstance(nil)
if state.instance == nil {
panic("WebGPU is not supported")
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},
)
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
wgpu.AdapterRequestDevice(adapter, nil, {callback = on_device})
}
// Present modes
get_present_modes()
state.vsync = settings.vsync
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
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,
// https://docs.rs/wgpu/latest/wgpu/enum.PresentMode.html
presentMode = .Fifo, // .Fifo is essentially VSync
alphaMode = .Opaque,
}
wgpu.SurfaceConfigure(state.surface, &state.config)
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},
},
)
}
// Device
wgpu.AdapterRequestDevice(state.adapter, nil, {callback = on_device})
}
instance_destroy :: proc() {
@@ -137,12 +69,23 @@ instance_destroy :: proc() {
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
@@ -173,7 +116,7 @@ frame :: proc "c" (dt: f32) {
case .Error:
// Fatal error
fmt.panicf(
"[triangle] get_current_texture status=%v",
"[wgpu] triangle get_current_texture status=%v",
surface_texture.status,
)
}
@@ -217,3 +160,133 @@ frame :: proc "c" (dt: f32) {
wgpu.QueueSubmit(state.queue, {command_buffer})
wgpu.SurfacePresent(state.surface)
}
// -----------------------------------------
// 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)
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},
},
)
}
@(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
}