Compare commits

..
7 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
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
Riyyi 80bad9aad5 First working version of hot-reloading feature 2026-09-06 23:35:21 +02:00
12 changed files with 1179 additions and 320 deletions
+35 -15
View File
@@ -5,34 +5,54 @@ set -eu
PROJECT="sindri"
VERSION="dev-$(date -u '+%Y-%m-%d')-$(git rev-parse --short HEAD)"
OPTION="${1:-}"
shift
# ------------------------------------------
# Setup compiled wgpu binary
./scripts/wgpu-init.sh
# ------------------------------------------
# Game compile
mkdir -p build
# Game.dll
odin build game/ -show-timings \
-collection:sindri=src \
-build-mode:dynamic \
-out:build/game -microarch:native -define:VERSION="$VERSION-debug" -debug "$@"
-collection:sindri=src \
-build-mode:dynamic \
-out:build/game_tmp -microarch:native -define:VERSION="$VERSION-debug" -debug "$@"
if [ "$1" = "debug" ]; then
shift
# Need to use a temp file on Linux/macOS because it first writes an empty file,
# which the engine will load before it is actually fully written.
[ "$(uname -s)" = "Darwin" ] && LIB_EXT="dylib" || LIB_EXT="so"
[ -f "build/game_tmp.$LIB_EXT" ] || {
echo "error: build produced no game lib.$LIB_EXT" >&2
exit 1
}
mv -f "build/game_tmp.$LIB_EXT" "build/game.$LIB_EXT"
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 "$@"
exit 0
# ------------------------------------------
# Engine compile
# If the executable is already running, then don't try to build and start it.
if pgrep -x $PROJECT >/dev/null; then
echo "Hot reloading..."
exit 0
fi
if [ "$OPTION" = "debug" ]; then
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 "$@"
exit 0
fi
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" "$@"
-collection:sindri=src \
-collection:gram=vendor/gram/src \
-collection:wgpu=vendor \
-out:build/$PROJECT -microarch:native -o:speed -define:VERSION="$VERSION" "$@"
+81
View File
@@ -1,11 +1,80 @@
package game
import "sindri:input"
import "core:time"
import "core:fmt"
import "sindri:core"
// -----------------------------------------
Game_Memory :: struct {
should_close: bool
}
g: ^Game_Memory
// -----------------------------------------
@(export)
memory :: proc() -> rawptr {
return g
}
@(export)
memory_free :: proc() {
free(g)
}
@(export)
memory_size :: proc() -> int {
return size_of(Game_Memory)
}
@(export)
memory_set :: proc(mem: rawptr) {
g = (^Game_Memory)(mem)
// Here you can also set your own global variables. A good idea is to make
// your global variables into pointers that point to something inside `g`.
}
@(export)
settings :: proc() -> core.Settings {
return core.Settings {
width = 960,
height = 540,
title = "WGPU Native Triangle",
mode = core.WindowMode.Windowed,
refresh = 60,
vsync = true,
}
}
@(export)
init_once :: proc() {
fmt.println("init once")
}
@(export)
init :: proc() {
fmt.println("hello from .dll!")
g = new(Game_Memory)
}
@(export)
update :: proc(dt: f32) {
fmt.println("dt:", dt)
if input.key_state(.Key_Escape) == .Press {
g.should_close = true
}
}
@(export)
destroy :: proc() {
fmt.println("bye")
}
start := time.tick_now()
@@ -16,5 +85,17 @@ should_close :: proc() -> bool {
seconds := time.duration_seconds(elapsed)
if seconds > 4 do return true
return g.should_close
}
@(export)
force_reload :: proc() -> bool {
// TODO: read keypress
return false
}
@(export)
force_restart :: proc() -> bool {
// TODO: read keypress
return false
}
+21
View File
@@ -0,0 +1,21 @@
package core
// -----------------------------------------
Settings :: struct {
width: u16,
height: u16,
title: string,
mode: WindowMode,
refresh: u16, // 0 = unlimited
vsync: bool,
// TODO:
// windowed resizable y/n
// exit key (ex: escape)
}
WindowMode :: enum u8 {
Windowed = 0,
Borderless = 1,
Fullscreen = 2,
}
+118
View File
@@ -0,0 +1,118 @@
package event
import "core:fmt"
import "sindri:input"
// -----------------------------------------
// event category, bitfield (?)
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,
}
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,
}
// -----------------------------------------
// 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)
}
}
-58
View File
@@ -1,58 +0,0 @@
package sindri
import "vendor:glfw"
import "wgpu:wgpu"
import "wgpu:wgpu/glfwglue"
OS :: struct {
window: glfw.WindowHandle,
}
os_init :: proc() {
if !glfw.Init() {
panic("[glfw] init failure")
}
glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API)
state.os.window = glfw.CreateWindow(
960,
540,
"WGPU Native Triangle",
nil,
nil,
)
glfw.SetFramebufferSizeCallback(state.os.window, size_callback)
}
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()
}
+100 -20
View File
@@ -3,6 +3,9 @@ package hot_reload
import "core:dynlib"
import "core:fmt"
import "core:os"
import "core:time"
import "sindri:core"
when ODIN_OS == .Windows {
LIB_EXT :: ".dll"
@@ -33,31 +36,39 @@ Error :: union #shared_nil {
}
Game_API :: struct {
lib: dynlib.Library,
init: proc(),
update: proc(),
render: proc(),
destroy: proc(),
should_close: proc() -> bool,
api_version: int, // iteration of the game lib
lib: dynlib.Library,
memory: proc() -> rawptr,
memory_free: proc(),
memory_size: proc() -> int,
memory_set: proc(mem: rawptr),
settings: proc() -> core.Settings,
init_once: proc(),
init: proc(),
update: proc(_: f32),
render: proc(),
destroy: proc(),
should_close: proc() -> bool,
force_reload: proc() -> bool,
force_restart: proc() -> bool,
modification_time: time.Time,
api_version: int, // iteration of the game lib
}
// -----------------------------------------
hot_reload_init :: proc() -> (state: Hot_Reload, error: Error) {
hot_reload_init :: proc() -> (hr: Hot_Reload, error: Error) {
api: Game_API
api.api_version = -1
api_ok := load_game_lib(&api)
if !api_ok {
fmt.println("Failed to load Game API")
fmt.println("error: failed to load Game API")
return {}, .Load_Game_Lib_Failed
}
state.loaded_libs = make([dynamic]Game_API)
append(&state.loaded_libs, api)
hr.loaded_libs = make([dynamic]Game_API, 0, 0, context.allocator)
append(&hr.loaded_libs, api)
return state, nil
return hr, nil
}
hot_reload_destroy :: proc(state: ^Hot_Reload) {
@@ -84,7 +95,8 @@ copy_lib :: proc(to: string) -> bool {
copy_err := os.copy_file(to, GAME_LIB_PATH)
if copy_err != nil {
fmt.printfln(
"error: Failed to copy " + GAME_LIB_PATH + " to {0}: %v",
"error: failed to copy {0} to {1}: {2:v}",
GAME_LIB_PATH,
to,
copy_err,
)
@@ -96,8 +108,16 @@ copy_lib :: proc(to: string) -> bool {
// Load game lib symbols
load_game_lib :: proc(api: ^Game_API) -> bool {
// Load next iteration
api.api_version += 1
mod_time, mod_time_err := os.last_write_time_by_name(GAME_LIB_PATH)
if mod_time_err != os.ERROR_NONE {
fmt.printfln(
"error: failed getting last write time of {0}, error code: {1}",
GAME_LIB_PATH,
mod_time_err,
)
return false
}
api.modification_time = mod_time
lib_name := fmt.tprintf(
GAME_LIB_DIR + "game_{0}" + LIB_EXT,
@@ -106,9 +126,12 @@ load_game_lib :: proc(api: ^Game_API) -> bool {
copy_lib(lib_name) or_return
// Match the names of the fields in Game_API to symbols in the game DLL
_, ok := dynlib.initialize_symbols(api, GAME_LIB_PATH, "", "lib")
_, ok := dynlib.initialize_symbols(api, lib_name, "", "lib")
if !ok {
fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error())
fmt.printfln(
"error: failed initializing symbols: {0}",
dynlib.last_error(),
)
}
return ok
@@ -119,7 +142,7 @@ unload_game_lib :: proc(api: ^Game_API) {
if api.lib != nil {
if !dynlib.unload_library(api.lib) {
fmt.eprintfln(
"error: Failed unloading lib: {0}",
"error: failed unloading lib: {0}",
dynlib.last_error(),
)
}
@@ -130,9 +153,66 @@ unload_game_lib :: proc(api: ^Game_API) {
) !=
nil {
fmt.printfln(
"error: Failed to remove {0}game_{1}" + LIB_EXT + " copy",
"error: failed to remove {0}game_{1}{2} copy",
GAME_LIB_DIR,
api.api_version,
LIB_EXT,
)
}
}
reload_game_lib :: proc(hr: ^Hot_Reload) -> (lib: ^Game_API, err: Error) {
api := active_lib(hr)
force_reload := api.force_reload()
force_restart := api.force_restart()
reload := force_reload || force_restart
lib_mod, lib_mod_err := os.last_write_time_by_name(GAME_LIB_PATH)
// TODO: Handle error
if lib_mod_err == os.ERROR_NONE && api.modification_time != lib_mod {
reload = true
}
if !reload do return api, nil
new_api: Game_API
new_api.api_version = api.api_version + 1
new_api_ok := load_game_lib(&new_api)
if !new_api_ok {
fmt.println("error: failed to load Game API")
return nil, .Load_Game_Lib_Failed
}
force_restart = force_restart || api.memory_size() != new_api.memory_size()
if !force_restart {
// This does the normal hot reload
// Note that we don't unload the old game APIs because that
// would unload the lib. The lib can contain stored info
// such as string literals. The old libs are only unloaded
// on a full reset or on shutdown.
memory := api.memory()
new_api.memory_set(memory)
} else {
// This does a full reset. That's basically like opening and
// closing the game, without having to restart the executable.
//
// You end up in here if the game requests a full reset OR
// if the size of the game memory has changed. That would
// probably lead to a crash anyways.
api.memory_free()
for &g in hr.loaded_libs {
unload_game_lib(&g)
}
clear(&hr.loaded_libs)
new_api.init()
}
append(&hr.loaded_libs, new_api)
return active_lib(hr), nil
}
+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,
}
+25 -10
View File
@@ -4,6 +4,7 @@ import "core:fmt"
import "core:time"
import "sindri:hot_reload"
import "sindri:platform"
VERSION :: #config(VERSION, "dev")
@@ -17,24 +18,38 @@ main :: proc() {
defer hot_reload.hot_reload_destroy(&hr)
api := hot_reload.active_lib(&hr)
api.init()
settings := api.settings()
// Initialize Window
os_init()
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()
gt: f32
api.init_once()
api.init()
for !os_should_close() && !hot_reload.should_close(&hr) {
gt: f64 = 0
dt: f32
for !platform.os_should_close() && !hot_reload.should_close(&hr) {
start := time.tick_now()
os_poll_events()
frame(gt)
platform.os_poll_events()
api.update(dt)
gt = f32(time.duration_seconds(time.tick_since(start)))
platform.frame(dt)
dt = f32(time.duration_seconds(time.tick_since(start)))
gt += f64(dt)
// Hot reload
api, err = hot_reload.reload_game_lib(&hr)
if err != nil do break
}
api.destroy()
}
+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
}
-217
View File
@@ -1,217 +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
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)
}