From 7cda6a3f7d516e3f4cc61b3304b7f6f5920f1a06 Mon Sep 17 00:00:00 2001 From: sergeypdev Date: Fri, 23 May 2025 14:50:21 +0400 Subject: [PATCH] Start implementing a proper build system --- build.sh | 3 + builder/builder.odin | 193 +++++++++++++++++++++++++++++ builder/helpers.odin | 80 ++++++++++++ libs/physfs/Makefile | 2 + libs/raylib/.gitignore | 3 + libs/raylib/raylib.odin | 10 +- libs/raylib/rlgl/rlgl.odin | 9 +- libs/tracy/.gitignore | 5 + main_web/emscripten_allocator.odin | 144 +++++++++++++++++++++ main_web/emscripten_logger.odin | 99 +++++++++++++++ main_web/main_web.odin | 59 +++++---- 11 files changed, 570 insertions(+), 37 deletions(-) create mode 100755 build.sh create mode 100644 builder/builder.odin create mode 100644 builder/helpers.odin create mode 100644 libs/tracy/.gitignore create mode 100644 main_web/emscripten_allocator.odin create mode 100644 main_web/emscripten_logger.odin diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..1329677 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + + diff --git a/builder/builder.odin b/builder/builder.odin new file mode 100644 index 0000000..15899e5 --- /dev/null +++ b/builder/builder.odin @@ -0,0 +1,193 @@ +package builder + +import "core:flags" +import "core:fmt" +import "core:log" +import os "core:os/os2" +import "core:slice" + +Build_Variant :: enum { + Hot_Reload, + Desktop, + Web, +} + +Options :: struct { + variant: Build_Variant `usage:"Variant of the build"`, + optimize: bool `args:"name=opt",usage:"Enable compiler optimizations"`, + debug: bool `usage:"Enable debug symbols"`, + rebuild_deps: bool `usage:"When enabled dependencies will be cleaned and rebuilt"`, + tracy: bool `usage:"Enable tracy profiler"`, +} + +Error :: union #shared_nil { + Run_Error, + Copy_Error, + os.Error, +} + +build_deps :: proc(opts: Options) { + log.infof("build_deps") + force := opts.rebuild_deps + shared := opts.variant == .Hot_Reload + // Raylib + { + cwd := "./libs/raylib" + out_dir := shared ? "zig-out-shared" : "zig-out-static" + if force { + remove_all(fmt.tprintf("./libs/raylib/%s", out_dir)) + } + + target := opts.variant == .Web ? "wasm32-emscripten" : "native" + handle_error( + run_cmd( + { + "zig", + "build", + "-p", + out_dir, + fmt.tprintf("-Dshared=%v", shared), + fmt.tprintf("-Dtarget=%s", target), + }, + cwd, + ), + ) + } + + // Physfs + { + cwd := "./libs/physfs" + file_name := shared ? "libphysfs.so" : "libphysfs.a" + is_built := os.is_file(fmt.tprintf("./libs/physfs/%s", file_name)) + if is_built && force { + handle_error(run_cmd({"make", "clean"}, cwd)) + } + if !is_built || force { + handle_error(run_cmd({"make", file_name}, cwd)) + } + } + + // Tracy + if opts.tracy { + cwd := "./libs/tracy" + + when ODIN_OS == .Windows { + TRACY_NAME_SHARED :: "tracydll.lib" + TRACY_NAME_STATIC :: "tracy.lib" + } else when ODIN_OS == .Linux { + TRACY_NAME_SHARED :: "tracy.so" + TRACY_NAME_STATIC :: "tracy.a" + } else when ODIN_OS == .Darwin { + TRACY_NAME_SHARED :: "tracy.dynlib" + TRACY_NAME_STATIC :: "tracy.a" + } + + file_path := fmt.tprintf("./libs/tracy/%s", shared ? TRACY_NAME_SHARED : TRACY_NAME_STATIC) + + is_built := os.is_file(file_path) + if is_built && force { + remove_file(file_path) + } + + if !is_built || force { + handle_error( + run_cmd( + slice.concatenate( + [][]string { + { + "zig", + "c++", + "-std=c++11", + "-DTRACY_ENABLE", + "-O2", + "vendor/tracy/public/TracyClient.cpp", + "-fPIC", + }, + shared ? {"-shared", "-o", TRACY_NAME_SHARED} : {"-c", "-o", "tracy.o"}, + }, + context.temp_allocator, + ), + cwd, + ), + ) + + if !shared { + handle_error(run_cmd({"zig", "ar", "rc", TRACY_NAME_STATIC, "tracy.o"}, cwd)) + } + } + } +} + +COMMON_FLAGS :: []string { + "-collection:libs=./libs", + "-collection:common=./common", + "-collection:game=./game", + "-strict-style", + "-vet", +} + +main :: proc() { + context.logger = log.create_console_logger() + opts := Options { + tracy = true, + debug = true, + } + flags.parse_or_exit(&opts, os.args, .Unix, context.temp_allocator) + if opts.variant == .Web { + log.warnf("tracy is not supported on Web") + opts.tracy = false + } + + tracy_flag: []string = opts.tracy ? {"-define:TRACY_ENABLE=true"} : {} + debug_flag: []string = opts.debug ? {"-debug"} : {} + optimize_flag: []string = opts.optimize ? {"-o:speed"} : {} + + build_deps(opts) + + #partial switch opts.variant { + case .Hot_Reload: + cmd := slice.concatenate( + [][]string { + []string { + "odin", + "build", + "game", + "-define:RAYLIB_SHARED=true", + "-define:PHYSFS_SHARED=true", + "-build-mode:dll", + "-out:game_tmp.so", + }, + tracy_flag, + debug_flag, + optimize_flag, + COMMON_FLAGS, + }, + context.temp_allocator, + ) + handle_error(run_cmd(cmd, ".")) + handle_error(os.rename("game_tmp.so", "game.so")) + case .Desktop: + cmd := slice.concatenate( + [][]string { + []string{"odin", "build", "main_release", "-out:game.bin"}, + tracy_flag, + debug_flag, + optimize_flag, + COMMON_FLAGS, + }, + context.temp_allocator, + ) + handle_error(run_cmd(cmd, ".")) + case: + cmd := slice.concatenate( + [][]string { + []string{"odin", "build", "main_web", "-build-mode:obj", "-out:game_web/game"}, + COMMON_FLAGS, + debug_flag, + optimize_flag, + }, + ) + + handle_error(run_cmd(cmd, ".")) + } +} diff --git a/builder/helpers.odin b/builder/helpers.odin new file mode 100644 index 0000000..0aa9325 --- /dev/null +++ b/builder/helpers.odin @@ -0,0 +1,80 @@ +package builder + +import "base:intrinsics" +import "core:fmt" +import "core:log" +import os "core:os/os2" +import "core:path/filepath" +import "core:strings" + +Process_Error :: enum { + OK, + Invalid_Exit_Code, + Crash, +} + +Run_Error :: union #shared_nil { + Process_Error, + os.Error, +} + +@(require_results) +run_cmd :: proc(cmd: []string, cwd: string, loc := #caller_location) -> Run_Error { + log.infof( + "running [%s]: %s", + cwd, + strings.join(cmd, " ", context.temp_allocator), + location = loc, + ) + desc := os.Process_Desc { + command = cmd, + working_dir = cwd, + stderr = os.stderr, + stdout = os.stdout, + } + process := os.process_start(desc) or_return + state := os.process_wait(process) or_return + + if !state.success { + return .Crash + } + + if state.exit_code != 0 { + return .Invalid_Exit_Code + } + + return nil +} + +Copy_Error :: union #shared_nil { + os.Error, + filepath.Match_Error, +} + +handle_error :: proc( + err: $E, + msg: string = "", + expr := #caller_expression, + loc := #caller_location, +) where intrinsics.type_is_enum(E) || + intrinsics.type_is_union(E) { + if err != nil { + log.panicf("%v %s error: %v", expr, msg, err, location = loc) + } +} + +remove_file :: proc(path: string, expr := #caller_expression, loc := #caller_location) { + log.infof("remove(%s)", path, location = loc) + err := os.remove(path) + if err != .Not_Exist { + handle_error(err, fmt.tprintf("failed to remove %s", path), expr = expr, loc = loc) + } +} + +remove_all :: proc(path: string, expr := #caller_expression, loc := #caller_location) { + log.infof("remove_all(%s)", path, location = loc) + err := os.remove_all(path) + if err != .Not_Exist { + handle_error(err, fmt.tprintf("failed to remove %s", path), expr = expr, loc = loc) + } +} diff --git a/libs/physfs/Makefile b/libs/physfs/Makefile index 118849f..61631b1 100644 --- a/libs/physfs/Makefile +++ b/libs/physfs/Makefile @@ -6,3 +6,5 @@ libphysfs.a: build_physfs cp ./build/libphysfs.a . libphysfs.so: build_physfs cp ./build/libphysfs.so* . +clean: + rm -r build; rm libphysfs.* diff --git a/libs/raylib/.gitignore b/libs/raylib/.gitignore index d857bf2..caad9c5 100644 --- a/libs/raylib/.gitignore +++ b/libs/raylib/.gitignore @@ -113,3 +113,6 @@ docgen_tmp/ # Parser stuff parser/raylib_parser + +zig-out-static +zig-out-shared diff --git a/libs/raylib/raylib.odin b/libs/raylib/raylib.odin index 0a4b43d..eed45af 100644 --- a/libs/raylib/raylib.odin +++ b/libs/raylib/raylib.odin @@ -103,13 +103,13 @@ RAYLIB_SHARED :: #config(RAYLIB_SHARED, false) when ODIN_OS == .Windows { @(extra_linker_flags = "/NODEFAULTLIB:" + ("msvcrt" when RAYLIB_SHARED else "libcmt")) - foreign import lib {"windows/raylibdll.lib" when RAYLIB_SHARED else "windows/raylib.lib", "system:Winmm.lib", "system:Gdi32.lib", "system:User32.lib", "system:Shell32.lib"} + foreign import lib {"zig-out-shared/lib/raylib.lib" when RAYLIB_SHARED else "zig-out-static/lib/raylib.lib", "system:Winmm.lib", "system:Gdi32.lib", "system:User32.lib", "system:Shell32.lib"} } else when ODIN_OS == .Linux { - foreign import lib {// Note(bumbread): I'm not sure why in `linux/` folder there are - // multiple copies of raylib.so, but since these bindings are for - "src/libraylib.so.550" when RAYLIB_SHARED else "src/libraylib.a", "system:dl", "system:pthread"} // particular version of the library, I better specify it. Ideally,// though, it's best specified in terms of major (.so.4) + foreign import lib {"zig-out-shared/lib/libraylib.so" when RAYLIB_SHARED else "zig-out-static/lib/libraylib.a", "system:dl", "system:pthread"} // Note(bumbread): I'm not sure why in `linux/` folder there are// multiple copies of raylib.so, but since these bindings are for// particular version of the library, I better specify it. Ideally,// though, it's best specified in terms of major (.so.4) } else when ODIN_OS == .Darwin { - foreign import lib {"src/libraylib.550.dylib" when RAYLIB_SHARED else "src/libraylib.a", "system:Cocoa.framework", "system:OpenGL.framework", "system:IOKit.framework"} + foreign import lib {"zig-out-shared/lib/libraylib.dylib" when RAYLIB_SHARED else "zig-out-static/lib/libraylib.a", "system:Cocoa.framework", "system:OpenGL.framework", "system:IOKit.framework"} +} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 { + foreign import lib "zig-out-static/lib/libraylib.a" } else { foreign import lib "system:raylib" } diff --git a/libs/raylib/rlgl/rlgl.odin b/libs/raylib/rlgl/rlgl.odin index 901bf68..4e4d7af 100644 --- a/libs/raylib/rlgl/rlgl.odin +++ b/libs/raylib/rlgl/rlgl.odin @@ -121,14 +121,11 @@ RAYLIB_SHARED :: #config(RAYLIB_SHARED, false) when ODIN_OS == .Windows { @(extra_linker_flags = "/NODEFAULTLIB:" + ("msvcrt" when RAYLIB_SHARED else "libcmt")) - foreign import lib {"../windows/raylibdll.lib" when RAYLIB_SHARED else "../windows/raylib.lib", "system:Winmm.lib", "system:Gdi32.lib", "system:User32.lib", "system:Shell32.lib"} + foreign import lib {"../zig-out-shared/lib/raylib.lib" when RAYLIB_SHARED else "../zig-out-static/lib/raylib.lib", "system:Winmm.lib", "system:Gdi32.lib", "system:User32.lib", "system:Shell32.lib"} } else when ODIN_OS == .Linux { - foreign import lib {// Note(bumbread): I'm not sure why in `linux/` folder there are - // multiple copies of raylib.so, but since these bindings are for - "../src/libraylib.so.550" when RAYLIB_SHARED else "../src/libraylib.a", "system:dl", "system:pthread"} // particular version of the library, I better specify it. Ideally,// though, it's best specified in terms of major (.so.4) + foreign import lib {"../zig-out-shared/lib/libraylib.so" when RAYLIB_SHARED else "../zig-out-static/lib/libraylib.a", "system:dl", "system:pthread"} // Note(bumbread): I'm not sure why in `linux/` folder there are// multiple copies of raylib.so, but since these bindings are for// particular version of the library, I better specify it. Ideally,// though, it's best specified in terms of major (.so.4) } else when ODIN_OS == .Darwin { - foreign import lib {"../macos" + ("-arm64" when ODIN_ARCH == - .arm64 else "") + "/libraylib" + (".500.dylib" when RAYLIB_SHARED else ".a"), "system:Cocoa.framework", "system:OpenGL.framework", "system:IOKit.framework"} + foreign import lib {"../zig-out-shared/lib/libraylib.dylib" when RAYLIB_SHARED else "../zig-out-static/lib/libraylib.a", "system:Cocoa.framework", "system:OpenGL.framework", "system:IOKit.framework"} } else { foreign import lib "system:raylib" } diff --git a/libs/tracy/.gitignore b/libs/tracy/.gitignore new file mode 100644 index 0000000..9a93a50 --- /dev/null +++ b/libs/tracy/.gitignore @@ -0,0 +1,5 @@ +tracy.o +tracy.so +tracy.a +tracy.lib +tracy.dll diff --git a/main_web/emscripten_allocator.odin b/main_web/emscripten_allocator.odin new file mode 100644 index 0000000..2672a87 --- /dev/null +++ b/main_web/emscripten_allocator.odin @@ -0,0 +1,144 @@ +/* +This allocator uses the malloc, calloc, free and realloc procs that emscripten +exposes in order to allocate memory. Just like Odin's default heap allocator +this uses proper alignment, so that maps and simd works. +*/ + +package main_web + +import "base:intrinsics" +import "core:c" +import "core:mem" + +// This will create bindings to emscripten's implementation of libc +// memory allocation features. +@(default_calling_convention = "c") +foreign _ { + calloc :: proc(num, size: c.size_t) -> rawptr --- + free :: proc(ptr: rawptr) --- + malloc :: proc(size: c.size_t) -> rawptr --- + realloc :: proc(ptr: rawptr, size: c.size_t) -> rawptr --- +} + +emscripten_allocator :: proc "contextless" () -> mem.Allocator { + return mem.Allocator{emscripten_allocator_proc, nil} +} + +emscripten_allocator_proc :: proc( + allocator_data: rawptr, + mode: mem.Allocator_Mode, + size, alignment: int, + old_memory: rawptr, + old_size: int, + location := #caller_location, +) -> ( + data: []byte, + err: mem.Allocator_Error, +) { + // These aligned alloc procs are almost indentical those in + // `_heap_allocator_proc` in `core:os`. Without the proper alignment you + // cannot use maps and simd features. + + aligned_alloc :: proc( + size, alignment: int, + zero_memory: bool, + old_ptr: rawptr = nil, + ) -> ( + []byte, + mem.Allocator_Error, + ) { + a := max(alignment, align_of(rawptr)) + space := size + a - 1 + + allocated_mem: rawptr + if old_ptr != nil { + original_old_ptr := mem.ptr_offset((^rawptr)(old_ptr), -1)^ + allocated_mem = realloc(original_old_ptr, c.size_t(space + size_of(rawptr))) + } else if zero_memory { + // calloc automatically zeros memory, but it takes a number + size + // instead of just size. + allocated_mem = calloc(c.size_t(space + size_of(rawptr)), 1) + } else { + allocated_mem = malloc(c.size_t(space + size_of(rawptr))) + } + aligned_mem := rawptr(mem.ptr_offset((^u8)(allocated_mem), size_of(rawptr))) + + ptr := uintptr(aligned_mem) + aligned_ptr := (ptr - 1 + uintptr(a)) & -uintptr(a) + diff := int(aligned_ptr - ptr) + if (size + diff) > space || allocated_mem == nil { + return nil, .Out_Of_Memory + } + + aligned_mem = rawptr(aligned_ptr) + mem.ptr_offset((^rawptr)(aligned_mem), -1)^ = allocated_mem + + return mem.byte_slice(aligned_mem, size), nil + } + + aligned_free :: proc(p: rawptr) { + if p != nil { + free(mem.ptr_offset((^rawptr)(p), -1)^) + } + } + + aligned_resize :: proc( + p: rawptr, + old_size: int, + new_size: int, + new_alignment: int, + ) -> ( + []byte, + mem.Allocator_Error, + ) { + if p == nil { + return nil, nil + } + return aligned_alloc(new_size, new_alignment, true, p) + } + + switch mode { + case .Alloc: + return aligned_alloc(size, alignment, true) + + case .Alloc_Non_Zeroed: + return aligned_alloc(size, alignment, false) + + case .Free: + aligned_free(old_memory) + return nil, nil + + case .Resize: + if old_memory == nil { + return aligned_alloc(size, alignment, true) + } + + bytes := aligned_resize(old_memory, old_size, size, alignment) or_return + + // realloc doesn't zero the new bytes, so we do it manually. + if size > old_size { + new_region := raw_data(bytes[old_size:]) + intrinsics.mem_zero(new_region, size - old_size) + } + + return bytes, nil + + case .Resize_Non_Zeroed: + if old_memory == nil { + return aligned_alloc(size, alignment, false) + } + + return aligned_resize(old_memory, old_size, size, alignment) + + case .Query_Features: + set := (^mem.Allocator_Mode_Set)(old_memory) + if set != nil { + set^ = {.Alloc, .Free, .Resize, .Query_Features} + } + return nil, nil + + case .Free_All, .Query_Info: + return nil, .Mode_Not_Implemented + } + return nil, .Mode_Not_Implemented +} diff --git a/main_web/emscripten_logger.odin b/main_web/emscripten_logger.odin new file mode 100644 index 0000000..c97f538 --- /dev/null +++ b/main_web/emscripten_logger.odin @@ -0,0 +1,99 @@ +/* +This logger is largely a copy of the console logger in `core:log`, but it uses +emscripten's `puts` proc to write into he console of the web browser. + +This is more or less identical to the logger in Aronicu's repository: +https://github.com/Aronicu/Raylib-WASM/tree/main +*/ + +package main_web + +import "core:c" +import "core:fmt" +import "core:log" +import "core:strings" + +Emscripten_Logger_Opts :: log.Options{.Level, .Short_File_Path, .Line} + +create_emscripten_logger :: proc( + lowest := log.Level.Debug, + opt := Emscripten_Logger_Opts, +) -> log.Logger { + return log.Logger{data = nil, procedure = logger_proc, lowest_level = lowest, options = opt} +} + +// This create's a binding to `puts` which will be linked in as part of the +// emscripten runtime. +@(default_calling_convention = "c") +foreign _ { + puts :: proc(buffer: cstring) -> c.int --- +} + +@(private = "file") +logger_proc :: proc( + logger_data: rawptr, + level: log.Level, + text: string, + options: log.Options, + location := #caller_location, +) { + b := strings.builder_make(context.temp_allocator) + strings.write_string(&b, Level_Headers[level]) + do_location_header(options, &b, location) + fmt.sbprint(&b, text) + + if bc, bc_err := strings.to_cstring(&b); bc_err == nil { + puts(bc) + } +} + +@(private = "file") +Level_Headers := [?]string { + 0 ..< 10 = "[DEBUG] --- ", + 10 ..< 20 = "[INFO ] --- ", + 20 ..< 30 = "[WARN ] --- ", + 30 ..< 40 = "[ERROR] --- ", + 40 ..< 50 = "[FATAL] --- ", +} + +@(private = "file") +do_location_header :: proc( + opts: log.Options, + buf: ^strings.Builder, + location := #caller_location, +) { + if log.Location_Header_Opts & opts == nil { + return + } + fmt.sbprint(buf, "[") + file := location.file_path + if .Short_File_Path in opts { + last := 0 + for r, i in location.file_path { + if r == '/' { + last = i + 1 + } + } + file = location.file_path[last:] + } + + if log.Location_File_Opts & opts != nil { + fmt.sbprint(buf, file) + } + if .Line in opts { + if log.Location_File_Opts & opts != nil { + fmt.sbprint(buf, ":") + } + fmt.sbprint(buf, location.line) + } + + if .Procedure in opts { + if (log.Location_File_Opts | {.Line}) & opts != nil { + fmt.sbprint(buf, ":") + } + fmt.sbprintf(buf, "%s()", location.procedure) + } + + fmt.sbprint(buf, "] ") +} + diff --git a/main_web/main_web.odin b/main_web/main_web.odin index 20baac3..def0f55 100644 --- a/main_web/main_web.odin +++ b/main_web/main_web.odin @@ -2,45 +2,52 @@ These procs are the ones that will be called from `main_wasm.c`. */ -#+build wasm32, wasm64p32 - package main_web +import game "../game" import "base:runtime" import "core:c" import "core:mem" -import rl "libs:raylib" -import "../game" -@(private="file") -wasm_context: runtime.Context +@(private = "file") +web_context: runtime.Context -// I'm not sure @thread_local works with WASM. We'll see if anyone makes a -// multi-threaded WASM game! -@(private="file") -@thread_local temp_allocator: WASM_Temp_Allocator - -@export -web_init :: proc "c" () { +@(export) +main_start :: proc "c" () { context = runtime.default_context() - context.allocator = rl.MemAllocator() - wasm_temp_allocator_init(&temp_allocator, 1*mem.Megabyte) - context.temp_allocator = wasm_temp_allocator(&temp_allocator) - context.logger = create_wasm_logger() - wasm_context = context + // The WASM allocator doesn't seem to work properly in combination with + // emscripten. There is some kind of conflict with how the manage memory. + // So this sets up an allocator that uses emscripten's malloc. + context.allocator = emscripten_allocator() + runtime.init_global_temporary_allocator(1 * mem.Megabyte) - game.game_init_window() + // Since we now use js_wasm32 we should be able to remove this and use + // context.logger = log.create_console_logger(). However, that one produces + // extra newlines on web. So it's a bug in that core lib. + context.logger = create_emscripten_logger() + + web_context = context + + game.game_init_window({}) game.game_init() } -@export -web_update :: proc "c" () { - context = wasm_context - game.game_update() +@(export) +main_update :: proc "c" () -> bool { + context = web_context + return game.game_update() } -@export -web_window_size_changed :: proc "c" (w: c.int, h: c.int) { - rl.SetWindowSize(w, h) +@(export) +main_end :: proc "c" () { + context = web_context + game.game_shutdown() + game.game_shutdown_window() +} + +@(export) +web_window_size_changed :: proc "c" (w: c.int, h: c.int) { + context = web_context + // game.game_parent_window_size_changed(int(w), int(h)) }