Compare commits

...
9 Commits
Author SHA1 Message Date
Riyyi be516d39a3 Add Windows build script 2026-09-15 18:41:04 +02:00
Riyyi e7cc935035 lol 2026-09-14 23:00:24 +02:00
Riyyi 416b42c27e . 2026-09-14 19:25:33 +02:00
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
Riyyi f1028acb10 Implement type-safe input handling 2026-09-12 15:08:50 +02:00
Riyyi 90602ff1ff Basic version of events 2026-09-08 22:54:33 +02:00
14 changed files with 1054 additions and 355 deletions
+52
View File
@@ -0,0 +1,52 @@
$ErrorActionPreference = "Stop"
$PROJECT = "sindri"
$COMMIT = git rev-parse --short HEAD
$VERSION = "dev-$(Get-Date -Format 'yyyy-MM-dd')-$COMMIT"
$OPTION = $args[0]
if ($args) { $args = $args[1..($args.Length - 1)] } else { $args = @() }
# ------------------------------------------
# Game compile
New-Item -ItemType Directory -Force -Path build | Out-Null
# Game.dll
odin build game/ -show-timings `
-collection:sindri=src `
-build-mode:dynamic `
-out:build/game_tmp -microarch:native "-define:VERSION=$VERSION-debug" -debug @args
# Need to use a temp file on Windows because it first writes an empty file,
# which the engine will load before it is actually fully written.
if (-not (Test-Path "build/game_tmp.dll")) {
Write-Error "error: build produced no game lib.dll"
exit 1
}
Move-Item -Force "build/game_tmp.dll" "build/game.dll"
# ------------------------------------------
# Engine compile
# If the executable is already running, then don't try to build and start it.
$proc = Get-Process -Name $PROJECT -ErrorAction SilentlyContinue
if ($proc) {
Write-Output "Hot reloading..."
exit 0
}
if ($OPTION -eq "debug") {
odin build src/ -show-timings `
-collection:sindri=src `
-collection:gram=vendor/gram/src `
-collection:wgpu=vendor `
-out:build/$PROJECT -microarch:native -use-separate-modules "-define:VERSION=$VERSION-debug" -debug @args
exit 0
}
odin build src/ -show-timings `
-collection:sindri=src `
-collection:gram=vendor/gram/src `
-collection:wgpu=vendor `
-out:build/$PROJECT -microarch:native -o:speed "-define:VERSION=$VERSION" @args
+4 -3
View File
@@ -5,6 +5,9 @@ set -eu
PROJECT="sindri"
VERSION="dev-$(date -u '+%Y-%m-%d')-$(git rev-parse --short HEAD)"
OPTION="${1:-}"
shift
# ------------------------------------------
# Setup compiled wgpu binary
@@ -39,9 +42,7 @@ if pgrep -x $PROJECT >/dev/null; then
exit 0
fi
if [ "$1" = "debug" ]; then
shift
if [ "$OPTION" = "debug" ]; then
odin build src/ -show-timings \
-collection:sindri=src \
-collection:gram=vendor/gram/src \
+22
View File
@@ -0,0 +1,22 @@
# Odin
This document contains specifics about the workings of the Odin programming
language, dynamic linking and state management design patterns. These are
tested and validated findings, for continued reference by me.
## Dynamic Library Memory
When a program loads in a dynamic library, what happens with package globals?
Findings (macOS):
- The package globals are shared between the host and the lib
- After hot reloading the lib, the memory of the packge is not refreshed,
it keeps pointing to the package global of the host
The clanker states this is due to "flat-namespace symbol interposition".
Linux "ELF has symbol preemption: when resolving references, ld.so searches
globals scope in order — executable first, then loaded shared objects".
Windows PE has no interposition at all, every module (exe and each DLL) has its
own symbol table.
+20 -6
View File
@@ -1,13 +1,16 @@
package game
import "sindri:input"
import "core:time"
import "core:fmt"
import "sindri:core"
import "sindri:test"
// -----------------------------------------
Game_Memory :: struct {
should_close: bool,
}
g: ^Game_Memory
@@ -52,6 +55,8 @@ settings :: proc() -> core.Settings {
@(export)
init_once :: proc() {
fmt.println("init once")
test.test_proc = proc() {}
}
@(export)
@@ -59,11 +64,22 @@ init :: proc() {
fmt.println("hello from .dll!")
g = new(Game_Memory)
memory_set(g)
test.test_proc = proc() { asd := 2 }
}
@(export)
update :: proc(dt: f32) {
fmt.println("dt:", dt)
if input.key_state(.Key_Escape) == .Press {
g.should_close = true
}
fmt.println("GAME:", test.test)
fmt.printf("pointer: %p | %p\n", &test.test, &test.test_proc)
test.test += 1
}
@(export)
@@ -77,19 +93,17 @@ start := time.tick_now()
should_close :: proc() -> bool {
elapsed := time.tick_since(start)
seconds := time.duration_seconds(elapsed)
if seconds > 4 do return true
// if seconds > 4 do return true
return false
return g.should_close
}
@(export)
force_reload :: proc() -> bool {
// TODO: read keypress
return false
return input.key_state(.Key_F5) == .Press
}
@(export)
force_restart :: proc() -> bool {
// TODO: read keypress
return false
return input.key_state(.Key_F6) == .Press
}
+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
+138
View File
@@ -0,0 +1,138 @@
package event
import "core:fmt"
import "sindri:input"
// -----------------------------------------
// Types
// event category, bitfield (?)
// Event_Data :: union {
Event :: union {
Window_Close_Event,
Window_Resize_Event,
Joystick_Connect_Event,
Joystick_Disconnect_Event,
Key_Press_Event,
Key_Release_Event,
Key_Repeat_Event,
Mouse_Button_Press_Event,
Mouse_Button_Release_Event,
Mouse_Position_Event,
Mouse_Scroll_Event,
}
// Event :: struct {
// data: Event_Data,
// handled: bool,
// }
Window_Close_Event :: struct {
handled: bool,
}
Window_Resize_Event :: struct {
handled: bool,
width: i32,
height: i32,
}
Joystick_Connect_Event :: struct {
handled: bool,
id: i32,
}
Joystick_Disconnect_Event :: struct {
handled: bool,
id: i32,
}
Key_Press_Event :: struct {
handled: bool,
key: input.Key,
mods: input.Mod_Set,
}
Key_Release_Event :: struct {
handled: bool,
key: input.Key,
mods: input.Mod_Set,
}
Key_Repeat_Event :: struct {
handled: bool,
key: input.Key,
mods: input.Mod_Set,
}
Mouse_Button_Press_Event :: struct {
handled: bool,
button: input.Mouse_Button,
mods: input.Mod_Set,
}
Mouse_Button_Release_Event :: struct {
handled: bool,
button: input.Mouse_Button,
mods: input.Mod_Set,
}
Mouse_Position_Event :: struct {
handled: bool,
x_pos: f32,
y_pos: f32,
}
Mouse_Scroll_Event :: struct {
handled: bool,
x_offset: f32,
y_offset: f32,
}
// -----------------------------------------
// Variables
// I want to have a list of Listeners per Event type, but this is state
// listeners: map[typeid][dynamic]Listener
// -----------------------------------------
// Public functions
// Dispatcher(Event) -> forwards to the right function
on_event :: proc(event: Event) {
fmt.println("toby!")
#partial switch e in event {
case Window_Close_Event:
fmt.println("Close")
case Window_Resize_Event:
fmt.println("Resize:", e.width, e.height)
case Key_Press_Event:
fmt.println("Key press", e.key, e.mods)
case Key_Release_Event:
fmt.println("Key release", e.key, e.mods)
case Key_Repeat_Event:
fmt.println("Key repeat", e.key, e.mods)
case Mouse_Button_Press_Event:
fmt.println("Mouse press", e.button, e.mods)
case Mouse_Button_Release_Event:
fmt.println("Mouse release", e.button, e.mods)
case Mouse_Position_Event:
fmt.printfln("Mouse pos: {}x{}", e.x_pos, e.y_pos)
case Joystick_Connect_Event:
fmt.println("Joy con:", e.id)
case Joystick_Disconnect_Event:
fmt.println("Joy dis:", e.id)
}
}
/*
Notes:
Have the events queued up into a frame buffer that gets drained at a specific time.
*/
-118
View File
@@ -1,118 +0,0 @@
package sindri
import "core:strings"
import "vendor:glfw"
import "wgpu:wgpu"
import "wgpu:wgpu/glfwglue"
import "sindri:core"
// -----------------------------------------
OS :: struct {
window: glfw.WindowHandle,
}
// -----------------------------------------
os_init :: proc(settings: core.Settings) {
if !glfw.Init() {
panic("[glfw] init failure")
}
glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API)
state.os.window = glfw.CreateWindow(
i32(settings.width),
i32(settings.height),
strings.unsafe_string_to_cstring(settings.title),
nil,
nil,
)
glfw.SetFramebufferSizeCallback(state.os.window, size_callback)
}
os_set_monitor :: proc(settings: core.Settings) {
monitor := glfw.GetPrimaryMonitor()
x_pos: i32
y_pos: i32
width := i32(settings.width)
height := i32(settings.height)
target_monitor: glfw.MonitorHandle
mode := glfw.GetVideoMode(monitor)
switch settings.mode {
case .Fullscreen:
target_monitor = monitor
case .Borderless:
// FIXME: On macOS, borderless also fills the notch area
// Monitor origin on the virtual desktop
x_pos, y_pos = glfw.GetMonitorPos(monitor)
width = mode.width
height = mode.height
glfw.SetWindowAttrib(
state.os.window,
glfw.DECORATED,
i32(glfw.FALSE),
)
case .Windowed:
// Put window in the center of the monitor
x_pos = (mode.width - width) / 2
y_pos = (mode.height - height) / 2
glfw.SetWindowAttrib(
state.os.window,
glfw.DECORATED,
i32(glfw.TRUE),
)
}
refresh := settings.vsync ? mode.refresh_rate : i32(settings.refresh)
if refresh == 0 do refresh = glfw.DONT_CARE
glfw.SetWindowMonitor(
state.os.window,
target_monitor,
x_pos,
y_pos,
width,
height,
refresh,
)
}
os_should_close :: proc() -> bool {
return bool(glfw.WindowShouldClose(state.os.window))
}
os_set_should_close :: proc(val: bool) {
glfw.SetWindowShouldClose(state.os.window, b32(val))
}
os_poll_events :: proc() {
glfw.PollEvents()
}
os_destroy :: proc() {
glfw.DestroyWindow(state.os.window)
glfw.Terminate()
}
os_get_framebuffer_size :: proc() -> (width, height: u32) {
iw, ih := glfw.GetFramebufferSize(state.os.window)
return u32(iw), u32(ih)
}
os_get_surface :: proc(instance: wgpu.Instance) -> wgpu.Surface {
return glfwglue.GetSurface(instance, state.os.window)
}
@(private = "file")
size_callback :: proc "c" (window: glfw.WindowHandle, width, height: i32) {
resize()
}
+19
View File
@@ -0,0 +1,19 @@
package input
// -----------------------------------------
// Public functions
// Returns the state of the keyboard key
// implementation in platform package, to prevent cyclic dependency
key_state: proc(key: Key) -> Action
// Returns the state of the mouse button
// 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
+183
View File
@@ -0,0 +1,183 @@
package input
// -----------------------------------------
// Button/Key states
Action :: enum i8 {
None = -1,
Release = 0,
Press = 1,
Repeat = 2,
}
Key :: enum i16 {
// The unknown key
Key_Unknown = -1,
// --- Printable keys ---
// Named printable keys
Key_Space = 32,
Key_Apostrophe = 39, // '
Key_Comma = 44, // ,
Key_Minus = 45, // -
Key_Period = 46, // .
Key_Slash = 47, // /
Key_Semicolon = 59, // ;
Key_Equal = 61, // =
Key_Left_Bracket = 91, // [
Key_Backslash = 92, // \
Key_Right_Bracket = 93, // ]
Key_Grave_Accent = 96, // `
Key_World_1 = 161, // non-US #1
Key_World_2 = 162, // non-US #2
// Alphanumeric characters
Key_0 = 48,
Key_1 = 49,
Key_2 = 50,
Key_3 = 51,
Key_4 = 52,
Key_5 = 53,
Key_6 = 54,
Key_7 = 55,
Key_8 = 56,
Key_9 = 57,
Key_A = 65,
Key_B = 66,
Key_C = 67,
Key_D = 68,
Key_E = 69,
Key_F = 70,
Key_G = 71,
Key_H = 72,
Key_I = 73,
Key_J = 74,
Key_K = 75,
Key_L = 76,
Key_M = 77,
Key_N = 78,
Key_O = 79,
Key_P = 80,
Key_Q = 81,
Key_R = 82,
Key_S = 83,
Key_T = 84,
Key_U = 85,
Key_V = 86,
Key_W = 87,
Key_X = 88,
Key_Y = 89,
Key_Z = 90,
// --- Function keys ---
// Named non-printable keys
Key_Escape = 256,
Key_Enter = 257,
Key_Tab = 258,
Key_Backspace = 259,
Key_Insert = 260,
Key_Delete = 261,
Key_Right = 262,
Key_Left = 263,
Key_Down = 264,
Key_Up = 265,
Key_Page_Up = 266,
Key_Page_Down = 267,
Key_Home = 268,
Key_End = 269,
Key_Caps_Lock = 280,
Key_Scroll_Lock = 281,
Key_Num_Lock = 282,
Key_Print_Screen = 283,
Key_Pause = 284,
// Function keys
Key_F1 = 290,
Key_F2 = 291,
Key_F3 = 292,
Key_F4 = 293,
Key_F5 = 294,
Key_F6 = 295,
Key_F7 = 296,
Key_F8 = 297,
Key_F9 = 298,
Key_F10 = 299,
Key_F11 = 300,
Key_F12 = 301,
Key_F13 = 302,
Key_F14 = 303,
Key_F15 = 304,
Key_F16 = 305,
Key_F17 = 306,
Key_F18 = 307,
Key_F19 = 308,
Key_F20 = 309,
Key_F21 = 310,
Key_F22 = 311,
Key_F23 = 312,
Key_F24 = 313,
Key_F25 = 314,
// Keypad numbers
Key_KP_0 = 320,
Key_KP_1 = 321,
Key_KP_2 = 322,
Key_KP_3 = 323,
Key_KP_4 = 324,
Key_KP_5 = 325,
Key_KP_6 = 326,
Key_KP_7 = 327,
Key_KP_8 = 328,
Key_KP_9 = 329,
// Keypad named function keys
Key_KP_Decimal = 330,
Key_KP_Divide = 331,
Key_KP_Multiply = 332,
Key_KP_Subtract = 333,
Key_KP_Add = 334,
Key_KP_Enter = 335,
Key_KP_Equal = 336,
// Modifier keys
Key_Left_Shift = 340,
Key_Left_Control = 341,
Key_Left_Alt = 342,
Key_Left_Super = 343,
Key_Right_Shift = 344,
Key_Right_Control = 345,
Key_Right_Alt = 346,
Key_Right_Super = 347,
Key_Menu = 348,
Key_Last = Key_Menu,
}
// Bitmask for modifier keys
Mod :: enum i8 {
Shift = 0,
Control = 1,
Alt = 2,
Super = 3,
Caps_Lock = 4,
Num_Lock = 5,
}
Mod_Set :: bit_set[Mod]
// Mouse buttons
Mouse_Button :: enum i8 {
Button_1 = 0,
Button_2 = 1,
Button_3 = 2,
Button_4 = 3,
Button_5 = 4,
Button_6 = 5,
Button_7 = 6,
Button_8 = 7,
// Alias names
Last = Button_8,
Left = Button_1,
Right = Button_2,
Middle = Button_3,
}
+13 -8
View File
@@ -2,8 +2,10 @@ package sindri
import "core:fmt"
import "core:time"
import "sindri:test"
import "sindri:hot_reload"
import "sindri:platform"
VERSION :: #config(VERSION, "dev")
@@ -20,13 +22,13 @@ main :: proc() {
settings := api.settings()
// Initialize Window
os_init(settings)
os_set_monitor(settings)
defer os_destroy()
platform.os_init(settings)
platform.os_set_monitor(settings)
defer platform.os_destroy()
// Initialize GPU resources
instance_init()
defer instance_destroy()
platform.instance_init(settings)
defer platform.instance_destroy()
api.init_once()
api.init()
@@ -34,13 +36,16 @@ main :: proc() {
gt: f64 = 0
dt: f32
for !os_should_close() && !hot_reload.should_close(&hr) {
for !platform.os_should_close() && !hot_reload.should_close(&hr) {
start := time.tick_now()
os_poll_events()
platform.os_poll_events()
api.update(dt)
frame(dt)
fmt.println("MAIN:", test.test)
fmt.printf("pointer: %p | %p\n", &test.test, &test.test_proc)
platform.frame(dt)
dt = f32(time.duration_seconds(time.tick_since(start)))
gt += f64(dt)
+305
View File
@@ -0,0 +1,305 @@
package platform
import "core:fmt"
import "core:strings"
import "vendor:glfw"
import "wgpu:wgpu"
import "wgpu:wgpu/glfwglue"
import "sindri:core"
import "sindri:event"
import "sindri:input"
// -----------------------------------------
// Types
OS :: struct {
window: glfw.WindowHandle,
}
// -----------------------------------------
// Constructor / destructor
os_init :: proc(settings: core.Settings) {
if !glfw.Init() {
panic("[glfw] init failure")
}
// Set window properties
glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API) // do not create OpenGL context
glfw.WindowHint(glfw.RESIZABLE, glfw.FALSE)
// Create GLFW window
state.os.window = glfw.CreateWindow(
i32(settings.width),
i32(settings.height),
strings.unsafe_string_to_cstring(settings.title),
nil,
nil,
)
glfw.SetErrorCallback(error_callback)
glfw.SetWindowCloseCallback(state.os.window, window_close_callback)
// NOTE: SetFramebufferSize over SetWindowSize, to handle different DPIs
glfw.SetFramebufferSizeCallback(state.os.window, size_callback)
glfw.SetKeyCallback(state.os.window, key_callback)
glfw.SetMouseButtonCallback(state.os.window, mouse_button_callback)
glfw.SetCursorPosCallback(state.os.window, cursor_pos_callback)
glfw.SetScrollCallback(state.os.window, scroll_callback)
glfw.SetJoystickCallback(joystick_callback)
// Register input functions
input.key_state = key_state
input.mouse_button_state = mouse_button_state
input.mouse_position = mouse_position
}
os_destroy :: proc() {
glfw.DestroyWindow(state.os.window)
glfw.Terminate()
}
// -----------------------------------------
// Public functions
os_set_monitor :: proc(settings: core.Settings) {
monitor := glfw.GetPrimaryMonitor()
x_pos: i32
y_pos: i32
width := i32(settings.width)
height := i32(settings.height)
target_monitor: glfw.MonitorHandle
mode := glfw.GetVideoMode(monitor)
switch settings.mode {
case .Fullscreen:
target_monitor = monitor
case .Borderless:
// FIXME: On macOS, borderless also fills the notch area
// Monitor origin on the virtual desktop
x_pos, y_pos = glfw.GetMonitorPos(monitor)
width = mode.width
height = mode.height
glfw.SetWindowAttrib(
state.os.window,
glfw.DECORATED,
i32(glfw.FALSE),
)
case .Windowed:
// Put window in the center of the monitor
x_pos = (mode.width - width) / 2
y_pos = (mode.height - height) / 2
glfw.SetWindowAttrib(
state.os.window,
glfw.DECORATED,
i32(glfw.TRUE),
)
}
refresh := settings.refresh == 0 ? glfw.DONT_CARE : i32(settings.refresh)
glfw.SetWindowMonitor(
state.os.window,
target_monitor,
x_pos,
y_pos,
width,
height,
refresh,
)
}
os_set_callback_proc :: proc(window: rawptr) {
glfw.SetWindowUserPointer(state.os.window, window)
}
os_should_close :: proc() -> bool {
return bool(glfw.WindowShouldClose(state.os.window))
}
os_set_should_close :: proc(val: bool) {
glfw.SetWindowShouldClose(state.os.window, b32(val))
}
os_poll_events :: proc() {
glfw.PollEvents()
}
os_get_framebuffer_size :: proc() -> (width, height: u32) {
iw, ih := glfw.GetFramebufferSize(state.os.window)
return u32(iw), u32(ih)
}
os_get_surface :: proc(instance: wgpu.Instance) -> wgpu.Surface {
return glfwglue.GetSurface(instance, state.os.window)
}
// -----------------------------------------
// Private functions
@(private = "file")
key_state :: proc(key: input.Key) -> input.Action {
return input_action(glfw.GetKey(state.os.window, i32(key)))
}
@(private = "file")
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
else if action == glfw.PRESS do return input.Action.Press
else if action == glfw.REPEAT do return input.Action.Repeat
when ODIN_DEBUG do panic("[glfw] unknown action")
return .None
}
@(private = "file")
input_key :: proc(key: i32) -> input.Key {
return input.Key(key) // values match
}
@(private = "file")
input_mod_set :: proc(mods: i32) -> (set: input.Mod_Set) {
return transmute(input.Mod_Set)i8(mods & 0x3f) // values match bit position
}
@(private = "file")
input_mouse_button :: proc(button: i32) -> input.Mouse_Button {
return input.Mouse_Button(button) // values match
}
// Error callback
@(private = "file")
error_callback :: proc "c" (error: i32, description: cstring) {
context = state.ctx
fmt.eprintfln("error: GLFW {}: {}", error, description)
} // GLFWerrorfun
// Window close callback
@(private = "file")
window_close_callback :: proc "c" (window: glfw.WindowHandle) {
context = state.ctx
event.on_event(event.Window_Close_Event{})
} // GLFWwindowclosefun
// Window resize callback
@(private = "file")
size_callback :: proc "c" (window: glfw.WindowHandle, width, height: i32) {
resize()
} // GLFWframebuffersizefun
// Keyboard callback
@(private = "file")
key_callback :: proc "c" (
window: glfw.WindowHandle,
key, scancode, action, mods: i32,
) {
context = state.ctx
if action == glfw.PRESS {
event.on_event(
event.Key_Press_Event {
key = input_key(key),
mods = input_mod_set(mods),
},
)
}
if action == glfw.RELEASE {
event.on_event(
event.Key_Release_Event {
key = input_key(key),
mods = input_mod_set(mods),
},
)
}
if action == glfw.REPEAT {
event.on_event(
event.Key_Repeat_Event {
key = input_key(key),
mods = input_mod_set(mods),
},
)
}
} // GLFWkeyfun
// Mouse button callback
@(private = "file")
mouse_button_callback :: proc "c" (
window: glfw.WindowHandle,
button, action, mods: i32,
) {
context = state.ctx
if action == glfw.PRESS {
event.on_event(
event.Mouse_Button_Press_Event {
button = input_mouse_button(button),
mods = input_mod_set(mods),
},
)
}
if action == glfw.RELEASE {
event.on_event(
event.Mouse_Button_Release_Event {
button = input_mouse_button(button),
mods = input_mod_set(mods),
},
)
}
} // GLFWmousebuttonfun
// Mouse position callback
@(private = "file")
cursor_pos_callback :: proc "c" (
window: glfw.WindowHandle,
x_pos, y_pos: f64,
) {
context = state.ctx
event.on_event(
event.Mouse_Position_Event{x_pos = f32(x_pos), y_pos = f32(y_pos)},
)
} // GLFWcursorposfun
// Mouse scroll callback
@(private = "file")
scroll_callback :: proc "c" (
window: glfw.WindowHandle,
x_offset, y_offset: f64,
) {
context = state.ctx
event.on_event(
event.Mouse_Scroll_Event {
x_offset = f32(x_offset),
y_offset = f32(y_offset),
},
)
} // GLFWscrollfun
// Joystick connected / disconnected callback
@(private = "file")
joystick_callback :: proc "c" (id, connected: i32) {
context = state.ctx
if connected == glfw.CONNECTED {
event.on_event(event.Joystick_Connect_Event{id = id})
} else {
event.on_event(event.Joystick_Disconnect_Event{id = id})
}
} // GLFWjoystickfun
// References:
// - https://www.glfw.org/docs/latest/group__init.html
// - https://www.glfw.org/docs/latest/group__window.html
// - https://www.glfw.org/docs/latest/group__input.html
+292
View File
@@ -0,0 +1,292 @@
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
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,
}
// -----------------------------------------
// Constructor/destructor
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("[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.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)
}
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 .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 = {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)
}
// -----------------------------------------
// 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
}
+5
View File
@@ -0,0 +1,5 @@
package test
test: int = 0
test_proc: proc()
-219
View File
@@ -1,219 +0,0 @@
package sindri
import "base:runtime"
import "core:fmt"
import "wgpu:wgpu"
// -----------------------------------------
// 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,
}
// -----------------------------------------
instance_init :: proc() {
state.ctx = context
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: 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})
}
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},
},
)
}
}
instance_destroy :: 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)
}
// -----------------------------------------
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)
}
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 .Occluded:
// Window is occluded (e.g. minimized), skip this frame.
return
case .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)
}