Compare commits

...
7 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
11 changed files with 297 additions and 109 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
+1 -2
View File
@@ -6,6 +6,7 @@ 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:-}" OPTION="${1:-}"
shift
# ------------------------------------------ # ------------------------------------------
@@ -42,8 +43,6 @@ if pgrep -x $PROJECT >/dev/null; then
fi fi
if [ "$OPTION" = "debug" ]; then if [ "$OPTION" = "debug" ]; then
shift
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 \
+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.
+14 -6
View File
@@ -5,11 +5,12 @@ import "core:time"
import "core:fmt" import "core:fmt"
import "sindri:core" import "sindri:core"
import "sindri:test"
// ----------------------------------------- // -----------------------------------------
Game_Memory :: struct { Game_Memory :: struct {
should_close: bool should_close: bool,
} }
g: ^Game_Memory g: ^Game_Memory
@@ -54,6 +55,8 @@ settings :: proc() -> core.Settings {
@(export) @(export)
init_once :: proc() { init_once :: proc() {
fmt.println("init once") fmt.println("init once")
test.test_proc = proc() {}
} }
@(export) @(export)
@@ -61,6 +64,9 @@ init :: proc() {
fmt.println("hello from .dll!") fmt.println("hello from .dll!")
g = new(Game_Memory) g = new(Game_Memory)
memory_set(g)
test.test_proc = proc() { asd := 2 }
} }
@(export) @(export)
@@ -70,6 +76,10 @@ update :: proc(dt: f32) {
if input.key_state(.Key_Escape) == .Press { if input.key_state(.Key_Escape) == .Press {
g.should_close = true g.should_close = true
} }
fmt.println("GAME:", test.test)
fmt.printf("pointer: %p | %p\n", &test.test, &test.test_proc)
test.test += 1
} }
@(export) @(export)
@@ -83,19 +93,17 @@ start := time.tick_now()
should_close :: proc() -> bool { should_close :: proc() -> bool {
elapsed := time.tick_since(start) elapsed := time.tick_since(start)
seconds := time.duration_seconds(elapsed) seconds := time.duration_seconds(elapsed)
if seconds > 4 do return true // if seconds > 4 do return true
return g.should_close return g.should_close
} }
@(export) @(export)
force_reload :: proc() -> bool { force_reload :: proc() -> bool {
// TODO: read keypress return input.key_state(.Key_F5) == .Press
return false
} }
@(export) @(export)
force_restart :: proc() -> bool { force_restart :: proc() -> bool {
// TODO: read keypress return input.key_state(.Key_F6) == .Press
return false
} }
+1 -1
View File
@@ -7,7 +7,7 @@ Settings :: struct {
height: u16, height: u16,
title: string, title: string,
mode: WindowMode, mode: WindowMode,
refresh: u16, refresh: u16, // 0 = unlimited
vsync: bool, vsync: bool,
// TODO: // TODO:
// windowed resizable y/n // windowed resizable y/n
+20
View File
@@ -5,9 +5,11 @@ import "core:fmt"
import "sindri:input" import "sindri:input"
// ----------------------------------------- // -----------------------------------------
// Types
// event category, bitfield (?) // event category, bitfield (?)
// Event_Data :: union {
Event :: union { Event :: union {
Window_Close_Event, Window_Close_Event,
Window_Resize_Event, Window_Resize_Event,
@@ -22,6 +24,11 @@ Event :: union {
Mouse_Scroll_Event, Mouse_Scroll_Event,
} }
// Event :: struct {
// data: Event_Data,
// handled: bool,
// }
Window_Close_Event :: struct { Window_Close_Event :: struct {
handled: bool, handled: bool,
} }
@@ -85,6 +92,13 @@ Mouse_Scroll_Event :: struct {
} }
// ----------------------------------------- // -----------------------------------------
// 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 // Dispatcher(Event) -> forwards to the right function
on_event :: proc(event: Event) { on_event :: proc(event: Event) {
@@ -116,3 +130,9 @@ on_event :: proc(event: Event) {
fmt.println("Joy dis:", e.id) fmt.println("Joy dis:", e.id)
} }
} }
/*
Notes:
Have the events queued up into a frame buffer that gets drained at a specific time.
*/
+4
View File
@@ -11,5 +11,9 @@ key_state: proc(key: Key) -> Action
// implementation in platform package, to prevent cyclic dependency // implementation in platform package, to prevent cyclic dependency
mouse_button_state: proc(button: Mouse_Button) -> Action 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 // Private functions
+5 -1
View File
@@ -2,6 +2,7 @@ package sindri
import "core:fmt" import "core:fmt"
import "core:time" import "core:time"
import "sindri:test"
import "sindri:hot_reload" import "sindri:hot_reload"
import "sindri:platform" import "sindri:platform"
@@ -26,7 +27,7 @@ main :: proc() {
defer platform.os_destroy() defer platform.os_destroy()
// Initialize GPU resources // Initialize GPU resources
platform.instance_init() platform.instance_init(settings)
defer platform.instance_destroy() defer platform.instance_destroy()
api.init_once() api.init_once()
@@ -41,6 +42,9 @@ main :: proc() {
platform.os_poll_events() platform.os_poll_events()
api.update(dt) api.update(dt)
fmt.println("MAIN:", test.test)
fmt.printf("pointer: %p | %p\n", &test.test, &test.test_proc)
platform.frame(dt) platform.frame(dt)
dt = f32(time.duration_seconds(time.tick_since(start))) dt = f32(time.duration_seconds(time.tick_since(start)))
+8 -7
View File
@@ -52,11 +52,7 @@ os_init :: proc(settings: core.Settings) {
// Register input functions // Register input functions
input.key_state = key_state input.key_state = key_state
input.mouse_button_state = mouse_button_state input.mouse_button_state = mouse_button_state
input.mouse_position = mouse_position
// TODO: Figure out proper vsync, found 3 spots so far
// - glfw.SwapInterval(0) this is only for OpenGL it seems?
// - glfw.SetWindowMonitor(refresh)
// - wgpu presentMode = .Fifo
} }
os_destroy :: proc() { os_destroy :: proc() {
@@ -106,8 +102,7 @@ os_set_monitor :: proc(settings: core.Settings) {
) )
} }
refresh := settings.vsync ? mode.refresh_rate : i32(settings.refresh) refresh := settings.refresh == 0 ? glfw.DONT_CARE : i32(settings.refresh)
if refresh == 0 do refresh = glfw.DONT_CARE
glfw.SetWindowMonitor( glfw.SetWindowMonitor(
state.os.window, state.os.window,
@@ -158,6 +153,12 @@ mouse_button_state :: proc(button: input.Mouse_Button) -> input.Action {
return input_action(glfw.GetMouseButton(state.os.window, i32(button))) 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") @(private = "file")
input_action :: proc(action: i32) -> input.Action { input_action :: proc(action: i32) -> input.Action {
if action == glfw.RELEASE do return input.Action.Release if action == glfw.RELEASE do return input.Action.Release
+165 -92
View File
@@ -2,15 +2,20 @@ package platform
import "base:runtime" import "base:runtime"
import "core:fmt" import "core:fmt"
import "core:slice"
import "wgpu:wgpu" import "wgpu:wgpu"
import "sindri:core"
// ----------------------------------------- // -----------------------------------------
// Variables
// Instance -> Surface -> Adapter -> Device -> Queue // Instance -> Surface -> Adapter -> Device -> Queue
state: struct { state: struct {
ctx: runtime.Context, ctx: runtime.Context,
os: OS, os: OS,
// ----------------------------------------
instance: wgpu.Instance, // entry point: creates Adapter, Device and Surface instance: wgpu.Instance, // entry point: creates Adapter, Device and Surface
surface: wgpu.Surface, // handle to a presentable surface (e.g. a window) surface: wgpu.Surface, // handle to a presentable surface (e.g. a window)
adapter: wgpu.Adapter, // handle to physical GPU adapter: wgpu.Adapter, // handle to physical GPU
@@ -20,115 +25,42 @@ state: struct {
module: wgpu.ShaderModule, module: wgpu.ShaderModule,
pipeline_layout: wgpu.PipelineLayout, pipeline_layout: wgpu.PipelineLayout,
pipeline: wgpu.RenderPipeline, pipeline: wgpu.RenderPipeline,
// ----------------------------------------
capabilities: wgpu.SurfaceCapabilities,
vsync: bool,
} }
// ----------------------------------------- // -----------------------------------------
// Constructor/destructor
instance_init :: proc() { instance_init :: proc(settings: core.Settings) {
state.ctx = context state.ctx = context
wgpu.SetLogCallback(log_callback, nil)
wgpu.SetLogLevel(.Warn)
// Instance
state.instance = wgpu.CreateInstance(nil) state.instance = wgpu.CreateInstance(nil)
if state.instance == nil { if state.instance == nil {
panic("WebGPU is not supported") panic("[wgpu] WebGPU is not supported")
} }
// Surface
state.surface = os_get_surface(state.instance) state.surface = os_get_surface(state.instance)
// Adapter
wgpu.InstanceRequestAdapter( wgpu.InstanceRequestAdapter(
state.instance, state.instance,
&{compatibleSurface = state.surface}, &{compatibleSurface = state.surface},
{callback = on_adapter}, {callback = on_adapter},
) )
on_adapter :: proc "c" ( // Present modes
status: wgpu.RequestAdapterStatus, get_present_modes()
adapter: wgpu.Adapter, state.vsync = settings.vsync
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" ( // Device
status: wgpu.RequestDeviceStatus, wgpu.AdapterRequestDevice(state.adapter, nil, {callback = on_device})
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() { instance_destroy :: proc() {
@@ -137,12 +69,23 @@ instance_destroy :: proc() {
wgpu.ShaderModuleRelease(state.module) wgpu.ShaderModuleRelease(state.module)
wgpu.QueueRelease(state.queue) wgpu.QueueRelease(state.queue)
wgpu.DeviceRelease(state.device) wgpu.DeviceRelease(state.device)
wgpu.SurfaceCapabilitiesFreeMembers(state.capabilities)
wgpu.AdapterRelease(state.adapter) wgpu.AdapterRelease(state.adapter)
wgpu.SurfaceRelease(state.surface) wgpu.SurfaceRelease(state.surface)
wgpu.InstanceRelease(state.instance) 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" () { resize :: proc "c" () {
context = state.ctx context = state.ctx
@@ -173,7 +116,7 @@ frame :: proc "c" (dt: f32) {
case .Error: case .Error:
// Fatal error // Fatal error
fmt.panicf( fmt.panicf(
"[triangle] get_current_texture status=%v", "[wgpu] triangle get_current_texture status=%v",
surface_texture.status, surface_texture.status,
) )
} }
@@ -217,3 +160,133 @@ frame :: proc "c" (dt: f32) {
wgpu.QueueSubmit(state.queue, {command_buffer}) wgpu.QueueSubmit(state.queue, {command_buffer})
wgpu.SurfacePresent(state.surface) 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()