From e9225639a845f45d3d29d96cfbff47857c0ffcbb Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Tue, 20 Feb 2024 00:11:02 +0200 Subject: [PATCH] implement basic wasm support --- README.md | 11 +++ build.zig | 194 ++++++++++++++++++++++++++++++++++++-- libs/raylib | 2 +- src/main-scene.zig | 5 +- src/main.zig | 35 +++---- src/platforms/desktop.zig | 21 +++++ src/platforms/web.zig | 174 ++++++++++++++++++++++++++++++++++ 7 files changed, 413 insertions(+), 29 deletions(-) create mode 100644 README.md create mode 100644 src/platforms/desktop.zig create mode 100644 src/platforms/web.zig diff --git a/README.md b/README.md new file mode 100644 index 0000000..34f76d7 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Flail survivor + +## Compile for desktop +``` +zig build +``` + +## Compile for web +``` +zig build -Doptimize=ReleaseSmall -Dtarget=wasm32-wasi --sysroot "$EMSDK/upstream/emscripten" +``` diff --git a/build.zig b/build.zig index 2ff3ef0..5d67402 100644 --- a/build.zig +++ b/build.zig @@ -1,13 +1,27 @@ const std = @import("std"); const raylib = @import("libs/raylib/build.zig"); +const fs = std.fs; -pub fn build(b: *std.Build) void { +const app_name = "step-kill"; + +const raylibSrc = "libs/raylib/raylib/src/"; +const raylibBindingSrc = "libs/raylib/"; + +pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + try switch (target.getOsTag()) { + .wasi, .emscripten => buildWeb(b, target, optimize), + else => buildDesktop(b, target, optimize) + }; +} + +fn buildDesktop(b: *std.Build, target: std.zig.CrossTarget, optimize: std.builtin.Mode) !void { const exe = b.addExecutable(.{ - .name = "step-kill", - .root_source_file = .{ .path = "src/main.zig" }, + .name = app_name, + .root_source_file = .{ .path = "src/platforms/desktop.zig" }, + .main_pkg_path = .{ .path = "src" }, .target = target, .optimize = optimize, }); @@ -22,11 +36,175 @@ pub fn build(b: *std.Build) void { const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| { - run_cmd.addArgs(args); - } - const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); - +} + +fn buildWeb(b: *std.Build, target: std.zig.CrossTarget, optimize: std.builtin.Mode) !void { + const emscriptenSrc = "libs/raylib/emscripten/"; + const webCachedir = "zig-cache/web/"; + const webOutdir = "zig-out/web/"; + + // TODO: Add depend step on 'git submodule update --recursive' + // TODO: Add depend step on downloading emsdk + + std.log.info("building for emscripten\n", .{}); + if (b.sysroot == null) { + std.log.err("\n\nUSAGE: Please build with 'zig build -Doptimize=ReleaseSmall -Dtarget=wasm32-wasi --sysroot \"$EMSDK/upstream/emscripten\"'\n\n", .{}); + return error.SysRootExpected; + } + const lib = b.addStaticLibrary(.{ + .name = app_name, + .root_source_file = std.Build.LazyPath.relative("src/platforms/web.zig"), + .main_pkg_path = .{ .path = "src" }, + .optimize = optimize, + .target = target, + }); + lib.addIncludePath(.{ .path = raylibSrc }); + + const emcc_file = switch (b.host.target.os.tag) { + .windows => "emcc.bat", + else => "emcc", + }; + const emar_file = switch (b.host.target.os.tag) { + .windows => "emar.bat", + else => "emar", + }; + const emranlib_file = switch (b.host.target.os.tag) { + .windows => "emranlib.bat", + else => "emranlib", + }; + + const emcc_path = try fs.path.join(b.allocator, &.{ b.sysroot.?, emcc_file }); + defer b.allocator.free(emcc_path); + const emranlib_path = try fs.path.join(b.allocator, &.{ b.sysroot.?, emranlib_file }); + defer b.allocator.free(emranlib_path); + const emar_path = try fs.path.join(b.allocator, &.{ b.sysroot.?, emar_file }); + defer b.allocator.free(emar_path); + const include_path = try fs.path.join(b.allocator, &.{ b.sysroot.?, "cache", "sysroot", "include" }); + defer b.allocator.free(include_path); + + fs.cwd().makePath(webCachedir) catch {}; + fs.cwd().makePath(webOutdir) catch {}; + + const warnings = ""; //-Wall + + const rcoreO = b.addSystemCommand(&.{ emcc_path, "-Os", warnings, "-c", raylibSrc ++ "rcore.c", "-o", webCachedir ++ "rcore.o", "-Os", warnings, "-DPLATFORM_WEB", "-DGRAPHICS_API_OPENGL_ES2" }); + const rshapesO = b.addSystemCommand(&.{ emcc_path, "-Os", warnings, "-c", raylibSrc ++ "rshapes.c", "-o", webCachedir ++ "rshapes.o", "-Os", warnings, "-DPLATFORM_WEB", "-DGRAPHICS_API_OPENGL_ES2" }); + const rtexturesO = b.addSystemCommand(&.{ emcc_path, "-Os", warnings, "-c", raylibSrc ++ "rtextures.c", "-o", webCachedir ++ "rtextures.o", "-Os", warnings, "-DPLATFORM_WEB", "-DGRAPHICS_API_OPENGL_ES2" }); + const rtextO = b.addSystemCommand(&.{ emcc_path, "-Os", warnings, "-c", raylibSrc ++ "rtext.c", "-o", webCachedir ++ "rtext.o", "-Os", warnings, "-DPLATFORM_WEB", "-DGRAPHICS_API_OPENGL_ES2" }); + const rmodelsO = b.addSystemCommand(&.{ emcc_path, "-Os", warnings, "-c", raylibSrc ++ "rmodels.c", "-o", webCachedir ++ "rmodels.o", "-Os", warnings, "-DPLATFORM_WEB", "-DGRAPHICS_API_OPENGL_ES2" }); + const utilsO = b.addSystemCommand(&.{ emcc_path, "-Os", warnings, "-c", raylibSrc ++ "utils.c", "-o", webCachedir ++ "utils.o", "-Os", warnings, "-DPLATFORM_WEB" }); + const raudioO = b.addSystemCommand(&.{ emcc_path, "-Os", warnings, "-c", raylibSrc ++ "raudio.c", "-o", webCachedir ++ "raudio.o", "-Os", warnings, "-DPLATFORM_WEB" }); + + const libraylibA = b.addSystemCommand(&.{ + emar_path, + "rcs", + webCachedir ++ "libraylib.a", + webCachedir ++ "rcore.o", + webCachedir ++ "rshapes.o", + webCachedir ++ "rtextures.o", + webCachedir ++ "rtext.o", + webCachedir ++ "rmodels.o", + webCachedir ++ "utils.o", + webCachedir ++ "raudio.o", + }); + const emranlib = b.addSystemCommand(&.{ + emranlib_path, + webCachedir ++ "libraylib.a", + }); + + libraylibA.step.dependOn(&rcoreO.step); + libraylibA.step.dependOn(&rshapesO.step); + libraylibA.step.dependOn(&rtexturesO.step); + libraylibA.step.dependOn(&rtextO.step); + libraylibA.step.dependOn(&rmodelsO.step); + libraylibA.step.dependOn(&utilsO.step); + libraylibA.step.dependOn(&raudioO.step); + emranlib.step.dependOn(&libraylibA.step); + + //only build raylib if not already there + _ = fs.cwd().statFile(webCachedir ++ "libraylib.a") catch { + lib.step.dependOn(&emranlib.step); + }; + + lib.defineCMacro("__EMSCRIPTEN__", null); + lib.defineCMacro("PLATFORM_WEB", null); + std.log.info("emscripten include path: {s}", .{include_path}); + lib.addIncludePath(.{ .path = include_path }); + lib.addIncludePath(.{ .path = emscriptenSrc }); + lib.addIncludePath(.{ .path = raylibBindingSrc }); + lib.addIncludePath(.{ .path = raylibSrc }); + lib.addIncludePath(.{ .path = raylibSrc ++ "extras/" }); + lib.addAnonymousModule("raylib", .{ .source_file = .{ .path = raylibBindingSrc ++ "raylib.zig" } }); + // lib.root_module.addAnonymousImport("raylib", .{ .root_source_file = .{ .path = raylibBindingSrc ++ "raylib.zig" } }); + + const libraryOutputFolder = "zig-out/lib/"; + // this installs the lib (libraylib-zig-examples.a) to the `libraryOutputFolder` folder + b.installArtifact(lib); + + const shell = switch (optimize) { + .Debug => emscriptenSrc ++ "shell.html", + else => emscriptenSrc ++ "minshell.html", + }; + + const emcc = b.addSystemCommand(&.{ + emcc_path, + "-o", + webOutdir ++ "game.html", + + emscriptenSrc ++ "entry.c", + raylibBindingSrc ++ "marshal.c", + + libraryOutputFolder ++ "lib" ++ app_name ++ ".a", + "-I.", + "-I" ++ raylibSrc, + "-I" ++ emscriptenSrc, + "-I" ++ raylibBindingSrc, + "-L.", + "-L" ++ webCachedir, + "-L" ++ libraryOutputFolder, + "-lraylib", + "-l" ++ app_name, + "--shell-file", + shell, + "-DPLATFORM_WEB", + "-sUSE_GLFW=3", + "-sWASM=1", + "-sALLOW_MEMORY_GROWTH=1", + "-sWASM_MEM_MAX=512MB", //going higher than that seems not to work on iOS browsers ¯\_(ツ)_/¯ + "-sTOTAL_MEMORY=512MB", + "-sABORTING_MALLOC=0", + "-sASYNCIFY", + "-sFORCE_FILESYSTEM=1", + "-sASSERTIONS=1", + // "--memory-init-file", + // "0", + // "--preload-file", + // "assets", + // "--source-map-base", + "-O1", + "-Os", + // "-sLLD_REPORT_UNDEFINED", + "-sERROR_ON_UNDEFINED_SYMBOLS=0", + + // optimizations + "-O1", + "-Os", + + // "-sUSE_PTHREADS=1", + // "--profiling", + // "-sTOTAL_STACK=128MB", + // "-sMALLOC='emmalloc'", + // "--no-entry", + "-sEXPORTED_FUNCTIONS=['_malloc','_free','_main', '_emsc_main','_emsc_set_window_size']", + "-sEXPORTED_RUNTIME_METHODS=ccall,cwrap", + }); + + emcc.step.dependOn(&lib.step); + + b.getInstallStep().dependOn(&emcc.step); + //------------------------------------------------------------------------------------- + + std.log.info("\n\nOutput files will be in {s}\n---\ncd {s}\npython -m http.server\n---\n\nbuilding...", .{ webOutdir, webOutdir }); } diff --git a/libs/raylib b/libs/raylib index 97f5012..26a0f45 160000 --- a/libs/raylib +++ b/libs/raylib @@ -1 +1 @@ -Subproject commit 97f501240b3b23fa338ae589d484e914270f13eb +Subproject commit 26a0f453914b84462bd8b07b6545810780319051 diff --git a/src/main-scene.zig b/src/main-scene.zig index fef90be..dc58abb 100644 --- a/src/main-scene.zig +++ b/src/main-scene.zig @@ -212,7 +212,6 @@ enemies: std.ArrayList(Enemy), player: Player, rope: Rope, ui: UI, -should_close: bool = false, paused: bool = false, pixel_effect: PixelPerfect, @@ -477,7 +476,7 @@ fn tickUI(self: *Self) !void { } if (self.ui.button("Exit?", content.left(), content.bottom() - 30, content.width, 30)) { - self.should_close = true; + return error.Exit; } } else if (self.paused) { const modal_size = rl.Vector2{ .x = 200, .y = 200 }; @@ -503,7 +502,7 @@ fn tickUI(self: *Self) !void { } if (self.ui.button("Exit?", content.left(), content.top() + 120, content.width, 30)) { - self.should_close = true; + return error.Exit; } } } diff --git a/src/main.zig b/src/main.zig index 87f0f2c..d6a50a7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,26 +3,27 @@ const std = @import("std"); const MainScene = @import("main-scene.zig"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - var allocator = gpa.allocator(); - defer _ = gpa.deinit(); +scene: MainScene, +pub fn init(allocator: std.mem.Allocator) @This() { rl.SetTargetFPS(60); - rl.SetConfigFlags(rl.ConfigFlags{ - .FLAG_WINDOW_RESIZABLE = true, - }); + rl.SetConfigFlags(rl.ConfigFlags{ .FLAG_WINDOW_RESIZABLE = true }); rl.InitWindow(1200, 1200, "Step kill"); rl.SetExitKey(.KEY_NULL); - defer rl.CloseWindow(); - var scene = MainScene.init(allocator); - defer scene.deinit(); - - while (!rl.WindowShouldClose() and !scene.should_close) { - rl.BeginDrawing(); - defer rl.EndDrawing(); - - try scene.tick(); - } + return @This(){ + .scene = MainScene.init(allocator) + }; +} + +pub fn deinit(self: *@This()) void { + self.scene.deinit(); + rl.CloseWindow(); +} + +pub fn tick(self: *@This()) !void { + rl.BeginDrawing(); + defer rl.EndDrawing(); + + try self.scene.tick(); } diff --git a/src/platforms/desktop.zig b/src/platforms/desktop.zig new file mode 100644 index 0000000..c684539 --- /dev/null +++ b/src/platforms/desktop.zig @@ -0,0 +1,21 @@ +const rl = @import("raylib"); +const std = @import("std"); + +const MainEntry = @import("../main.zig"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + var main_entry = MainEntry.init(allocator); + defer main_entry.deinit(); + + while (!rl.WindowShouldClose()) { + main_entry.tick() catch |err| { + if (err == error.Exit) { break; } + return err; + }; + } +} + diff --git a/src/platforms/web.zig b/src/platforms/web.zig new file mode 100644 index 0000000..c9f20e1 --- /dev/null +++ b/src/platforms/web.zig @@ -0,0 +1,174 @@ +const rl = @import("raylib"); +const std = @import("std"); +const emsdk = @cImport({ + @cDefine("__EMSCRIPTEN__", "1"); + @cDefine("PLATFORM_WEB", "1"); + @cInclude("emscripten/emscripten.h"); +}); +const Allocator = std.mem.Allocator; +const mem = std.mem; +const assert = std.debug.assert; + +const MainEntry = @import("../main.zig"); + +var main_entry: MainEntry = undefined; + +//// special entry point for Emscripten build, called from src/marshall/emscripten_entry.c +export fn emsc_main() callconv(.C) c_int { + return safeMain() catch |err| { + std.log.err("ERROR: {?}", .{err}); + return 1; + }; +} + +export fn emsc_set_window_size(width: c_int, height: c_int) callconv(.C) void { + rl.SetWindowSize(@intCast(width), @intCast(height)); +} +export fn gameLoop() callconv(.C) void { + main_entry.tick() catch |err| std.log.err("ERROR: {?}", .{err}); +} + +fn safeMain() !c_int { + var allocator = Allocator{ + .ptr = undefined, + .vtable = &e_allocator_vtable, + }; + // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + // var allocator = gpa.allocator(); + // defer _ = gpa.deinit(); + + main_entry = MainEntry.init(allocator); + defer main_entry.deinit(); + + emsdk.emscripten_set_main_loop(gameLoop, 0, 1); + return 0; +} + +const e_allocator_vtable = Allocator.VTable{ + .alloc = EmscriptenAllocator.alloc, + .resize = EmscriptenAllocator.resize, + .free = EmscriptenAllocator.free, +}; + +/// basically copied the std.heap.c_allocator and replaced with emscripten malloc & free +const EmscriptenAllocator = struct { + const c = @cImport({ + @cDefine("__EMSCRIPTEN__", "1"); + @cInclude("emscripten/emscripten.h"); + @cInclude("stdlib.h"); + }); + + usingnamespace if (@hasDecl(c, "malloc_size")) + struct { + pub const supports_malloc_size = true; + pub const malloc_size = c.malloc_size; + } + else if (@hasDecl(c, "malloc_usable_size")) + struct { + pub const supports_malloc_size = true; + pub const malloc_size = c.malloc_usable_size; + } + else if (@hasDecl(c, "_msize")) + struct { + pub const supports_malloc_size = true; + pub const malloc_size = c._msize; + } + else + struct { + pub const supports_malloc_size = false; + }; + + pub const supports_posix_memalign = @hasDecl(c, "posix_memalign"); + + fn getHeader(ptr: [*]u8) *[*]u8 { + return @as(*[*]u8, @ptrFromInt(@intFromPtr(ptr) - @sizeOf(usize))); + } + + fn alignedAlloc(len: usize, log2_align: u8) ?[*]u8 { + const alignment = @as(usize, 1) << @as(Allocator.Log2Align, @intCast(log2_align)); + if (supports_posix_memalign) { + // The posix_memalign only accepts alignment values that are a + // multiple of the pointer size + const eff_alignment = @max(alignment, @sizeOf(usize)); + + var aligned_ptr: ?*anyopaque = undefined; + if (c.posix_memalign(&aligned_ptr, eff_alignment, len) != 0) + return null; + + return @as([*]u8, @ptrCast(aligned_ptr)); + } + + // Thin wrapper around regular malloc, overallocate to account for + // alignment padding and store the orignal malloc()'ed pointer before + // the aligned address. + const unaligned_ptr = @as([*]u8, @ptrCast(c.malloc(len + alignment - 1 + @sizeOf(usize)) orelse return null)); + const unaligned_addr = @intFromPtr(unaligned_ptr); + const aligned_addr = mem.alignForward(unaligned_addr + @sizeOf(usize), alignment); + const aligned_ptr = unaligned_ptr + (aligned_addr - unaligned_addr); + getHeader(aligned_ptr).* = unaligned_ptr; + + return aligned_ptr; + } + + fn alignedFree(ptr: [*]u8) void { + if (supports_posix_memalign) { + return c.free(ptr); + } + + const unaligned_ptr = getHeader(ptr).*; + c.free(unaligned_ptr); + } + + fn alignedAllocSize(ptr: [*]u8) usize { + if (supports_posix_memalign) { + return EmscriptenAllocator.malloc_size(ptr); + } + + const unaligned_ptr = getHeader(ptr).*; + const delta = @intFromPtr(ptr) - @intFromPtr(unaligned_ptr); + return EmscriptenAllocator.malloc_size(unaligned_ptr) - delta; + } + + fn alloc( + _: *anyopaque, + len: usize, + log2_align: u8, + return_address: usize, + ) ?[*]u8 { + _ = return_address; + assert(len > 0); + return alignedAlloc(len, log2_align); + } + + fn resize( + _: *anyopaque, + buf: []u8, + log2_buf_align: u8, + new_len: usize, + return_address: usize, + ) bool { + _ = log2_buf_align; + _ = return_address; + if (new_len <= buf.len) { + return true; + } + if (@hasDecl(c, "malloc_size")) { + const full_len = alignedAllocSize(buf.ptr); + if (new_len <= full_len) { + return true; + } + } + return false; + } + + fn free( + _: *anyopaque, + buf: []u8, + log2_buf_align: u8, + return_address: usize, + ) void { + _ = log2_buf_align; + _ = return_address; + alignedFree(buf.ptr); + } +};