Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6feb0c1575 | ||
|
|
148b511009 | ||
|
|
af063209ff | ||
|
|
163aa3052b | ||
|
|
426504c748 | ||
|
|
30de42caca | ||
|
|
31005e0861 | ||
|
|
d12118e80a | ||
|
|
8b30fa4e7c | ||
|
|
3549b0be95 |
@@ -0,0 +1,55 @@
|
||||
# Simple textured cube
|
||||
# 8 vertices, 6 faces (12 triangles), per-face UVs
|
||||
|
||||
o Cube
|
||||
|
||||
# Positions
|
||||
v -1.0 -1.0 1.0
|
||||
v 1.0 -1.0 1.0
|
||||
v 1.0 1.0 1.0
|
||||
v -1.0 1.0 1.0
|
||||
v -1.0 -1.0 -1.0
|
||||
v 1.0 -1.0 -1.0
|
||||
v 1.0 1.0 -1.0
|
||||
v -1.0 1.0 -1.0
|
||||
|
||||
# Texture coordinates
|
||||
vt 0.0 0.0
|
||||
vt 1.0 0.0
|
||||
vt 1.0 1.0
|
||||
vt 0.0 1.0
|
||||
|
||||
# Normals
|
||||
vn 0.0 0.0 1.0
|
||||
vn 0.0 0.0 -1.0
|
||||
vn 1.0 0.0 0.0
|
||||
vn -1.0 0.0 0.0
|
||||
vn 0.0 1.0 0.0
|
||||
vn 0.0 -1.0 0.0
|
||||
|
||||
# Faces (front, back, right, left, top, bottom)
|
||||
# Each face uses two triangles: (0,1,2) and (0,2,3)
|
||||
|
||||
# Front (+Z)
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
f 1/1/1 3/3/1 4/4/1
|
||||
|
||||
# Back (-Z)
|
||||
f 6/1/2 5/2/2 8/3/2
|
||||
f 6/1/2 8/3/2 7/4/2
|
||||
|
||||
# Right (+X)
|
||||
f 2/1/3 6/2/3 7/3/3
|
||||
f 2/1/3 7/3/3 3/4/3
|
||||
|
||||
# Left (-X)
|
||||
f 5/1/4 1/2/4 4/3/4
|
||||
f 5/1/4 4/3/4 8/4/4
|
||||
|
||||
# Top (+Y)
|
||||
f 4/1/5 3/2/5 7/3/5
|
||||
f 4/1/5 7/3/5 8/4/5
|
||||
|
||||
# Bottom (-Y)
|
||||
f 5/1/6 6/2/6 2/3/6
|
||||
f 5/1/6 2/3/6 1/4/6
|
||||
+2027
File diff suppressed because it is too large
Load Diff
+34
-29
@@ -32,8 +32,6 @@ PROJECT_NAME :: "sindri"
|
||||
BUILD_DIRECTORY :: "out/"
|
||||
|
||||
GAME_SRC_DIR :: "game/"
|
||||
GAME_LIB_PATH :: BUILD_DIRECTORY + "game" + LIB_EXT
|
||||
GAME_LIB_TMP_PATH :: BUILD_DIRECTORY + "game_tmp" + LIB_EXT
|
||||
|
||||
ENGINE_DIR :: "src/"
|
||||
ENGINE_OUTPUT :: BUILD_DIRECTORY + PROJECT_NAME
|
||||
@@ -47,6 +45,7 @@ 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."`,
|
||||
@@ -77,6 +76,7 @@ main :: proc() {
|
||||
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()
|
||||
@@ -91,17 +91,12 @@ main :: proc() {
|
||||
// Build project
|
||||
|
||||
// Build the game library ('game/') and the engine ('src/')
|
||||
build_project :: proc(debug: bool, odin_extra_args: []string) {
|
||||
build_project :: proc(debug: bool, extra_args: []string) {
|
||||
version := project_version()
|
||||
|
||||
// Setup compiled wgpu binary
|
||||
wgpu_init()
|
||||
|
||||
// ----------------------------------------
|
||||
// Game compile
|
||||
|
||||
build_game(debug, version, odin_extra_args)
|
||||
|
||||
// ----------------------------------------
|
||||
// Engine compile
|
||||
|
||||
@@ -111,7 +106,7 @@ build_project :: proc(debug: bool, odin_extra_args: []string) {
|
||||
return
|
||||
}
|
||||
|
||||
build_engine(debug, version, odin_extra_args)
|
||||
build_engine(debug, version, extra_args)
|
||||
}
|
||||
|
||||
// Shared builder between game/engine
|
||||
@@ -124,6 +119,7 @@ build_common :: proc(
|
||||
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")
|
||||
@@ -144,24 +140,6 @@ build_common :: proc(
|
||||
return b
|
||||
}
|
||||
|
||||
// Build the game as a dynamic library loaded by the engine
|
||||
build_game :: proc(debug: bool, version: string, extra_args: []string) {
|
||||
b := build_common(GAME_SRC_DIR, debug, version, extra_args)
|
||||
defer brokkr.builder_destroy(&b)
|
||||
brokkr.mode(&b, .Dynamic)
|
||||
// Use a temp file because it first writes an empty file, which the engine
|
||||
// would load before it is actually fully written
|
||||
brokkr.out(&b, GAME_LIB_TMP_PATH)
|
||||
if err := brokkr.execute(&b); err != nil {
|
||||
os.exit(1)
|
||||
}
|
||||
|
||||
if !os.is_file(GAME_LIB_TMP_PATH) {
|
||||
die("build produced no game lib %q", GAME_LIB_TMP_PATH)
|
||||
}
|
||||
os.rename(GAME_LIB_TMP_PATH, GAME_LIB_PATH)
|
||||
}
|
||||
|
||||
// Build the engine executable
|
||||
build_engine :: proc(debug: bool, version: string, extra_args: []string) {
|
||||
b := build_common(ENGINE_DIR, debug, version, extra_args)
|
||||
@@ -174,8 +152,7 @@ build_engine :: proc(debug: bool, version: string, extra_args: []string) {
|
||||
|
||||
run_project :: proc(extra_args: []string) {
|
||||
// Build project if it wasnt already
|
||||
if !os.exists(ENGINE_OUTPUT) ||
|
||||
!os.exists(GAME_LIB_PATH) { build_project(true, {}) }
|
||||
if !os.exists(output_path(ENGINE_OUTPUT)) do build_project(true, {})
|
||||
|
||||
cmd := []string{output_path(ENGINE_OUTPUT)}
|
||||
line := slice.concatenate(
|
||||
@@ -206,6 +183,34 @@ run_project :: proc(extra_args: []string) {
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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)
|
||||
```
|
||||
+35
-30
@@ -7,39 +7,42 @@ import "sindri:base"
|
||||
import "sindri:core"
|
||||
|
||||
// -----------------------------------------
|
||||
// Types
|
||||
|
||||
Game_Memory :: struct {
|
||||
should_close: bool,
|
||||
}
|
||||
|
||||
g: ^Game_Memory
|
||||
Test_Memory :: struct {
|
||||
asd: int,
|
||||
qwe: int,
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Variables
|
||||
|
||||
@(export)
|
||||
memory :: proc() -> rawptr {
|
||||
return g
|
||||
g: Game_Memory
|
||||
t: Test_Memory
|
||||
|
||||
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)
|
||||
memory_free :: proc() {
|
||||
free(g)
|
||||
// -----------------------------------------
|
||||
// Public functions
|
||||
|
||||
api :: proc() -> ^core.Game_API {
|
||||
return &game_api
|
||||
}
|
||||
|
||||
@(export)
|
||||
memory_size :: proc() -> int {
|
||||
return size_of(Game_Memory)
|
||||
}
|
||||
|
||||
@(export)
|
||||
memory_set :: proc(mem: rawptr) {
|
||||
g = (^Game_Memory)(mem)
|
||||
|
||||
// Here you can also set your own global variables. A good idea is to make
|
||||
// your global variables into pointers that point to something inside `g`.
|
||||
}
|
||||
|
||||
@(export)
|
||||
settings :: proc() -> base.Settings {
|
||||
return base.Settings {
|
||||
width = 960,
|
||||
@@ -51,36 +54,40 @@ settings :: proc() -> base.Settings {
|
||||
}
|
||||
}
|
||||
|
||||
@(export)
|
||||
init_once :: proc() {
|
||||
fmt.println("init once")
|
||||
|
||||
base.register_region("game_memory", &g, size_of(Game_Memory))
|
||||
base.register_region("test_memory", &t, size_of(Test_Memory))
|
||||
}
|
||||
|
||||
@(export)
|
||||
init :: proc() {
|
||||
fmt.println("hello from .dll!")
|
||||
|
||||
g = new(Game_Memory)
|
||||
memory_set(g)
|
||||
g.should_close = false
|
||||
}
|
||||
|
||||
@(export)
|
||||
update :: proc(dt: f32) {
|
||||
fmt.println("dt:", dt)
|
||||
|
||||
if core.key_state(.Key_Escape) == .Press {
|
||||
g.should_close = true
|
||||
}
|
||||
|
||||
if core.key_state(.Key_F5) == .Press do t.asd = 555
|
||||
if core.key_state(.Key_F6) == .Press do t.asd = 666
|
||||
}
|
||||
|
||||
@(export)
|
||||
render :: proc() { }
|
||||
|
||||
destroy :: proc() {
|
||||
fmt.println("bye")
|
||||
|
||||
fmt.println(t.asd, t.qwe)
|
||||
}
|
||||
|
||||
start := time.tick_now()
|
||||
|
||||
@(export)
|
||||
should_close :: proc() -> bool {
|
||||
elapsed := time.tick_since(start)
|
||||
seconds := time.duration_seconds(elapsed)
|
||||
@@ -89,12 +96,10 @@ should_close :: proc() -> bool {
|
||||
return g.should_close
|
||||
}
|
||||
|
||||
@(export)
|
||||
force_reload :: proc() -> bool {
|
||||
return core.key_state(.Key_F5) == .Press
|
||||
}
|
||||
|
||||
@(export)
|
||||
force_restart :: proc() -> bool {
|
||||
return core.key_state(.Key_F6) == .Press
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"collections": [
|
||||
{ "name": "game", "path": "." },
|
||||
{ "name": "sindri", "path": "src" },
|
||||
{ "name": "gram", "path": "vendor/gram/src" },
|
||||
{ "name": "wgpu", "path": "vendor" }
|
||||
{ "name": "wgpu", "path": "vendor" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package base
|
||||
|
||||
should_close: bool
|
||||
@@ -158,8 +158,6 @@ on_event :: proc(data: Event_Data) {
|
||||
drain_event_queue :: proc() {
|
||||
assert(event_bus.queue != nil)
|
||||
|
||||
fmt.println("toby!")
|
||||
|
||||
for &event in event_bus.queue {
|
||||
|
||||
#partial switch e in event.data {
|
||||
|
||||
+265
-194
@@ -1,231 +1,302 @@
|
||||
package base
|
||||
|
||||
import "core:c"
|
||||
import "core:dynlib"
|
||||
import "base:runtime"
|
||||
import "core:crypto/hash"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
import "core:time"
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
LIB_EXT :: ".dll"
|
||||
} else when ODIN_OS == .Darwin {
|
||||
LIB_EXT :: ".dylib"
|
||||
} else {
|
||||
LIB_EXT :: ".so"
|
||||
}
|
||||
SNAPSHOT_NAME :: "snapshot"
|
||||
SNAPSHOT_EXTENSION :: "sndr"
|
||||
|
||||
GAME_LIB_DIR :: "out/"
|
||||
GAME_LIB_PATH :: GAME_LIB_DIR + "game" + LIB_EXT
|
||||
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 {
|
||||
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() -> 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
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
@(private)
|
||||
hr: Hot_Reload
|
||||
|
||||
// -----------------------------------------
|
||||
// Constructor & destructor
|
||||
|
||||
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,
|
||||
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,
|
||||
)
|
||||
copy_lib(lib_name) or_return
|
||||
snapshot_path_cpy := strings.clone(snapshot_path, allocator)
|
||||
|
||||
// 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(),
|
||||
// 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,
|
||||
)
|
||||
} else {
|
||||
// Odin's shared libraries don't register a constructor, so the lib's
|
||||
// runtime init (@init procs like os.stdout setup) never runs on load.
|
||||
// Call it explicitly.
|
||||
// Windows doesn't need this: its dynamic build exports DllMain, which
|
||||
// the OS calls on load and which runs _startup_runtime itself.
|
||||
when ODIN_OS != .Windows {
|
||||
Entry :: #type proc "c" () -> c.int
|
||||
entry_ptr, entry_ok := dynlib.symbol_address(
|
||||
api.lib,
|
||||
"_odin_entry_point",
|
||||
)
|
||||
if entry_ok do (transmute(Entry)entry_ptr)()
|
||||
}
|
||||
}
|
||||
region := &hr.regions[key]
|
||||
region.digest = digest
|
||||
|
||||
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,
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
// Next write will be a full memory snapshot
|
||||
set_write_full_snapshot :: proc() {
|
||||
hr.write_full_snapshot = true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// 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")
|
||||
|
||||
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)
|
||||
snapshot: ^os.File
|
||||
if hr.write_full_snapshot || !os.exists(hr.snapshot_path) {
|
||||
snapshot = os.create(hr.snapshot_path) or_return
|
||||
} 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.
|
||||
snapshot = os.open(hr.snapshot_path, {.Read, .Write}) or_return
|
||||
}
|
||||
defer os.close(snapshot)
|
||||
|
||||
api.memory_free()
|
||||
offset: i64
|
||||
|
||||
for &g in hr.loaded_libs {
|
||||
unload_game_lib(&g)
|
||||
// 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
|
||||
}
|
||||
clear(&hr.loaded_libs)
|
||||
offset += size_of(Region_Header)
|
||||
|
||||
new_api.init()
|
||||
// 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
|
||||
}
|
||||
|
||||
append(&hr.loaded_libs, new_api)
|
||||
hr.write_full_snapshot = false
|
||||
|
||||
return active_lib(hr), nil
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package base
|
||||
|
||||
// -----------------------------------------
|
||||
// Types
|
||||
|
||||
Vec2 :: [2]f32
|
||||
Vec3 :: [3]f32
|
||||
Vec4 :: [4]f32
|
||||
|
||||
Mat3 :: matrix[3, 3]f32
|
||||
Mat4 :: matrix[4, 4]f32
|
||||
|
||||
Vertex :: struct #align (4) {
|
||||
pos: Vec3,
|
||||
col: Vec3,
|
||||
uv: Vec2, // texture coordinate
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package core
|
||||
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
|
||||
import "sindri:base"
|
||||
|
||||
// -----------------------------------------
|
||||
// Types
|
||||
|
||||
Model :: struct {
|
||||
vertices: []base.Vertex,
|
||||
indices: []u16,
|
||||
// Some file formats embed their texture
|
||||
// texture: ^Texture2D,
|
||||
}
|
||||
|
||||
Texture :: struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
id: u32,
|
||||
internal_format: u32,
|
||||
data_format: u32,
|
||||
data_type: u32,
|
||||
}
|
||||
|
||||
Assets :: struct {
|
||||
allocator: runtime.Allocator,
|
||||
model_list: map[string]Model,
|
||||
texture_list: map[string]Texture,
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Constructor/destructor
|
||||
|
||||
assets_init :: proc(allocator := context.allocator) -> Assets {
|
||||
return {
|
||||
allocator = allocator,
|
||||
model_list = make(map[string]Model, allocator),
|
||||
texture_list = make(map[string]Texture, allocator),
|
||||
}
|
||||
}
|
||||
|
||||
assets_destroy :: proc(a: ^Assets) {
|
||||
for _, &model in a.model_list {
|
||||
delete(model.vertices, a.allocator)
|
||||
delete(model.indices, a.allocator)
|
||||
}
|
||||
|
||||
for _, &texture in a.texture_list {
|
||||
}
|
||||
|
||||
delete(a.model_list)
|
||||
delete(a.texture_list)
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Public functions
|
||||
|
||||
load_model :: proc(a: ^Assets, path: string) -> ^Model {
|
||||
if path in a.model_list do return &a.model_list[path]
|
||||
|
||||
vertices, indices, ok := load_obj_model(path, a.allocator)
|
||||
fmt.assertf(ok, "[assets] failed to read model: %s", path)
|
||||
|
||||
a.model_list[path] = {
|
||||
vertices = vertices,
|
||||
indices = indices,
|
||||
}
|
||||
|
||||
return &a.model_list[path]
|
||||
}
|
||||
|
||||
// Batching:
|
||||
// max_vertices 60k
|
||||
// max_elements 60k
|
||||
// max_texture_slots 32, query gpu or can wgpu do a larger buffer?
|
||||
|
||||
// Scene load: loop through all models, add all model sizes together,
|
||||
// then create a vertex_buffer using the max size, storing the index somewhere.
|
||||
@@ -0,0 +1,149 @@
|
||||
package core
|
||||
|
||||
import "core:fmt"
|
||||
import "core:math/linalg"
|
||||
import "core:os"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
|
||||
import "sindri:base"
|
||||
|
||||
// Minimal OBJ loader: reads v, vt, vn and triangulates convex polygon faces.
|
||||
// Produces one GPU vertex per face corner, then centers and normalizes the
|
||||
// model so it fits roughly inside a [-1, 1] cube at the origin
|
||||
load_obj_model :: proc(
|
||||
path: string,
|
||||
allocator := context.allocator,
|
||||
) -> (
|
||||
vertices: []base.Vertex,
|
||||
indices: []u16,
|
||||
ok: bool,
|
||||
) {
|
||||
data, err := os.read_entire_file_from_path(path, allocator)
|
||||
if err != os.ERROR_NONE {
|
||||
fmt.eprintfln("[obj] failed to read model: %s", path)
|
||||
return nil, nil, false
|
||||
}
|
||||
defer delete(data)
|
||||
|
||||
positions: [dynamic]base.Vec3
|
||||
uvs: [dynamic]base.Vec2
|
||||
normals: [dynamic]base.Vec3
|
||||
temp_vertices: [dynamic]base.Vertex
|
||||
temp_indices: [dynamic]u16
|
||||
defer {
|
||||
delete(positions)
|
||||
delete(uvs)
|
||||
delete(normals)
|
||||
delete(temp_vertices)
|
||||
delete(temp_indices)
|
||||
}
|
||||
|
||||
text := string(data)
|
||||
for line in strings.split_lines_iterator(&text) {
|
||||
trimmed := strings.trim_space(line)
|
||||
if len(trimmed) == 0 || trimmed[0] == '#' do continue
|
||||
|
||||
fields := strings.fields(trimmed)
|
||||
defer delete(fields)
|
||||
if len(fields) == 0 do continue
|
||||
|
||||
switch fields[0] {
|
||||
case "v":
|
||||
if len(fields) < 4 do continue
|
||||
x, _ := strconv.parse_f32(fields[1])
|
||||
y, _ := strconv.parse_f32(fields[2])
|
||||
z, _ := strconv.parse_f32(fields[3])
|
||||
append(&positions, base.Vec3{x, y, z})
|
||||
case "vt":
|
||||
if len(fields) < 3 do continue
|
||||
u, _ := strconv.parse_f32(fields[1])
|
||||
v, _ := strconv.parse_f32(fields[2])
|
||||
append(&uvs, base.Vec2{u, v})
|
||||
case "vn":
|
||||
if len(fields) < 4 do continue
|
||||
x, _ := strconv.parse_f32(fields[1])
|
||||
y, _ := strconv.parse_f32(fields[2])
|
||||
z, _ := strconv.parse_f32(fields[3])
|
||||
append(&normals, base.Vec3{x, y, z})
|
||||
case "f":
|
||||
face_indices: [dynamic]u16
|
||||
defer delete(face_indices)
|
||||
|
||||
for i := 1; i < len(fields); i += 1 {
|
||||
token := fields[i]
|
||||
slash := strings.index_byte(token, '/')
|
||||
|
||||
vi_str := token
|
||||
vti_str := ""
|
||||
if slash != -1 {
|
||||
vi_str = token[:slash]
|
||||
slash2 := strings.index_byte(token[slash + 1:], '/')
|
||||
if slash2 != -1 {
|
||||
vti_str = token[slash + 1:slash + 1 + slash2]
|
||||
} else {
|
||||
vti_str = token[slash + 1:]
|
||||
}
|
||||
}
|
||||
|
||||
vi, _ := strconv.parse_int(vi_str)
|
||||
vi -= 1 // OBJ indices are 1-based
|
||||
if vi < 0 || vi >= len(positions) do continue
|
||||
|
||||
uv := base.Vec2{0, 0}
|
||||
if len(vti_str) > 0 {
|
||||
vti, _ := strconv.parse_int(vti_str)
|
||||
vti -= 1
|
||||
if vti >= 0 && vti < len(uvs) {
|
||||
uv = uvs[vti]
|
||||
}
|
||||
} else if vi < len(uvs) {
|
||||
uv = uvs[vi]
|
||||
}
|
||||
|
||||
append(
|
||||
&temp_vertices,
|
||||
base.Vertex {
|
||||
pos = positions[vi],
|
||||
col = {1, 1, 1},
|
||||
uv = uv,
|
||||
},
|
||||
)
|
||||
append(&face_indices, cast(u16)(len(temp_vertices) - 1))
|
||||
}
|
||||
|
||||
// Triangulate convex polygon by fanning from the first vertex
|
||||
for i := 2; i < len(face_indices); i += 1 {
|
||||
append(&temp_indices, face_indices[0])
|
||||
append(&temp_indices, face_indices[i - 1])
|
||||
append(&temp_indices, face_indices[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(temp_vertices) == 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// Center and normalize: fit the model inside a roughly [-1, 1] cube
|
||||
min_p := temp_vertices[0].pos
|
||||
max_p := temp_vertices[0].pos
|
||||
for v in temp_vertices {
|
||||
min_p = linalg.min(min_p, v.pos)
|
||||
max_p = linalg.max(max_p, v.pos)
|
||||
}
|
||||
center := (min_p + max_p) * 0.5
|
||||
size := max_p - min_p
|
||||
max_size := max(size.x, size.y, size.z)
|
||||
scale := 1.8 / max_size
|
||||
|
||||
for &v in temp_vertices {
|
||||
v.pos = (v.pos - center) * scale
|
||||
}
|
||||
|
||||
vertices = make([]base.Vertex, len(temp_vertices), allocator)
|
||||
indices = make([]u16, len(temp_indices), allocator)
|
||||
copy(vertices, temp_vertices[:])
|
||||
copy(indices, temp_indices[:])
|
||||
return vertices, indices, true
|
||||
}
|
||||
+66
-16
@@ -3,7 +3,9 @@ package sindri
|
||||
import "core:fmt"
|
||||
import "core:time"
|
||||
|
||||
import "game:game"
|
||||
import "sindri:base"
|
||||
import "sindri:core"
|
||||
import "sindri:platform"
|
||||
|
||||
VERSION :: #config(VERSION, "dev")
|
||||
@@ -14,8 +16,13 @@ main :: proc() {
|
||||
fmt.println("hello world!", VERSION)
|
||||
|
||||
// Hot reload development functionality
|
||||
hr, err := base.hot_reload_init()
|
||||
defer base.hot_reload_destroy(&hr)
|
||||
base.hot_reload_init(VERSION)
|
||||
defer base.hot_reload_destroy()
|
||||
|
||||
// Load game API & settings
|
||||
api := game.api()
|
||||
defer api.destroy()
|
||||
settings := api.settings()
|
||||
|
||||
// Event system
|
||||
base.event_bus_init()
|
||||
@@ -23,8 +30,9 @@ main :: proc() {
|
||||
|
||||
base.event_subscribe(base.Key_Press_Event, test)
|
||||
|
||||
api := base.active_lib(&hr)
|
||||
settings := api.settings()
|
||||
// Listen for OS signals (SIGINT, SIGTERM)
|
||||
platform.signals_init()
|
||||
defer platform.signals_destroy()
|
||||
|
||||
// Initialize Window
|
||||
platform.os_init(settings)
|
||||
@@ -34,34 +42,76 @@ main :: proc() {
|
||||
// Initialize GPU resources
|
||||
platform.instance_init(settings)
|
||||
defer platform.instance_destroy()
|
||||
load_test_model()
|
||||
|
||||
api.init_once()
|
||||
|
||||
base.read_snapshot()
|
||||
|
||||
api.init()
|
||||
|
||||
gt: f64 = 0
|
||||
dt: f32
|
||||
dt: f64
|
||||
gt: f64
|
||||
st: f64
|
||||
|
||||
for !api.should_close() &&
|
||||
!platform.os_should_close() &&
|
||||
!base.should_close {
|
||||
|
||||
for !platform.os_should_close() && !base.should_close(&hr) {
|
||||
start := time.tick_now()
|
||||
|
||||
platform.os_poll_events()
|
||||
base.drain_event_queue()
|
||||
api.update(dt)
|
||||
api.update(cast(f32)dt)
|
||||
base.write_snapshot()
|
||||
|
||||
platform.frame(dt)
|
||||
platform.frame()
|
||||
api.render()
|
||||
|
||||
dt = f32(time.duration_seconds(time.tick_since(start)))
|
||||
gt += f64(dt)
|
||||
dt = time.duration_seconds(time.tick_since(start))
|
||||
gt += dt
|
||||
st += dt
|
||||
|
||||
// Hot reload
|
||||
api, err = base.reload_game_lib(&hr)
|
||||
if err != nil do break
|
||||
if st > 5 {
|
||||
st -= 5
|
||||
base.set_write_full_snapshot()
|
||||
}
|
||||
|
||||
if api.force_reload() do base.force_reload()
|
||||
if api.force_restart() do base.force_restart()
|
||||
}
|
||||
|
||||
api.destroy()
|
||||
}
|
||||
|
||||
test :: proc(e: ^base.Key_Press_Event, _: rawptr) -> bool {
|
||||
fmt.println("main: @@@@@@@@@@@", e)
|
||||
return true
|
||||
}
|
||||
|
||||
// Load model from disk and upload it
|
||||
load_test_model :: proc() {
|
||||
assets := core.assets_init()
|
||||
defer core.assets_destroy(&assets)
|
||||
|
||||
cube := core.load_model(&assets, "assets/cube.obj")
|
||||
suzanne := core.load_model(&assets, "assets/suzanne.obj")
|
||||
|
||||
platform.create_material("dummy")
|
||||
platform.create_renderable(
|
||||
"assets/cube.obj",
|
||||
cube.vertices,
|
||||
cube.indices,
|
||||
0,
|
||||
)
|
||||
platform.create_renderable(
|
||||
"assets/suzanne.obj",
|
||||
suzanne.vertices,
|
||||
suzanne.indices,
|
||||
3,
|
||||
)
|
||||
platform.create_renderable(
|
||||
"assets/suzanne.obj",
|
||||
suzanne.vertices,
|
||||
suzanne.indices,
|
||||
-3,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package platform
|
||||
|
||||
import "base:intrinsics"
|
||||
import "core:fmt"
|
||||
import "core:strings"
|
||||
import "vendor:glfw"
|
||||
@@ -20,9 +21,7 @@ OS :: struct {
|
||||
// Constructor / destructor
|
||||
|
||||
os_init :: proc(settings: base.Settings) {
|
||||
if !glfw.Init() {
|
||||
panic("[glfw] init failure")
|
||||
}
|
||||
assert(cast(bool)glfw.Init(), "[glfw] init failure")
|
||||
|
||||
// Set window properties
|
||||
glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API) // do not create OpenGL context
|
||||
@@ -113,7 +112,8 @@ os_set_callback_proc :: proc(window: rawptr) {
|
||||
}
|
||||
|
||||
os_should_close :: proc() -> bool {
|
||||
return bool(glfw.WindowShouldClose(state.os.window))
|
||||
window_close := bool(glfw.WindowShouldClose(state.os.window))
|
||||
return window_close || intrinsics.atomic_load(&quit_signal_received)
|
||||
}
|
||||
|
||||
os_set_should_close :: proc(val: bool) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+697
-32
@@ -2,40 +2,103 @@ package platform
|
||||
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:math"
|
||||
import "core:math/linalg"
|
||||
import "core:os"
|
||||
import "core:slice"
|
||||
|
||||
import "wgpu:wgpu"
|
||||
|
||||
import "sindri:base"
|
||||
|
||||
// -----------------------------------------
|
||||
// Types
|
||||
|
||||
// Updated once per frame
|
||||
Camera_Uniform :: struct {
|
||||
mvp: base.Mat4,
|
||||
}
|
||||
|
||||
// Updated once per material
|
||||
Material_Uniform :: struct {
|
||||
dummy: f32,
|
||||
}
|
||||
|
||||
// Updated once per mesh
|
||||
Object_Uniform :: struct {
|
||||
transform: base.Mat4,
|
||||
}
|
||||
|
||||
Material :: struct {
|
||||
bind_group: wgpu.BindGroup,
|
||||
uniform_buffer: wgpu.Buffer,
|
||||
}
|
||||
|
||||
Object :: struct {
|
||||
name: string,
|
||||
bind_group: wgpu.BindGroup,
|
||||
uniform_buffer: wgpu.Buffer,
|
||||
}
|
||||
|
||||
Mesh :: struct {
|
||||
vertex_buffer: wgpu.Buffer,
|
||||
index_buffer: wgpu.Buffer,
|
||||
index_count: u32,
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Variables
|
||||
|
||||
// Instance -> Surface -> Adapter -> Device -> Queue
|
||||
state: struct {
|
||||
ctx: runtime.Context,
|
||||
os: OS,
|
||||
ctx: runtime.Context,
|
||||
os: OS,
|
||||
// ----------------------------------------
|
||||
instance: wgpu.Instance, // entry point: creates Adapter, Device and Surface
|
||||
surface: wgpu.Surface, // handle to a presentable surface (e.g. a window)
|
||||
adapter: wgpu.Adapter, // handle to physical GPU
|
||||
device: wgpu.Device, // open connection to a GPU
|
||||
queue: wgpu.Queue, // handle to command queue on a Device
|
||||
config: wgpu.SurfaceConfiguration,
|
||||
module: wgpu.ShaderModule,
|
||||
pipeline_layout: wgpu.PipelineLayout,
|
||||
pipeline: wgpu.RenderPipeline,
|
||||
instance: wgpu.Instance, // entry point: creates Adapter, Device and Surface
|
||||
surface: wgpu.Surface, // handle to a presentable surface (e.g. a window)
|
||||
adapter: wgpu.Adapter, // handle to physical GPU
|
||||
device: wgpu.Device, // open connection to a GPU
|
||||
queue: wgpu.Queue, // handle to command queue on a Device
|
||||
config: wgpu.SurfaceConfiguration,
|
||||
module: wgpu.ShaderModule,
|
||||
pipeline_layout: wgpu.PipelineLayout,
|
||||
pipeline: wgpu.RenderPipeline,
|
||||
// ----------------------------------------
|
||||
capabilities: wgpu.SurfaceCapabilities,
|
||||
vsync: bool,
|
||||
capabilities: wgpu.SurfaceCapabilities,
|
||||
vsync: bool,
|
||||
// ----------------------------------------
|
||||
depth_texture: wgpu.Texture,
|
||||
depth_texture_view: wgpu.TextureView,
|
||||
// ----------------------------------------
|
||||
camera_uniform_buffer: wgpu.Buffer,
|
||||
camera_bind_group_layout: wgpu.BindGroupLayout,
|
||||
camera_bind_group: wgpu.BindGroup,
|
||||
// ----------------------------------------
|
||||
material_bind_group_layout: wgpu.BindGroupLayout,
|
||||
materials: map[string]Material,
|
||||
// ----------------------------------------
|
||||
objects_bind_group_layout: wgpu.BindGroupLayout,
|
||||
objects: [dynamic]Object,
|
||||
meshes: map[string]Mesh,
|
||||
// ----------------------------------------
|
||||
diffuse_texture: wgpu.Texture,
|
||||
diffuse_texture_view: wgpu.TextureView,
|
||||
diffuse_sampler: wgpu.Sampler,
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Constructor/destructor
|
||||
|
||||
instance_init :: proc(settings: base.Settings) {
|
||||
instance_init :: proc(
|
||||
settings: base.Settings,
|
||||
allocator := context.allocator,
|
||||
) {
|
||||
state.ctx = context
|
||||
|
||||
state.materials = make(map[string]Material, allocator)
|
||||
state.objects = make([dynamic]Object, allocator)
|
||||
state.meshes = make(map[string]Mesh, allocator)
|
||||
|
||||
wgpu.SetLogCallback(log_callback, nil)
|
||||
wgpu.SetLogLevel(.Warn)
|
||||
|
||||
@@ -64,6 +127,42 @@ instance_init :: proc(settings: base.Settings) {
|
||||
}
|
||||
|
||||
instance_destroy :: proc() {
|
||||
wgpu.TextureViewRelease(state.depth_texture_view)
|
||||
wgpu.TextureRelease(state.depth_texture)
|
||||
|
||||
wgpu.SamplerRelease(state.diffuse_sampler)
|
||||
wgpu.TextureViewRelease(state.diffuse_texture_view)
|
||||
wgpu.TextureRelease(state.diffuse_texture)
|
||||
|
||||
wgpu.BufferRelease(state.camera_uniform_buffer)
|
||||
wgpu.BindGroupRelease(state.camera_bind_group)
|
||||
wgpu.BindGroupLayoutRelease(state.camera_bind_group_layout)
|
||||
|
||||
if state.materials != nil {
|
||||
for _, m in state.materials {
|
||||
wgpu.BufferRelease(m.uniform_buffer)
|
||||
wgpu.BindGroupRelease(m.bind_group)
|
||||
}
|
||||
delete(state.materials)
|
||||
}
|
||||
wgpu.BindGroupLayoutRelease(state.material_bind_group_layout)
|
||||
|
||||
if state.meshes != nil {
|
||||
for _, m in state.meshes {
|
||||
wgpu.BufferRelease(m.vertex_buffer)
|
||||
wgpu.BufferRelease(m.index_buffer)
|
||||
}
|
||||
delete(state.meshes)
|
||||
}
|
||||
if state.objects != nil {
|
||||
for r in state.objects {
|
||||
wgpu.BufferRelease(r.uniform_buffer)
|
||||
wgpu.BindGroupRelease(r.bind_group)
|
||||
}
|
||||
delete(state.objects)
|
||||
}
|
||||
wgpu.BindGroupLayoutRelease(state.objects_bind_group_layout)
|
||||
|
||||
wgpu.RenderPipelineRelease(state.pipeline)
|
||||
wgpu.PipelineLayoutRelease(state.pipeline_layout)
|
||||
wgpu.ShaderModuleRelease(state.module)
|
||||
@@ -94,9 +193,18 @@ resize :: proc "c" () {
|
||||
|
||||
state.config.width, state.config.height = os_get_framebuffer_size()
|
||||
wgpu.SurfaceConfigure(state.surface, &state.config)
|
||||
|
||||
create_depth_texture()
|
||||
|
||||
// The projection depends on aspect, so rebuild and re-upload the matrix
|
||||
upload_camera(
|
||||
state.queue,
|
||||
state.camera_uniform_buffer,
|
||||
framebuffer_aspect(),
|
||||
)
|
||||
}
|
||||
|
||||
frame :: proc "c" (dt: f32) {
|
||||
frame :: proc "c" () {
|
||||
context = state.ctx
|
||||
|
||||
surface_texture := wgpu.SurfaceGetCurrentTexture(state.surface)
|
||||
@@ -137,20 +245,74 @@ frame :: proc "c" (dt: f32) {
|
||||
loadOp = .Clear,
|
||||
storeOp = .Store,
|
||||
depthSlice = wgpu.DEPTH_SLICE_UNDEFINED,
|
||||
clearValue = {0, 1, 0, 1},
|
||||
clearValue = {55 / 255.0, 73 / 255.0, 73 / 255.0, 1},
|
||||
},
|
||||
depthStencilAttachment = &wgpu.RenderPassDepthStencilAttachment {
|
||||
view = state.depth_texture_view,
|
||||
depthLoadOp = .Clear,
|
||||
depthStoreOp = .Store,
|
||||
depthClearValue = 1.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
wgpu.RenderPassEncoderSetPipeline(render_pass_encoder, state.pipeline)
|
||||
wgpu.RenderPassEncoderDraw(
|
||||
|
||||
// Uniform group 0, camera MVP matrix
|
||||
wgpu.RenderPassEncoderSetBindGroup(
|
||||
render_pass_encoder,
|
||||
vertexCount = 3,
|
||||
instanceCount = 1,
|
||||
firstVertex = 0,
|
||||
firstInstance = 0,
|
||||
0,
|
||||
state.camera_bind_group,
|
||||
dynamicOffsets = nil, // reuse bind group af different byte offsets, per binding
|
||||
)
|
||||
|
||||
// Uniform group 1, materials
|
||||
m := state.materials["dummy"]
|
||||
wgpu.RenderPassEncoderSetBindGroup(
|
||||
render_pass_encoder,
|
||||
1,
|
||||
m.bind_group,
|
||||
dynamicOffsets = nil, // reuse bind group af different byte offsets, per binding
|
||||
)
|
||||
|
||||
// Render all objects
|
||||
for o in state.objects {
|
||||
m := state.meshes[o.name]
|
||||
|
||||
// Uniform group 2, object
|
||||
wgpu.RenderPassEncoderSetBindGroup(
|
||||
render_pass_encoder,
|
||||
2,
|
||||
o.bind_group,
|
||||
dynamicOffsets = nil, // reuse bind group af different byte offsets, per binding
|
||||
)
|
||||
|
||||
wgpu.RenderPassEncoderSetVertexBuffer(
|
||||
render_pass_encoder,
|
||||
slot = 0,
|
||||
buffer = m.vertex_buffer,
|
||||
offset = 0, // offset into buffer to start from
|
||||
size = wgpu.WHOLE_SIZE, // x bytes of the buffer to use
|
||||
)
|
||||
|
||||
wgpu.RenderPassEncoderSetIndexBuffer(
|
||||
render_pass_encoder,
|
||||
buffer = m.index_buffer,
|
||||
format = .Uint16, // type of each element in the buffer
|
||||
offset = 0, // offset into buffer to start from
|
||||
size = wgpu.WHOLE_SIZE, // x bytes of the buffer to use
|
||||
)
|
||||
|
||||
wgpu.RenderPassEncoderDrawIndexed(
|
||||
render_pass_encoder,
|
||||
m.index_count,
|
||||
instanceCount = 1, // draw x instance(s) of the mesh
|
||||
firstIndex = 0, // offset into buffer to start from, useful for drawing sub-mesh
|
||||
baseVertex = 0, // value added to index before vertex fetch, useful for multi-mesh vertex buffers
|
||||
firstInstance = 0, // number to start instance index from
|
||||
)
|
||||
}
|
||||
|
||||
wgpu.RenderPassEncoderEnd(render_pass_encoder)
|
||||
wgpu.RenderPassEncoderRelease(render_pass_encoder)
|
||||
|
||||
@@ -161,6 +323,152 @@ frame :: proc "c" (dt: f32) {
|
||||
wgpu.SurfacePresent(state.surface)
|
||||
}
|
||||
|
||||
create_material :: proc(name: string) {
|
||||
if name in state.materials do return
|
||||
|
||||
uniform_buffer := wgpu.DeviceCreateBuffer(
|
||||
state.device,
|
||||
&{
|
||||
label = "material uniform",
|
||||
usage = {.Uniform, .CopyDst}, // shader reads, queue writes
|
||||
size = cast(u64)size_of(Material_Uniform),
|
||||
},
|
||||
)
|
||||
|
||||
uniform := Material_Uniform{}
|
||||
|
||||
// bufferOffset = offset into buffer to start from
|
||||
wgpu.QueueWriteBuffer(
|
||||
state.queue,
|
||||
uniform_buffer,
|
||||
0,
|
||||
&uniform,
|
||||
size_of(Material_Uniform),
|
||||
)
|
||||
|
||||
entries := [?]wgpu.BindGroupEntry {
|
||||
{
|
||||
binding = 0, // slot 0
|
||||
buffer = uniform_buffer,
|
||||
offset = 0, // offset into buffer to start from
|
||||
size = cast(u64)size_of(Material_Uniform),
|
||||
},
|
||||
}
|
||||
bind_group := wgpu.DeviceCreateBindGroup(
|
||||
state.device,
|
||||
&{
|
||||
label = "material bind group",
|
||||
layout = state.material_bind_group_layout,
|
||||
entryCount = len(entries),
|
||||
entries = raw_data(entries[:]),
|
||||
},
|
||||
)
|
||||
|
||||
state.materials[name] = {
|
||||
bind_group = bind_group,
|
||||
uniform_buffer = uniform_buffer,
|
||||
}
|
||||
}
|
||||
|
||||
create_renderable :: proc(
|
||||
name: string,
|
||||
vertices: []base.Vertex,
|
||||
indices: []u16,
|
||||
x_offset: f32,
|
||||
) {
|
||||
uniform_buffer := wgpu.DeviceCreateBuffer(
|
||||
state.device,
|
||||
&{
|
||||
label = "object uniform",
|
||||
usage = {.Uniform, .CopyDst}, // shader reads, queue writes
|
||||
size = cast(u64)size_of(Object_Uniform),
|
||||
},
|
||||
)
|
||||
|
||||
uniform := Object_Uniform {
|
||||
// transform = linalg.identity(base.Mat4)
|
||||
transform = linalg.matrix4_translate(base.Vec3{x_offset, 0, 0}),
|
||||
}
|
||||
|
||||
// bufferOffset = offset into buffer to start from
|
||||
wgpu.QueueWriteBuffer(
|
||||
state.queue,
|
||||
uniform_buffer,
|
||||
0,
|
||||
&uniform,
|
||||
size_of(Object_Uniform),
|
||||
)
|
||||
|
||||
entries := [?]wgpu.BindGroupEntry {
|
||||
{
|
||||
binding = 0, // slot 0
|
||||
buffer = uniform_buffer,
|
||||
offset = 0, // offset into buffer to start from
|
||||
size = cast(u64)size_of(Object_Uniform),
|
||||
},
|
||||
}
|
||||
bind_group := wgpu.DeviceCreateBindGroup(
|
||||
state.device,
|
||||
&{
|
||||
label = "object bind group",
|
||||
layout = state.objects_bind_group_layout,
|
||||
entryCount = len(entries),
|
||||
entries = raw_data(entries[:]),
|
||||
},
|
||||
)
|
||||
|
||||
append(
|
||||
&state.objects,
|
||||
Object {
|
||||
name = name,
|
||||
bind_group = bind_group,
|
||||
uniform_buffer = uniform_buffer,
|
||||
},
|
||||
)
|
||||
|
||||
if name in state.meshes do return
|
||||
|
||||
vertex_buffer := wgpu.DeviceCreateBuffer(
|
||||
state.device,
|
||||
&{
|
||||
label = "vertex buffer",
|
||||
usage = {.Vertex, .CopyDst}, // shader reads, queue writes
|
||||
size = cast(u64)(len(vertices) * size_of(base.Vertex)),
|
||||
},
|
||||
)
|
||||
|
||||
wgpu.QueueWriteBuffer(
|
||||
state.queue,
|
||||
vertex_buffer,
|
||||
0,
|
||||
raw_data(vertices),
|
||||
cast(uint)(len(vertices) * size_of(base.Vertex)),
|
||||
)
|
||||
|
||||
index_buffer := wgpu.DeviceCreateBuffer(
|
||||
state.device,
|
||||
&{
|
||||
label = "index buffer",
|
||||
usage = {.Index, .CopyDst}, // shader reads, queue writes
|
||||
size = cast(u64)(len(indices) * size_of(u16)),
|
||||
},
|
||||
)
|
||||
|
||||
wgpu.QueueWriteBuffer(
|
||||
state.queue,
|
||||
index_buffer,
|
||||
0,
|
||||
raw_data(indices),
|
||||
cast(uint)(len(indices) * size_of(u16)),
|
||||
)
|
||||
|
||||
state.meshes[name] = {
|
||||
vertex_buffer = vertex_buffer,
|
||||
index_buffer = index_buffer,
|
||||
index_count = cast(u32)len(indices),
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Private functions
|
||||
|
||||
@@ -208,18 +516,70 @@ on_device :: proc "c" (
|
||||
}
|
||||
wgpu.SurfaceConfigure(state.surface, &state.config)
|
||||
|
||||
shader :: `
|
||||
// ----------------------------------------
|
||||
// Depth buffer
|
||||
|
||||
create_depth_texture()
|
||||
|
||||
// ----------------------------------------
|
||||
// Compile shader
|
||||
|
||||
uniforms_struct := to_wgsl_struct(Camera_Uniform, "Camera")
|
||||
defer delete(uniforms_struct)
|
||||
vertex_input_struct := to_wgsl_struct(
|
||||
base.Vertex,
|
||||
"VertexInput",
|
||||
locations = true,
|
||||
)
|
||||
defer delete(vertex_input_struct)
|
||||
|
||||
material_struct := to_wgsl_struct(Material_Uniform, "Material")
|
||||
defer delete(material_struct)
|
||||
|
||||
renderable_struct := to_wgsl_struct(Object_Uniform, "Renderable")
|
||||
defer delete(renderable_struct)
|
||||
|
||||
shader := fmt.tprintf(
|
||||
`
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
|
||||
@group(0) @binding(0) var<uniform> c: Camera;
|
||||
@group(0) @binding(1) var t_diffuse: texture_2d<f32>;
|
||||
@group(0) @binding(2) var s_diffuse: sampler;
|
||||
|
||||
@group(1) @binding(0) var<uniform> m: Material;
|
||||
|
||||
@group(2) @binding(0) var<uniform> r: Renderable;
|
||||
|
||||
%s
|
||||
|
||||
struct VertexOutput {{
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) col: vec3<f32>,
|
||||
@location(1) uv: vec2<f32>,
|
||||
}};
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
|
||||
let x = f32(i32(in_vertex_index) - 1);
|
||||
let y = f32(i32(in_vertex_index & 1u) * 2 - 1);
|
||||
return vec4<f32>(x, y, 0.0, 1.0);
|
||||
}
|
||||
fn vs_main(in: VertexInput) -> VertexOutput {{
|
||||
var out: VertexOutput;
|
||||
out.position = c.mvp * r.transform * vec4<f32>(in.pos, 1.0);
|
||||
out.col = in.col;
|
||||
out.uv = in.uv;
|
||||
return out;
|
||||
}}
|
||||
|
||||
@fragment
|
||||
fn fs_main() -> @location(0) vec4<f32> {
|
||||
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
|
||||
}`
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {{
|
||||
return textureSample(t_diffuse, s_diffuse, in.uv) * vec4<f32>(in.col, 1.0);
|
||||
}}
|
||||
`,
|
||||
uniforms_struct,
|
||||
material_struct,
|
||||
renderable_struct,
|
||||
vertex_input_struct,
|
||||
)
|
||||
|
||||
state.module = wgpu.DeviceCreateShaderModule(
|
||||
state.device,
|
||||
@@ -231,12 +591,140 @@ on_device :: proc "c" (
|
||||
},
|
||||
)
|
||||
|
||||
state.pipeline_layout = wgpu.DeviceCreatePipelineLayout(state.device, &{})
|
||||
// ----------------------------------------
|
||||
// Camera uniform: buffer, then the baked layout it will be read through
|
||||
|
||||
state.camera_uniform_buffer = wgpu.DeviceCreateBuffer(
|
||||
state.device,
|
||||
&{
|
||||
label = "camera uniform",
|
||||
usage = {.Uniform, .CopyDst}, // shader reads, queue writes
|
||||
size = cast(u64)size_of(Camera_Uniform),
|
||||
},
|
||||
)
|
||||
|
||||
camera_layout_entries := [?]wgpu.BindGroupLayoutEntry {
|
||||
{
|
||||
binding = 0, // slot 0
|
||||
visibility = {.Vertex},
|
||||
buffer = {
|
||||
type = .Uniform,
|
||||
minBindingSize = cast(u64)size_of(Camera_Uniform),
|
||||
},
|
||||
},
|
||||
{
|
||||
binding = 1, // slot 1
|
||||
visibility = {.Fragment},
|
||||
texture = {
|
||||
sampleType = .Float,
|
||||
viewDimension = ._2D,
|
||||
multisampled = false,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding = 2, // slot 2
|
||||
visibility = {.Fragment},
|
||||
sampler = {type = .Filtering},
|
||||
},
|
||||
}
|
||||
state.camera_bind_group_layout = wgpu.DeviceCreateBindGroupLayout(
|
||||
state.device,
|
||||
&{
|
||||
label = "camera layout",
|
||||
entryCount = len(camera_layout_entries),
|
||||
entries = raw_data(camera_layout_entries[:]),
|
||||
},
|
||||
)
|
||||
|
||||
// ----------------------------------------
|
||||
// Material baked layout
|
||||
|
||||
material_layout_entries := [?]wgpu.BindGroupLayoutEntry {
|
||||
{
|
||||
binding = 0, // slot 0
|
||||
visibility = {.Fragment},
|
||||
buffer = {
|
||||
type = .Uniform,
|
||||
minBindingSize = cast(u64)size_of(Material_Uniform),
|
||||
},
|
||||
},
|
||||
}
|
||||
state.material_bind_group_layout = wgpu.DeviceCreateBindGroupLayout(
|
||||
state.device,
|
||||
&{
|
||||
label = "material layout",
|
||||
entryCount = len(material_layout_entries),
|
||||
entries = raw_data(material_layout_entries[:]),
|
||||
},
|
||||
)
|
||||
|
||||
// ----------------------------------------
|
||||
// Renderable baked layout
|
||||
|
||||
renderable_layout_entries := [?]wgpu.BindGroupLayoutEntry {
|
||||
{
|
||||
binding = 0, // slot 0
|
||||
visibility = {.Vertex},
|
||||
buffer = {
|
||||
type = .Uniform,
|
||||
minBindingSize = cast(u64)size_of(Object_Uniform),
|
||||
},
|
||||
},
|
||||
}
|
||||
state.objects_bind_group_layout = wgpu.DeviceCreateBindGroupLayout(
|
||||
state.device,
|
||||
&{
|
||||
label = "renderable layout",
|
||||
entryCount = len(renderable_layout_entries),
|
||||
entries = raw_data(renderable_layout_entries[:]),
|
||||
},
|
||||
)
|
||||
|
||||
// ----------------------------------------
|
||||
// Diffuse texture + sampler
|
||||
|
||||
state.diffuse_texture, state.diffuse_texture_view, state.diffuse_sampler =
|
||||
create_diffuse_texture(state.device, state.queue)
|
||||
|
||||
// ----------------------------------------
|
||||
// Buffer shape
|
||||
|
||||
vertex_attributes := to_vertex_attributes(base.Vertex)
|
||||
|
||||
vertex_layout := wgpu.VertexBufferLayout {
|
||||
stepMode = .Vertex,
|
||||
arrayStride = size_of(base.Vertex),
|
||||
attributeCount = len(vertex_attributes),
|
||||
attributes = raw_data(vertex_attributes),
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Render pipeline
|
||||
|
||||
group_layouts := [?]wgpu.BindGroupLayout {
|
||||
state.camera_bind_group_layout,
|
||||
state.material_bind_group_layout,
|
||||
state.objects_bind_group_layout,
|
||||
}
|
||||
state.pipeline_layout = wgpu.DeviceCreatePipelineLayout(
|
||||
state.device,
|
||||
&{
|
||||
bindGroupLayoutCount = len(group_layouts),
|
||||
bindGroupLayouts = raw_data(group_layouts[:]),
|
||||
},
|
||||
)
|
||||
|
||||
state.pipeline = wgpu.DeviceCreateRenderPipeline(
|
||||
state.device,
|
||||
&{
|
||||
label = "pipeline",
|
||||
layout = state.pipeline_layout,
|
||||
vertex = {module = state.module, entryPoint = "vs_main"},
|
||||
vertex = {
|
||||
module = state.module,
|
||||
entryPoint = "vs_main",
|
||||
bufferCount = 1,
|
||||
buffers = &vertex_layout,
|
||||
},
|
||||
fragment = &{
|
||||
module = state.module,
|
||||
entryPoint = "fs_main",
|
||||
@@ -248,8 +736,43 @@ on_device :: proc "c" (
|
||||
},
|
||||
primitive = {topology = .TriangleList},
|
||||
multisample = {count = 1, mask = 0xFFFFFFFF},
|
||||
depthStencil = &wgpu.DepthStencilState {
|
||||
format = .Depth24Plus,
|
||||
depthWriteEnabled = .True,
|
||||
depthCompare = .Less,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// ----------------------------------------
|
||||
// Camera bind group
|
||||
|
||||
bind_group_entries := [?]wgpu.BindGroupEntry {
|
||||
{
|
||||
binding = 0, // slot 0
|
||||
buffer = state.camera_uniform_buffer,
|
||||
offset = 0, // offset into buffer to start from
|
||||
size = cast(u64)size_of(Camera_Uniform),
|
||||
},
|
||||
{binding = 1, textureView = state.diffuse_texture_view},
|
||||
{binding = 2, sampler = state.diffuse_sampler},
|
||||
}
|
||||
state.camera_bind_group = wgpu.DeviceCreateBindGroup(
|
||||
state.device,
|
||||
&{
|
||||
label = "camera bind group",
|
||||
layout = state.camera_bind_group_layout,
|
||||
entryCount = len(bind_group_entries),
|
||||
entries = raw_data(bind_group_entries[:]),
|
||||
},
|
||||
)
|
||||
|
||||
// Initial upload of the matrix
|
||||
upload_camera(
|
||||
state.queue,
|
||||
state.camera_uniform_buffer,
|
||||
framebuffer_aspect(),
|
||||
)
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
@@ -290,3 +813,145 @@ pick_present_mode :: proc(vsync: bool) -> wgpu.PresentMode {
|
||||
|
||||
return .Fifo // Vsync On
|
||||
}
|
||||
|
||||
// Depth buffer: same size as the surface, should get recreated on resize
|
||||
@(private = "file")
|
||||
create_depth_texture :: proc() {
|
||||
if state.depth_texture != nil {
|
||||
wgpu.TextureViewRelease(state.depth_texture_view)
|
||||
wgpu.TextureRelease(state.depth_texture)
|
||||
}
|
||||
|
||||
state.depth_texture = wgpu.DeviceCreateTexture(
|
||||
state.device,
|
||||
&{
|
||||
label = "depth texture",
|
||||
usage = {.RenderAttachment},
|
||||
dimension = ._2D,
|
||||
size = {
|
||||
width = state.config.width,
|
||||
height = state.config.height,
|
||||
depthOrArrayLayers = 1,
|
||||
},
|
||||
format = .Depth24Plus,
|
||||
mipLevelCount = 1,
|
||||
sampleCount = 1,
|
||||
},
|
||||
)
|
||||
state.depth_texture_view = wgpu.TextureCreateView(state.depth_texture, nil)
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
framebuffer_aspect :: proc() -> f32 {
|
||||
width, height := os_get_framebuffer_size()
|
||||
if height == 0 {
|
||||
return 1
|
||||
}
|
||||
return cast(f32)width / cast(f32)height
|
||||
}
|
||||
|
||||
// Perspective projection, WebGPU flavour
|
||||
@(private = "file")
|
||||
camera_perspective :: proc(fovy_degrees, aspect, near, far: f32) -> base.Mat4 {
|
||||
gl := linalg.matrix4_perspective(
|
||||
fovy_degrees * math.PI / 180.0,
|
||||
aspect,
|
||||
near,
|
||||
far,
|
||||
)
|
||||
// GL's clip space is [-1, 1] and WebGPU's clip space is [0, 1],
|
||||
// so fold z with a remap: z' = 1/2 z + 1/2 w
|
||||
remap := linalg.identity(base.Mat4)
|
||||
remap[2, 2] = 0.5
|
||||
remap[2, 3] = 0.5
|
||||
return remap * gl
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
camera_mvp :: proc(aspect: f32) -> base.Mat4 {
|
||||
projection := camera_perspective(60, aspect, 0.1, 100)
|
||||
view := linalg.matrix4_look_at_f32({0, 2, 5}, {0, 0, 0}, {0, 1, 0})
|
||||
// model = identity: the loader already centered the model at the origin
|
||||
return projection * view
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
upload_camera :: proc(queue: wgpu.Queue, buffer: wgpu.Buffer, aspect: f32) {
|
||||
uniforms := Camera_Uniform {
|
||||
mvp = camera_mvp(aspect),
|
||||
}
|
||||
// bufferOffset = offset into buffer to start from
|
||||
wgpu.QueueWriteBuffer(queue, buffer, 0, &uniforms, size_of(Camera_Uniform))
|
||||
}
|
||||
|
||||
// Test 256x256 checkerboard texture, uploaded once
|
||||
@(private = "file")
|
||||
create_diffuse_texture :: proc(
|
||||
device: wgpu.Device,
|
||||
queue: wgpu.Queue,
|
||||
) -> (
|
||||
wgpu.Texture,
|
||||
wgpu.TextureView,
|
||||
wgpu.Sampler,
|
||||
) {
|
||||
width, height: u32 = 256, 256
|
||||
pixels := make([]u8, width * height * 4)
|
||||
defer delete(pixels)
|
||||
|
||||
for y in 0 ..< height {
|
||||
for x in 0 ..< width {
|
||||
checker := ((x / 32) + (y / 32)) % 2 == 0
|
||||
idx := (y * width + x) * 4
|
||||
if checker {
|
||||
pixels[idx + 0] = 240
|
||||
pixels[idx + 1] = 240
|
||||
pixels[idx + 2] = 240
|
||||
pixels[idx + 3] = 255
|
||||
} else {
|
||||
pixels[idx + 0] = 64
|
||||
pixels[idx + 1] = 64
|
||||
pixels[idx + 2] = 64
|
||||
pixels[idx + 3] = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
texture := wgpu.DeviceCreateTexture(
|
||||
device,
|
||||
&{
|
||||
label = "diffuse texture",
|
||||
usage = {.TextureBinding, .CopyDst}, // shader samples, queue writes
|
||||
dimension = ._2D,
|
||||
size = {width = width, height = height, depthOrArrayLayers = 1},
|
||||
format = .RGBA8Unorm,
|
||||
mipLevelCount = 1,
|
||||
sampleCount = 1,
|
||||
},
|
||||
)
|
||||
|
||||
// Upload texture to GPU
|
||||
wgpu.QueueWriteTexture(
|
||||
queue,
|
||||
&{texture = texture, mipLevel = 0, origin = {0, 0, 0}, aspect = .All}, // destination
|
||||
raw_data(pixels),
|
||||
len(pixels),
|
||||
&{offset = 0, bytesPerRow = width * 4, rowsPerImage = height}, // data layout
|
||||
&{width = width, height = height, depthOrArrayLayers = 1}, // 3D extent
|
||||
)
|
||||
|
||||
view := wgpu.TextureCreateView(texture, nil)
|
||||
|
||||
sampler := wgpu.DeviceCreateSampler(
|
||||
device,
|
||||
&{
|
||||
addressModeU = .Repeat,
|
||||
addressModeV = .Repeat,
|
||||
magFilter = .Nearest,
|
||||
minFilter = .Nearest,
|
||||
mipmapFilter = .Nearest,
|
||||
maxAnisotropy = 1,
|
||||
},
|
||||
)
|
||||
|
||||
return texture, view, sampler
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
package platform
|
||||
|
||||
import "core:fmt"
|
||||
import "core:reflect"
|
||||
import "core:strings"
|
||||
|
||||
import "wgpu:wgpu"
|
||||
|
||||
// -----------------------------------------
|
||||
// Types
|
||||
|
||||
// -----------------------------------------
|
||||
// Public functions
|
||||
|
||||
// Generate a WGSL struct declaration from an Odin struct type.
|
||||
// The caller owns the returned string (allocated on context.allocator).
|
||||
//
|
||||
// If locations is true, each member is prefixed with @location(n). This is
|
||||
// required for vertex input structs; uniform structs should use false.
|
||||
to_wgsl_struct :: proc(
|
||||
$T: typeid,
|
||||
name: string,
|
||||
locations := false,
|
||||
) -> string {
|
||||
ti := base_type_info(type_info_of(T))
|
||||
s, ok := ti.variant.(reflect.Type_Info_Struct)
|
||||
if !ok do fmt.panicf("[wgpu_reflect] %s is not a struct", typeid_of(T))
|
||||
|
||||
b: strings.Builder
|
||||
strings.write_string(&b, "struct ")
|
||||
strings.write_string(&b, name)
|
||||
strings.write_string(&b, " {\n")
|
||||
|
||||
names := s.names[:s.field_count]
|
||||
types := s.types[:s.field_count]
|
||||
for i in 0 ..< s.field_count {
|
||||
wgsl_type, type_ok := to_wgsl_type(types[i])
|
||||
if !type_ok {
|
||||
fmt.panicf("[wgpu_reflect] cannot map field %s to WGSL", names[i])
|
||||
}
|
||||
if locations {
|
||||
fmt.sbprintf(
|
||||
&b,
|
||||
"\t@location(%d) %s: %s,\n",
|
||||
i,
|
||||
names[i],
|
||||
wgsl_type,
|
||||
)
|
||||
} else {
|
||||
fmt.sbprintf(&b, "\t%s: %s,\n", names[i], wgsl_type)
|
||||
}
|
||||
}
|
||||
|
||||
strings.write_string(&b, "};")
|
||||
return strings.to_string(b)
|
||||
}
|
||||
|
||||
// Generate a []wgpu.VertexAttribute array that matches the fields of struct T.
|
||||
// The caller owns the returned slice (allocated on context.allocator)
|
||||
to_vertex_attributes :: proc($T: typeid) -> []wgpu.VertexAttribute {
|
||||
ti := base_type_info(type_info_of(T))
|
||||
s, ok := ti.variant.(reflect.Type_Info_Struct)
|
||||
if !ok do fmt.panicf("[wgpu_reflect] %s is not a struct", typeid_of(T))
|
||||
|
||||
attrs := make([]wgpu.VertexAttribute, s.field_count)
|
||||
names := s.names[:s.field_count]
|
||||
types := s.types[:s.field_count]
|
||||
offsets := s.offsets[:s.field_count]
|
||||
|
||||
for i in 0 ..< s.field_count {
|
||||
format, format_ok := to_vertex_format(types[i])
|
||||
if !format_ok {
|
||||
fmt.panicf(
|
||||
"[wgpu_reflect] cannot map field %s to a vertex format",
|
||||
names[i],
|
||||
)
|
||||
}
|
||||
|
||||
attrs[i] = wgpu.VertexAttribute {
|
||||
format = format,
|
||||
offset = cast(u64)offsets[i],
|
||||
shaderLocation = u32(i),
|
||||
}
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Private functions
|
||||
|
||||
// Strips named-type wrappers so we can inspect the underlying type
|
||||
@(private = "file")
|
||||
base_type_info :: proc(type_info: ^reflect.Type_Info) -> ^reflect.Type_Info {
|
||||
#partial switch v in type_info.variant {
|
||||
case reflect.Type_Info_Named:
|
||||
return base_type_info(v.base)
|
||||
}
|
||||
return type_info
|
||||
}
|
||||
|
||||
// Map an Odin type to a WGSL scalar/vector/matrix type name
|
||||
@(private = "file")
|
||||
to_wgsl_type :: proc(type_info: ^reflect.Type_Info) -> (string, bool) {
|
||||
ti := base_type_info(type_info)
|
||||
|
||||
#partial switch v in ti.variant {
|
||||
case reflect.Type_Info_Float:
|
||||
switch ti.size {
|
||||
case 4:
|
||||
return "f32", true
|
||||
case 8:
|
||||
return "f64", true
|
||||
}
|
||||
|
||||
case reflect.Type_Info_Integer:
|
||||
prefix := v.signed ? "i" : "u"
|
||||
switch ti.size {
|
||||
case 1:
|
||||
return fmt.tprintf("%s8", prefix), true
|
||||
case 2:
|
||||
return fmt.tprintf("%s16", prefix), true
|
||||
case 4:
|
||||
return fmt.tprintf("%s32", prefix), true
|
||||
case 8:
|
||||
return fmt.tprintf("%s64", prefix), true
|
||||
}
|
||||
|
||||
case reflect.Type_Info_Array:
|
||||
elem_type, elem_ok := to_wgsl_type(v.elem)
|
||||
if !elem_ok do return "", false
|
||||
switch v.count {
|
||||
case 2:
|
||||
return fmt.tprintf("vec2<%s>", elem_type), true
|
||||
case 3:
|
||||
return fmt.tprintf("vec3<%s>", elem_type), true
|
||||
case 4:
|
||||
return fmt.tprintf("vec4<%s>", elem_type), true
|
||||
}
|
||||
|
||||
case reflect.Type_Info_Matrix:
|
||||
elem_type, elem_ok := to_wgsl_type(v.elem)
|
||||
if !elem_ok do return "", false
|
||||
return fmt.tprintf(
|
||||
"mat%dx%d<%s>",
|
||||
v.column_count,
|
||||
v.row_count,
|
||||
elem_type,
|
||||
),
|
||||
true
|
||||
|
||||
case:
|
||||
fmt.panicf("[wgpu_reflect] unimplemented type translation: %v", ti)
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Map an Odin type to a wgpu.VertexFormat.
|
||||
// Only handles the scalar/vector numeric formats commonly used as vertex attributes
|
||||
@(private = "file")
|
||||
to_vertex_format :: proc(
|
||||
type_info: ^reflect.Type_Info,
|
||||
) -> (
|
||||
wgpu.VertexFormat,
|
||||
bool,
|
||||
) {
|
||||
ti := base_type_info(type_info)
|
||||
|
||||
#partial switch v in ti.variant {
|
||||
case reflect.Type_Info_Float:
|
||||
if ti.size == 2 do return .Float16, true
|
||||
else if ti.size == 4 do return .Float32, true
|
||||
|
||||
case reflect.Type_Info_Integer:
|
||||
if v.signed {
|
||||
switch ti.size {
|
||||
case 1:
|
||||
return .Sint8, true
|
||||
case 2:
|
||||
return .Sint16, true
|
||||
case 4:
|
||||
return .Sint32, true
|
||||
}
|
||||
} else {
|
||||
switch ti.size {
|
||||
case 1:
|
||||
return .Uint8, true
|
||||
case 2:
|
||||
return .Uint16, true
|
||||
case 4:
|
||||
return .Uint32, true
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Type_Info_Array:
|
||||
elem := base_type_info(v.elem)
|
||||
#partial switch ev in elem.variant {
|
||||
case reflect.Type_Info_Float:
|
||||
if elem.size == 2 {
|
||||
switch v.count {
|
||||
case 2:
|
||||
return .Float16x2, true
|
||||
case 4:
|
||||
return .Float16x4, true
|
||||
}
|
||||
} else if elem.size == 4 {
|
||||
switch v.count {
|
||||
case 2:
|
||||
return .Float32x2, true
|
||||
case 3:
|
||||
return .Float32x3, true
|
||||
case 4:
|
||||
return .Float32x4, true
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Type_Info_Integer:
|
||||
if ev.signed {
|
||||
switch elem.size {
|
||||
case 1:
|
||||
switch v.count {
|
||||
case 2:
|
||||
return .Sint8x2, true
|
||||
case 4:
|
||||
return .Sint8x4, true
|
||||
}
|
||||
case 2:
|
||||
switch v.count {
|
||||
case 2:
|
||||
return .Sint16x2, true
|
||||
case 4:
|
||||
return .Sint16x4, true
|
||||
}
|
||||
case 4:
|
||||
switch v.count {
|
||||
case 2:
|
||||
return .Sint32x2, true
|
||||
case 3:
|
||||
return .Sint32x3, true
|
||||
case 4:
|
||||
return .Sint32x4, true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch elem.size {
|
||||
case 1:
|
||||
switch v.count {
|
||||
case 2:
|
||||
return .Uint8x2, true
|
||||
case 4:
|
||||
return .Uint8x4, true
|
||||
}
|
||||
case 2:
|
||||
switch (v.count) {
|
||||
case 2:
|
||||
return .Uint16x2, true
|
||||
case 4:
|
||||
return .Uint16x4, true
|
||||
}
|
||||
case 4:
|
||||
switch (v.count) {
|
||||
case 2:
|
||||
return .Uint32x2, true
|
||||
case 3:
|
||||
return .Uint32x3, true
|
||||
case 4:
|
||||
return .Uint32x4, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .Float32, false
|
||||
}
|
||||
Reference in New Issue
Block a user