Compare commits

..
9 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
Riyyi 6edcce16c6 Continue work on hot-reload functionality 2026-09-06 18:06:12 +02:00
Riyyi aea791fe2b Start working on hot-reload functionality 2026-09-06 16:37:49 +02:00
13 changed files with 1351 additions and 295 deletions
+12
View File
@@ -36,3 +36,15 @@ vendor
- microui (temp) - microui (temp)
- box3d? - box3d?
- build.sh -> build.odin - build.sh -> build.odin
## Hot Reload
build.sh -> hot reload enabled, debug mode enabled
build_release.sh -> hot reload disabled, debug mode disabled
build_debug.sh -> hot reload disabled, debug mode enabled
hot reload builds engine as .exe, then game as dynamic lib.
skips building the engine if its already running (pgrep).
no hot reload does not use game.dll, instead it imports the game source
as a normal Odin package during build.
+39 -13
View File
@@ -1,32 +1,58 @@
#!/bin/sh #!/bin/sh
set -e set -eu
PROJECT="sindri" PROJECT="sindri"
VERSION="dev-$(date -u '+%Y-%m-%d')-$(git rev-parse --short HEAD)" VERSION="dev-$(date -u '+%Y-%m-%d')-$(git rev-parse --short HEAD)"
OPTION="${1:-}"
shift
# ------------------------------------------ # ------------------------------------------
# Setup compiled wgpu binary # Setup compiled wgpu binary
./scripts/wgpu-init.sh ./scripts/wgpu-init.sh
# ------------------------------------------ # ------------------------------------------
# Game compile
mkdir -p build mkdir -p build
if [ "$1" = "debug" ]; then # Game.dll
shift odin build game/ -show-timings \
-collection:sindri=src \
-build-mode:dynamic \
-out:build/game_tmp -microarch:native -define:VERSION="$VERSION-debug" -debug "$@"
odin build src/ -show-timings \ # Need to use a temp file on Linux/macOS because it first writes an empty file,
-collection:sindri=src \ # which the engine will load before it is actually fully written.
-collection:gram=vendor/gram/src \ [ "$(uname -s)" = "Darwin" ] && LIB_EXT="dylib" || LIB_EXT="so"
-collection:wgpu=vendor \ [ -f "build/game_tmp.$LIB_EXT" ] || {
-out:build/$PROJECT -microarch:native -use-separate-modules -define:VERSION="$VERSION-debug" -debug "$@" echo "error: build produced no game lib.$LIB_EXT" >&2
exit 0 exit 1
}
mv -f "build/game_tmp.$LIB_EXT" "build/game.$LIB_EXT"
# ------------------------------------------
# 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 fi
odin build src/ -show-timings \ odin build src/ -show-timings \
-collection:sindri=src \ -collection:sindri=src \
-collection:gram=vendor/gram/src \ -collection:gram=vendor/gram/src \
-collection:wgpu=vendor \ -collection:wgpu=vendor \
-out:build/$PROJECT -microarch:native -o:speed -define:VERSION="$VERSION" "$@" -out:build/$PROJECT -microarch:native -o:speed -define:VERSION="$VERSION" "$@"
+101
View File
@@ -0,0 +1,101 @@
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()
@(export)
should_close :: proc() -> bool {
elapsed := time.tick_since(start)
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)
}
}
-60
View File
@@ -1,60 +0,0 @@
package sindri
import "core:time"
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_run :: proc() {
dt: f32
for !glfw.WindowShouldClose(state.os.window) {
start := time.tick_now()
glfw.PollEvents()
frame(dt)
dt = f32(time.duration_seconds(time.tick_since(start)))
}
instance_destroy()
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()
}
+218
View File
@@ -0,0 +1,218 @@
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"
} else when ODIN_OS == .Darwin {
LIB_EXT :: ".dylib"
} else {
LIB_EXT :: ".so"
}
GAME_LIB_DIR :: "build/"
GAME_LIB_PATH :: GAME_LIB_DIR + "game" + LIB_EXT
// -----------------------------------------
Hot_Reload :: struct {
loaded_libs: [dynamic]Game_API,
}
Hot_Reload_Error :: enum u8 {
None = 0,
Load_Game_Lib_Failed = 1,
Unimplemented = 127,
Okay = None,
}
Error :: union #shared_nil {
Hot_Reload_Error,
}
Game_API :: struct {
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() -> (hr: Hot_Reload, error: Error) {
api: Game_API
api_ok := load_game_lib(&api)
if !api_ok {
fmt.println("error: failed to load Game API")
return {}, .Load_Game_Lib_Failed
}
hr.loaded_libs = make([dynamic]Game_API, 0, 0, context.allocator)
append(&hr.loaded_libs, api)
return hr, nil
}
hot_reload_destroy :: proc(state: ^Hot_Reload) {
for &api in state.loaded_libs {
unload_game_lib(&api)
}
delete(state.loaded_libs)
}
// -----------------------------------------
active_lib :: proc(hr: ^Hot_Reload) -> ^Game_API {
return &hr.loaded_libs[len(hr.loaded_libs) - 1]
}
should_close :: proc(hr: ^Hot_Reload) -> bool {
api := active_lib(hr)
if api.lib == nil do return true
return api.should_close()
}
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 {0} to {1}: {2:v}",
GAME_LIB_PATH,
to,
copy_err,
)
return false
}
return true
}
// Load game lib symbols
load_game_lib :: proc(api: ^Game_API) -> bool {
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,
api.api_version,
)
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, lib_name, "", "lib")
if !ok {
fmt.printfln(
"error: failed initializing symbols: {0}",
dynlib.last_error(),
)
}
return ok
}
// Unload game lib, used during full restarts
unload_game_lib :: proc(api: ^Game_API) {
if api.lib != nil {
if !dynlib.unload_library(api.lib) {
fmt.eprintfln(
"error: failed unloading lib: {0}",
dynlib.last_error(),
)
}
}
if os.remove(
fmt.tprintf(GAME_LIB_DIR + "game_{0}" + LIB_EXT, api.api_version),
) !=
nil {
fmt.printfln(
"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,
}
+43 -1
View File
@@ -1,6 +1,10 @@
package sindri package sindri
import "core:fmt" import "core:fmt"
import "core:time"
import "sindri:hot_reload"
import "sindri:platform"
VERSION :: #config(VERSION, "dev") VERSION :: #config(VERSION, "dev")
@@ -9,5 +13,43 @@ VERSION :: #config(VERSION, "dev")
main :: proc() { main :: proc() {
fmt.println("hello world!") fmt.println("hello world!")
instance_init() // Hot reload development functionality
hr, err := hot_reload.hot_reload_init()
defer hot_reload.hot_reload_destroy(&hr)
api := hot_reload.active_lib(&hr)
settings := api.settings()
// Initialize Window
platform.os_init(settings)
platform.os_set_monitor(settings)
defer platform.os_destroy()
// Initialize GPU resources
platform.instance_init(settings)
defer platform.instance_destroy()
api.init_once()
api.init()
gt: f64 = 0
dt: f32
for !platform.os_should_close() && !hot_reload.should_close(&hr) {
start := time.tick_now()
platform.os_poll_events()
api.update(dt)
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
}
-221
View File
@@ -1,221 +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
os_init()
state.instance = wgpu.CreateInstance(nil)
if state.instance == nil {
panic("WebGPU is not supported")
}
state.surface = os_get_surface(state.instance)
wgpu.InstanceRequestAdapter(
state.instance,
&{compatibleSurface = state.surface},
{callback = on_adapter},
)
on_adapter :: proc "c" (
status: wgpu.RequestAdapterStatus,
adapter: wgpu.Adapter,
message: string,
userdata1: 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},
},
)
os_run()
}
}
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)
}