diff --git a/build.zig b/build.zig index 45979a946..8df41371f 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); const std = @import("std"); +pub const zems = @import("libs/zems/build.zig"); pub const min_zig_version = std.SemanticVersion{ .major = 0, .minor = 11, .patch = 0, .pre = "dev.4332" }; @@ -23,7 +24,7 @@ pub fn build(b: *std.Build) void { ) orelse false, .zpix_enable = b.option(bool, "zpix-enable", "Enable PIX for Windows profiler") orelse false, }; - ensureTarget(options.target) catch return; + ensureTarget(options) catch return; ensureGit(b.allocator) catch return; ensureGitLfs(b.allocator, "install") catch return; ensureGitLfs(b.allocator, "pull") catch return; @@ -47,7 +48,8 @@ pub fn build(b: *std.Build) void { // // Sample applications // - samplesCrossPlatform(b, options); + if (options.target.getOsTag() == .emscripten) samplesEmscripten(b, options) + else samplesCrossPlatform(b, options); if (options.target.isWindows() and (builtin.target.os.tag == .windows or builtin.target.os.tag == .linux)) @@ -81,15 +83,9 @@ fn packagesCrossPlatform(b: *std.Build, options: Options) void { const target = options.target; const optimize = options.optimize; - zopengl_pkg = zopengl.package(b, target, optimize, .{}); zmath_pkg = zmath.package(b, target, optimize, .{}); zpool_pkg = zpool.package(b, target, optimize, .{}); zglfw_pkg = zglfw.package(b, target, optimize, .{}); - zsdl_pkg = zsdl.package(b, target, optimize, .{}); - zmesh_pkg = zmesh.package(b, target, optimize, .{}); - znoise_pkg = znoise.package(b, target, optimize, .{}); - zstbi_pkg = zstbi.package(b, target, optimize, .{}); - zbullet_pkg = zbullet.package(b, target, optimize, .{}); zgui_pkg = zgui.package(b, target, optimize, .{ .options = .{ .backend = .glfw_wgpu }, }); @@ -97,12 +93,22 @@ fn packagesCrossPlatform(b: *std.Build, options: Options) void { .options = .{ .uniforms_buffer_size = 4 * 1024 * 1024 }, .deps = .{ .zpool = zpool_pkg.zpool, .zglfw = zglfw_pkg.zglfw }, }); + zems_pkg = zems.package(b, target, optimize, .{}); ztracy_pkg = ztracy.package(b, target, optimize, .{ .options = .{ - .enable_ztracy = !target.isDarwin(), // TODO: ztracy fails to compile on macOS. - .enable_fibers = !target.isDarwin(), + .enable_ztracy = !target.isDarwin() and options.target.getOsTag() != .emscripten, // TODO: ztracy fails to compile on macOS. + .enable_fibers = !target.isDarwin() and options.target.getOsTag() != .emscripten, }, }); + zstbi_pkg = zstbi.package(b, target, optimize, .{}); + + if (options.target.getOsTag() == .emscripten) return; + + zopengl_pkg = zopengl.package(b, target, optimize, .{}); + zsdl_pkg = zsdl.package(b, target, optimize, .{}); + zmesh_pkg = zmesh.package(b, target, optimize, .{}); + znoise_pkg = znoise.package(b, target, optimize, .{}); + zbullet_pkg = zbullet.package(b, target, optimize, .{}); zphysics_pkg = zphysics.package(b, target, optimize, .{}); zaudio_pkg = zaudio.package(b, target, optimize, .{}); zflecs_pkg = zflecs.package(b, target, optimize, .{}); @@ -165,6 +171,7 @@ fn samplesCrossPlatform(b: *std.Build, options: Options) void { const gamepad_wgpu = @import("samples/gamepad_wgpu/build.zig"); const physics_test_wgpu = @import("samples/physics_test_wgpu/build.zig"); const monolith = @import("samples/monolith/build.zig"); + const triangle_wgpu_emscripten = @import("samples/triangle_wgpu_emscripten/build.zig"); install(b, minimal_gl.build(b, options), "minimal_gl"); install(b, triangle_wgpu.build(b, options), "triangle_wgpu"); @@ -179,6 +186,17 @@ fn samplesCrossPlatform(b: *std.Build, options: Options) void { install(b, physics_test_wgpu.build(b, options), "physics_test_wgpu"); install(b, monolith.build(b, options), "monolith"); install(b, audio_experiments_wgpu.build(b, options), "audio_experiments_wgpu"); + install(b, triangle_wgpu_emscripten.build(b, options), "triangle_wgpu_emscripten"); +} + +fn samplesEmscripten(b: *std.Build, options: Options) void { + const triangle_wgpu_emscripten = @import("samples/triangle_wgpu_emscripten/build.zig"); + const gui_test_wgpu = @import("samples/gui_test_wgpu/build.zig"); + const instanced_pills_wgpu = @import("samples/instanced_pills_wgpu/build.zig"); + + installEmscripten(b, triangle_wgpu_emscripten.buildEmscripten(b, options), "triangle_wgpu_emscripten"); + installEmscripten(b, gui_test_wgpu.buildEmscripten(b, options), "gui_test_wgpu"); + installEmscripten(b, instanced_pills_wgpu.buildEmscripten(b, options), "instanced_pills_wgpu"); } fn samplesWindowsLinux(b: *std.Build, options: Options) void { @@ -247,6 +265,7 @@ pub var ztracy_pkg: ztracy.Package = undefined; pub var zphysics_pkg: zphysics.Package = undefined; pub var zaudio_pkg: zaudio.Package = undefined; pub var zflecs_pkg: zflecs.Package = undefined; +pub var zems_pkg : zems.Package = undefined; pub var zwin32_pkg: zwin32.Package = undefined; pub var zd3d12_pkg: zd3d12.Package = undefined; @@ -291,7 +310,7 @@ pub const Options = struct { fn install(b: *std.Build, exe: *std.Build.CompileStep, comptime name: []const u8) void { // TODO: Problems with LTO on Windows. exe.want_lto = false; - if (exe.optimize == .ReleaseFast) + if (exe.optimize == .ReleaseFast or exe.optimize == .ReleaseSmall) exe.strip = true; //comptime var desc_name: [256]u8 = [_]u8{0} ** 256; @@ -309,6 +328,12 @@ fn install(b: *std.Build, exe: *std.Build.CompileStep, comptime name: []const u8 b.getInstallStep().dependOn(install_step); } +fn installEmscripten(b: *std.Build, exe: *zems.EmscriptenStep, comptime name: []const u8) void { + const install_step = b.step(name, "Build '" ++ name ++ "' demo"); + install_step.dependOn(&exe.link_step.?.step); + b.getInstallStep().dependOn(install_step); +} + fn ensureZigVersion() !void { var installed_ver = @import("builtin").zig_version; installed_ver.build = null; @@ -332,7 +357,8 @@ fn ensureZigVersion() !void { } } -fn ensureTarget(cross: std.zig.CrossTarget) !void { +fn ensureTarget(options: Options) !void { + const cross = options.target; const target = (std.zig.system.NativeTargetInfo.detect(cross) catch unreachable).target; const supported = switch (target.os.tag) { @@ -348,6 +374,7 @@ fn ensureTarget(cross: std.zig.CrossTarget) !void { ) == .lt) break :blk false; break :blk true; }, + .emscripten => target.cpu.arch == .wasm32, else => false, }; if (!supported) { diff --git a/libs/zems/README.md b/libs/zems/README.md new file mode 100644 index 000000000..f0192ee52 --- /dev/null +++ b/libs/zems/README.md @@ -0,0 +1 @@ +# zems - emscripten binding helpers and utilities diff --git a/libs/zems/build.zig b/libs/zems/build.zig new file mode 100644 index 000000000..55843d8b7 --- /dev/null +++ b/libs/zems/build.zig @@ -0,0 +1,179 @@ +const std = @import("std"); + +pub const Package = struct { + module: *std.Build.Module, + emscripten: bool, + + pub fn link(pkg: Package, exe: *std.Build.CompileStep) void { + exe.addModule("zems", pkg.module); + } +}; + +pub fn package( + b: *std.Build, + target: std.zig.CrossTarget, + optimize: std.builtin.Mode, + args: struct {}, +) Package { + _ = optimize; + _ = args; + const emscripten = target.getOsTag() == .emscripten; + const src_root = if (emscripten) "zems.zig" else "dummy.zig"; + var module = b.createModule(.{ + .source_file = .{ .path = b.pathJoin(&.{ thisDir(), src_root }) }, + }); + return .{ + .module = module, + .emscripten = emscripten, + }; +} + +// +// Build utils +// + +pub const EmscriptenStep = struct { + b: *std.Build.Builder, + args: EmscriptenArgs, + out_path: ?[]const u8 = null, // zig-out/web/ if unset + lib_exe: ?*std.Build.CompileStep = null, + link_step: ?*std.Build.Step.Run = null, + emsdk_path: []const u8, + emsdk_include_path: []const u8, + + pub fn init(b: *std.Build.Builder) *@This() { + const emsdk_path = b.env_map.get("EMSDK") orelse @panic("Failed to get emscripten SDK path, have you installed & sourced the SDK?"); + var r = b.allocator.create(EmscriptenStep) catch unreachable; + r.* = .{ + .b = b, + .args = EmscriptenArgs.init(b.allocator), + .emsdk_path = emsdk_path, + .emsdk_include_path = b.pathJoin(&.{ emsdk_path, "upstream", "emscripten", "cache", "sysroot", "include" }), + }; + return r; + } + + pub fn link(self: *@This(), exe: *std.Build.CompileStep) void { + std.debug.assert(self.lib_exe == null); + std.debug.assert(self.link_step == null); + const b = self.b; + + exe.addSystemIncludePath(.{ .path = self.emsdk_include_path }); + exe.stack_protector = false; + exe.disable_stack_probing = true; + exe.linkLibC(); + + const emlink = b.addSystemCommand(&.{"emcc"}); + emlink.addArtifactArg(exe); + for (exe.link_objects.items) |link_dependency| { + switch (link_dependency) { + .other_step => |o| emlink.addArtifactArg(o), + // .c_source_file => |f| emlink.addFileSourceArg(f.source), // f.args? + // .c_source_files => |fs| for (fs.files) |f| emlink.addArg(f), // fs.flags? + else => {}, + } + } + const out_path: []const u8 = self.out_path orelse b.pathJoin(&.{ b.pathFromRoot("."), "zig-out", "web", exe.name }); + std.fs.cwd().makePath(out_path) catch unreachable; + const out_file = std.mem.concat(b.allocator, u8, &.{ out_path, std.fs.path.sep_str ++ "index.html" }) catch unreachable; + emlink.addArgs(&.{ "-o", out_file }); + + if (self.args.exported_functions.items.len > 0) { + var s = std.mem.join(self.b.allocator, "','", self.args.exported_functions.items) catch unreachable; + var ss = std.fmt.allocPrint(self.b.allocator, "-sEXPORTED_FUNCTIONS=['{s}']", .{s}) catch unreachable; + emlink.addArg(ss); + } + + var op_it = self.args.options.iterator(); + while (op_it.next()) |opt| { + if (opt.value_ptr.*.len > 0) { + emlink.addArg(std.mem.join(b.allocator, "", &.{ "-s", opt.key_ptr.*, "=", opt.value_ptr.* }) catch unreachable); + } else { + emlink.addArg(std.mem.join(b.allocator, "", &.{ "-s", opt.key_ptr.* }) catch unreachable); + } + } + + emlink.addArgs(self.args.other_args.items); + + if (self.args.shell_file) |sf| emlink.addArgs(&.{ "--shell-file", sf }); + + emlink.step.dependOn(&exe.step); + self.link_step = emlink; + self.lib_exe = exe; + } +}; + +pub const EmscriptenArgs = struct { + source_map_base: []const u8 = "./", + shell_file: ?[]const u8 = null, + + exported_functions: std.ArrayList([]const u8), + options: std.StringHashMap([]const u8), // in args formated as -sKEY=VALUE + other_args: std.ArrayList([]const u8), + + pub fn init(alloc: std.mem.Allocator) @This() { + return .{ + .exported_functions = std.ArrayList([]const u8).init(alloc), + .options = std.StringHashMap([]const u8).init(alloc), + .other_args = std.ArrayList([]const u8).init(alloc), + }; + } + + /// set common args + /// param max_optimize - use maximum optimizations which are much slower to compile + pub fn setDefault(self: *@This(), build_mode: std.builtin.Mode, max_optimizations: bool) void { + //_ = r.options.fetchPut("FILESYSTEM", "0") catch unreachable; + _ = self.options.getOrPutValue("ASYNCIFY", "") catch unreachable; + _ = self.options.getOrPutValue("EXIT_RUNTIME", "0") catch unreachable; + _ = self.options.getOrPutValue("WASM_BIGINT", "") catch unreachable; + _ = self.options.getOrPutValue("MALLOC", "emmalloc") catch unreachable; + _ = self.options.getOrPutValue("ABORTING_MALLOC", "0") catch unreachable; + _ = self.options.getOrPutValue("INITIAL_MEMORY", "64MB") catch unreachable; + _ = self.options.getOrPutValue("ALLOW_MEMORY_GROWTH", "1") catch unreachable; + + self.shell_file = thisDir() ++ std.fs.path.sep_str ++ "shell_minimal.html"; + self.exported_functions.appendSlice(&.{ "_main", "_malloc", "_free" }) catch unreachable; + + self.other_args.append("-fno-rtti") catch unreachable; + self.other_args.append("-fno-exceptions") catch unreachable; + switch (build_mode) { + .Debug => { + self.other_args.append("-g") catch unreachable; + self.other_args.appendSlice(&.{ "--closure", "0" }) catch unreachable; + const source_map_base: []const u8 = "./"; + self.other_args.appendSlice(&.{ "-gsource-map", "--source-map-base", source_map_base }) catch unreachable; + }, + .ReleaseSmall => { + if (max_optimizations) self.other_args.append("-Oz") catch unreachable else self.other_args.append("-Os") catch unreachable; + }, + .ReleaseSafe, .ReleaseFast => { + if (max_optimizations) { + self.other_args.append("-O3") catch unreachable; + self.other_args.append("-flto") catch unreachable; + } else { + self.other_args.append("-O2") catch unreachable; + } + }, + } + } + + /// sets option argument to specific value if its not set or assert if value differs + pub fn setOrAssertOption(self: *@This(), key: []const u8, value: []const u8) void { + var v = self.options.getOrPut(key) catch unreachable; + if (v.found_existing) { + if (!std.ascii.eqlIgnoreCase(v.value_ptr.*, value)) { + std.debug.panic("Emscripten argument conflict: want `{s}` to be `{s}` but `{s}` was already set", .{ key, value, v.key_ptr.* }); + } + } else { + v.value_ptr.* = value; + } + } + + pub fn overwriteOption(self: *@This(), key: []const u8, value: []const u8) void { + _ = self.options.fetchPut(key, value); + } +}; + +inline fn thisDir() []const u8 { + return comptime std.fs.path.dirname(@src().file) orelse "."; +} diff --git a/libs/zems/dummy.zig b/libs/zems/dummy.zig new file mode 100644 index 000000000..5ec4eaf10 --- /dev/null +++ b/libs/zems/dummy.zig @@ -0,0 +1,2 @@ +// dummy interface for non emscripten builds +pub const is_emscripten = false; diff --git a/libs/zems/zems.zig b/libs/zems/zems.zig new file mode 100644 index 000000000..b058e1f3e --- /dev/null +++ b/libs/zems/zems.zig @@ -0,0 +1,97 @@ +pub const is_emscripten = true; + +const std = @import("std"); +/// For emscripten specific api see docs: +/// https://emscripten.org/docs/api_reference/index.html +pub const c = @cImport({ + @cInclude("emscripten/emscripten.h"); + @cInclude("emscripten/console.h"); + @cInclude("emscripten/html5.h"); + @cInclude("emscripten/emmalloc.h"); +}); +pub usingnamespace c; + + + +/// EmmalocAllocator allocator +/// use with linker flag -sMALLOC=emmalloc +/// for details see docs: https://github.com/emscripten-core/emscripten/blob/main/system/lib/emmalloc.c +pub const EmmalocAllocator = struct { + const Self = @This(); + dummy: u32 = undefined, + + pub fn allocator(self: *Self) std.mem.Allocator { + return .{ + .ptr = self, + .vtable = &.{ + .alloc = &alloc, + .resize = &resize, + .free = &free, + }, + }; + } + + fn alloc( + ctx: *anyopaque, + len: usize, + ptr_align_log2: u8, + return_address: usize, + ) ?[*]u8 { + _ = ctx; + _ = return_address; + const ptr_align: u32 = @as(u32, 1) << @as(u5, @intCast(ptr_align_log2)); + if (!std.math.isPowerOfTwo(ptr_align)) unreachable; + const ptr = c.emmalloc_memalign(ptr_align, len) orelse return null; + return @ptrCast(ptr); + } + + fn resize( + ctx: *anyopaque, + buf: []u8, + buf_align_log2: u8, + new_len: usize, + return_address: usize, + ) bool { + _ = ctx; + _ = return_address; + _ = buf_align_log2; + return c.emmalloc_realloc_try(buf.ptr, new_len) != null; + } + + fn free( + ctx: *anyopaque, + buf: []u8, + buf_align_log2: u8, + return_address: usize, + ) void { + _ = ctx; + _ = buf_align_log2; + _ = return_address; + return c.emmalloc_free(buf.ptr); + } +}; + +/// std.log function that writes to dev-tools console +pub fn emscriptenLog( + comptime level: std.log.Level, + comptime scope: @TypeOf(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + const level_txt = comptime level.asText(); + const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; + const prefix = level_txt ++ prefix2; + + var buf: [1024 * 8]u8 = undefined; + var slice = std.fmt.bufPrint(buf[0 .. buf.len - 1], prefix ++ format, args) catch { + c.emscripten_console_error("emscriptenLog: formatting message failed - log message skipped!"); + return; + }; + buf[slice.len] = 0; + switch (level) { + .err => c.emscripten_console_error(@ptrCast(slice.ptr)), + .warn => c.emscripten_console_warn(@ptrCast(slice.ptr)), + else => c.emscripten_console_log(@ptrCast(slice.ptr)), + } +} + diff --git a/libs/zglfw/build.zig b/libs/zglfw/build.zig index 991de13d9..680e00a78 100644 --- a/libs/zglfw/build.zig +++ b/libs/zglfw/build.zig @@ -3,12 +3,15 @@ const std = @import("std"); pub const Package = struct { zglfw: *std.Build.Module, zglfw_c_cpp: *std.Build.CompileStep, + options: Options, pub fn link(pkg: Package, exe: *std.Build.CompileStep) void { exe.addModule("zglfw", pkg.zglfw); const host = (std.zig.system.NativeTargetInfo.detect(exe.target) catch unreachable).target; + if (host.os.tag == .emscripten or host.os.tag == .freestanding) return; // emscripten + switch (host.os.tag) { .windows => {}, .macos => { @@ -53,6 +56,15 @@ pub fn package( .source_file = .{ .path = thisDir() ++ "/src/zglfw.zig" }, }); + // currently at link stage freestanding target is assumed to be emscripten + // if non emscripten .freestanding target is being implemented then this needs to be changed + std.debug.assert(target.getOsTag() != .freestanding or target.getCpuArch() == .wasm32); + if (target.getOsTag() == .emscripten) return .{ + .zglfw = zglfw, + .zglfw_c_cpp = undefined, + .options = args.options, + }; + const zglfw_c_cpp = if (args.options.shared) blk: { const lib = b.addSharedLibrary(.{ .name = "zglfw", @@ -166,6 +178,7 @@ pub fn package( return .{ .zglfw = zglfw, .zglfw_c_cpp = zglfw_c_cpp, + .options = args.options, }; } diff --git a/libs/zglfw/src/zglfw.zig b/libs/zglfw/src/zglfw.zig index 05343c5bd..1f9e562f1 100644 --- a/libs/zglfw/src/zglfw.zig +++ b/libs/zglfw/src/zglfw.zig @@ -1019,3 +1019,20 @@ test "zglfw.basic" { try maybeError(); } //-------------------------------------------------------------------------------------------------- + +usingnamespace if (@import("builtin").target.os.tag == .emscripten or @import("builtin").target.os.tag == .freestanding) struct { + // GLFW - emscripten uses older version that doesn't have these functions - implement dummies + var glfwGetGamepadStateWarnPrinted: bool = false; + pub export fn glfwGetGamepadState(_: i32, _: ?*anyopaque) i32 { + if (!glfwGetGamepadStateWarnPrinted) { + std.log.err("glfwGetGamepadState(): not implemented! Use emscripten specific functions: https://emscripten.org/docs/api_reference/html5.h.html?highlight=gamepadstate#c.emscripten_get_gamepad_status", .{}); + glfwGetGamepadStateWarnPrinted = true; + } + return 0; // false - failure + } + + /// use glfwSetCallback instead + pub export fn glfwGetError() i32 { + return 0; // no error + } +} else struct {}; diff --git a/libs/zgpu/build.zig b/libs/zgpu/build.zig index c5e121d39..70c69c073 100644 --- a/libs/zgpu/build.zig +++ b/libs/zgpu/build.zig @@ -66,7 +66,12 @@ pub const Package = struct { exe.linkFramework("IOSurface"); exe.linkFramework("QuartzCore"); }, - else => unreachable, + .emscripten, .freestanding => { + // assumes emscripten + std.debug.assert(target.cpu.arch == .wasm32); + return; + }, + else => std.debug.panic("Unexpected target os: {}", .{target.os}), } exe.linkSystemLibraryName("dawn"); @@ -89,7 +94,7 @@ pub const Package = struct { pub fn package( b: *std.Build, - _: std.zig.CrossTarget, + target: std.zig.CrossTarget, _: std.builtin.Mode, args: struct { options: Options = .{}, @@ -111,6 +116,7 @@ pub fn package( step.addOption(u32, "bind_group_pool_size", args.options.bind_group_pool_size); step.addOption(u32, "bind_group_layout_pool_size", args.options.bind_group_layout_pool_size); step.addOption(u32, "pipeline_layout_pool_size", args.options.pipeline_layout_pool_size); + step.addOption(bool, "emscripten", target.getOsTag() == .emscripten); const zgpu_options = step.createModule(); diff --git a/libs/zgpu/src/binding_tests.zig b/libs/zgpu/src/binding_tests.zig new file mode 100644 index 000000000..ec3fd2f8e --- /dev/null +++ b/libs/zgpu/src/binding_tests.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const testing = std.testing; +const c = @cImport({ + @cInclude("webgpu/webgpu.h"); +}); +const gpu = @import("wgpu.zig"); + +// test struct sizes +fn assertStructBindings(zig_type: anytype, c_type: anytype) void { + if (@sizeOf(zig_type) != @sizeOf(c_type)) { + @compileLog("emscripten zgpu type has different size from webgpu.h type:", zig_type, c_type, @sizeOf(zig_type), @sizeOf(c_type)); + unreachable; + } + // slow to build tests that try to verify each struct field size and alignment to match between webgpu.h (either shipped with dawn or emscripten) and wgpu.zig + // @setEvalBranchQuota(2000); + // const zig_fields = @typeInfo(zig_type).Struct.fields; + // const c_fields = @typeInfo(c_type).Struct.fields; + // std.debug.assert(zig_fields.len == c_fields.len); + // var i = 0; + // while (i { - std.log.err("\n" ++ + slog.err("\n" ++ \\--------------------------------------------------------------------------- \\ \\This program requires: @@ -88,14 +93,14 @@ pub const GraphicsContext = struct { }, }; - dawnProcSetProcs(dnGetProcs()); + if (!emscripten) dawnProcSetProcs(dnGetProcs()); - const native_instance = dniCreate(); - errdefer dniDestroy(native_instance); + const native_instance = if (!emscripten) dniCreate(); + errdefer if (!emscripten) dniDestroy(native_instance); - dniDiscoverDefaultAdapters(native_instance); + if (!emscripten) dniDiscoverDefaultAdapters(native_instance); - const instance = dniGetWgpuInstance(native_instance).?; + const instance = if (emscripten) wgpu.createInstance(.{}) else dniGetWgpuInstance(native_instance).?; const adapter = adapter: { const Response = struct { @@ -124,8 +129,16 @@ pub const GraphicsContext = struct { @ptrCast(&response), ); + if (emscripten) { + // wait for response. requires emscripten `-sASYNC` flag + // otherwise whole api would need to be changed in a way that allows whole program to return from main and wait to js to call back + slog.debug("wait for instance.requestAdapter...", .{}); + while (response.status == .unknown) emscripten_sleep(5); + slog.debug("{}", .{response.status}); + } + if (response.status != .success) { - std.log.err("Failed to request GPU adapter (status: {s}).", .{@tagName(response.status)}); + slog.err("Failed to request GPU adapter (status: {s}).", .{@tagName(response.status)}); return error.NoGraphicsAdapter; } break :adapter response.adapter; @@ -135,11 +148,11 @@ pub const GraphicsContext = struct { var properties: wgpu.AdapterProperties = undefined; properties.next_in_chain = null; adapter.getProperties(&properties); - std.log.info("[zgpu] High-performance device has been selected:", .{}); - std.log.info("[zgpu] Name: {s}", .{properties.name}); - std.log.info("[zgpu] Driver: {s}", .{properties.driver_description}); - std.log.info("[zgpu] Adapter type: {s}", .{@tagName(properties.adapter_type)}); - std.log.info("[zgpu] Backend type: {s}", .{@tagName(properties.backend_type)}); + slog.info("High-performance device has been selected:", .{}); + slog.info(" Name: {s}", .{properties.name}); + slog.info(" Driver: {s}", .{properties.driver_description}); + slog.info(" Adapter type: {s}", .{@tagName(properties.adapter_type)}); + slog.info(" Backend type: {s}", .{@tagName(properties.backend_type)}); const device = device: { const Response = struct { @@ -180,8 +193,16 @@ pub const GraphicsContext = struct { @ptrCast(&response), ); + if (emscripten) { + // wait for response. requires emscripten `-sASYNC` flag + // otherwise whole api would need to be changed in a way that allows whole program to return from main and wait to js to call back + slog.debug("wait for adapter.requestDevice...", .{}); + while (response.status == .unknown) emscripten_sleep(5); + slog.debug("{}", .{response.status}); + } + if (response.status != .success) { - std.log.err("Failed to request GPU device (status: {s}).", .{@tagName(response.status)}); + slog.err("Failed to request GPU device (status: {s}).", .{@tagName(response.status)}); return error.NoGraphicsDevice; } break :device response.device; @@ -208,7 +229,7 @@ pub const GraphicsContext = struct { const gctx = try allocator.create(GraphicsContext); gctx.* = .{ - .native_instance = native_instance, + .native_instance = if (emscripten) null else native_instance, .instance = instance, .device = device, .queue = device.getQueue(), @@ -264,7 +285,7 @@ pub const GraphicsContext = struct { gctx.swapchain.release(); gctx.queue.release(); gctx.device.release(); - dniDestroy(gctx.native_instance); + if (!emscripten) dniDestroy(gctx.native_instance); allocator.destroy(gctx); } @@ -282,13 +303,14 @@ pub const GraphicsContext = struct { const offset = gctx.uniforms.offset; const aligned_size = (size + (uniforms_alloc_alignment - 1)) & ~(uniforms_alloc_alignment - 1); if ((offset + aligned_size) >= uniforms_buffer_size) { - std.log.err("[zgpu] Uniforms buffer size is too small. " ++ + slog.err("Uniforms buffer size is too small. " ++ "Consider increasing 'zgpu.BuildOptions.uniforms_buffer_size' constant.", .{}); return .{ .slice = @as([*]T, undefined)[0..0], .offset = 0 }; } const current = gctx.uniforms.stage.current; - const slice = (gctx.uniforms.stage.buffers[current].slice.?.ptr + offset)[0..size]; + const buf_ptr = (gctx.uniforms.stage.buffers[current].slice.?.ptr + offset); + const slice = (buf_ptr)[0..size]; gctx.uniforms.offset += aligned_size; return .{ @@ -318,8 +340,9 @@ pub const GraphicsContext = struct { assert(usb.slice == null); if (status == .success) { usb.slice = usb.buffer.getMappedRange(u8, 0, uniforms_buffer_size).?; + if (emscripten) assert(@intFromPtr(usb.slice.?.ptr) % 16 == 0); // see: https://github.com/emscripten-core/emscripten/pull/19477/commits/f4bb4f578131578cd13abbbf78d7f4273788d76f } else { - std.log.err("[zgpu] Failed to map buffer (status: {s}).", .{@tagName(status)}); + slog.err("Failed to map buffer (status: {s}).", .{@tagName(status)}); } } @@ -348,6 +371,11 @@ pub const GraphicsContext = struct { } if (gctx.uniforms.stage.num >= uniforms_staging_pipeline_len) { + if (emscripten) { + // we can't block in requestAnimationFrame + slog.warn("uniformsNextStagingBuffer: Out of buffers! canRender() must be checked next frame, otherwise we will crash!", .{}); + return; // use canRender() to check each frame if buffer is available + } // Wait until one of the buffers is mapped and ready to use. while (true) { gctx.device.tick(); @@ -364,6 +392,7 @@ pub const GraphicsContext = struct { assert(gctx.uniforms.stage.num < uniforms_staging_pipeline_len); const current = gctx.uniforms.stage.num; + if (emscripten) slog.debug("Adding staging uniform buffer: {}", .{current}); gctx.uniforms.stage.current = current; gctx.uniforms.stage.num += 1; @@ -425,7 +454,7 @@ pub const GraphicsContext = struct { const gpu_frame_number: *u64 = @ptrCast(@alignCast(userdata)); gpu_frame_number.* += 1; if (status != .success) { - std.log.err("[zgpu] Failed to complete GPU work (status: {s}).", .{@tagName(status)}); + slog.err("Failed to complete GPU work (status: {s}).", .{@tagName(status)}); } } @@ -433,7 +462,7 @@ pub const GraphicsContext = struct { normal_execution, swap_chain_resized, } { - gctx.swapchain.present(); + if (!emscripten) gctx.swapchain.present(); const fb_size = gctx.window.getFramebufferSize(); if (gctx.swapchain_descriptor.width != fb_size[0] or @@ -446,8 +475,8 @@ pub const GraphicsContext = struct { gctx.swapchain = gctx.device.createSwapChain(gctx.surface, gctx.swapchain_descriptor); - std.log.info( - "[zgpu] Window has been resized to: {d}x{d}.", + slog.info( + "Window has been resized to: {d}x{d}.", .{ gctx.swapchain_descriptor.width, gctx.swapchain_descriptor.height }, ); return .swap_chain_resized; @@ -457,6 +486,22 @@ pub const GraphicsContext = struct { return .normal_execution; } + pub fn canRender(gctx: *GraphicsContext) bool { + if (emscripten) { + if (gctx.uniforms.stage.buffers[gctx.uniforms.stage.current].slice == null) { + var i: u32 = 0; + while (i < gctx.uniforms.stage.num) : (i += 1) { + if (gctx.uniforms.stage.buffers[i].slice != null) { + gctx.uniforms.stage.current = i; + return true; + } + } + return false; + } + } + return true; + } + // // Resources // @@ -562,8 +607,8 @@ pub const GraphicsContext = struct { .{ .gpuobj = pipeline, .pipeline_layout_handle = op.pipeline_layout }, ); } else { - std.log.err( - "[zgpu] Failed to async create render pipeline (status: {s}, message: {?s}).", + slog.err( + "Failed to async create render pipeline (status: {s}, message: {?s}).", .{ @tagName(status), message }, ); } @@ -623,8 +668,8 @@ pub const GraphicsContext = struct { .{ .gpuobj = pipeline, .pipeline_layout_handle = op.pipeline_layout }, ); } else { - std.log.err( - "[zgpu] Failed to async create compute pipeline (status: {s}, message: {?s}).", + slog.err( + "Failed to async create compute pipeline (status: {s}, message: {?s}).", .{ @tagName(status), message }, ); } @@ -1045,6 +1090,8 @@ extern fn dnGetProcs() DawnProcsTable; // Defined in Dawn codebase extern fn dawnProcSetProcs(procs: DawnProcsTable) void; +extern fn emscripten_sleep(ms: u32) void; + /// Helper to create a buffer BindGroupLayoutEntry. pub fn bufferEntry( binding: u32, @@ -1258,7 +1305,7 @@ pub fn imageInfoToTextureFormat(num_components: u32, bytes_per_component: u32, i pub const BufferInfo = struct { gpuobj: ?wgpu.Buffer = null, - size: usize = 0, + size: u64 = 0, usage: wgpu.BufferUsage = .{}, }; @@ -1511,6 +1558,7 @@ const SurfaceDescriptorTag = enum { metal_layer, windows_hwnd, xlib, + canvas_html, }; const SurfaceDescriptor = union(SurfaceDescriptorTag) { @@ -1528,6 +1576,10 @@ const SurfaceDescriptor = union(SurfaceDescriptorTag) { display: *anyopaque, window: u32, }, + canvas_html: struct { + label: ?[*:0]const u8 = null, + selector: [*:0]const u8, + } }; fn createSurfaceForWindow(instance: wgpu.Instance, window: *zglfw.Window) wgpu.Surface { @@ -1565,6 +1617,11 @@ fn createSurfaceForWindow(instance: wgpu.Instance, window: *zglfw.Window) wgpu.S .layer = layer.?, }, }; + } else if (emscripten) SurfaceDescriptor{ + .canvas_html = .{ + .label = "basic surface", + .selector = "#canvas", // TODO: can this be somehow exposed through api? + }, } else unreachable; return switch (descriptor) { @@ -1600,6 +1657,16 @@ fn createSurfaceForWindow(instance: wgpu.Instance, window: *zglfw.Window) wgpu.S .label = if (src.label) |l| l else null, }); }, + .canvas_html => |src| blk: { + var desc: wgpu.SurfaceDescriptorFromCanvasHTMLSelector = .{ + .chain = .{ .struct_type = .surface_descriptor_from_canvas_html_selector, .next = null }, + .selector = src.selector, + }; + break :blk instance.createSurface(.{ + .next_in_chain = @as(*const wgpu.ChainedStruct, @ptrCast(&desc)), + .label = if (src.label) |l| l else null, + }); + }, }; } @@ -1655,12 +1722,12 @@ fn logUnhandledError( ) callconv(.C) void { _ = userdata; switch (err_type) { - .no_error => std.log.info("[zgpu] No error: {?s}", .{message}), - .validation => std.log.err("[zgpu] Validation: {?s}", .{message}), - .out_of_memory => std.log.err("[zgpu] Out of memory: {?s}", .{message}), - .device_lost => std.log.err("[zgpu] Device lost: {?s}", .{message}), - .internal => std.log.err("[zgpu] Internal error: {?s}", .{message}), - .unknown => std.log.err("[zgpu] Unknown error: {?s}", .{message}), + .no_error => slog.info("No error: {?s}", .{message}), + .validation => slog.err("Validation: {?s}", .{message}), + .out_of_memory => slog.err("Out of memory: {?s}", .{message}), + .device_lost => slog.err("Device lost: {?s}", .{message}), + .internal => slog.err("Internal error: {?s}", .{message}), + .unknown => slog.err("Unknown error: {?s}", .{message}), } // Exit the process for easier debugging. @@ -1708,3 +1775,16 @@ fn formatToShaderFormat(format: wgpu.TextureFormat) []const u8 { else => unreachable, }; } + +const emscripten = @import("zgpu_options").emscripten; +usingnamespace if (emscripten) struct { + // Missing symbols + var wgpuDeviceTickWarnPrinted : bool = false; + pub export fn wgpuDeviceTick() void { + if (!wgpuDeviceTickWarnPrinted) { + std.log.warn("wgpuDeviceTick(): this fn should be avoided! RequestAnimationFrame() is advised for smooth rendering in browser.", .{}); + wgpuDeviceTickWarnPrinted = true; + } + emscripten_sleep(1); + } +} else struct {}; \ No newline at end of file diff --git a/libs/zgui/build.zig b/libs/zgui/build.zig index d58037111..a6edbdf55 100644 --- a/libs/zgui/build.zig +++ b/libs/zgui/build.zig @@ -39,6 +39,7 @@ pub fn package( step.addOption(Backend, "backend", args.options.backend); step.addOption(bool, "shared", args.options.shared); + const emscripten = target.getOsTag() == .emscripten; const zgui_options = step.createModule(); const zgui = b.createModule(.{ @@ -69,11 +70,22 @@ pub fn package( .optimize = optimize, }); + if (emscripten) { + zgui_c_cpp.defineCMacro("__EMSCRIPTEN__", null); + // TODO: read from enviroment or `emcc --version` + zgui_c_cpp.defineCMacro("__EMSCRIPTEN_major__", "3"); + zgui_c_cpp.defineCMacro("__EMSCRIPTEN_minor__", "1"); + zgui_c_cpp.stack_protector = false; + zgui_c_cpp.disable_stack_probing = true; + } + zgui_c_cpp.addIncludePath(.{ .path = thisDir() ++ "/libs" }); zgui_c_cpp.addIncludePath(.{ .path = thisDir() ++ "/libs/imgui" }); - zgui_c_cpp.linkLibC(); - zgui_c_cpp.linkLibCpp(); + if (!emscripten) { + zgui_c_cpp.linkLibC(); + zgui_c_cpp.linkLibCpp(); + } const cflags = &.{"-fno-sanitize=undefined"}; @@ -102,8 +114,14 @@ pub fn package( switch (args.options.backend) { .glfw_wgpu => { - zgui_c_cpp.addIncludePath(.{ .path = thisDir() ++ "/../zglfw/libs/glfw/include" }); - zgui_c_cpp.addIncludePath(.{ .path = thisDir() ++ "/../zgpu/libs/dawn/include" }); + if (emscripten) { + const emsdk_path = b.env_map.get("EMSDK") orelse @panic("Failed to get emscripten SDK path, have you installed & sourced the SDK?"); + const emscripten_include = b.pathJoin(&.{ emsdk_path, "upstream", "emscripten", "cache", "sysroot", "include" }); + zgui_c_cpp.addSystemIncludePath(.{ .path = emscripten_include }); + } else { + zgui_c_cpp.addIncludePath(.{ .path = thisDir() ++ "/../zglfw/libs/glfw/include" }); + zgui_c_cpp.addIncludePath(.{ .path = thisDir() ++ "/../zgpu/libs/dawn/include" }); + } zgui_c_cpp.addCSourceFiles(&.{ thisDir() ++ "/libs/imgui/backends/imgui_impl_glfw.cpp", thisDir() ++ "/libs/imgui/backends/imgui_impl_wgpu.cpp", diff --git a/libs/zgui/libs/imgui/backends/imgui_impl_wgpu.cpp b/libs/zgui/libs/imgui/backends/imgui_impl_wgpu.cpp index 635418744..4a268ead8 100644 --- a/libs/zgui/libs/imgui/backends/imgui_impl_wgpu.cpp +++ b/libs/zgui/libs/imgui/backends/imgui_impl_wgpu.cpp @@ -206,6 +206,50 @@ static uint32_t __glsl_shader_frag_spv[] = 0x0003003e,0x00000009,0x00000022,0x000100fd,0x00010038 }; +// sligtly modified version from: https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_wgpu.cpp +static const char __shader_vert_wgsl[] = R"( +struct VertexInput { + @location(0) position: vec2, + @location(1) uv: vec2, + @location(2) color: vec4, +}; +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) uv: vec2, +}; +struct Uniforms { + mvp: mat4x4, +}; +@group(0) @binding(0) var uniforms: Uniforms; +@vertex +fn main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.position = uniforms.mvp * vec4(in.position, 0.0, 1.0); + out.color = in.color; + out.uv = in.uv; + return out; +} +)"; + +static const char __shader_frag_wgsl[] = R"( +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) uv: vec2, +}; +struct Uniforms { + mvp: mat4x4, +}; +@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var s: sampler; +@group(1) @binding(0) var t: texture_2d; +@fragment +fn main(in: VertexOutput) -> @location(0) vec4 { + return in.color * textureSample(t, s, in.uv); +} +)"; + static void SafeRelease(ImDrawIdx*& res) { if (res) @@ -286,6 +330,21 @@ static void SafeRelease(FrameResources& res) SafeRelease(res.VertexBufferHost); } +#ifdef __EMSCRIPTEN__ +static WGPUProgrammableStageDescriptor ImGui_ImplWGPU_CreateShaderModule(const char* source_data) { + WGPUShaderModuleWGSLDescriptor wgsl_desc = {}; + wgsl_desc.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor; + wgsl_desc.code = source_data; + + WGPUShaderModuleDescriptor desc = {}; + desc.nextInChain = reinterpret_cast(&wgsl_desc); + + WGPUProgrammableStageDescriptor stage_desc = {}; + stage_desc.module = wgpuDeviceCreateShaderModule(g_wgpuDevice, &desc); + stage_desc.entryPoint = "main"; + return stage_desc; +} +#else static WGPUProgrammableStageDescriptor ImGui_ImplWGPU_CreateShaderModule(uint32_t* binary_data, uint32_t binary_data_size) { WGPUShaderModuleSPIRVDescriptor spirv_desc = {}; @@ -301,6 +360,7 @@ static WGPUProgrammableStageDescriptor ImGui_ImplWGPU_CreateShaderModule(uint32_ stage_desc.entryPoint = "main"; return stage_desc; } +#endif static WGPUBindGroup ImGui_ImplWGPU_CreateImageBindGroup(WGPUBindGroupLayout layout, WGPUTextureView texture) { @@ -591,7 +651,11 @@ bool ImGui_ImplWGPU_CreateDeviceObjects(void) graphics_pipeline_desc.layout = nullptr; // Use automatic layout generation // Create the vertex shader + #ifdef __EMSCRIPTEN__ + WGPUProgrammableStageDescriptor vertex_shader_desc = ImGui_ImplWGPU_CreateShaderModule(__shader_vert_wgsl); + #else WGPUProgrammableStageDescriptor vertex_shader_desc = ImGui_ImplWGPU_CreateShaderModule(__glsl_shader_vert_spv, sizeof(__glsl_shader_vert_spv) / sizeof(uint32_t)); + #endif graphics_pipeline_desc.vertex.module = vertex_shader_desc.module; graphics_pipeline_desc.vertex.entryPoint = vertex_shader_desc.entryPoint; @@ -613,7 +677,11 @@ bool ImGui_ImplWGPU_CreateDeviceObjects(void) graphics_pipeline_desc.vertex.buffers = buffer_layouts; // Create the pixel shader + #ifdef __EMSCRIPTEN__ + WGPUProgrammableStageDescriptor pixel_shader_desc = ImGui_ImplWGPU_CreateShaderModule(__shader_frag_wgsl); + #else WGPUProgrammableStageDescriptor pixel_shader_desc = ImGui_ImplWGPU_CreateShaderModule(__glsl_shader_frag_spv, sizeof(__glsl_shader_frag_spv) / sizeof(uint32_t)); + #endif // Create the blending setup WGPUBlendState blend_state = {}; diff --git a/libs/zgui/src/gui.zig b/libs/zgui/src/gui.zig index 1be97bcf7..4014e9a31 100644 --- a/libs/zgui/src/gui.zig +++ b/libs/zgui/src/gui.zig @@ -3116,13 +3116,13 @@ extern fn zguiIsKeyDown(key: Key) bool; var temp_buffer: ?std.ArrayList(u8) = null; pub fn format(comptime fmt: []const u8, args: anytype) []const u8 { - const len = std.fmt.count(fmt, args); - if (len > temp_buffer.?.items.len) temp_buffer.?.resize(len + 64) catch unreachable; + const len =std.fmt.count(fmt, args); + if (len > temp_buffer.?.items.len) temp_buffer.?.resize(@intCast(len + 64)) catch unreachable; return std.fmt.bufPrint(temp_buffer.?.items, fmt, args) catch unreachable; } pub fn formatZ(comptime fmt: []const u8, args: anytype) [:0]const u8 { const len = std.fmt.count(fmt ++ "\x00", args); - if (len > temp_buffer.?.items.len) temp_buffer.?.resize(len + 64) catch unreachable; + if (len > temp_buffer.?.items.len) temp_buffer.?.resize(@intCast(len + 64)) catch unreachable; return std.fmt.bufPrintZ(temp_buffer.?.items, fmt, args) catch unreachable; } //-------------------------------------------------------------------------------------------------- diff --git a/libs/zstbi/build.zig b/libs/zstbi/build.zig index 97bda9be4..764d833aa 100644 --- a/libs/zstbi/build.zig +++ b/libs/zstbi/build.zig @@ -12,10 +12,12 @@ pub const Package = struct { pub fn package( b: *std.Build, - target: std.zig.CrossTarget, + target_: std.zig.CrossTarget, optimize: std.builtin.Mode, _: struct {}, ) Package { + const emscripten = target_.getOsTag() == .emscripten; + const target = if (emscripten) std.zig.CrossTarget.parse(.{ .arch_os_abi = "wasm32-freestanding" }) catch unreachable else target_; const zstbi = b.createModule(.{ .source_file = .{ .path = thisDir() ++ "/src/zstbi.zig" }, }); @@ -46,6 +48,10 @@ pub fn package( }); } zstbi_c_cpp.linkLibC(); + if (emscripten) { + zstbi_c_cpp.stack_protector = false; + zstbi_c_cpp.disable_stack_probing = true; + } return .{ .zstbi = zstbi, diff --git a/samples/gui_test_wgpu/build.zig b/samples/gui_test_wgpu/build.zig index 1fc867168..47d10a653 100644 --- a/samples/gui_test_wgpu/build.zig +++ b/samples/gui_test_wgpu/build.zig @@ -6,24 +6,29 @@ const demo_name = "gui_test_wgpu"; const content_dir = demo_name ++ "_content/"; pub fn build(b: *std.Build, options: Options) *std.Build.CompileStep { - const exe = b.addExecutable(.{ + const emscripten = options.target.getOsTag() == .emscripten; + const target = if (emscripten) std.zig.CrossTarget.parse(.{ .arch_os_abi = "wasm32-freestanding" }) catch unreachable else options.target; + const exe_desc = .{ .name = demo_name, .root_source_file = .{ .path = thisDir() ++ "/src/" ++ demo_name ++ ".zig" }, - .target = options.target, + .target = target, .optimize = options.optimize, - }); + }; + const exe = if (emscripten) b.addStaticLibrary(exe_desc) else b.addExecutable(exe_desc); const zgui_pkg = @import("../../build.zig").zgui_pkg; const zmath_pkg = @import("../../build.zig").zmath_pkg; const zgpu_pkg = @import("../../build.zig").zgpu_pkg; const zglfw_pkg = @import("../../build.zig").zglfw_pkg; const zstbi_pkg = @import("../../build.zig").zstbi_pkg; + const zems_pkg = @import("../../build.zig").zems_pkg; zgui_pkg.link(exe); zgpu_pkg.link(exe); zglfw_pkg.link(exe); zstbi_pkg.link(exe); zmath_pkg.link(exe); + zems_pkg.link(exe); const exe_options = b.addOptions(); exe.addOptions("build_options", exe_options); @@ -39,6 +44,20 @@ pub fn build(b: *std.Build, options: Options) *std.Build.CompileStep { return exe; } +const zems = @import("../../build.zig").zems; +pub fn buildEmscripten(b: *std.Build, options: Options) *zems.EmscriptenStep { + const exe = build(b, options); + var ems_step = zems.EmscriptenStep.init(b); + ems_step.args.setDefault(options.optimize, false); + ems_step.args.setOrAssertOption("USE_GLFW", "3"); + ems_step.args.setOrAssertOption("USE_WEBGPU", ""); + ems_step.args.setOrAssertOption("FILESYSTEM", "1"); + // didn't work with absolute path, uses relative path from build.root and puts it in virtual filesystem at path after @ + ems_step.args.other_args.appendSlice(&.{"--preload-file", "samples/gui_test_wgpu/gui_test_wgpu_content" ++ "@gui_test_wgpu_content"}) catch unreachable; + ems_step.link(exe); + return ems_step; +} + inline fn thisDir() []const u8 { return comptime std.fs.path.dirname(@src().file) orelse "."; } diff --git a/samples/gui_test_wgpu/src/gui_test_wgpu.zig b/samples/gui_test_wgpu/src/gui_test_wgpu.zig index 53477fdbc..a50e43cb4 100644 --- a/samples/gui_test_wgpu/src/gui_test_wgpu.zig +++ b/samples/gui_test_wgpu/src/gui_test_wgpu.zig @@ -6,6 +6,12 @@ const zgpu = @import("zgpu"); const wgpu = zgpu.wgpu; const zgui = @import("zgui"); const zstbi = @import("zstbi"); +const zems = @import("zems"); + +pub const std_options = struct { + // pub const log_level = .info; + pub const logFn = if (zems.is_emscripten) zems.emscriptenLog else std.log.defaultLog; +}; const content_dir = @import("build_options").content_dir; const window_title = "zig-gamedev: gui test (wgpu)"; @@ -579,40 +585,65 @@ fn draw(demo: *DemoState) void { } pub fn main() !void { - zglfw.init() catch { - std.log.err("Failed to initialize GLFW library.", .{}); + zglfw.init() catch |err| { + std.log.err("Failed to initialize GLFW library: {}", .{err}); return; }; defer zglfw.terminate(); - // Change current working directory to where the executable is located. - { + if (!zems.is_emscripten) { + // Change current working directory to where the executable is located. var buffer: [1024]u8 = undefined; const path = std.fs.selfExeDirPath(buffer[0..]) catch "."; std.os.chdir(path) catch {}; } - const window = zglfw.Window.create(1600, 1000, window_title, null) catch { - std.log.err("Failed to create demo window.", .{}); + if (zems.is_emscripten) zglfw.WindowHint.set(.client_api, @intFromEnum(zglfw.ClientApi.no_api)); + const window = zglfw.Window.create(1600, 1000, window_title, null) catch |err| { + std.log.err("Failed to create demo window: {}", .{err}); return; }; defer window.destroy(); window.setSizeLimits(400, 400, -1, -1); - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); + var gpa = if (zems.is_emscripten) zems.EmmalocAllocator{} else std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = if (!zems.is_emscripten) gpa.deinit(); const allocator = gpa.allocator(); - const demo = create(allocator, window) catch { - std.log.err("Failed to initialize the demo.", .{}); + const demo = create(allocator, window) catch |err| { + std.log.err("Failed to initialize the demo: {}", .{err}); return; }; defer destroy(allocator, demo); - while (!window.shouldClose() and window.getKey(.escape) != .press) { - zglfw.pollEvents(); - try update(demo); - draw(demo); + if (zems.is_emscripten) { + // requestAnimationFrame usage is needed to avoid flickering, otherwise could work in loop as well + zems.emscripten_request_animation_frame_loop(&tickEmcripten, demo); + // TODO: ideally we would store all state in globals and return, but for now I don't want to refactor it, so lets spin forever + while (true) zems.emscripten_sleep(1000); + } else while (!window.shouldClose() and window.getKey(.escape) != .press) { + try tick(demo); } } + +pub fn tick(demo: *DemoState) !void { + zglfw.pollEvents(); + try update(demo); + draw(demo); +} + +usingnamespace if (zems.is_emscripten) struct { + pub export fn tickEmcripten(time: f64, user_data: ?*anyopaque) callconv(.C) c_int { + _ = time; + const demo : *DemoState = @ptrCast(@alignCast(user_data.?)); + if (demo.gctx.canRender()) tick(demo) catch |err| { + std.log.err("animation frame canceled! tick failed with: {}", .{err}); + return 0; // FALSE - stop animation frame callback loop + } else { + std.log.warn("canRender(): Frame skipped!", .{}); + } + return 1; // TRUE - continue animation frame callback loop + } +} else struct {}; +extern fn tickEmcripten(time: f64, user_data: ?*anyopaque) callconv(.C) c_int; diff --git a/samples/instanced_pills_wgpu/build.zig b/samples/instanced_pills_wgpu/build.zig index 9cdeac250..7534750d8 100644 --- a/samples/instanced_pills_wgpu/build.zig +++ b/samples/instanced_pills_wgpu/build.zig @@ -7,22 +7,27 @@ const demo_filename = "instanced_pills_wgpu"; const content_dir = demo_name ++ "_content/"; pub fn build(b: *std.Build, options: Options) *std.Build.CompileStep { - const exe = b.addExecutable(.{ + const emscripten = options.target.getOsTag() == .emscripten; + const target = if (emscripten) std.zig.CrossTarget.parse(.{ .arch_os_abi = "wasm32-freestanding" }) catch unreachable else options.target; + const exe_desc = .{ .name = demo_name, .root_source_file = .{ .path = thisDir() ++ "/src/" ++ demo_filename ++ ".zig" }, - .target = options.target, + .target = target, .optimize = options.optimize, - }); + }; + const exe = if (emscripten) b.addStaticLibrary(exe_desc) else b.addExecutable(exe_desc); const zgui_pkg = @import("../../build.zig").zgui_pkg; const zmath_pkg = @import("../../build.zig").zmath_pkg; const zgpu_pkg = @import("../../build.zig").zgpu_pkg; const zglfw_pkg = @import("../../build.zig").zglfw_pkg; + const zems_pkg = @import("../../build.zig").zems_pkg; zgui_pkg.link(exe); zgpu_pkg.link(exe); zglfw_pkg.link(exe); zmath_pkg.link(exe); + zems_pkg.link(exe); const exe_options = b.addOptions(); exe.addOptions("build_options", exe_options); @@ -38,6 +43,18 @@ pub fn build(b: *std.Build, options: Options) *std.Build.CompileStep { return exe; } +const zems = @import("../../build.zig").zems; +pub fn buildEmscripten(b: *std.Build, options: Options) *zems.EmscriptenStep { + const exe = build(b, options); + var ems_step = zems.EmscriptenStep.init(b); + ems_step.args.setDefault(options.optimize, true); + ems_step.args.setOrAssertOption("USE_GLFW", "3"); + ems_step.args.setOrAssertOption("USE_WEBGPU", ""); + ems_step.args.other_args.appendSlice(&.{"--preload-file", "samples/instanced_pills_wgpu/instanced_pills_wgpu_content" ++ "@instanced_pills_wgpu_content"}) catch unreachable; + ems_step.link(exe); + return ems_step; +} + inline fn thisDir() []const u8 { return comptime std.fs.path.dirname(@src().file) orelse "."; } diff --git a/samples/instanced_pills_wgpu/src/instanced_pills_wgpu.zig b/samples/instanced_pills_wgpu/src/instanced_pills_wgpu.zig index 6d0e95020..37aa780e9 100644 --- a/samples/instanced_pills_wgpu/src/instanced_pills_wgpu.zig +++ b/samples/instanced_pills_wgpu/src/instanced_pills_wgpu.zig @@ -7,6 +7,12 @@ const wgpu = zgpu.wgpu; const zgui = @import("zgui"); const zm = @import("zmath"); const vertex_generator = @import("vertex_generator.zig"); +const zems = @import("zems"); + +pub const std_options = struct { + // pub const log_level = .info; + pub const logFn = if (zems.is_emscripten) zems.emscriptenLog else std.log.defaultLog; +}; const content_dir = @import("build_options").content_dir; const window_title = "zig-gamedev: instanced pills (wgpu)"; @@ -755,13 +761,19 @@ pub fn main() !void { }; defer zglfw.terminate(); - // Change current working directory to where the executable is located. + if (!zems.is_emscripten) { + // Change current working directory to where the executable is located. var buffer: [1024]u8 = undefined; const path = std.fs.selfExeDirPath(buffer[0..]) catch "."; std.os.chdir(path) catch {}; } + if (zems.is_emscripten) { + // by default emscripten initializes on window creation WebGL context + // this flag skips context creation. otherwise we later can't create webgpu surface + zglfw.WindowHint.set(.client_api, @intFromEnum(zglfw.ClientApi.no_api)); + } const window = zglfw.Window.create(1600, 1000, window_title, null) catch { std.log.err("Failed to create demo window.", .{}); return; @@ -769,8 +781,8 @@ pub fn main() !void { defer window.destroy(); window.setSizeLimits(400, 400, -1, -1); - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); + var gpa = if (zems.is_emscripten) zems.EmmalocAllocator{} else std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = if (!zems.is_emscripten) gpa.deinit(); const allocator = gpa.allocator(); @@ -781,8 +793,13 @@ pub fn main() !void { defer demo.deinit(allocator); while (!window.shouldClose() and window.getKey(.escape) != .press) { + if (!demo.gctx.canRender()) { + std.log.info("canRender: frame skipped!", .{}); + continue; + } zglfw.pollEvents(); try demo.update(allocator); demo.draw(); + if (zems.is_emscripten) zems.emscripten_sleep(1); } } diff --git a/samples/procedural_mesh_wgpu/build.zig b/samples/procedural_mesh_wgpu/build.zig index e935ce5d7..d38e68201 100644 --- a/samples/procedural_mesh_wgpu/build.zig +++ b/samples/procedural_mesh_wgpu/build.zig @@ -41,6 +41,17 @@ pub fn build(b: *std.Build, options: Options) *std.Build.CompileStep { return exe; } +const zems = @import("../../build.zig").zems; +pub fn buildEmscripten(b: *std.Build, options: Options) *zems.EmscriptenStep { + const exe = build(b, options); + var ems_step = zems.EmscriptenStep.init(b); + ems_step.args.setDefault(options.optimize, false); + ems_step.args.setOrAssertOption("USE_GLFW", "3"); + ems_step.args.setOrAssertOption("USE_WEBGPU", ""); + ems_step.link(exe); + return ems_step; +} + inline fn thisDir() []const u8 { return comptime std.fs.path.dirname(@src().file) orelse "."; } diff --git a/samples/triangle_wgpu_emscripten/README.md b/samples/triangle_wgpu_emscripten/README.md new file mode 100644 index 000000000..275afd97a --- /dev/null +++ b/samples/triangle_wgpu_emscripten/README.md @@ -0,0 +1,61 @@ +## triangle (wgpu + emscripten) + +Same sample as `triangle_wgpu` but with added emscripten sdk support to compile for web. Still can be compiled natively. + +### Build +#### Native +```bash +zig build triangle_wgpu_emscripten-run +``` +#### Wasm for web +#### Install emscripten sdk +* [Follow these instructions](https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended) +* emscripten needs this [patch](https://github.com/emscripten-core/emscripten/pull/19477/commits/f4bb4f578131578cd13abbbf78d7f4273788d76f) currently for this sample to run until it gets merged/released. + +#### Compile +```bash +zig build triangle_wgpu_emscripten -Dtarget=wasm32-emscripten -Doptimize=ReleaseFast +# or all esmcripten supported samples +zig build -Dtarget=wasm32-emscripten -Doptimize=ReleaseFast +``` +See output under `zig-out\web\triangle_wgpu_emscripten`. It should contain: +* index.html +* index.js +* index.wasm +* (optional) `index.wasm.map` source mappings. If setup correctly with webserver it should allow debugging including previewing zig files, placing breakpoints and stepping through zig code from browser. + +To run it in browser it is required that the files are served by web-server. +Many IDEs have extensions for serving files, I used vscode extensions `liveServer` that worked. +Open hosted site with browser that supports WebGPU, tested to work on: +* [chrome canary](https://www.google.com/chrome/canary/) ✔️ +* Edge DEV ✔️ + +Didn't work on: + +* Firefox DEV ❌ (could not run any webgpu sample to run, seems to be my system or browser issue) + +![image](screenshot.png) + +### Modifications and notable changes needed for emscripten + +* Instead of compiling as executable, compile to static library and link it with emscripten to get final wasm and accompanying `html` and `js`. See root `build.zig` and `linkEmscripten()`. +* Add `zglfw.ClientApi.no_api` when creating window, otherwise emscripten will create webgl context that won't work with webgpu. +* Override default logger to emscripten console commands. Zig std does not have implementation for freestanding target. Emscripten can provide posix dummy file system, stdin/stdout so it might work otherwise. But even then it might not be recommended due to generated file-size etc. +* Don't use GeneralPurposeAllocator - all allocations will fail. Currently easiest is to compile with emscripten emmaloc and use its interface to get memory from system. +Otherwise it should be possible to export custom malloc/free interface to emscripten but not has been investigated. +* WGSL changes/deprecated features: use `@vertex` instead of `@stage(vertex)` etc. Natively dawn doesn't seem to enforce these, but browser won't compile deprecated shader stuff. +* Using main loop is not recommended. Refactor your code to instead use `requestAnimationFrame` with callback. +* On each animation frame you MUST check for `gctx.canRender()` for robustness. And skip frame if it returns `false`. It is because `submit` in zgpu can run out of uniform buffers. On native platforms it spins waiting for buffer to free up. This is not possible on web because requestAnimationFrame should not block but return for other system callbacks to be fired. On my system mapAsync in browser runs much slower and can consume all 8(default) buffers in debug. Usually by time next frame starts buffers are ready, but it requires calling canRender(). I have not caught situation where frame needs to be skipped with 8 buffers, but behavior can be easily tested by reducing buffer count to 4. +* When using custom html shell you might need to resize frame buffer from js side. Glfw glue wont fire events on dom size changes just framebuffer resize. Required js sample code to keep native framebuffer size: + + ```js + window.addEventListener("resize", function (e) { + Module.setCanvasSize(Module.canvas.clientWidth, Module.canvas.clientHeight, false); + }); + // resize event only fires when window is resized, add second check in case dom changed etc. + setInterval(function () { + if (Module.canvas.width != Module.canvas.clientWidth || Module.canvas.height != Module.canvas.clientHeight) { + Module.setCanvasSize(Module.canvas.clientWidth, Module.canvas.clientHeight, false); + } + }, 100); + ``` diff --git a/samples/triangle_wgpu_emscripten/build.zig b/samples/triangle_wgpu_emscripten/build.zig new file mode 100644 index 000000000..5b48ee5f3 --- /dev/null +++ b/samples/triangle_wgpu_emscripten/build.zig @@ -0,0 +1,54 @@ +const std = @import("std"); + +const Options = @import("../../build.zig").Options; +const content_dir = "triangle_wgpu_emscripten_content/"; + +pub fn build(b: *std.Build, options: Options) *std.Build.CompileStep { + const emscripten = options.target.getOsTag() == .emscripten; + const target = if (emscripten) std.zig.CrossTarget.parse(.{ .arch_os_abi = "wasm32-freestanding" }) catch unreachable else options.target; + const exe_desc = .{ + .name = "triangle_wgpu_emscripten", + .root_source_file = .{ .path = thisDir() ++ "/src/triangle_wgpu.zig" }, + .target = target, + .optimize = options.optimize, + }; + const exe = if (emscripten) b.addStaticLibrary(exe_desc) else b.addExecutable(exe_desc); + + const zgui_pkg = @import("../../build.zig").zgui_pkg; + const zmath_pkg = @import("../../build.zig").zmath_pkg; + const zgpu_pkg = @import("../../build.zig").zgpu_pkg; + const zglfw_pkg = @import("../../build.zig").zglfw_pkg; + const zems_pkg = @import("../../build.zig").zems_pkg; + + zgui_pkg.link(exe); + zgpu_pkg.link(exe); + zglfw_pkg.link(exe); + zmath_pkg.link(exe); + zems_pkg.link(exe); + + // exe_options.addOption([]const u8, "content_dir", content_dir); + + // const install_content_step = b.addInstallDirectory(.{ + // .source_dir = thisDir() ++ "/" ++ content_dir, + // .install_dir = .{ .custom = "" }, + // .install_subdir = "bin/" ++ content_dir, + // }); + // exe.step.dependOn(&install_content_step.step); + + return exe; +} + +const zems = @import("../../build.zig").zems; +pub fn buildEmscripten(b: *std.Build, options: Options) *zems.EmscriptenStep { + const exe = build(b, options); + var ems_step = zems.EmscriptenStep.init(b); + ems_step.args.setDefault(options.optimize, false); + ems_step.args.setOrAssertOption("USE_GLFW", "3"); + ems_step.args.setOrAssertOption("USE_WEBGPU", ""); + ems_step.link(exe); + return ems_step; +} + +inline fn thisDir() []const u8 { + return comptime std.fs.path.dirname(@src().file) orelse "."; +} diff --git a/samples/triangle_wgpu_emscripten/screenshot.png b/samples/triangle_wgpu_emscripten/screenshot.png new file mode 100644 index 000000000..2823579ec --- /dev/null +++ b/samples/triangle_wgpu_emscripten/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2d13535297a7393dff504ecf8fcb231f9ed9b52cb6d467e3dafb559b17a6cfd +size 267706 diff --git a/samples/triangle_wgpu_emscripten/src/triangle_wgpu.zig b/samples/triangle_wgpu_emscripten/src/triangle_wgpu.zig new file mode 100644 index 000000000..6b422944a --- /dev/null +++ b/samples/triangle_wgpu_emscripten/src/triangle_wgpu.zig @@ -0,0 +1,433 @@ +const std = @import("std"); +const math = std.math; +const zglfw = @import("zglfw"); +const zgpu = @import("zgpu"); +const wgpu = zgpu.wgpu; +const zgui = @import("zgui"); +const zm = @import("zmath"); +const zems = @import("zems"); + +const build_options = @import("build_options"); +const content_dir = build_options.content_dir; +const emscripten = zems.is_emscripten; +const window_title = "zig-gamedev: triangle (wgpu)"; + +pub const std_options = struct { + // pub const log_level = .info; + pub const logFn = if (emscripten) zems.emscriptenLog else std.log.defaultLog; +}; + +// zig fmt: off +const wgsl_vs = +\\ @group(0) @binding(0) var object_to_clip: mat4x4; +\\ struct VertexOut { +\\ @builtin(position) position_clip: vec4, +\\ @location(0) color: vec3, +\\ } +\\ @vertex fn main( +\\ @location(0) position: vec3, +\\ @location(1) color: vec3, +\\ ) -> VertexOut { +\\ var output: VertexOut; +\\ output.position_clip = vec4(position, 1.0) * object_to_clip; +\\ output.color = color; +\\ return output; +\\ } +; +const wgsl_fs = +\\ @fragment fn main( +\\ @location(0) color: vec3, +\\ ) -> @location(0) vec4 { +\\ return vec4(color, 1.0); +\\ } +// zig fmt: on +; + +const Vertex = struct { + position: [3]f32, + color: [3]f32, +}; + +const DemoState = struct { + gctx: *zgpu.GraphicsContext, + + pipeline: zgpu.RenderPipelineHandle, + bind_group: zgpu.BindGroupHandle, + + vertex_buffer: zgpu.BufferHandle, + index_buffer: zgpu.BufferHandle, + + depth_texture: zgpu.TextureHandle, + depth_texture_view: zgpu.TextureViewHandle, +}; + +fn init(allocator: std.mem.Allocator, window: *zglfw.Window) !DemoState { + const gctx = try zgpu.GraphicsContext.create(allocator, window, .{}); + + // Create a bind group layout needed for our render pipeline. + const bind_group_layout = gctx.createBindGroupLayout(&.{ + zgpu.bufferEntry(0, .{ .vertex = true }, .uniform, true, 0), + }); + defer gctx.releaseResource(bind_group_layout); + + const pipeline_layout = gctx.createPipelineLayout(&.{bind_group_layout}); + defer gctx.releaseResource(pipeline_layout); + + const pipeline = pipline: { + const vs_module = zgpu.createWgslShaderModule(gctx.device, wgsl_vs, "vs"); + defer vs_module.release(); + + const fs_module = zgpu.createWgslShaderModule(gctx.device, wgsl_fs, "fs"); + defer fs_module.release(); + + const color_targets = [_]wgpu.ColorTargetState{.{ + .format = zgpu.GraphicsContext.swapchain_format, + }}; + + const vertex_attributes = [_]wgpu.VertexAttribute{ + .{ .format = .float32x3, .offset = 0, .shader_location = 0 }, + .{ .format = .float32x3, .offset = @offsetOf(Vertex, "color"), .shader_location = 1 }, + }; + const vertex_buffers = [_]wgpu.VertexBufferLayout{.{ + .array_stride = @sizeOf(Vertex), + .attribute_count = vertex_attributes.len, + .attributes = &vertex_attributes, + }}; + + const pipeline_descriptor = wgpu.RenderPipelineDescriptor{ + .vertex = wgpu.VertexState{ + .module = vs_module, + .entry_point = "main", + .buffer_count = vertex_buffers.len, + .buffers = &vertex_buffers, + }, + .primitive = wgpu.PrimitiveState{ + .front_face = .ccw, + .cull_mode = .none, + .topology = .triangle_list, + }, + .depth_stencil = &wgpu.DepthStencilState{ + .format = .depth32_float, + .depth_write_enabled = true, + .depth_compare = .less, + }, + .fragment = &wgpu.FragmentState{ + .module = fs_module, + .entry_point = "main", + .target_count = color_targets.len, + .targets = &color_targets, + }, + }; + break :pipline gctx.createRenderPipeline(pipeline_layout, pipeline_descriptor); + }; + + const bind_group = gctx.createBindGroup(bind_group_layout, &[_]zgpu.BindGroupEntryInfo{ + .{ .binding = 0, .buffer_handle = gctx.uniforms.buffer, .offset = 0, .size = @sizeOf(zm.Mat) }, + }); + + // Create a vertex buffer. + const vertex_buffer = gctx.createBuffer(.{ + .usage = .{ .copy_dst = true, .vertex = true }, + .size = 3 * @sizeOf(Vertex), + }); + const vertex_data = [_]Vertex{ + .{ .position = [3]f32{ 0.0, 0.5, 0.0 }, .color = [3]f32{ 1.0, 0.0, 0.0 } }, + .{ .position = [3]f32{ -0.5, -0.5, 0.0 }, .color = [3]f32{ 0.0, 1.0, 0.0 } }, + .{ .position = [3]f32{ 0.5, -0.5, 0.0 }, .color = [3]f32{ 0.0, 0.0, 1.0 } }, + }; + gctx.queue.writeBuffer(gctx.lookupResource(vertex_buffer).?, 0, Vertex, vertex_data[0..]); + + // Create an index buffer. + const index_buffer = gctx.createBuffer(.{ + .usage = .{ .copy_dst = true, .index = true }, + .size = 3 * @sizeOf(u32), + }); + const index_data = [_]u32{ 0, 1, 2 }; + gctx.queue.writeBuffer(gctx.lookupResource(index_buffer).?, 0, u32, index_data[0..]); + + // Create a depth texture and its 'view'. + const depth = createDepthTexture(gctx); + + return DemoState{ + .gctx = gctx, + .pipeline = pipeline, + .bind_group = bind_group, + .vertex_buffer = vertex_buffer, + .index_buffer = index_buffer, + .depth_texture = depth.texture, + .depth_texture_view = depth.view, + }; +} + +fn deinit(allocator: std.mem.Allocator, demo: *DemoState) void { + demo.gctx.destroy(allocator); + demo.* = undefined; +} + +fn update(demo: *DemoState) void { + zgui.backend.newFrame( + demo.gctx.swapchain_descriptor.width, + demo.gctx.swapchain_descriptor.height, + ); + zgui.showDemoWindow(null); +} + +fn draw(demo: *DemoState) void { + const gctx = demo.gctx; + const fb_width = gctx.swapchain_descriptor.width; + const fb_height = gctx.swapchain_descriptor.height; + const t: f32 = @floatCast(gctx.stats.time); + + if (!gctx.canRender()) { + std.log.err("Can't render out of buffers!", .{}); + } + + const cam_world_to_view = zm.lookAtLh( + zm.f32x4(3.0, 3.0, -3.0, 1.0), + zm.f32x4(0.0, 0.0, 0.0, 1.0), + zm.f32x4(0.0, 1.0, 0.0, 0.0), + ); + const cam_view_to_clip = zm.perspectiveFovLh( + 0.25 * math.pi, + @as(f32, @floatFromInt(fb_width)) / @as(f32, @floatFromInt(fb_height)), + 0.01, + 200.0, + ); + const cam_world_to_clip = zm.mul(cam_world_to_view, cam_view_to_clip); + + const back_buffer_view = gctx.swapchain.getCurrentTextureView(); + defer back_buffer_view.release(); + + const commands = commands: { + const encoder = gctx.device.createCommandEncoder(null); + defer encoder.release(); + + pass: { + const vb_info = gctx.lookupResourceInfo(demo.vertex_buffer) orelse break :pass; + const ib_info = gctx.lookupResourceInfo(demo.index_buffer) orelse break :pass; + const pipeline = gctx.lookupResource(demo.pipeline) orelse break :pass; + const bind_group = gctx.lookupResource(demo.bind_group) orelse break :pass; + const depth_view = gctx.lookupResource(demo.depth_texture_view) orelse break :pass; + + const color_attachments = [_]wgpu.RenderPassColorAttachment{.{ + .view = back_buffer_view, + .load_op = .clear, + .store_op = .store, + }}; + const depth_attachment = wgpu.RenderPassDepthStencilAttachment{ + .view = depth_view, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }; + const render_pass_info = wgpu.RenderPassDescriptor{ + .color_attachment_count = color_attachments.len, + .color_attachments = &color_attachments, + .depth_stencil_attachment = &depth_attachment, + }; + const pass = encoder.beginRenderPass(render_pass_info); + defer { + pass.end(); + pass.release(); + } + + pass.setVertexBuffer(0, vb_info.gpuobj.?, 0, vb_info.size); + pass.setIndexBuffer(ib_info.gpuobj.?, .uint32, 0, ib_info.size); + + pass.setPipeline(pipeline); + + // Draw triangle 1. + { + const object_to_world = zm.mul(zm.rotationY(t), zm.translation(-1.0, 0.0, 0.0)); + const object_to_clip = zm.mul(object_to_world, cam_world_to_clip); + + const mem = gctx.uniformsAllocate(zm.Mat, 1); + mem.slice[0] = zm.transpose(object_to_clip); + + pass.setBindGroup(0, bind_group, &.{mem.offset}); + pass.drawIndexed(3, 1, 0, 0, 0); + } + + // Draw triangle 2. + { + const object_to_world = zm.mul(zm.rotationY(0.75 * t), zm.translation(1.0, 0.0, 0.0)); + const object_to_clip = zm.mul(object_to_world, cam_world_to_clip); + + const mem = gctx.uniformsAllocate(zm.Mat, 1); + mem.slice[0] = zm.transpose(object_to_clip); + + pass.setBindGroup(0, bind_group, &.{mem.offset}); + pass.drawIndexed(3, 1, 0, 0, 0); + } + } + { + const color_attachments = [_]wgpu.RenderPassColorAttachment{.{ + .view = back_buffer_view, + .load_op = .load, + .store_op = .store, + }}; + const render_pass_info = wgpu.RenderPassDescriptor{ + .color_attachment_count = color_attachments.len, + .color_attachments = &color_attachments, + }; + const pass = encoder.beginRenderPass(render_pass_info); + defer { + pass.end(); + pass.release(); + } + + zgui.backend.draw(pass); + } + + break :commands encoder.finish(null); + }; + defer commands.release(); + + gctx.submit(&.{commands}); + + if (gctx.present() == .swap_chain_resized) { + std.log.debug("resize framebuffer", .{}); + // Release old depth texture. + gctx.releaseResource(demo.depth_texture_view); + gctx.destroyResource(demo.depth_texture); + + // Create a new depth texture to match the new window size. + const depth = createDepthTexture(gctx); + demo.depth_texture = depth.texture; + demo.depth_texture_view = depth.view; + } +} + +fn createDepthTexture(gctx: *zgpu.GraphicsContext) struct { + texture: zgpu.TextureHandle, + view: zgpu.TextureViewHandle, +} { + const texture = gctx.createTexture(.{ + .usage = .{ .render_attachment = true }, + .dimension = .tdim_2d, + .size = .{ + .width = gctx.swapchain_descriptor.width, + .height = gctx.swapchain_descriptor.height, + .depth_or_array_layers = 1, + }, + .format = .depth32_float, + .mip_level_count = 1, + .sample_count = 1, + }); + const view = gctx.createTextureView(texture, .{}); + return .{ .texture = texture, .view = view }; +} + +pub const GPA = if (emscripten) zems.EmmalocAllocator else std.heap.GeneralPurposeAllocator(.{}); +pub const MainState = struct { + is_init: bool = false, // main fully initialized + window: *zglfw.Window = undefined, + gpa: GPA = undefined, + demo: DemoState = undefined, +}; +pub var main_state: MainState = .{}; + +// still main should cleans up with errdefer, but if all goes well this cleans up successfully init state +pub fn mainDeinit() void { + if (!main_state.is_init) return; + const allocator = main_state.gpa.allocator(); + zgui.backend.deinit(); + zgui.deinit(); + deinit(allocator, &main_state.demo); + main_state.window.destroy(); + zglfw.terminate(); + _ = main_state.gpa.deinit(); +} + +pub fn main() !void { + defer if (!emscripten) mainDeinit(); + + zglfw.init() catch { + std.log.err("Failed to initialize GLFW library.", .{}); + return; + }; + errdefer zglfw.terminate(); + + // Change current working directory to where the executable is located. + if (!emscripten) { + var buffer: [1024]u8 = undefined; + const path = std.fs.selfExeDirPath(buffer[0..]) catch "."; + std.os.chdir(path) catch {}; + } + + if (emscripten) { + // by default emscripten initializes on window creation WebGL context + // this flag skips context creation. otherwise we later can't create webgpu surface + zglfw.WindowHint.set(.client_api, @intFromEnum(zglfw.ClientApi.no_api)); + } + const window = zglfw.Window.create(1600, 1000, window_title, null) catch |err| { + std.log.err("Failed to create demo window. {}", .{err}); + return; + }; + errdefer window.destroy(); + main_state.window = window; + window.setSizeLimits(400, 400, -1, -1); + + main_state.gpa = GPA{}; + const gpa = &main_state.gpa; + errdefer _ = if (!emscripten) gpa.deinit(); + + const allocator = gpa.allocator(); + + main_state.demo = init(allocator, window) catch |err| { + std.log.err("Failed to initialize the demo. {}", .{err}); + return; + }; + errdefer deinit(allocator, &main_state.demo); + const demo = &main_state.demo; + + const scale_factor = scale_factor: { + const scale = window.getContentScale(); + break :scale_factor @max(scale[0], scale[1]); + }; + + zgui.init(allocator); + errdefer zgui.deinit(); + + if (emscripten) { + zgui.io.setIniFilename(null); + // todo: font - embed and load from wasm memory? + } + //_ = zgui.io.addFontFromFile(content_dir ++ "Roboto-Medium.ttf", math.floor(16.0 * scale_factor)); + + zgui.backend.init( + window, + demo.gctx.device, + @intFromEnum(zgpu.GraphicsContext.swapchain_format), + ); + errdefer zgui.backend.deinit(); + + zgui.getStyle().scaleAllSizes(scale_factor); + + main_state.is_init = true; + if (comptime !emscripten) { + while (!window.shouldClose() and window.getKey(.escape) != .press) { + tick(); + } + } else { + const id = zems.emscripten_request_animation_frame_loop(&tickCB, null); + _ = id; + } +} + +pub fn tick() void { + if (!main_state.demo.gctx.canRender()) { + std.log.err("can't render!", .{}); + return; + } + zglfw.pollEvents(); + update(&main_state.demo); + draw(&main_state.demo); +} + +export fn tickCB(time: f64, user_data: ?*anyopaque) c_int { + _ = user_data; + _ = time; + tick(); + return 1; // return 0 to stop emscripten_request_animation_frame_loop +}