Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be516d39a3 | ||
|
|
e7cc935035 | ||
|
|
416b42c27e |
+2
-7
@@ -2,12 +2,7 @@
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
build
|
|
||||||
build.exe
|
|
||||||
build.exe.old
|
|
||||||
build.old
|
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
|
|
||||||
out/
|
build
|
||||||
vendor/wgpu/
|
vendor/wgpu
|
||||||
|
|||||||
+1
-4
@@ -1,6 +1,3 @@
|
|||||||
[submodule "vendor/brokkr"]
|
[submodule "sindri/vendor/gram"]
|
||||||
path = vendor/brokkr
|
|
||||||
url = https://codeberg.org/chivvii/brokkr.git
|
|
||||||
[submodule "vendor/gram"]
|
|
||||||
path = vendor/gram
|
path = vendor/gram
|
||||||
url = https://github.com/riyyi/gram.git
|
url = https://github.com/riyyi/gram.git
|
||||||
|
|||||||
@@ -10,18 +10,14 @@ Game engine.
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- `git`
|
|
||||||
- `odin`
|
- `odin`
|
||||||
- `unzip` on macOS or Linux
|
- `sh` a POSIX compatible shell
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./build.sh # debug game and engine (default)
|
./build.sh
|
||||||
./build.sh -debug # debug game and engine
|
./build.sh debug
|
||||||
./build.sh -release # build game and engine
|
|
||||||
./build.sh -run # run game
|
|
||||||
./build.sh -fmt # format sources
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
@@ -39,7 +35,7 @@ vendor
|
|||||||
- miniaudio
|
- miniaudio
|
||||||
- microui (temp)
|
- microui (temp)
|
||||||
- box3d?
|
- box3d?
|
||||||
- build.sh -> bootstrap shim for `compile/build.odin`
|
- build.sh -> build.odin
|
||||||
|
|
||||||
## Hot Reload
|
## Hot Reload
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,52 @@
|
|||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
Set-Location $PSScriptRoot
|
$PROJECT = "sindri"
|
||||||
|
$COMMIT = git rev-parse --short HEAD
|
||||||
|
$VERSION = "dev-$(Get-Date -Format 'yyyy-MM-dd')-$COMMIT"
|
||||||
|
|
||||||
$exe = Join-Path $PSScriptRoot "build.exe"
|
$OPTION = $args[0]
|
||||||
if (-not (Test-Path $exe)) {
|
if ($args) { $args = $args[1..($args.Length - 1)] } else { $args = @() }
|
||||||
& odin build compile/build.odin -file ("-out:" + $exe)
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# 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
|
||||||
}
|
}
|
||||||
|
|
||||||
& $exe @args
|
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
|
||||||
|
}
|
||||||
|
|
||||||
Set-Location -
|
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
|
||||||
|
|||||||
@@ -2,12 +2,57 @@
|
|||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
cd -- "$(dirname -- "$0")"
|
PROJECT="sindri"
|
||||||
|
VERSION="dev-$(date -u '+%Y-%m-%d')-$(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
if [ ! -x ./build ]; then
|
OPTION="${1:-}"
|
||||||
odin build compile/build.odin -file -out:build
|
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_tmp -microarch:native -define:VERSION="$VERSION-debug" -debug "$@"
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# 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
|
fi
|
||||||
|
|
||||||
./build "$@"
|
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
|
||||||
|
|
||||||
cd - > /dev/null
|
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" "$@"
|
||||||
|
|||||||
@@ -1,635 +0,0 @@
|
|||||||
package build
|
|
||||||
|
|
||||||
import "core:flags"
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:log"
|
|
||||||
import "core:os"
|
|
||||||
import "core:path/filepath"
|
|
||||||
import "core:slice"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:time"
|
|
||||||
|
|
||||||
import "../vendor/brokkr"
|
|
||||||
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
LIB_EXT :: ".dll"
|
|
||||||
OS_NAME :: "windows"
|
|
||||||
} else when ODIN_OS == .Darwin {
|
|
||||||
LIB_EXT :: ".dylib"
|
|
||||||
OS_NAME :: "macos"
|
|
||||||
} else {
|
|
||||||
LIB_EXT :: ".so"
|
|
||||||
OS_NAME :: "linux"
|
|
||||||
}
|
|
||||||
|
|
||||||
when ODIN_ARCH == .amd64 {
|
|
||||||
ARCH_NAME :: "x86_64"
|
|
||||||
} else when ODIN_ARCH == .arm64 {
|
|
||||||
ARCH_NAME :: "aarch64"
|
|
||||||
}
|
|
||||||
|
|
||||||
PROJECT_NAME :: "sindri"
|
|
||||||
BUILD_DIRECTORY :: "out/"
|
|
||||||
|
|
||||||
GAME_SRC_DIR :: "game/"
|
|
||||||
|
|
||||||
ENGINE_DIR :: "src/"
|
|
||||||
ENGINE_OUTPUT :: BUILD_DIRECTORY + PROJECT_NAME
|
|
||||||
|
|
||||||
BUILD_INPUT_FILES :: []string{"compile/build.odin"}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Types
|
|
||||||
|
|
||||||
Options :: struct {
|
|
||||||
debug: bool `args:"optional" usage:"Build project in debug-mode."`,
|
|
||||||
release: bool `args:"optional" usage:"Build project in release-mode."`,
|
|
||||||
run: bool `args:"optional" usage:"Run project."`,
|
|
||||||
hot_reload: bool `args:"optional" usage:"Hot-reload project."`,
|
|
||||||
wgpu_init: bool `args:"optional" usage:"Prepare wgpu-native."`,
|
|
||||||
wgpu_version: string `args:"optional" usage:"Override the wgpu release version to init."`,
|
|
||||||
fmt: bool `args:"optional" usage:"Run odinfmt."`,
|
|
||||||
pre_commit: bool `args:"optional" usage:"Run git pre-commit hook."`,
|
|
||||||
overflow: [dynamic]string `args:"manifold" usage:"Extra arguments passed through to the odin build calls."`,
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Main
|
|
||||||
|
|
||||||
main :: proc() {
|
|
||||||
// Create logger
|
|
||||||
context.logger = log.create_console_logger(
|
|
||||||
.Warning,
|
|
||||||
{.Level, .Terminal_Color},
|
|
||||||
"build",
|
|
||||||
)
|
|
||||||
defer log.destroy_console_logger(context.logger)
|
|
||||||
|
|
||||||
// Change working directory
|
|
||||||
chdir_project_root()
|
|
||||||
|
|
||||||
brokkr.auto_rebuild(..BUILD_INPUT_FILES)
|
|
||||||
|
|
||||||
opts: Options
|
|
||||||
flags.parse_or_exit(&opts, os.args, .Unix)
|
|
||||||
|
|
||||||
if opts.debug do build_project(true, opts.overflow[:])
|
|
||||||
if opts.release do build_project(false, opts.overflow[:])
|
|
||||||
if opts.run do run_project(opts.overflow[:])
|
|
||||||
if opts.hot_reload do hot_reload(!opts.release, opts.overflow[:])
|
|
||||||
if opts.wgpu_init do wgpu_init(opts.wgpu_version)
|
|
||||||
if opts.fmt do format_source()
|
|
||||||
if opts.pre_commit do pre_commit()
|
|
||||||
|
|
||||||
// Fallback to building project, if no option provided
|
|
||||||
if len(os.args) - 1 == len(opts.overflow) {
|
|
||||||
build_project(true, opts.overflow[:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Build project
|
|
||||||
|
|
||||||
// Build the game library ('game/') and the engine ('src/')
|
|
||||||
build_project :: proc(debug: bool, extra_args: []string) {
|
|
||||||
version := project_version()
|
|
||||||
|
|
||||||
// Setup compiled wgpu binary
|
|
||||||
wgpu_init()
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Engine compile
|
|
||||||
|
|
||||||
// If the executable is already running, don't try to build and start it
|
|
||||||
if engine_is_running() {
|
|
||||||
fmt.println("Hot reloading...")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
build_engine(debug, version, extra_args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared builder between game/engine
|
|
||||||
build_common :: proc(
|
|
||||||
dir: string,
|
|
||||||
debug: bool,
|
|
||||||
version: string,
|
|
||||||
extra_args: []string,
|
|
||||||
) -> brokkr.Builder {
|
|
||||||
b := brokkr.builder_make()
|
|
||||||
brokkr.build(&b, dir)
|
|
||||||
brokkr.append(&b, "-show-timings")
|
|
||||||
brokkr.collection(&b, "game", ".")
|
|
||||||
brokkr.collection(&b, PROJECT_NAME, "src")
|
|
||||||
brokkr.collection(&b, "gram", "vendor/gram/src")
|
|
||||||
brokkr.collection(&b, "wgpu", "vendor")
|
|
||||||
brokkr.append(&b, "-microarch:native")
|
|
||||||
brokkr.opt(&b, debug ? .Minimal : .Speed)
|
|
||||||
brokkr.define(
|
|
||||||
&b,
|
|
||||||
"VERSION",
|
|
||||||
debug ? fmt.tprintf("%v-debug", version) : version,
|
|
||||||
)
|
|
||||||
if debug {
|
|
||||||
brokkr.debug(&b)
|
|
||||||
brokkr.append(&b, "-use-separate-modules")
|
|
||||||
}
|
|
||||||
for arg in extra_args {
|
|
||||||
brokkr.append(&b, arg)
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the engine executable
|
|
||||||
build_engine :: proc(debug: bool, version: string, extra_args: []string) {
|
|
||||||
b := build_common(ENGINE_DIR, debug, version, extra_args)
|
|
||||||
defer brokkr.builder_destroy(&b)
|
|
||||||
brokkr.out(&b, output_path(ENGINE_OUTPUT))
|
|
||||||
if err := brokkr.execute(&b); err != nil {
|
|
||||||
os.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run_project :: proc(extra_args: []string) {
|
|
||||||
// Build project if it wasnt already
|
|
||||||
if !os.exists(output_path(ENGINE_OUTPUT)) do build_project(true, {})
|
|
||||||
|
|
||||||
cmd := []string{output_path(ENGINE_OUTPUT)}
|
|
||||||
line := slice.concatenate(
|
|
||||||
[][]string{cmd, extra_args},
|
|
||||||
context.temp_allocator,
|
|
||||||
)
|
|
||||||
|
|
||||||
process, s_err := os.process_start(
|
|
||||||
{
|
|
||||||
command = line,
|
|
||||||
stdin = os.stdin,
|
|
||||||
stdout = os.stdout,
|
|
||||||
stderr = os.stderr,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if s_err != nil {
|
|
||||||
die("failed to start project process: %v", os.error_string(s_err))
|
|
||||||
}
|
|
||||||
|
|
||||||
res, w_err := os.process_wait(process)
|
|
||||||
if w_err != nil {
|
|
||||||
die("failed to wait for project process: %v", os.error_string(w_err))
|
|
||||||
}
|
|
||||||
if !res.success {
|
|
||||||
die("project process failed with exit code: %v", res.exit_code)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.exit(res.exit_code)
|
|
||||||
}
|
|
||||||
|
|
||||||
hot_reload :: proc(debug: bool, extra_args: []string) {
|
|
||||||
if engine_is_running() {
|
|
||||||
process := output_path(PROJECT_NAME)
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
// Ask process to shut down gracefully (equivalent to SIGTERM)
|
|
||||||
_, ok, _ := exec([]string{"taskkill", "/IM", process + ".exe"})
|
|
||||||
if !ok {
|
|
||||||
die("hot reload: failed to terminate running %s", process)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, ok, _ := exec([]string{"pkill", "-TERM", "-x", process})
|
|
||||||
if !ok {
|
|
||||||
die(
|
|
||||||
"hot reload: failed to send SIGTERM to running %s",
|
|
||||||
process,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for process to terminate
|
|
||||||
for engine_is_running() {
|
|
||||||
time.sleep(time.Millisecond * 20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run_project(extra_args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// wgpu setup
|
|
||||||
|
|
||||||
// Prepare wgpu for usage: download, unzip, copy vendor code
|
|
||||||
wgpu_init :: proc(version_override := "") {
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
target_name :=
|
|
||||||
"wgpu-" + OS_NAME + "-" + ARCH_NAME + "-msvc" + "-release"
|
|
||||||
} else {
|
|
||||||
target_name := "wgpu-" + OS_NAME + "-" + ARCH_NAME + "-release"
|
|
||||||
}
|
|
||||||
|
|
||||||
target := join_path({"vendor", "wgpu", "lib", target_name})
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// wgpu version detection
|
|
||||||
|
|
||||||
// Detect the wgpu-native version the bundled bindings expect
|
|
||||||
odin_root, ok, _ := exec([]string{"odin", "root"})
|
|
||||||
if !ok do os.exit(1)
|
|
||||||
defer delete(odin_root)
|
|
||||||
|
|
||||||
wgpu_version := version_override
|
|
||||||
if len(wgpu_version) == 0 {
|
|
||||||
v, found := bindings_version_from_file(
|
|
||||||
join_path({odin_root, "vendor", "wgpu", "wgpu_native_types.odin"}),
|
|
||||||
)
|
|
||||||
if !found {
|
|
||||||
die("wgpu setup: could not detect the wgpu-native version")
|
|
||||||
}
|
|
||||||
wgpu_version = fmt.tprintf("v%v", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect the version of the vendored wgpu bindings
|
|
||||||
current := ""
|
|
||||||
if wgpu_types_path := join_path(
|
|
||||||
{"vendor", "wgpu", "wgpu_native_types.odin"},
|
|
||||||
); os.is_file(wgpu_types_path) {
|
|
||||||
if ver, found := bindings_version_from_file(wgpu_types_path); found {
|
|
||||||
current = fmt.tprintf("v%v", ver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the vendored wgpu is already up to date
|
|
||||||
if current == wgpu_version && os.is_dir(target) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Copy vendor/wgpu
|
|
||||||
|
|
||||||
// Remove any stale copy.
|
|
||||||
os.remove_all("vendor/wgpu")
|
|
||||||
|
|
||||||
// Copy wgpu from the odin installation.
|
|
||||||
os.copy_directory_all(
|
|
||||||
"vendor/wgpu",
|
|
||||||
join_path({odin_root, "vendor", "wgpu"}),
|
|
||||||
)
|
|
||||||
chmod_recursively("vendor/wgpu")
|
|
||||||
|
|
||||||
// Point imports at the copied vendor wgpu package.
|
|
||||||
rewrite_imports("vendor/wgpu", `"vendor:wgpu"`, `"wgpu:wgpu"`)
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Download
|
|
||||||
|
|
||||||
if !os.is_dir(target) {
|
|
||||||
url := fmt.tprintf(
|
|
||||||
"https://github.com/gfx-rs/wgpu-native/releases/download/%v/%v.zip",
|
|
||||||
wgpu_version,
|
|
||||||
target_name,
|
|
||||||
)
|
|
||||||
fmt.println("wgpu setup: downloading", url)
|
|
||||||
|
|
||||||
tmp_dir, err := os.make_directory_temp(
|
|
||||||
"",
|
|
||||||
"out-tmp-*",
|
|
||||||
context.temp_allocator,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
die(
|
|
||||||
"wgpu setup: failed to create a temporary directory: %v",
|
|
||||||
os.error_string(err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
defer os.remove_all(tmp_dir)
|
|
||||||
zip_path := join_path({tmp_dir, "wgpu.zip"})
|
|
||||||
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
if dl_err := brokkr.execute_command(
|
|
||||||
[]string {
|
|
||||||
"powershell",
|
|
||||||
"-NoProfile",
|
|
||||||
"-Command",
|
|
||||||
fmt.tprintf(
|
|
||||||
"$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -UseBasicParsing -Uri %q -OutFile %q",
|
|
||||||
url,
|
|
||||||
zip_path,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
); dl_err != nil {
|
|
||||||
die("wgpu setup: failed to download wgpu-native release")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if dl_err := brokkr.execute_command(
|
|
||||||
[]string{"curl", "-fL", "--retry", "3", "-o", zip_path, url},
|
|
||||||
); dl_err != nil {
|
|
||||||
die("wgpu setup: failed to download wgpu-native release")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
// Uses the PowerShell cmdlet the same way scripts/wgpu-init.ps1 did.
|
|
||||||
if ex_err := brokkr.execute_command(
|
|
||||||
[]string {
|
|
||||||
"powershell",
|
|
||||||
"-NoProfile",
|
|
||||||
"-Command",
|
|
||||||
fmt.tprintf(
|
|
||||||
"Expand-Archive -Force -Path %q -DestinationPath %q",
|
|
||||||
zip_path,
|
|
||||||
target,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
); ex_err != nil {
|
|
||||||
die("wgpu setup: failed to extract wgpu-native release")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Matches scripts/wgpu-init.sh.
|
|
||||||
if ex_err := brokkr.execute_command(
|
|
||||||
[]string{"unzip", "-o", "-q", zip_path, "-d", target},
|
|
||||||
); ex_err != nil {
|
|
||||||
die("wgpu setup: failed to extract wgpu-native release")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extracts the wgpu-native version from BINDINGS_VERSION_STRING
|
|
||||||
bindings_version_from_file :: proc(
|
|
||||||
path: string,
|
|
||||||
) -> (
|
|
||||||
version: string,
|
|
||||||
found: bool,
|
|
||||||
) {
|
|
||||||
if !os.is_file(path) do return
|
|
||||||
|
|
||||||
data, err := os.read_entire_file(path, context.temp_allocator)
|
|
||||||
if err != nil do return
|
|
||||||
|
|
||||||
s := string(data)
|
|
||||||
id := strings.index(s, "BINDINGS_VERSION_STRING")
|
|
||||||
if id == -1 do return
|
|
||||||
|
|
||||||
quote_start := strings.index(s[id:], `"`)
|
|
||||||
if quote_start == -1 do return
|
|
||||||
quote_start += id
|
|
||||||
|
|
||||||
quote_end := strings.index(s[quote_start + 1:], `"`)
|
|
||||||
if quote_end == -1 do return
|
|
||||||
quote_end += quote_start + 1
|
|
||||||
|
|
||||||
version = s[quote_start + 1:quote_end]
|
|
||||||
found = len(version) != 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'chmod -R +w' equivalent: makes everything under 'path' writable,
|
|
||||||
// keeping every other permission bit
|
|
||||||
chmod_recursively :: proc(path: string) {
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
info, err := os.stat(path, context.temp_allocator)
|
|
||||||
if err != nil {
|
|
||||||
die(
|
|
||||||
"wgpu setup: failed to stat %q: %v",
|
|
||||||
path,
|
|
||||||
os.error_string(err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add writable (+w)
|
|
||||||
if err := os.change_mode(path, info.mode + os.Permissions_Write_All);
|
|
||||||
err != nil {
|
|
||||||
die("wgpu setup: chmod %q failed: %v", path, os.error_string(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurse into directories
|
|
||||||
if info.type == .Directory {
|
|
||||||
entries, read_err := os.read_directory_by_path(
|
|
||||||
path,
|
|
||||||
-1,
|
|
||||||
context.temp_allocator,
|
|
||||||
)
|
|
||||||
if read_err != nil {
|
|
||||||
die(
|
|
||||||
"wgpu setup: failed to read %q: %v",
|
|
||||||
path,
|
|
||||||
os.error_string(read_err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for entry in entries {
|
|
||||||
if entry.name == "." || entry.name == ".." do continue
|
|
||||||
chmod_recursively(join_path({path, entry.name}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replaces 'old_import' with 'new_import' in every '.odin' file under 'path'
|
|
||||||
rewrite_imports :: proc(path: string, old_import, new_import: string) {
|
|
||||||
info, err := os.stat(path, context.temp_allocator)
|
|
||||||
if err != nil {
|
|
||||||
die("wgpu setup: failed to stat %q: %v", path, os.error_string(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurse into directories
|
|
||||||
if info.type == .Directory {
|
|
||||||
entries, read_err := os.read_directory_by_path(
|
|
||||||
path,
|
|
||||||
-1,
|
|
||||||
context.temp_allocator,
|
|
||||||
)
|
|
||||||
if read_err != nil {
|
|
||||||
die(
|
|
||||||
"wgpu setup: failed to read %q: %v",
|
|
||||||
path,
|
|
||||||
os.error_string(read_err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for entry in entries {
|
|
||||||
if entry.name == "." || entry.name == ".." do continue
|
|
||||||
rewrite_imports(
|
|
||||||
join_path({path, entry.name}),
|
|
||||||
old_import,
|
|
||||||
new_import,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.type != .Regular || !strings.ends_with(info.name, ".odin") do return
|
|
||||||
|
|
||||||
data, read_err := os.read_entire_file(path, context.temp_allocator)
|
|
||||||
if read_err != nil {
|
|
||||||
die(
|
|
||||||
"wgpu setup: failed to read %q: %v",
|
|
||||||
path,
|
|
||||||
os.error_string(read_err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
replacement, was_allocation := strings.replace_all(
|
|
||||||
string(data),
|
|
||||||
old_import,
|
|
||||||
new_import,
|
|
||||||
context.temp_allocator,
|
|
||||||
)
|
|
||||||
if was_allocation {
|
|
||||||
if write_err := os.write_entire_file(path, replacement);
|
|
||||||
write_err != nil {
|
|
||||||
die(
|
|
||||||
"wgpu setup: failed to write %q: %v",
|
|
||||||
path,
|
|
||||||
os.error_string(write_err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Formatting
|
|
||||||
|
|
||||||
// Run `odinfmt -w` on source directory
|
|
||||||
format_source :: proc() {
|
|
||||||
for dir in ([]string{GAME_SRC_DIR, ENGINE_DIR}) {
|
|
||||||
err := brokkr.execute_command([]string{"odinfmt", "-w", dir})
|
|
||||||
if err != nil do os.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Git pre-commit hook
|
|
||||||
|
|
||||||
pre_commit :: proc() {
|
|
||||||
err := brokkr.execute_command(
|
|
||||||
[]string{"git", "stash", "push", "--keep-index", "--quiet"},
|
|
||||||
)
|
|
||||||
if err != nil do os.exit(1)
|
|
||||||
|
|
||||||
format_source()
|
|
||||||
|
|
||||||
err = brokkr.execute_command([]string{"git", "diff", "--quiet"})
|
|
||||||
if err != nil {
|
|
||||||
log.error("formatting changed staged files; commit aborted")
|
|
||||||
_ = brokkr.execute_command([]string{"git", "checkout", "--", "."})
|
|
||||||
_ = brokkr.execute_command([]string{"git", "stash", "pop", "--quiet"})
|
|
||||||
os.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = brokkr.execute_command([]string{"git", "stash", "pop", "--quiet"})
|
|
||||||
if err != nil do os.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Helpers
|
|
||||||
|
|
||||||
// Log an error and exit(1)
|
|
||||||
die :: proc(fmt_str: string, args: ..any, loc := #caller_location) -> ! {
|
|
||||||
log.errorf(fmt_str, ..args, location = loc)
|
|
||||||
os.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change working directory to project root
|
|
||||||
chdir_project_root :: proc() {
|
|
||||||
// Get full path
|
|
||||||
binary_path, abs_err := filepath.abs(os.args[0])
|
|
||||||
if abs_err != nil {
|
|
||||||
die(
|
|
||||||
"failed to get absolute binary path %q: %v",
|
|
||||||
os.args[0],
|
|
||||||
os.error_string(abs_err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set working directory to project root
|
|
||||||
project_root_dir := filepath.dir(binary_path)
|
|
||||||
if err := os.set_working_directory(project_root_dir); err != nil {
|
|
||||||
die(
|
|
||||||
"failed to change working directory to %q: %v",
|
|
||||||
project_root_dir,
|
|
||||||
os.error_string(err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set program argument to the full path of the binary
|
|
||||||
os.args[0] = binary_path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if engine (executable named `Project_Name`) is already running
|
|
||||||
engine_is_running :: proc() -> bool {
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
out, ok := exec(
|
|
||||||
[]string {
|
|
||||||
"tasklist",
|
|
||||||
"/FI",
|
|
||||||
("IMAGENAME eq " + PROJECT_NAME + ".exe"),
|
|
||||||
"/NH",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if !ok do return false
|
|
||||||
return strings.contains(string(out), PROJECT_NAME + ".exe")
|
|
||||||
} else {
|
|
||||||
_, ok, code := exec([]string{"pgrep", "-x", PROJECT_NAME}, false)
|
|
||||||
if !ok do return false
|
|
||||||
return code == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs a command. Use this over brokkr.execute_command when you need to
|
|
||||||
// capture the contents of stdout, or the return code
|
|
||||||
exec :: proc(
|
|
||||||
cmd: []string,
|
|
||||||
print_error := true,
|
|
||||||
) -> (
|
|
||||||
out: string,
|
|
||||||
ok: bool,
|
|
||||||
code: int,
|
|
||||||
) {
|
|
||||||
state, stdout, stderr, err := os.process_exec(
|
|
||||||
{command = cmd},
|
|
||||||
context.temp_allocator,
|
|
||||||
)
|
|
||||||
defer delete(stdout, context.temp_allocator)
|
|
||||||
defer delete(stderr, context.temp_allocator)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.errorf("exec failed %v: %v", cmd, os.error_string(err))
|
|
||||||
return "", false, 1
|
|
||||||
}
|
|
||||||
if state.exit_code != 0 {
|
|
||||||
if print_error {
|
|
||||||
log.errorf("exec failed %v: %s", cmd, stderr)
|
|
||||||
}
|
|
||||||
return "", false, state.exit_code
|
|
||||||
}
|
|
||||||
|
|
||||||
str, a_err := strings.clone_from_bytes(stdout)
|
|
||||||
if a_err != nil {
|
|
||||||
return "", false, 1
|
|
||||||
}
|
|
||||||
return strings.trim_space(str), true, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Joins segments into a full path, ex: {"a", "b", "c"} -> a/b/c
|
|
||||||
join_path :: proc(elems: []string) -> string {
|
|
||||||
joined, err := filepath.join(elems, context.temp_allocator)
|
|
||||||
if err != nil {
|
|
||||||
die("failed to join path %v: %v", elems, os.error_string(err))
|
|
||||||
}
|
|
||||||
return joined
|
|
||||||
}
|
|
||||||
|
|
||||||
output_path :: proc(binary_path: string) -> string {
|
|
||||||
when ODIN_OS == .Windows {
|
|
||||||
return binary_path + ".exe"
|
|
||||||
}
|
|
||||||
return binary_path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get project version, example: `dev-2026-09-23-a4084b3`
|
|
||||||
project_version :: proc() -> string {
|
|
||||||
commit, ok, _ := exec([]string{"git", "rev-parse", "--short", "HEAD"})
|
|
||||||
if !ok do os.exit(1)
|
|
||||||
defer delete(commit)
|
|
||||||
|
|
||||||
buf: [time.MIN_YYYY_DATE_LEN]u8
|
|
||||||
date := time.to_string_yyyy_mm_dd(time.now(), buf[:])
|
|
||||||
return fmt.tprintf("dev-%v-%v", date, commit)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Hot Reloading
|
|
||||||
|
|
||||||
Instead of having the engine as an exe and game as a dll, and having to jump
|
|
||||||
through hoops to share libraries internal state between them we do it
|
|
||||||
differently. The engine and game live in the same binary, compiled fully each
|
|
||||||
time. We rely mostly on static memory, and register these memory regions. Then
|
|
||||||
when we want to hot reload: we send a close signal to the running instance, it
|
|
||||||
writes a snapshot of the active memory to disk, instance closes, recompile,
|
|
||||||
restart the application, it restores the memory from the snapshot.
|
|
||||||
|
|
||||||
## Byte Layout On Disk
|
|
||||||
|
|
||||||
```odin
|
|
||||||
Snapshot_Header :: struct #packed #all_or_none {
|
|
||||||
magic_string: u32,
|
|
||||||
version: u16, // spec of the snapshot
|
|
||||||
app_version: [4 + time.MIN_YYYY_DATE_LEN + 8]u8, // example: `dev-2026-09-23-a4084b3`
|
|
||||||
region_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
Region_Header :: struct #packed #all_or_none {
|
|
||||||
name_size: u32,
|
|
||||||
data_size: u32,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
[ Snapshot_Header ]
|
|
||||||
[ Region_Header ]
|
|
||||||
[ name bytes ]
|
|
||||||
[ data bytes ]
|
|
||||||
[ Region_Header ]
|
|
||||||
[ name bytes ]
|
|
||||||
[ data bytes ]
|
|
||||||
[ etc.. ]
|
|
||||||
|
|
||||||
## Diff Checking
|
|
||||||
|
|
||||||
Use hashing per memory segment to do a sanity diff-check.
|
|
||||||
|
|
||||||
```odin
|
|
||||||
import "core:crypto/hash"
|
|
||||||
|
|
||||||
data: []byte = ...
|
|
||||||
|
|
||||||
digest := hash.hash_bytes(hash.Algorithm.SHA256, data)
|
|
||||||
defer delete(digest)
|
|
||||||
```
|
|
||||||
+6
-77
@@ -4,90 +4,19 @@ This document contains specifics about the workings of the Odin programming
|
|||||||
language, dynamic linking and state management design patterns. These are
|
language, dynamic linking and state management design patterns. These are
|
||||||
tested and validated findings, for continued reference by me.
|
tested and validated findings, for continued reference by me.
|
||||||
|
|
||||||
## Compiler
|
|
||||||
|
|
||||||
Odin's compiler is demand-driver: a procedure's body is only semantically
|
|
||||||
analyzed when the proc is actually referenced. They are syntactically checked
|
|
||||||
always.
|
|
||||||
|
|
||||||
## Dynamic Library Memory
|
## Dynamic Library Memory
|
||||||
|
|
||||||
When a program loads in a dynamic library, what happens with package globals?
|
When a program loads in a dynamic library, what happens with package globals?
|
||||||
|
|
||||||
Findings (macOS):
|
Findings (macOS):
|
||||||
- Package globals are shared between modules, but only indirectly
|
- The package globals are shared between the host and the lib
|
||||||
- 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,
|
- After hot reloading the lib, the memory of the packge is not refreshed,
|
||||||
it keeps pointing to the package global of the host
|
it keeps pointing to the package global of the host
|
||||||
|
|
||||||
Findings (Windows):
|
The clanker states this is due to "flat-namespace symbol interposition".
|
||||||
- Package globals are not shared between modules, they have their own copy
|
|
||||||
|
|
||||||
Findings (Linux):
|
Linux "ELF has symbol preemption: when resolving references, ld.so searches
|
||||||
- Package globals are not shared between modules, they have their own copy
|
globals scope in order — executable first, then loaded shared objects".
|
||||||
|
|
||||||
Conclusion: avoid package globals, or pass pointers to the dynamic library so
|
Windows PE has no interposition at all, every module (exe and each DLL) has its
|
||||||
it can match its package globals with the host module.
|
own symbol table.
|
||||||
|
|
||||||
## 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging strategy
|
|
||||||
|
|
||||||
From the docs:
|
|
||||||
|
|
||||||
> While it is ok for simple apps to use the core:fmt package, libraries and
|
|
||||||
> complex apps should prefer the core:log package. By using the implicit logger
|
|
||||||
> library and application authors allow the caller to decide how to process log
|
|
||||||
> messages.
|
|
||||||
|
|
||||||
## Naming Conventions
|
|
||||||
|
|
||||||
From core.odin:
|
|
||||||
|
|
||||||
> In general, Ada_Case for types and snake_case for values
|
|
||||||
>
|
|
||||||
> - Package Name: snake_case (but prefer single word)
|
|
||||||
> - Import Name: snake_case (but prefer single word)
|
|
||||||
> - Types: Ada_Case
|
|
||||||
> - Enum Values: Ada_Case
|
|
||||||
> - Procedures: snake_case
|
|
||||||
> - Local Variables: snake_case
|
|
||||||
> - Constant Variables: SCREAMING_SNAKE_CASE
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
odinfmt -w src/ > /dev/null
|
||||||
|
# odinfmt -w tests/ > /dev/null
|
||||||
+46
-42
@@ -1,105 +1,109 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import "core:fmt"
|
import "sindri:input"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
|
import "core:fmt"
|
||||||
|
|
||||||
import "sindri:base"
|
|
||||||
import "sindri:core"
|
import "sindri:core"
|
||||||
|
import "sindri:test"
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// Types
|
|
||||||
|
|
||||||
Game_Memory :: struct {
|
Game_Memory :: struct {
|
||||||
should_close: bool,
|
should_close: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
Test_Memory :: struct {
|
g: ^Game_Memory
|
||||||
asd: int,
|
|
||||||
qwe: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// Variables
|
|
||||||
|
|
||||||
g: Game_Memory
|
@(export)
|
||||||
t: Test_Memory
|
memory :: proc() -> rawptr {
|
||||||
|
return g
|
||||||
game_api := core.Game_API {
|
|
||||||
settings = settings,
|
|
||||||
init_once = init_once,
|
|
||||||
init = init,
|
|
||||||
update = update,
|
|
||||||
render = render,
|
|
||||||
destroy = destroy,
|
|
||||||
should_close = should_close,
|
|
||||||
force_reload = force_reload,
|
|
||||||
force_restart = force_restart,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------
|
@(export)
|
||||||
// Public functions
|
memory_free :: proc() {
|
||||||
|
free(g)
|
||||||
api :: proc() -> ^core.Game_API {
|
|
||||||
return &game_api
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settings :: proc() -> base.Settings {
|
@(export)
|
||||||
return base.Settings {
|
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,
|
width = 960,
|
||||||
height = 540,
|
height = 540,
|
||||||
title = "WGPU Native Triangle",
|
title = "WGPU Native Triangle",
|
||||||
mode = base.WindowMode.Windowed,
|
mode = core.WindowMode.Windowed,
|
||||||
refresh = 60,
|
refresh = 60,
|
||||||
vsync = true,
|
vsync = true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
init_once :: proc() {
|
init_once :: proc() {
|
||||||
fmt.println("init once")
|
fmt.println("init once")
|
||||||
|
|
||||||
base.register_region("game_memory", &g, size_of(Game_Memory))
|
test.test_proc = proc() {}
|
||||||
base.register_region("test_memory", &t, size_of(Test_Memory))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
init :: proc() {
|
init :: proc() {
|
||||||
fmt.println("hello from .dll!")
|
fmt.println("hello from .dll!")
|
||||||
|
|
||||||
g.should_close = false
|
g = new(Game_Memory)
|
||||||
|
memory_set(g)
|
||||||
|
|
||||||
|
test.test_proc = proc() { asd := 2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
update :: proc(dt: f32) {
|
update :: proc(dt: f32) {
|
||||||
fmt.println("dt:", dt)
|
fmt.println("dt:", dt)
|
||||||
|
|
||||||
if core.key_state(.Key_Escape) == .Press {
|
if input.key_state(.Key_Escape) == .Press {
|
||||||
g.should_close = true
|
g.should_close = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if core.key_state(.Key_F5) == .Press do t.asd = 555
|
fmt.println("GAME:", test.test)
|
||||||
if core.key_state(.Key_F6) == .Press do t.asd = 666
|
fmt.printf("pointer: %p | %p\n", &test.test, &test.test_proc)
|
||||||
|
test.test += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
render :: proc() { }
|
@(export)
|
||||||
|
|
||||||
destroy :: proc() {
|
destroy :: proc() {
|
||||||
fmt.println("bye")
|
fmt.println("bye")
|
||||||
|
|
||||||
fmt.println(t.asd, t.qwe)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.tick_now()
|
start := time.tick_now()
|
||||||
|
|
||||||
|
@(export)
|
||||||
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)
|
||||||
force_reload :: proc() -> bool {
|
force_reload :: proc() -> bool {
|
||||||
return core.key_state(.Key_F5) == .Press
|
return input.key_state(.Key_F5) == .Press
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(export)
|
||||||
force_restart :: proc() -> bool {
|
force_restart :: proc() -> bool {
|
||||||
return core.key_state(.Key_F6) == .Press
|
return input.key_state(.Key_F6) == .Press
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"collections": [
|
"collections": [
|
||||||
{ "name": "game", "path": "." },
|
|
||||||
{ "name": "sindri", "path": "src" },
|
{ "name": "sindri", "path": "src" },
|
||||||
{ "name": "gram", "path": "vendor/gram/src" },
|
{ "name": "gram", "path": "vendor/gram/src" },
|
||||||
{ "name": "wgpu", "path": "vendor" }
|
{ "name": "wgpu", "path": "vendor" }
|
||||||
|
|||||||
+14
-3
@@ -1,7 +1,18 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
|
SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
|
||||||
exec "$SCRIPT_DIR/../build.sh" -pre-commit
|
git stash push --keep-index --quiet
|
||||||
|
|
||||||
|
"$SCRIPT_DIR/../fmt.sh"
|
||||||
|
|
||||||
|
if ! git diff --quiet; then
|
||||||
|
echo "Formatting changed staged files; commit aborted."
|
||||||
|
git checkout -- "$SCRIPT_DIR/.."
|
||||||
|
git stash pop --quiet
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# "$SCRIPT_DIR/../test.sh"
|
||||||
|
|
||||||
|
git stash pop --quiet
|
||||||
|
|||||||
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
VENDOR_DIR="$SCRIPT_DIR/../vendor"
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# Platform detection
|
||||||
|
|
||||||
|
# OS detection
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin)
|
||||||
|
os="macos"
|
||||||
|
;;
|
||||||
|
Linux)
|
||||||
|
os="linux"
|
||||||
|
;;
|
||||||
|
MINGW* | MSYS* | CYGWIN*)
|
||||||
|
os="windows"
|
||||||
|
extra="-msvc"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "wgpu setup: unsupported OS '$(uname -s)'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Arch detection
|
||||||
|
case "$(uname -m)" in
|
||||||
|
arm64 | aarch64)
|
||||||
|
arch="aarch64"
|
||||||
|
;;
|
||||||
|
x86_64 | amd64)
|
||||||
|
arch="x86_64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "wgpu setup: unsupported arch '$(uname -m)'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Download filename
|
||||||
|
name="wgpu-${os}-${arch}${extra:-}-release"
|
||||||
|
|
||||||
|
# Compiler filename: lib/wgpu-macos-aarch64-release/lib/libwgpu_native.a
|
||||||
|
target="$VENDOR_DIR/wgpu/lib/$name"
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# WGPU version detection
|
||||||
|
|
||||||
|
# Detect the wgpu-native version the bundled bindings expect, can be overridden
|
||||||
|
if [ "$1" != "" ]; then
|
||||||
|
WGPU_VERSION="$1"
|
||||||
|
else
|
||||||
|
WGPU_VERSION="v$(sed -n 's/.*BINDINGS_VERSION_STRING :: "\([^"]*\)".*/\1/p' "$(odin root)/vendor/wgpu/wgpu_native_types.odin")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$WGPU_VERSION" = "v" ]; then
|
||||||
|
echo "wgpu setup: could not detect the wgpu-native version" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect the version of the vendored wgpu bindings
|
||||||
|
current="v$(sed -n 's/.*BINDINGS_VERSION_STRING :: "\([^"]*\)".*/\1/p' "$VENDOR_DIR/wgpu/wgpu_native_types.odin" 2> /dev/null)"
|
||||||
|
|
||||||
|
# Check if the vendored wgpu is already up to date
|
||||||
|
if [ "$current" = "$WGPU_VERSION" ] && [ -d "$target" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# Copy vendor/wgpu
|
||||||
|
|
||||||
|
# Remove any stale copy
|
||||||
|
rm -rf "$VENDOR_DIR/wgpu"
|
||||||
|
|
||||||
|
# Copy wgpu
|
||||||
|
wgpu_path="$(odin root)/vendor/wgpu"
|
||||||
|
cp -r "$wgpu_path" "$VENDOR_DIR"
|
||||||
|
chmod -R +w "$VENDOR_DIR/wgpu"
|
||||||
|
|
||||||
|
# Point imports at the copied vendor wgpu package
|
||||||
|
grep -rl '"vendor:wgpu"' "$VENDOR_DIR/wgpu" --include='*.odin' |
|
||||||
|
while IFS= read -r file; do
|
||||||
|
sed 's|"vendor:wgpu"|"wgpu:wgpu"|g' "$file" >"$file.tmp" && mv "$file.tmp" "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
# Download
|
||||||
|
|
||||||
|
ASSET_BASE="https://github.com/gfx-rs/wgpu-native/releases/download/${WGPU_VERSION}"
|
||||||
|
|
||||||
|
# Download
|
||||||
|
if [ ! -d "$target" ]; then
|
||||||
|
url="${ASSET_BASE}/${name}.zip"
|
||||||
|
echo "wgpu setup: downloading $url"
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
curl -fL --retry 3 -o "$tmp/wgpu.zip" "$url"
|
||||||
|
unzip -o -q "$tmp/wgpu.zip" -d "$target"
|
||||||
|
fi
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
should_close: bool
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import "base:intrinsics"
|
|
||||||
import "base:runtime"
|
|
||||||
import "core:fmt"
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Types
|
|
||||||
|
|
||||||
// event category, bitfield (?)
|
|
||||||
|
|
||||||
Event_Data :: union {
|
|
||||||
Window_Close_Event,
|
|
||||||
Window_Resize_Event,
|
|
||||||
Joystick_Connect_Event,
|
|
||||||
Joystick_Disconnect_Event,
|
|
||||||
Key_Press_Event,
|
|
||||||
Key_Release_Event,
|
|
||||||
Key_Repeat_Event,
|
|
||||||
Mouse_Button_Press_Event,
|
|
||||||
Mouse_Button_Release_Event,
|
|
||||||
Mouse_Position_Event,
|
|
||||||
Mouse_Scroll_Event,
|
|
||||||
}
|
|
||||||
|
|
||||||
Event :: struct {
|
|
||||||
data: Event_Data,
|
|
||||||
handled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
Window_Close_Event :: struct {}
|
|
||||||
|
|
||||||
Window_Resize_Event :: struct {
|
|
||||||
width: i32,
|
|
||||||
height: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
Key_Press_Event :: struct {
|
|
||||||
key: Key,
|
|
||||||
mods: Mod_Set,
|
|
||||||
}
|
|
||||||
|
|
||||||
Key_Release_Event :: struct {
|
|
||||||
key: Key,
|
|
||||||
mods: Mod_Set,
|
|
||||||
}
|
|
||||||
|
|
||||||
Key_Repeat_Event :: struct {
|
|
||||||
key: Key,
|
|
||||||
mods: Mod_Set,
|
|
||||||
}
|
|
||||||
|
|
||||||
Mouse_Button_Press_Event :: struct {
|
|
||||||
button: Mouse_Button,
|
|
||||||
mods: Mod_Set,
|
|
||||||
}
|
|
||||||
|
|
||||||
Mouse_Button_Release_Event :: struct {
|
|
||||||
button: Mouse_Button,
|
|
||||||
mods: Mod_Set,
|
|
||||||
}
|
|
||||||
|
|
||||||
Mouse_Position_Event :: struct {
|
|
||||||
x_pos: f32,
|
|
||||||
y_pos: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
Mouse_Scroll_Event :: struct {
|
|
||||||
x_offset: f32,
|
|
||||||
y_offset: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
Joystick_Connect_Event :: struct {
|
|
||||||
id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
Joystick_Disconnect_Event :: struct {
|
|
||||||
id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message Receiver
|
|
||||||
@(private)
|
|
||||||
Listener :: struct {
|
|
||||||
callback: proc(event: ^Event_Data, user_ptr: rawptr) -> bool, // return true = don't propagate
|
|
||||||
user_ptr: rawptr,
|
|
||||||
// TODO: test if hot_reload keeps the same function pointers and the listeners remain working.
|
|
||||||
// if they dont add a flag "engine_listener: bool", which engine calls should set true,
|
|
||||||
// then clear all game listeners
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transfers messages between Sender and Receiver
|
|
||||||
Event_Bus :: struct {
|
|
||||||
queue: [dynamic]Event,
|
|
||||||
listeners: map[typeid][dynamic]Listener,
|
|
||||||
allocator: runtime.Allocator,
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
event_bus: Event_Bus
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Constructor/destructor
|
|
||||||
|
|
||||||
event_bus_init :: proc(allocator := context.allocator) {
|
|
||||||
// make() doesnt allocate until there's capacity, so set it > 0
|
|
||||||
queue := make([dynamic]Event, 0, 4, allocator)
|
|
||||||
listeners := make(map[typeid][dynamic]Listener, 4, allocator)
|
|
||||||
event_bus = {
|
|
||||||
queue = queue,
|
|
||||||
listeners = listeners,
|
|
||||||
allocator = allocator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event_bus_destroy :: proc() {
|
|
||||||
assert(event_bus.queue != nil)
|
|
||||||
assert(event_bus.listeners != nil)
|
|
||||||
|
|
||||||
for _, ls in event_bus.listeners do delete(ls)
|
|
||||||
|
|
||||||
delete(event_bus.queue)
|
|
||||||
delete(event_bus.listeners)
|
|
||||||
|
|
||||||
event_bus.queue = nil
|
|
||||||
event_bus.listeners = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Public functions
|
|
||||||
|
|
||||||
event_subscribe :: proc(
|
|
||||||
$T: typeid,
|
|
||||||
callback: proc(event: ^T, user_ptr: rawptr) -> bool,
|
|
||||||
user_ptr: rawptr = nil,
|
|
||||||
) where intrinsics.type_is_variant_of(Event_Data, T) {
|
|
||||||
cb := cast(proc(_: ^Event_Data, _: rawptr) -> bool)callback // upcast
|
|
||||||
event_subscribe_raw(T, cb, user_ptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
event_unsubscribe :: proc(
|
|
||||||
$T: typeid,
|
|
||||||
callback: proc(event: ^T, user_ptr: rawptr) -> bool,
|
|
||||||
user_ptr: rawptr = nil,
|
|
||||||
) where intrinsics.type_is_variant_of(Event_Data, T) {
|
|
||||||
cb := cast(proc(_: ^Event_Data, _: rawptr) -> bool)callback // upcast
|
|
||||||
event_unsubscribe_raw(T, cb, user_ptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
on_event :: proc(data: Event_Data) {
|
|
||||||
assert(event_bus.queue != nil)
|
|
||||||
|
|
||||||
append(&event_bus.queue, Event{data = data})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drain all the events that have been queued up, in enqueue order
|
|
||||||
drain_event_queue :: proc() {
|
|
||||||
assert(event_bus.queue != nil)
|
|
||||||
|
|
||||||
for &event in event_bus.queue {
|
|
||||||
|
|
||||||
#partial switch e in event.data {
|
|
||||||
case Window_Close_Event:
|
|
||||||
event_dispatch(Window_Close_Event, &event)
|
|
||||||
fmt.println("Close")
|
|
||||||
case Window_Resize_Event:
|
|
||||||
event_dispatch(Window_Resize_Event, &event)
|
|
||||||
fmt.println("Resize:", e.width, e.height)
|
|
||||||
|
|
||||||
case Key_Press_Event:
|
|
||||||
event_dispatch(Key_Press_Event, &event)
|
|
||||||
fmt.println("Key press", e.key, e.mods)
|
|
||||||
case Key_Release_Event:
|
|
||||||
event_dispatch(Key_Release_Event, &event)
|
|
||||||
fmt.println("Key release", e.key, e.mods)
|
|
||||||
case Key_Repeat_Event:
|
|
||||||
event_dispatch(Key_Repeat_Event, &event)
|
|
||||||
fmt.println("Key repeat", e.key, e.mods)
|
|
||||||
|
|
||||||
case Mouse_Button_Press_Event:
|
|
||||||
event_dispatch(Mouse_Button_Press_Event, &event)
|
|
||||||
fmt.println("Mouse press", e.button, e.mods)
|
|
||||||
case Mouse_Button_Release_Event:
|
|
||||||
event_dispatch(Mouse_Button_Release_Event, &event)
|
|
||||||
fmt.println("Mouse release", e.button, e.mods)
|
|
||||||
case Mouse_Position_Event:
|
|
||||||
event_dispatch(Mouse_Position_Event, &event)
|
|
||||||
fmt.printfln("Mouse pos: {}x{}", e.x_pos, e.y_pos)
|
|
||||||
case Mouse_Scroll_Event:
|
|
||||||
event_dispatch(Mouse_Scroll_Event, &event)
|
|
||||||
fmt.printfln("Mouse scroll: {}x{}", e.x_offset, e.y_offset)
|
|
||||||
|
|
||||||
case Joystick_Connect_Event:
|
|
||||||
event_dispatch(Joystick_Connect_Event, &event)
|
|
||||||
fmt.println("Joy con:", e.id)
|
|
||||||
case Joystick_Disconnect_Event:
|
|
||||||
event_dispatch(Joystick_Disconnect_Event, &event)
|
|
||||||
fmt.println("Joy dis:", e.id)
|
|
||||||
|
|
||||||
case:
|
|
||||||
panic("[event] unimplemented event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(&event_bus.queue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch to the listeners registered for T, in reverse subscription order.
|
|
||||||
// First listener to return true claims the event and stops propagation
|
|
||||||
event_dispatch :: proc(
|
|
||||||
$T: typeid,
|
|
||||||
event: ^Event,
|
|
||||||
) -> bool where intrinsics.type_is_variant_of(Event_Data, T) {
|
|
||||||
assert(event_bus.listeners != nil)
|
|
||||||
|
|
||||||
ls, ok := event_bus.listeners[T]
|
|
||||||
if !ok do return false
|
|
||||||
|
|
||||||
#reverse for &l in ls {
|
|
||||||
callback := cast(proc(_: ^T, _: rawptr) -> bool)l.callback // downcast
|
|
||||||
if callback(&event.data.(T), l.user_ptr) {
|
|
||||||
event.handled = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Private functions
|
|
||||||
|
|
||||||
@(private)
|
|
||||||
event_subscribe_raw :: proc(
|
|
||||||
$T: typeid,
|
|
||||||
callback: proc(event: ^Event_Data, user_ptr: rawptr) -> bool,
|
|
||||||
user_ptr: rawptr = nil,
|
|
||||||
) where intrinsics.type_is_variant_of(Event_Data, T) {
|
|
||||||
assert(event_bus.listeners != nil)
|
|
||||||
|
|
||||||
if _, ok := event_bus.listeners[T]; !ok {
|
|
||||||
event_bus.listeners[T] = make([dynamic]Listener, event_bus.allocator)
|
|
||||||
}
|
|
||||||
|
|
||||||
append(&event_bus.listeners[T], Listener{callback, user_ptr})
|
|
||||||
}
|
|
||||||
|
|
||||||
@(private)
|
|
||||||
event_unsubscribe_raw :: proc(
|
|
||||||
$T: typeid,
|
|
||||||
callback: proc(event: ^Event_Data, user_ptr: rawptr) -> bool,
|
|
||||||
user_ptr: rawptr = nil,
|
|
||||||
) where intrinsics.type_is_variant_of(Event_Data, T) {
|
|
||||||
assert(event_bus.listeners != nil)
|
|
||||||
|
|
||||||
ls, ok := event_bus.listeners[T]
|
|
||||||
if !ok do return
|
|
||||||
|
|
||||||
for l, i in ls {
|
|
||||||
if l.callback == callback && l.user_ptr == user_ptr {
|
|
||||||
ordered_remove(&event_bus.listeners[T], i) // preserve subscription order
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import "base:runtime"
|
|
||||||
import "core:crypto/hash"
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:mem"
|
|
||||||
import "core:os"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:time"
|
|
||||||
|
|
||||||
SNAPSHOT_NAME :: "snapshot"
|
|
||||||
SNAPSHOT_EXTENSION :: "sndr"
|
|
||||||
|
|
||||||
MAGIC_STRING :: 0x72646E73 // "rdns" -> "sndr" in little endian
|
|
||||||
HOT_RELOAD_VERSION :: 1
|
|
||||||
APP_VERSION_SIZE :: 4 + time.MIN_YYYY_DATE_LEN + 8 + 6 // example: `dev-2026-09-23-a4084b3-debug`
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Types
|
|
||||||
|
|
||||||
@(private)
|
|
||||||
Snapshot_Header :: struct #packed #all_or_none {
|
|
||||||
magic_string: u32,
|
|
||||||
version: u16, // spec of the snapshot
|
|
||||||
app_version: [APP_VERSION_SIZE]u8,
|
|
||||||
region_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
@(private)
|
|
||||||
Region_Header :: struct #packed #all_or_none {
|
|
||||||
name_size: u32,
|
|
||||||
data_size: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
@(private)
|
|
||||||
Memory_Region :: struct {
|
|
||||||
ptr_location: rawptr,
|
|
||||||
// TODO: How do we do hash compare without having to hash current memory every frame?
|
|
||||||
digest: []u8, // hash of this region in last snapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
@(private)
|
|
||||||
Hot_Reload :: struct {
|
|
||||||
allocator: runtime.Allocator,
|
|
||||||
snapshot_path: string,
|
|
||||||
snapshot_header: Snapshot_Header,
|
|
||||||
region_headers: map[string]Region_Header, // hash of name:size
|
|
||||||
regions: map[string]Memory_Region, // hash of name:size
|
|
||||||
write_full_snapshot: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
@(private)
|
|
||||||
hr: Hot_Reload
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Constructor & destructor
|
|
||||||
|
|
||||||
hot_reload_init :: proc(
|
|
||||||
app_version: string,
|
|
||||||
allocator := context.allocator,
|
|
||||||
) -> os.Error {
|
|
||||||
// Get snapshot path
|
|
||||||
out := os.get_executable_directory(allocator) or_return
|
|
||||||
defer delete(out, allocator)
|
|
||||||
snapshot_path := fmt.tprintf(
|
|
||||||
"%v/%v.%v",
|
|
||||||
out,
|
|
||||||
SNAPSHOT_NAME,
|
|
||||||
SNAPSHOT_EXTENSION,
|
|
||||||
)
|
|
||||||
snapshot_path_cpy := strings.clone(snapshot_path, allocator)
|
|
||||||
|
|
||||||
// Copy version string
|
|
||||||
version: [APP_VERSION_SIZE]u8
|
|
||||||
n := min(len(app_version), APP_VERSION_SIZE)
|
|
||||||
copy(version[:n], app_version[:n])
|
|
||||||
|
|
||||||
hr = {
|
|
||||||
allocator = allocator,
|
|
||||||
snapshot_path = snapshot_path_cpy,
|
|
||||||
snapshot_header = {
|
|
||||||
magic_string = MAGIC_STRING,
|
|
||||||
version = HOT_RELOAD_VERSION,
|
|
||||||
app_version = version,
|
|
||||||
region_count = 0,
|
|
||||||
},
|
|
||||||
region_headers = make(map[string]Region_Header, 1, allocator), // initial capacity for nil checks
|
|
||||||
regions = make(map[string]Memory_Region, 1, allocator), // initial capacity for nil checks
|
|
||||||
write_full_snapshot = true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hot_reload_destroy :: proc() {
|
|
||||||
assert(
|
|
||||||
hr.region_headers != nil,
|
|
||||||
"[hot-reload] destroying uninitialized data",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(hr.regions != nil, "[hot-reload] destroying uninitialized data")
|
|
||||||
|
|
||||||
delete(hr.snapshot_path, hr.allocator)
|
|
||||||
|
|
||||||
for key, region in hr.region_headers do delete(key, hr.allocator)
|
|
||||||
for key, region in hr.regions {
|
|
||||||
if region.digest != nil do delete(region.digest, hr.allocator)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(hr.region_headers)
|
|
||||||
delete(hr.regions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Public functions
|
|
||||||
|
|
||||||
register_region :: proc(
|
|
||||||
name: string,
|
|
||||||
ptr_location: rawptr,
|
|
||||||
data_size: u32,
|
|
||||||
) -> os.Error {
|
|
||||||
assert(hr.region_headers != nil, "[hot-reload] uninitialized")
|
|
||||||
assert(hr.regions != nil, "[hot-reload] uninitialized")
|
|
||||||
assert(ptr_location != nil, "[hot_reload] invalid location")
|
|
||||||
assert(data_size > 0, "[hot_reload] invalid type size")
|
|
||||||
|
|
||||||
hash := fmt.tprintf("%v:%d", name, data_size)
|
|
||||||
assert(!(hash in hr.regions), "[hot_reload] double registration")
|
|
||||||
hash_cpy := strings.clone(hash, hr.allocator)
|
|
||||||
hr.region_headers[hash_cpy] = Region_Header {
|
|
||||||
name_size = cast(u32)len(name),
|
|
||||||
data_size = data_size,
|
|
||||||
}
|
|
||||||
hr.regions[hash_cpy] = Memory_Region {
|
|
||||||
ptr_location = ptr_location,
|
|
||||||
}
|
|
||||||
|
|
||||||
hr.snapshot_header.region_count += 1
|
|
||||||
|
|
||||||
hr.write_full_snapshot = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
read_snapshot :: proc() -> os.Error {
|
|
||||||
assert(hr.region_headers != nil, "[hot-reload] uninitialized")
|
|
||||||
assert(hr.regions != nil, "[hot-reload] uninitialized")
|
|
||||||
|
|
||||||
if !os.exists(hr.snapshot_path) {
|
|
||||||
hr.write_full_snapshot = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot := os.open(hr.snapshot_path, {.Read}) or_return
|
|
||||||
defer os.close(snapshot)
|
|
||||||
|
|
||||||
offset: i64
|
|
||||||
|
|
||||||
// Read snapshot header
|
|
||||||
snapshot_header: Snapshot_Header
|
|
||||||
header_bytes := mem.ptr_to_bytes(&snapshot_header)
|
|
||||||
os.read_at(snapshot, header_bytes, offset) or_return
|
|
||||||
offset += size_of(Snapshot_Header)
|
|
||||||
assert(
|
|
||||||
snapshot_header.magic_string == MAGIC_STRING,
|
|
||||||
"[hot-reload] unknown filetype",
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
snapshot_header.version == HOT_RELOAD_VERSION,
|
|
||||||
"[hot-reload] incompatible snapshot version",
|
|
||||||
)
|
|
||||||
|
|
||||||
region_header: Region_Header
|
|
||||||
for i in 0 ..< snapshot_header.region_count {
|
|
||||||
|
|
||||||
// Read region header
|
|
||||||
header_bytes := mem.ptr_to_bytes(®ion_header)
|
|
||||||
os.read_at(snapshot, header_bytes, offset) or_return
|
|
||||||
offset += size_of(Region_Header)
|
|
||||||
|
|
||||||
// Read region name
|
|
||||||
name_bytes := make([]byte, region_header.name_size, hr.allocator)
|
|
||||||
defer delete(name_bytes, hr.allocator)
|
|
||||||
os.read_at(snapshot, name_bytes, offset) or_return
|
|
||||||
offset += cast(i64)region_header.name_size
|
|
||||||
name := transmute(string)name_bytes[:]
|
|
||||||
|
|
||||||
// Read region data
|
|
||||||
data_bytes := make([]byte, region_header.data_size, hr.allocator)
|
|
||||||
defer delete(data_bytes, hr.allocator)
|
|
||||||
os.read_at(snapshot, data_bytes, offset) or_return
|
|
||||||
offset += cast(i64)region_header.data_size
|
|
||||||
|
|
||||||
// Compare currently registered region with the loaded region
|
|
||||||
key := fmt.tprintf("%v:%d", name, region_header.data_size)
|
|
||||||
new_region_header, ok := hr.region_headers[key]
|
|
||||||
if !ok || region_header.data_size != new_region_header.data_size {
|
|
||||||
hr.write_full_snapshot = true
|
|
||||||
fmt.println("[hot-reload] bail loading region:", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create digest and store in registered regions
|
|
||||||
digest := hash.hash_bytes(
|
|
||||||
hash.Algorithm.SHA256,
|
|
||||||
data_bytes,
|
|
||||||
hr.allocator,
|
|
||||||
)
|
|
||||||
region := &hr.regions[key]
|
|
||||||
region.digest = digest
|
|
||||||
|
|
||||||
// Write snapshot memory to the memory region
|
|
||||||
dest := mem.byte_slice(
|
|
||||||
region.ptr_location,
|
|
||||||
cast(int)region_header.data_size,
|
|
||||||
)
|
|
||||||
copy(dest, data_bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next write will be a full memory snapshot
|
|
||||||
set_write_full_snapshot :: proc() {
|
|
||||||
hr.write_full_snapshot = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write memory snapshot, save on writes by diff-ing
|
|
||||||
write_snapshot :: proc() -> os.Error {
|
|
||||||
assert(hr.region_headers != nil, "[hot-reload] uninitialized")
|
|
||||||
assert(hr.regions != nil, "[hot-reload] uninitialized")
|
|
||||||
|
|
||||||
snapshot: ^os.File
|
|
||||||
if hr.write_full_snapshot || !os.exists(hr.snapshot_path) {
|
|
||||||
snapshot = os.create(hr.snapshot_path) or_return
|
|
||||||
} else {
|
|
||||||
snapshot = os.open(hr.snapshot_path, {.Read, .Write}) or_return
|
|
||||||
}
|
|
||||||
defer os.close(snapshot)
|
|
||||||
|
|
||||||
offset: i64
|
|
||||||
|
|
||||||
// Write header
|
|
||||||
if hr.write_full_snapshot {
|
|
||||||
header_bytes := mem.ptr_to_bytes(&hr.snapshot_header)
|
|
||||||
n := os.write_at(snapshot, header_bytes, offset) or_return
|
|
||||||
}
|
|
||||||
offset += size_of(Snapshot_Header)
|
|
||||||
|
|
||||||
for key, ®ion in hr.regions {
|
|
||||||
header := hr.region_headers[key]
|
|
||||||
|
|
||||||
// Write region header
|
|
||||||
if hr.write_full_snapshot {
|
|
||||||
header_bytes := mem.ptr_to_bytes(&header)
|
|
||||||
os.write_at(snapshot, header_bytes, offset) or_return
|
|
||||||
}
|
|
||||||
offset += size_of(Region_Header)
|
|
||||||
|
|
||||||
// Write region name
|
|
||||||
if hr.write_full_snapshot {
|
|
||||||
name := key[:header.name_size] // get name from first part of the hash
|
|
||||||
name_bytes := transmute([]byte)name
|
|
||||||
os.write_at(snapshot, name_bytes, offset) or_return
|
|
||||||
}
|
|
||||||
offset += cast(i64)header.name_size
|
|
||||||
|
|
||||||
// Write region data (if bytes differ)
|
|
||||||
data_bytes := mem.byte_slice(region.ptr_location, header.data_size)
|
|
||||||
digest := hash.hash_bytes(
|
|
||||||
hash.Algorithm.SHA256,
|
|
||||||
data_bytes,
|
|
||||||
hr.allocator,
|
|
||||||
)
|
|
||||||
if hr.write_full_snapshot || mem.compare(digest, region.digest) != 0 {
|
|
||||||
delete(region.digest, hr.allocator)
|
|
||||||
region.digest = digest
|
|
||||||
os.write_at(snapshot, data_bytes, offset) or_return
|
|
||||||
} else {
|
|
||||||
delete(digest, hr.allocator)
|
|
||||||
}
|
|
||||||
offset += cast(i64)header.data_size
|
|
||||||
}
|
|
||||||
|
|
||||||
hr.write_full_snapshot = false
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
force_reload :: proc() {
|
|
||||||
should_close = true
|
|
||||||
}
|
|
||||||
|
|
||||||
force_restart :: proc() -> os.Error {
|
|
||||||
should_close = true
|
|
||||||
os.remove(hr.snapshot_path) or_return
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import "sindri:base"
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Types
|
|
||||||
|
|
||||||
Game_API :: struct {
|
|
||||||
settings: proc() -> base.Settings,
|
|
||||||
init_once: proc(),
|
|
||||||
init: proc(),
|
|
||||||
update: proc(dt: f32),
|
|
||||||
render: proc(),
|
|
||||||
destroy: proc(),
|
|
||||||
should_close: proc() -> bool,
|
|
||||||
force_reload: proc() -> bool,
|
|
||||||
force_restart: proc() -> bool,
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import "sindri:base"
|
|
||||||
import "sindri:platform"
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Public functions
|
|
||||||
|
|
||||||
// Returns the state of the keyboard key
|
|
||||||
key_state :: proc(key: base.Key) -> base.Action {
|
|
||||||
return platform.os_key_state(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the state of the mouse button
|
|
||||||
mouse_button_state :: proc(button: base.Mouse_Button) -> base.Action {
|
|
||||||
return platform.os_mouse_button_state(button)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the position of the mouse cursor
|
|
||||||
mouse_position :: proc() -> (x_pos: f32, y_pos: f32) {
|
|
||||||
return platform.os_mouse_position()
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Private functions
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package base
|
package core
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
|
||||||
|
import "sindri:input"
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// Types
|
||||||
|
|
||||||
|
// event category, bitfield (?)
|
||||||
|
|
||||||
|
// Event_Data :: union {
|
||||||
|
Event :: union {
|
||||||
|
Window_Close_Event,
|
||||||
|
Window_Resize_Event,
|
||||||
|
Joystick_Connect_Event,
|
||||||
|
Joystick_Disconnect_Event,
|
||||||
|
Key_Press_Event,
|
||||||
|
Key_Release_Event,
|
||||||
|
Key_Repeat_Event,
|
||||||
|
Mouse_Button_Press_Event,
|
||||||
|
Mouse_Button_Release_Event,
|
||||||
|
Mouse_Position_Event,
|
||||||
|
Mouse_Scroll_Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event :: struct {
|
||||||
|
// data: Event_Data,
|
||||||
|
// handled: bool,
|
||||||
|
// }
|
||||||
|
|
||||||
|
Window_Close_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
Window_Resize_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Joystick_Connect_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Joystick_Disconnect_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Key_Press_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
key: input.Key,
|
||||||
|
mods: input.Mod_Set,
|
||||||
|
}
|
||||||
|
|
||||||
|
Key_Release_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
key: input.Key,
|
||||||
|
mods: input.Mod_Set,
|
||||||
|
}
|
||||||
|
|
||||||
|
Key_Repeat_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
key: input.Key,
|
||||||
|
mods: input.Mod_Set,
|
||||||
|
}
|
||||||
|
|
||||||
|
Mouse_Button_Press_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
button: input.Mouse_Button,
|
||||||
|
mods: input.Mod_Set,
|
||||||
|
}
|
||||||
|
|
||||||
|
Mouse_Button_Release_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
button: input.Mouse_Button,
|
||||||
|
mods: input.Mod_Set,
|
||||||
|
}
|
||||||
|
|
||||||
|
Mouse_Position_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
x_pos: f32,
|
||||||
|
y_pos: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Mouse_Scroll_Event :: struct {
|
||||||
|
handled: bool,
|
||||||
|
x_offset: f32,
|
||||||
|
y_offset: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
// I want to have a list of Listeners per Event type, but this is state
|
||||||
|
// listeners: map[typeid][dynamic]Listener
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// Public functions
|
||||||
|
|
||||||
|
// Dispatcher(Event) -> forwards to the right function
|
||||||
|
on_event :: proc(event: Event) {
|
||||||
|
fmt.println("toby!")
|
||||||
|
|
||||||
|
#partial switch e in event {
|
||||||
|
case Window_Close_Event:
|
||||||
|
fmt.println("Close")
|
||||||
|
case Window_Resize_Event:
|
||||||
|
fmt.println("Resize:", e.width, e.height)
|
||||||
|
|
||||||
|
case Key_Press_Event:
|
||||||
|
fmt.println("Key press", e.key, e.mods)
|
||||||
|
case Key_Release_Event:
|
||||||
|
fmt.println("Key release", e.key, e.mods)
|
||||||
|
case Key_Repeat_Event:
|
||||||
|
fmt.println("Key repeat", e.key, e.mods)
|
||||||
|
|
||||||
|
case Mouse_Button_Press_Event:
|
||||||
|
fmt.println("Mouse press", e.button, e.mods)
|
||||||
|
case Mouse_Button_Release_Event:
|
||||||
|
fmt.println("Mouse release", e.button, e.mods)
|
||||||
|
case Mouse_Position_Event:
|
||||||
|
fmt.printfln("Mouse pos: {}x{}", e.x_pos, e.y_pos)
|
||||||
|
|
||||||
|
case Joystick_Connect_Event:
|
||||||
|
fmt.println("Joy con:", e.id)
|
||||||
|
case Joystick_Disconnect_Event:
|
||||||
|
fmt.println("Joy dis:", e.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
Have the events queued up into a frame buffer that gets drained at a specific time.
|
||||||
|
*/
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package base
|
package input
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
|
||||||
+20
-47
@@ -2,9 +2,9 @@ package sindri
|
|||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
|
import "sindri:test"
|
||||||
|
|
||||||
import "game:game"
|
import "sindri:hot_reload"
|
||||||
import "sindri:base"
|
|
||||||
import "sindri:platform"
|
import "sindri:platform"
|
||||||
|
|
||||||
VERSION :: #config(VERSION, "dev")
|
VERSION :: #config(VERSION, "dev")
|
||||||
@@ -12,27 +12,15 @@ VERSION :: #config(VERSION, "dev")
|
|||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
fmt.println("hello world!", VERSION)
|
fmt.println("hello world!")
|
||||||
|
|
||||||
// Hot reload development functionality
|
// Hot reload development functionality
|
||||||
base.hot_reload_init(VERSION)
|
hr, err := hot_reload.hot_reload_init()
|
||||||
defer base.hot_reload_destroy()
|
defer hot_reload.hot_reload_destroy(&hr)
|
||||||
|
|
||||||
// Load game API & settings
|
api := hot_reload.active_lib(&hr)
|
||||||
api := game.api()
|
|
||||||
defer api.destroy()
|
|
||||||
settings := api.settings()
|
settings := api.settings()
|
||||||
|
|
||||||
// Event system
|
|
||||||
base.event_bus_init()
|
|
||||||
defer base.event_bus_destroy()
|
|
||||||
|
|
||||||
base.event_subscribe(base.Key_Press_Event, test)
|
|
||||||
|
|
||||||
// Listen for OS signals (SIGINT, SIGTERM)
|
|
||||||
platform.signals_init()
|
|
||||||
defer platform.signals_destroy()
|
|
||||||
|
|
||||||
// Initialize Window
|
// Initialize Window
|
||||||
platform.os_init(settings)
|
platform.os_init(settings)
|
||||||
platform.os_set_monitor(settings)
|
platform.os_set_monitor(settings)
|
||||||
@@ -43,44 +31,29 @@ main :: proc() {
|
|||||||
defer platform.instance_destroy()
|
defer platform.instance_destroy()
|
||||||
|
|
||||||
api.init_once()
|
api.init_once()
|
||||||
|
|
||||||
base.read_snapshot()
|
|
||||||
|
|
||||||
api.init()
|
api.init()
|
||||||
|
|
||||||
dt: f64
|
gt: f64 = 0
|
||||||
gt: f64
|
dt: f32
|
||||||
st: f64
|
|
||||||
|
|
||||||
for !api.should_close() &&
|
|
||||||
!platform.os_should_close() &&
|
|
||||||
!base.should_close {
|
|
||||||
|
|
||||||
|
for !platform.os_should_close() && !hot_reload.should_close(&hr) {
|
||||||
start := time.tick_now()
|
start := time.tick_now()
|
||||||
|
|
||||||
platform.os_poll_events()
|
platform.os_poll_events()
|
||||||
base.drain_event_queue()
|
api.update(dt)
|
||||||
api.update(cast(f32)dt)
|
|
||||||
base.write_snapshot()
|
|
||||||
|
|
||||||
platform.frame()
|
fmt.println("MAIN:", test.test)
|
||||||
api.render()
|
fmt.printf("pointer: %p | %p\n", &test.test, &test.test_proc)
|
||||||
|
|
||||||
dt = time.duration_seconds(time.tick_since(start))
|
platform.frame(dt)
|
||||||
gt += dt
|
|
||||||
st += dt
|
|
||||||
|
|
||||||
if st > 5 {
|
dt = f32(time.duration_seconds(time.tick_since(start)))
|
||||||
st -= 5
|
gt += f64(dt)
|
||||||
base.set_write_full_snapshot()
|
|
||||||
|
// Hot reload
|
||||||
|
api, err = hot_reload.reload_game_lib(&hr)
|
||||||
|
if err != nil do break
|
||||||
}
|
}
|
||||||
|
|
||||||
if api.force_reload() do base.force_reload()
|
api.destroy()
|
||||||
if api.force_restart() do base.force_restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test :: proc(e: ^base.Key_Press_Event, _: rawptr) -> bool {
|
|
||||||
fmt.println("main: @@@@@@@@@@@", e)
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-46
@@ -1,6 +1,5 @@
|
|||||||
package platform
|
package platform
|
||||||
|
|
||||||
import "base:intrinsics"
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "vendor:glfw"
|
import "vendor:glfw"
|
||||||
@@ -8,7 +7,9 @@ import "vendor:glfw"
|
|||||||
import "wgpu:wgpu"
|
import "wgpu:wgpu"
|
||||||
import "wgpu:wgpu/glfwglue"
|
import "wgpu:wgpu/glfwglue"
|
||||||
|
|
||||||
import "sindri:base"
|
import "sindri:core"
|
||||||
|
import "sindri:event"
|
||||||
|
import "sindri:input"
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// Types
|
// Types
|
||||||
@@ -20,7 +21,7 @@ OS :: struct {
|
|||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// Constructor / destructor
|
// Constructor / destructor
|
||||||
|
|
||||||
os_init :: proc(settings: base.Settings) {
|
os_init :: proc(settings: core.Settings) {
|
||||||
if !glfw.Init() {
|
if !glfw.Init() {
|
||||||
panic("[glfw] init failure")
|
panic("[glfw] init failure")
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,11 @@ os_init :: proc(settings: base.Settings) {
|
|||||||
glfw.SetCursorPosCallback(state.os.window, cursor_pos_callback)
|
glfw.SetCursorPosCallback(state.os.window, cursor_pos_callback)
|
||||||
glfw.SetScrollCallback(state.os.window, scroll_callback)
|
glfw.SetScrollCallback(state.os.window, scroll_callback)
|
||||||
glfw.SetJoystickCallback(joystick_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() {
|
os_destroy :: proc() {
|
||||||
@@ -57,7 +63,7 @@ os_destroy :: proc() {
|
|||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// Public functions
|
// Public functions
|
||||||
|
|
||||||
os_set_monitor :: proc(settings: base.Settings) {
|
os_set_monitor :: proc(settings: core.Settings) {
|
||||||
monitor := glfw.GetPrimaryMonitor()
|
monitor := glfw.GetPrimaryMonitor()
|
||||||
x_pos: i32
|
x_pos: i32
|
||||||
y_pos: i32
|
y_pos: i32
|
||||||
@@ -114,8 +120,7 @@ os_set_callback_proc :: proc(window: rawptr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
os_should_close :: proc() -> bool {
|
os_should_close :: proc() -> bool {
|
||||||
window_close := bool(glfw.WindowShouldClose(state.os.window))
|
return bool(glfw.WindowShouldClose(state.os.window))
|
||||||
return window_close || intrinsics.atomic_load(&quit_signal_received)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
os_set_should_close :: proc(val: bool) {
|
os_set_should_close :: proc(val: bool) {
|
||||||
@@ -135,44 +140,47 @@ os_get_surface :: proc(instance: wgpu.Instance) -> wgpu.Surface {
|
|||||||
return glfwglue.GetSurface(instance, state.os.window)
|
return glfwglue.GetSurface(instance, state.os.window)
|
||||||
}
|
}
|
||||||
|
|
||||||
os_key_state :: proc(key: base.Key) -> base.Action {
|
|
||||||
return input_action(glfw.GetKey(state.os.window, i32(key)))
|
|
||||||
}
|
|
||||||
|
|
||||||
os_mouse_button_state :: proc(button: base.Mouse_Button) -> base.Action {
|
|
||||||
return input_action(glfw.GetMouseButton(state.os.window, i32(button)))
|
|
||||||
}
|
|
||||||
|
|
||||||
os_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 functions
|
// Private functions
|
||||||
|
|
||||||
@(private = "file")
|
@(private = "file")
|
||||||
input_action :: proc(action: i32) -> base.Action {
|
key_state :: proc(key: input.Key) -> input.Action {
|
||||||
if action == glfw.RELEASE do return base.Action.Release
|
return input_action(glfw.GetKey(state.os.window, i32(key)))
|
||||||
else if action == glfw.PRESS do return base.Action.Press
|
}
|
||||||
else if action == glfw.REPEAT do return base.Action.Repeat
|
|
||||||
|
@(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")
|
when ODIN_DEBUG do panic("[glfw] unknown action")
|
||||||
return .None
|
return .None
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private = "file")
|
@(private = "file")
|
||||||
input_key :: proc(key: i32) -> base.Key {
|
input_key :: proc(key: i32) -> input.Key {
|
||||||
return base.Key(key) // values match
|
return input.Key(key) // values match
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private = "file")
|
@(private = "file")
|
||||||
input_mod_set :: proc(mods: i32) -> (set: base.Mod_Set) {
|
input_mod_set :: proc(mods: i32) -> (set: input.Mod_Set) {
|
||||||
return transmute(base.Mod_Set)i8(mods & 0x3f) // values match bit position
|
return transmute(input.Mod_Set)i8(mods & 0x3f) // values match bit position
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private = "file")
|
@(private = "file")
|
||||||
input_mouse_button :: proc(button: i32) -> base.Mouse_Button {
|
input_mouse_button :: proc(button: i32) -> input.Mouse_Button {
|
||||||
return base.Mouse_Button(button) // values match
|
return input.Mouse_Button(button) // values match
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error callback
|
// Error callback
|
||||||
@@ -186,7 +194,7 @@ error_callback :: proc "c" (error: i32, description: cstring) {
|
|||||||
@(private = "file")
|
@(private = "file")
|
||||||
window_close_callback :: proc "c" (window: glfw.WindowHandle) {
|
window_close_callback :: proc "c" (window: glfw.WindowHandle) {
|
||||||
context = state.ctx
|
context = state.ctx
|
||||||
base.on_event(base.Window_Close_Event{})
|
event.on_event(event.Window_Close_Event{})
|
||||||
} // GLFWwindowclosefun
|
} // GLFWwindowclosefun
|
||||||
|
|
||||||
// Window resize callback
|
// Window resize callback
|
||||||
@@ -203,24 +211,24 @@ key_callback :: proc "c" (
|
|||||||
) {
|
) {
|
||||||
context = state.ctx
|
context = state.ctx
|
||||||
if action == glfw.PRESS {
|
if action == glfw.PRESS {
|
||||||
base.on_event(
|
event.on_event(
|
||||||
base.Key_Press_Event {
|
event.Key_Press_Event {
|
||||||
key = input_key(key),
|
key = input_key(key),
|
||||||
mods = input_mod_set(mods),
|
mods = input_mod_set(mods),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if action == glfw.RELEASE {
|
if action == glfw.RELEASE {
|
||||||
base.on_event(
|
event.on_event(
|
||||||
base.Key_Release_Event {
|
event.Key_Release_Event {
|
||||||
key = input_key(key),
|
key = input_key(key),
|
||||||
mods = input_mod_set(mods),
|
mods = input_mod_set(mods),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if action == glfw.REPEAT {
|
if action == glfw.REPEAT {
|
||||||
base.on_event(
|
event.on_event(
|
||||||
base.Key_Repeat_Event {
|
event.Key_Repeat_Event {
|
||||||
key = input_key(key),
|
key = input_key(key),
|
||||||
mods = input_mod_set(mods),
|
mods = input_mod_set(mods),
|
||||||
},
|
},
|
||||||
@@ -236,16 +244,16 @@ mouse_button_callback :: proc "c" (
|
|||||||
) {
|
) {
|
||||||
context = state.ctx
|
context = state.ctx
|
||||||
if action == glfw.PRESS {
|
if action == glfw.PRESS {
|
||||||
base.on_event(
|
event.on_event(
|
||||||
base.Mouse_Button_Press_Event {
|
event.Mouse_Button_Press_Event {
|
||||||
button = input_mouse_button(button),
|
button = input_mouse_button(button),
|
||||||
mods = input_mod_set(mods),
|
mods = input_mod_set(mods),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if action == glfw.RELEASE {
|
if action == glfw.RELEASE {
|
||||||
base.on_event(
|
event.on_event(
|
||||||
base.Mouse_Button_Release_Event {
|
event.Mouse_Button_Release_Event {
|
||||||
button = input_mouse_button(button),
|
button = input_mouse_button(button),
|
||||||
mods = input_mod_set(mods),
|
mods = input_mod_set(mods),
|
||||||
},
|
},
|
||||||
@@ -260,8 +268,8 @@ cursor_pos_callback :: proc "c" (
|
|||||||
x_pos, y_pos: f64,
|
x_pos, y_pos: f64,
|
||||||
) {
|
) {
|
||||||
context = state.ctx
|
context = state.ctx
|
||||||
base.on_event(
|
event.on_event(
|
||||||
base.Mouse_Position_Event{x_pos = f32(x_pos), y_pos = f32(y_pos)},
|
event.Mouse_Position_Event{x_pos = f32(x_pos), y_pos = f32(y_pos)},
|
||||||
)
|
)
|
||||||
} // GLFWcursorposfun
|
} // GLFWcursorposfun
|
||||||
|
|
||||||
@@ -272,8 +280,8 @@ scroll_callback :: proc "c" (
|
|||||||
x_offset, y_offset: f64,
|
x_offset, y_offset: f64,
|
||||||
) {
|
) {
|
||||||
context = state.ctx
|
context = state.ctx
|
||||||
base.on_event(
|
event.on_event(
|
||||||
base.Mouse_Scroll_Event {
|
event.Mouse_Scroll_Event {
|
||||||
x_offset = f32(x_offset),
|
x_offset = f32(x_offset),
|
||||||
y_offset = f32(y_offset),
|
y_offset = f32(y_offset),
|
||||||
},
|
},
|
||||||
@@ -285,9 +293,9 @@ scroll_callback :: proc "c" (
|
|||||||
joystick_callback :: proc "c" (id, connected: i32) {
|
joystick_callback :: proc "c" (id, connected: i32) {
|
||||||
context = state.ctx
|
context = state.ctx
|
||||||
if connected == glfw.CONNECTED {
|
if connected == glfw.CONNECTED {
|
||||||
base.on_event(base.Joystick_Connect_Event{id = id})
|
event.on_event(event.Joystick_Connect_Event{id = id})
|
||||||
} else {
|
} else {
|
||||||
base.on_event(base.Joystick_Disconnect_Event{id = id})
|
event.on_event(event.Joystick_Disconnect_Event{id = id})
|
||||||
}
|
}
|
||||||
} // GLFWjoystickfun
|
} // GLFWjoystickfun
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package platform
|
|
||||||
|
|
||||||
import "base:intrinsics"
|
|
||||||
import "core:c/libc"
|
|
||||||
import "core:fmt"
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
// Read with atomic_load, safe across threads / interrupt contexts
|
|
||||||
quit_signal_received: bool
|
|
||||||
|
|
||||||
// -----------------------------------------
|
|
||||||
// Public functions
|
|
||||||
|
|
||||||
// Register signal handlers
|
|
||||||
signals_init :: proc() {
|
|
||||||
// SIGINT = Interrupt, typically Ctrl+C
|
|
||||||
if libc.signal(libc.SIGINT, signal_handler) ==
|
|
||||||
cast(proc "c" (_: i32))libc.SIG_ERR {
|
|
||||||
fmt.eprintln("error: failed to register SIGINT handler")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SIGTERM = Normal termination
|
|
||||||
if libc.signal(libc.SIGTERM, signal_handler) ==
|
|
||||||
cast(proc "c" (_: i32))libc.SIG_ERR {
|
|
||||||
fmt.eprintln("error: failed to register SIGTERM handler")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore default signal handlers
|
|
||||||
signals_destroy :: proc() {
|
|
||||||
libc.signal(libc.SIGINT, cast(proc "c" (_: i32))libc.SIG_DFL)
|
|
||||||
libc.signal(libc.SIGTERM, cast(proc "c" (_: i32))libc.SIG_DFL)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(private = "file")
|
|
||||||
signal_handler :: proc "c" (sig: i32) {
|
|
||||||
switch sig {
|
|
||||||
case libc.SIGINT:
|
|
||||||
fallthrough
|
|
||||||
case libc.SIGTERM:
|
|
||||||
intrinsics.atomic_store(&quit_signal_received, true)
|
|
||||||
case:
|
|
||||||
context = state.ctx
|
|
||||||
panic("[signal] unhandled signal type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import "core:slice"
|
|||||||
|
|
||||||
import "wgpu:wgpu"
|
import "wgpu:wgpu"
|
||||||
|
|
||||||
import "sindri:base"
|
import "sindri:core"
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// Variables
|
// Variables
|
||||||
@@ -33,7 +33,7 @@ state: struct {
|
|||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// Constructor/destructor
|
// Constructor/destructor
|
||||||
|
|
||||||
instance_init :: proc(settings: base.Settings) {
|
instance_init :: proc(settings: core.Settings) {
|
||||||
state.ctx = context
|
state.ctx = context
|
||||||
|
|
||||||
wgpu.SetLogCallback(log_callback, nil)
|
wgpu.SetLogCallback(log_callback, nil)
|
||||||
@@ -96,7 +96,7 @@ resize :: proc "c" () {
|
|||||||
wgpu.SurfaceConfigure(state.surface, &state.config)
|
wgpu.SurfaceConfigure(state.surface, &state.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
frame :: proc "c" () {
|
frame :: proc "c" (dt: f32) {
|
||||||
context = state.ctx
|
context = state.ctx
|
||||||
|
|
||||||
surface_texture := wgpu.SurfaceGetCurrentTexture(state.surface)
|
surface_texture := wgpu.SurfaceGetCurrentTexture(state.surface)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
test: int = 0
|
||||||
|
|
||||||
|
test_proc: proc()
|
||||||
Vendored
-1
Submodule vendor/brokkr deleted from 154ced730e
Reference in New Issue
Block a user