Compare commits
4
Commits
2b8488a1e2
...
eb495f67c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb495f67c2 | ||
|
|
5cc4f9c7f5 | ||
|
|
b436c4dff2 | ||
|
|
43bd34a505 |
@@ -0,0 +1,57 @@
|
|||||||
|
$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 = @() }
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
|
||||||
|
# Setup compiled wgpu binary
|
||||||
|
& (Join-Path $PSScriptRoot "scripts\wgpu-init.ps1")
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# 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.dll -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.exe -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.exe -microarch:native -o:speed "-define:VERSION=$VERSION" @args
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 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):
|
||||||
|
- Package globals are shared between modules, but only indirectly
|
||||||
|
- This is due to the dynamic linker merging symbols into a (single) flat namespace
|
||||||
|
- Practically this means the lib's unset package globals get shadowed by the hosts'
|
||||||
|
- After hot reloading the lib, the memory of the packge is not refreshed,
|
||||||
|
it keeps pointing to the package global of the host
|
||||||
|
|
||||||
|
Findings (Windows):
|
||||||
|
- Package globals are not shared between modules, they have their own copy
|
||||||
|
|
||||||
|
Findings (Linux):
|
||||||
|
- Package globals are not shared between modules, they have their own copy
|
||||||
|
|
||||||
|
Conclusion: avoid package globals, or pass pointers to the dynamic library so
|
||||||
|
it can match its package globals with the host module.
|
||||||
|
|
||||||
|
## Dependency Patterns
|
||||||
|
|
||||||
|
1. Strict layering, only call downwards. If two modules need each other, one of
|
||||||
|
the modules is in a wrong layer. Move it, or extract the shared part into a
|
||||||
|
lower layer.
|
||||||
|
2. Dependency inversion: if required, define an interface in the lower layer,
|
||||||
|
upper layer registers itself.
|
||||||
|
3. Dependency Injection: Prevent dependencies via data, over includes. Pass what
|
||||||
|
a function needs as arguments instead of reaching another module's state.
|
||||||
|
4. Events instead of callbacks both ways If A and B would call each other,
|
||||||
|
have A push events into a queue and B consume them. Neither module includes
|
||||||
|
the other; they only share the event type (which lives in a lower layer).
|
||||||
|
5. Merge or split as a last resort, if two modules genuinely can't be untangled.
|
||||||
|
|
||||||
|
Sindri Engine:
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------------------+ Game:
|
||||||
|
| Game | - Game
|
||||||
|
+---------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------------+ +---------------------+ Core:
|
||||||
|
| Core |<----+----| main | - Input
|
||||||
|
+---------------------+ | +---------------------+
|
||||||
|
| |
|
||||||
|
v |
|
||||||
|
+---------------------+ | Platform:
|
||||||
|
| Platform |<----+ - GLFW
|
||||||
|
+---------------------+ | - wgpu
|
||||||
|
| |
|
||||||
|
v |
|
||||||
|
+---------------------+ | Base:
|
||||||
|
| Base |<----+ - Hot Reload
|
||||||
|
+---------------------+ - Settings
|
||||||
|
- Event
|
||||||
|
- Keycodes
|
||||||
|
```
|
||||||
+4
-5
@@ -9,7 +9,7 @@ import "sindri:core"
|
|||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
|
||||||
Game_Memory :: struct {
|
Game_Memory :: struct {
|
||||||
should_close: bool
|
should_close: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
g: ^Game_Memory
|
g: ^Game_Memory
|
||||||
@@ -61,6 +61,7 @@ init :: proc() {
|
|||||||
fmt.println("hello from .dll!")
|
fmt.println("hello from .dll!")
|
||||||
|
|
||||||
g = new(Game_Memory)
|
g = new(Game_Memory)
|
||||||
|
memory_set(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(export)
|
@(export)
|
||||||
@@ -90,12 +91,10 @@ should_close :: proc() -> bool {
|
|||||||
|
|
||||||
@(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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$VENDOR_DIR = Join-Path $PSScriptRoot "..\vendor" | Resolve-Path
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# Platform detection (target dir name, only used for the up-to-date check)
|
||||||
|
|
||||||
|
# Windows PowerShell 5.1 doesn't define $IsWindows/$IsMacOS/$IsLinux
|
||||||
|
if (-not (Test-Path variable:IsWindows)) { $IsWindows = $env:OS -eq "Windows_NT" }
|
||||||
|
if (-not (Test-Path variable:IsMacOS)) { $IsMacOS = $false }
|
||||||
|
if (-not (Test-Path variable:IsLinux)) { $IsLinux = $false }
|
||||||
|
|
||||||
|
if ($IsWindows) {
|
||||||
|
$os = "windows"
|
||||||
|
$extra = "-msvc"
|
||||||
|
} elseif ($IsMacOS) {
|
||||||
|
$os = "macos"
|
||||||
|
$extra = ""
|
||||||
|
} elseif ($IsLinux) {
|
||||||
|
$os = "linux"
|
||||||
|
$extra = ""
|
||||||
|
} else {
|
||||||
|
Write-Error "wgpu setup: unsupported OS"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$procArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
||||||
|
$arch = if ($procArch -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "aarch64" } else { "x86_64" }
|
||||||
|
|
||||||
|
$odinRoot = (odin root).Trim()
|
||||||
|
$name = "wgpu-$os-$arch$extra-release"
|
||||||
|
$target = Join-Path $VENDOR_DIR "wgpu\lib\$name"
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# WGPU version detection
|
||||||
|
|
||||||
|
if ($args.Count -gt 0 -and $args[0]) {
|
||||||
|
$WGPU_VERSION = $args[0]
|
||||||
|
} else {
|
||||||
|
$odinTypes = Join-Path $odinRoot "vendor\wgpu\wgpu_native_types.odin"
|
||||||
|
$typesContent = Get-Content $odinTypes -Raw
|
||||||
|
if ($typesContent -match 'BINDINGS_VERSION_STRING :: "([^"]*)"') {
|
||||||
|
$WGPU_VERSION = "v" + $Matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $WGPU_VERSION -or $WGPU_VERSION -eq "v") {
|
||||||
|
Write-Error "wgpu setup: could not detect the wgpu-native version"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect the version of the vendored wgpu bindings
|
||||||
|
$currentTypes = Join-Path $VENDOR_DIR "wgpu\wgpu_native_types.odin"
|
||||||
|
$current = $null
|
||||||
|
if (Test-Path $currentTypes) {
|
||||||
|
$cur = Get-Content $currentTypes -Raw
|
||||||
|
if ($cur -match 'BINDINGS_VERSION_STRING :: "([^"]*)"') {
|
||||||
|
$current = "v" + $Matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the vendored wgpu is already up to date
|
||||||
|
if ($current -eq $WGPU_VERSION -and (Test-Path $target)) {
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# Copy vendor/wgpu
|
||||||
|
|
||||||
|
# Remove any stale copy
|
||||||
|
if (Test-Path (Join-Path $VENDOR_DIR "wgpu")) {
|
||||||
|
Remove-Item -Recurse -Force (Join-Path $VENDOR_DIR "wgpu")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy wgpu
|
||||||
|
$wgpuPath = Join-Path $odinRoot "vendor\wgpu"
|
||||||
|
Copy-Item -Recurse $wgpuPath $VENDOR_DIR
|
||||||
|
|
||||||
|
# Point imports at the copied vendor wgpu package
|
||||||
|
Get-ChildItem -Path (Join-Path $VENDOR_DIR "wgpu") -Recurse -Filter *.odin | ForEach-Object {
|
||||||
|
(Get-Content $_.FullName -Raw) -replace '"vendor:wgpu"', '"wgpu:wgpu"' | Set-Content $_.FullName -NoNewline
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# Download
|
||||||
|
|
||||||
|
$assetBase = "https://github.com/gfx-rs/wgpu-native/releases/download/$WGPU_VERSION"
|
||||||
|
|
||||||
|
if (-not (Test-Path $target)) {
|
||||||
|
$url = "$assetBase/$name.zip"
|
||||||
|
Write-Output "wgpu setup: downloading $url"
|
||||||
|
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
|
||||||
|
New-Item -ItemType Directory -Path $tmp | Out-Null
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -MaximumRedirection 5 -Uri $url -OutFile (Join-Path $tmp "wgpu.zip")
|
||||||
|
Expand-Archive -Force -Path (Join-Path $tmp "wgpu.zip") -DestinationPath $target
|
||||||
|
} finally {
|
||||||
|
Remove-Item -Recurse -Force $tmp
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package hot_reload
|
package hot_reload
|
||||||
|
|
||||||
|
import "core:c"
|
||||||
import "core:dynlib"
|
import "core:dynlib"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
@@ -132,6 +133,20 @@ load_game_lib :: proc(api: ^Game_API) -> bool {
|
|||||||
"error: failed initializing symbols: {0}",
|
"error: failed initializing symbols: {0}",
|
||||||
dynlib.last_error(),
|
dynlib.last_error(),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// Odin's shared libraries don't register a constructor, so the lib's
|
||||||
|
// runtime init (@init procs like os.stdout setup) never runs on load.
|
||||||
|
// Call it explicitly.
|
||||||
|
// Windows doesn't need this: its dynamic build exports DllMain, which
|
||||||
|
// the OS calls on load and which runs _startup_runtime itself.
|
||||||
|
when ODIN_OS != .Windows {
|
||||||
|
Entry :: #type proc "c" () -> c.int
|
||||||
|
entry_ptr, entry_ok := dynlib.symbol_address(
|
||||||
|
api.lib,
|
||||||
|
"_odin_entry_point",
|
||||||
|
)
|
||||||
|
if entry_ok do (transmute(Entry)entry_ptr)()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok
|
return ok
|
||||||
|
|||||||
Reference in New Issue
Block a user