// MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS=1 -- use this if you're getting that odd crash on mac b: *std.Build, nw_builder: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.Mode, nw_mod: *std.Build.Module, spirvReflect: SpirvReflect.SpirvGenerator2, gltf2ozz: ozz.GltfToOzz, options: *std.Build.Step.Options, cookShaders: bool, backlogRoot: []const u8, // list of all shaders discovered under // content/_shaders/def reflectShaderPathList: [][]u8 = undefined, staticBuild: bool = false, nwdep: *std.Build.Dependency, apigen: *std.Build.Step.Compile, shaderEmbedGen: *std.Build.Step.Compile, loadDynamicsGen: *std.Build.Step.Compile, rcGen: *std.Build.Step.Compile, generatedApis: std.StringHashMapUnmanaged(*std.Build.Module) = .{}, const engineDepList = [_][]const u8{ "assets", "audio", "core", "net", "papyrus", "platform", "rend", "ui", "imgui", "physics", "sys", }; const BuildSystem = @This(); const std = @import("std"); const Build = std.Build; const LazyPath = Build.LazyPath; const SpirvReflect = @import("SpirvReflect"); const ozz = @import("ozz"); pub const InitOptions = struct { import_name: []const u8 = "Backlog", backlogRoot: []const u8 = "./BacklogEngine", staticBuild: bool = false, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, }; pub fn init(b: *std.Build, opts: InitOptions) BuildSystem { var buildOpts = declareBuildOptions(b); if (opts.target.result.os.tag == .linux) { // using a static build for linux... object loading hell is not fun std.debug.print("Important! only supporting static builds for linux", .{}); buildOpts.static_build = true; } const nwdep = b.dependency(opts.import_name, .{ .target = opts.target, .optimize = opts.optimize, .static_build = buildOpts.static_build, }); var self = BuildSystem{ .b = b, .nw_builder = nwdep.builder, .target = opts.target, .optimize = opts.optimize, .nw_mod = nwdep.module("Backlog"), .backlogRoot = opts.backlogRoot, .spirvReflect = SpirvReflect.SpirvGenerator2.init(nwdep.builder, .{}), .options = createGameOptions(b, buildOpts), .gltf2ozz = ozz.GltfToOzz.init(nwdep.builder, .{}), .cookShaders = b.option(bool, "cookShaders", "generates shaders and updates .json files before running the build. (needs to be done whenever shaders are updated, this just runs tools/scripts/cook-shaders.py)") orelse false, .staticBuild = buildOpts.static_build, .nwdep = nwdep, .apigen = nwdep.artifact("backlog-apigen"), .loadDynamicsGen = nwdep.artifact("backlog-generate-loadDynamics"), .rcGen = nwdep.artifact("backlog-generate-rc"), .shaderEmbedGen = nwdep.artifact("backlog-shaderEmbedGen"), }; const exeList = [2]*std.Build.Step.Compile{ self.gltf2ozz.exe, self.spirvReflect.reflect }; const install_tools = b.step("tools", "installs tools needed to generate outputs for the engine"); for (exeList) |exe| { const toolsInstall = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = "tools" } }, }); install_tools.dependOn(&toolsInstall.step); } { const runArtifact = b.addRunArtifact(self.gltf2ozz.exe); if (b.args) |args| { runArtifact.addArgs(args); } const run_exe = b.step("gltf2ozz", "runs the gltf animation converter."); run_exe.dependOn(&runArtifact.step); } { const runArtifact = b.addRunArtifact(self.spirvReflect.reflect); if (b.args) |args| { runArtifact.addArgs(args); } const run_exe = b.step("spv-reflect", "runs the gltf animation converter."); run_exe.dependOn(&runArtifact.step); } self.addDependencyInstalls(self.b, .ReleaseFast); return self; } pub const AddProgramOptions = struct { name: []const u8, desc: []const u8, root_source_file: LazyPath, imports: []const Build.Module.Import = &.{}, }; pub fn addProgram(self: *BuildSystem, opts: AddProgramOptions) *std.Build.Module { const b = self.b; const exe = self.nw_builder.addExecutable(.{ .name = opts.name, .target = self.target, .optimize = self.optimize, .root_source_file = self.nw_builder.path("engine/main.zig"), }); b.installArtifact(exe); const runArtifact = b.addRunArtifact(exe); if (b.args) |args| { runArtifact.addArgs(args); } const run_exe = b.step(self.b.fmt("run-{s}", .{opts.name}), opts.desc); run_exe.dependOn(&runArtifact.step); // main path = name/main.zig const mod = b.addModule(opts.name, .{ .target = self.target, .optimize = self.optimize, .root_source_file = opts.root_source_file, .imports = opts.imports, }); exe.root_module.addImport("main", mod); // todo.. remove this one and see what happens exe.root_module.addImport("core", self.nwdep.module("core")); // mod.addImport("Backlog", self.nw_mod); exe.root_module.addOptions("BacklogOptions", self.options); if (self.cookShaders) { const cookShadersScript = b.fmt("{s}/tools/scripts/cookShaders.py", .{self.backlogRoot}); const cookShadersCommand = b.addSystemCommand(&[_][]const u8{"python"}); cookShadersCommand.addArg(cookShadersScript); run_exe.dependOn(&cookShadersCommand.step); } b.getInstallStep().dependOn(self.nw_builder.getInstallStep()); // run_exe.dependOn(b.getInstallStep()); return mod; } pub const BuildOptions = struct { mutex_job_queue: bool, static_build: bool, zero_logging: bool, slow_logging: bool, force_mailbox: bool, }; pub fn declareBuildOptions(b: *std.Build) BuildOptions { return .{ .mutex_job_queue = b.option(bool, "mutex_job_queue", "temporary test, reverts to old mutex based queue behaviour in jobs.zig:JobManager") orelse false, .static_build = b.option(bool, "static_build", "builds the entire game as a single executable") orelse false, .zero_logging = b.option(bool, "zero_logging", "disables all logging, only intended for use on job dispatch testing") orelse false, .slow_logging = b.option(bool, "slow_logging", "Disables buffered logging, takes a hit to performance but gain timing information on logging") orelse false, .force_mailbox = b.option(bool, "force_mailbox", "forces mailbox mode for present mode. unlocks framerate to irresponsible levels") orelse false, }; } pub fn createGameOptions(b: *std.Build, options: BuildOptions) *std.Build.Step.Options { const opts = b.addOptions(); inline for (@typeInfo(BuildOptions).@"struct".fields) |field| { opts.addOption(bool, field.name, @field(options, field.name)); } // build options for core.zig // opts.addOption( // bool, // "mutex_job_queue", // ); // opts.addOption( // bool, // "static_build", // ); // opts.addOption( // bool, // "zero_logging", // ); // opts.addOption( // bool, // "slow_logging", // ); // opts.addOption( // bool, // "force_mailbox", // ); return opts; } pub fn addExtraModule(self: *BuildSystem, mod: *std.Build.Module, moduleName: []const u8) void { const dep = self.b.dependency(moduleName, .{ .target = self.target, .optimize = self.optimize, .static_build = self.staticBuild }); mod.addImport(moduleName, dep.module(moduleName)); } pub fn addDependencyInstalls(self: *BuildSystem, b: *std.Build, optimize: std.builtin.OptimizeMode) void { //if (self.staticBuild) { //return; // } inline for (DynamicDepList) |d| { b.installArtifact(b.dependency( d.dep, .{ .target = self.target, .optimize = optimize, .static_build = self.staticBuild }, ).artifact(d.artifact)); } // b.installArtifact(b.dependency( // "sdl3", // .{ .target = self.target, .optimize = optimize, .static_build = self.staticBuild }, // ).artifact("SDL3")); } const DynamicDepList: []const struct { dep: []const u8, artifact: []const u8 } = &.{ .{ .dep = "sdl3", .artifact = "SDL3" }, .{ .dep = "spng", .artifact = "spng_c" }, .{ .dep = "lua", .artifact = "luac" }, .{ .dep = "miniaudio", .artifact = "miniaudio_c" }, .{ .dep = "zphysics", .artifact = "joltc" }, .{ .dep = "enet", .artifact = "enet_c" }, .{ .dep = "ozz", .artifact = "ozz_cpp" }, }; // all other modules are disabled by default pub const defaultEnabledModules: []const []const u8 = &.{ "core", "assets", "platform", "rend", }; pub const moduleOrder: []const []const u8 = &.{ "core", "sys", "assets", "platform", "net", "physics", "audio", "rend", "ui", "imgui", "papyrus", }; pub const DynamicModule = struct { name: []const u8, allocator: std.mem.Allocator, buildSystem: *BuildSystem, programName: []const u8, opts: AddProgramOptions, extras: std.ArrayListUnmanaged(GameModule) = .{}, gameModules: std.ArrayListUnmanaged(GameModule) = .{}, library: ?*std.Build.Step.Compile = null, pub fn addExtraModule(self: *@This(), module: []const u8) void { self.extras.append(self.allocator, .{ .name = module, .enabled = true }) catch @panic("out of memory"); } pub fn init(name: []const u8, p: *Program) *@This() { var self: *@This() = p.allocator.create(@This()) catch @panic("out of memory"); self.* = .{ .opts = p.opts, .programName = p.opts.name, .buildSystem = p.buildSystem, .allocator = p.allocator, .name = name, }; for (p.gameModules.items) |module| { self.gameModules.append(self.allocator, module) catch @panic("out of memory"); } for (p.extras.items) |module| { self.extras.append(self.allocator, module) catch @panic("out of memory"); } return self; } pub fn compileInstall(self: *@This()) *std.Build.Step.Compile { if (self.library) |lib| { return lib; } const b = self.buildSystem.b; const static = self.buildSystem.staticBuild; const lib = if (static) b.addStaticLibrary(.{ .root_source_file = self.opts.root_source_file, .link_libc = true, .optimize = self.buildSystem.optimize, .target = self.buildSystem.target, .name = self.name, }) else b.addSharedLibrary(.{ .root_source_file = self.opts.root_source_file, .link_libc = true, .optimize = self.buildSystem.optimize, .target = self.buildSystem.target, .name = self.name, }); var modlist = std.ArrayList([]const u8).init(self.buildSystem.b.allocator); for (self.gameModules.items) |mod| { if (mod.enabled) { modlist.append(mod.name) catch @panic("out of memory"); } } for (self.extras.items) |extra| { self.buildSystem.addExtraModule(lib.root_module, extra.name); } lib.root_module.addImport("backlog", self.buildSystem.generateApi(self.name, modlist.items, null)); lib.root_module.addOptions("BacklogOptions", self.buildSystem.options); if (static) { // generateApi should create static callers for the main program } else { const installExtern = b.addInstallArtifact(lib, .{ .dest_dir = .{ .override = .{ .custom = "modules" } }, }); b.getInstallStep().dependOn(&installExtern.step); } return lib; } }; pub const GameModule = struct { name: []const u8, enabled: bool = true, }; pub const Program = struct { allocator: std.mem.Allocator, opts: AddProgramOptions, gameModules: std.ArrayListUnmanaged(GameModule) = .{}, extras: std.ArrayListUnmanaged(GameModule) = .{}, dynamicModules: std.ArrayListUnmanaged(*DynamicModule) = .{}, buildSystem: *BuildSystem, iconPath: ?std.Build.LazyPath = null, pub fn setIconPath(self: *@This(), path: []const u8) void { if (self.iconPath != null) { return; } self.iconPath = self.buildSystem.generateRc(self.opts.name, path) catch return; } pub fn setModuleEnabled(self: *@This(), module: []const u8, enable: bool) void { for (self.gameModules.items) |*m| { if (std.mem.eql(u8, m.name, module)) { m.enabled = enable; return; } } // module not found, add it here. self.gameModules.append(self.allocator, .{ .name = module, .enabled = enable }) catch @panic("out of memory"); } pub fn addExtraModule(self: *@This(), module: []const u8) void { self.extras.append(self.allocator, .{ .name = module, .enabled = true }) catch @panic("out of memory"); } pub fn addDynamicModule(self: *@This(), module: []const u8, root_source_file: std.Build.LazyPath) *DynamicModule { const dynamicModule = DynamicModule.init(module, self); dynamicModule.opts.root_source_file = root_source_file; self.dynamicModules.append(self.allocator, dynamicModule) catch @panic("out of memory"); return dynamicModule; } pub fn compileInstall(self: *@This()) *std.Build.Module { const exe = self.buildSystem.addProgram(self.opts); var modlist = std.ArrayList([]const u8).init(self.buildSystem.b.allocator); for (self.gameModules.items) |mod| { if (mod.enabled) { modlist.append(mod.name) catch @panic("out of memory"); } } for (self.extras.items) |extra| { self.buildSystem.addExtraModule(exe, extra.name); } exe.addImport("backlog", self.buildSystem.generateApi(self.opts.name, modlist.items, self.dynamicModules.items)); if (self.buildSystem.target.result.os.tag == .windows) { if (self.iconPath) |ip| { exe.addWin32ResourceFile(.{ .file = ip, .flags = &.{}, }); } } return exe; } }; pub fn program(self: *BuildSystem, opts: AddProgramOptions) *Program { const prog = self.b.allocator.create(Program) catch unreachable; prog.* = .{ .allocator = self.b.allocator, .opts = opts, .buildSystem = self, }; for (moduleOrder) |module| { prog.setModuleEnabled(module, false); } for (defaultEnabledModules) |module| { prog.setModuleEnabled(module, true); } return prog; } // ========= standalone build instance ======= // maybe the default should be like an engine launcher/project launcher or something pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const static_build = b.option(bool, "static_build", "builds backlog dependencies for static linking") orelse false; const spirvDep = b.dependency("SpirvReflect", .{ .target = target, .optimize = optimize, .static_build = static_build, }); _ = spirvDep; const fwdGeneratorExe = b.addExecutable(.{ .name = "backlog-generate-fwd", .target = b.graph.host, .optimize = .Debug, .root_source_file = b.path("build/generateFwd.zig"), }); const generateExe = b.addExecutable(.{ .name = "backlog-apigen", .target = b.graph.host, .optimize = .Debug, .root_source_file = b.path("build/generateApi.zig"), }); const generateShaders = b.addExecutable(.{ .name = "backlog-shaderEmbedGen", .target = b.graph.host, .optimize = .Debug, .root_source_file = b.path("build/generateEmbeddedShaders.zig"), }); const generateRcExe = b.addExecutable(.{ .name = "backlog-generate-rc", .target = b.graph.host, .optimize = .Debug, .root_source_file = b.path("build/generateRc.zig"), }); const generateLoadDynamicsExe = b.addExecutable(.{ .name = "backlog-generate-loadDynamics", .target = b.graph.host, .optimize = .Debug, .root_source_file = b.path("build/generateLoadDynamics.zig"), }); b.installArtifact(generateLoadDynamicsExe); b.installArtifact(generateExe); b.installArtifact(generateShaders); b.installArtifact(generateRcExe); { const loadDynamicsStub = b.addModule("loadDynamicsStub", .{ .target = target, .optimize = optimize, .root_source_file = b.path("engine/loadDynamicsStub.zig"), }); _ = loadDynamicsStub; const mod = b.addModule("Backlog", .{ .target = target, .optimize = optimize, .root_source_file = b.path("engine/backlog.zig"), }); for (engineDepList) |depName| { const dep = b.dependency( depName, .{ .target = target, .optimize = optimize, .static_build = static_build, }, ); const run = b.addRunArtifact(fwdGeneratorExe); const output = run.addOutputFileArg(b.fmt("{s}_fwd.zig", .{depName})); const modFwd = b.addModule(depName, .{ .target = target, .optimize = optimize, .root_source_file = output, }); mod.addImport(b.fmt("{s}", .{depName}), modFwd); modFwd.addImport("module", dep.module(depName)); // mod.addImport(depName, dep.module(depName)); } // link in large platform support functions... special case. { const dep = b.dependency("sdl3", .{ .target = target, .optimize = optimize, .static_build = static_build, }); const lib = dep.module("SDL3"); mod.addImport("sdl3_fwd", lib); } } } pub fn generateApi(self: *@This(), programName: []const u8, moduleList: []const []const u8, dynamicModules: ?[]const *DynamicModule) *std.Build.Module { // std.debug.print("GENERATE API:: {s}\n", .{programName}); for (moduleList) |mod| { _ = mod; // std.debug.print("{s}\n", .{mod}); } // if (self.generatedApis.get(programName)) |m| { return m; } const run = self.b.addRunArtifact(self.apigen); const pfile = self.b.fmt("{s}Api.zig", .{programName}); const output = run.addOutputFileArg(pfile); for (moduleList) |modName| { run.addArg(modName); } const mod = self.b.addModule(pfile, .{ .target = self.target, .optimize = self.optimize, .root_source_file = output, }); for (moduleList) |modName| { mod.addImport(modName, self.nwdep.module(modName)); } const loadStatics = self.generateInstallStaticResources(programName, "content/_shaders") catch unreachable; mod.addImport("staticShaderLoader", loadStatics); // deal with dynamics if (dynamicModules) |dynamics| { const loadDynamics = self.generateLoadDynamics(programName, dynamics) catch @panic("unknown"); mod.addImport("loadDynamics", loadDynamics); } else { mod.addImport("loadDynamics", self.nwdep.module("loadDynamicsStub")); } self.generatedApis.put(self.b.allocator, programName, mod) catch @panic("out of memory"); return mod; } pub fn generateRc(self: *@This(), programName: []const u8, iconPath: []const u8) !std.Build.LazyPath { const run = self.b.addRunArtifact(self.rcGen); const pfile = self.b.fmt("{s}.rc", .{programName}); const output = run.addOutputFileArg(pfile); run.addArg(iconPath); return output; } pub fn generateLoadDynamics(self: *@This(), programName: []const u8, dynamics: []const *DynamicModule) !*std.Build.Module { const run = self.b.addRunArtifact(self.loadDynamicsGen); const pfile = self.b.fmt("{s}LoadDynamics.zig", .{programName}); const output = run.addOutputFileArg(pfile); if (self.staticBuild) { run.addArg("static"); } else { run.addArg("dynamic"); } for (dynamics) |dyn| { run.addArg(dyn.name); } const mod = self.b.addModule(pfile, .{ .target = self.target, .optimize = self.optimize, .root_source_file = output, }); mod.addImport("core", self.nwdep.module("core")); for (dynamics) |dyn| { const dynmod = dyn.compileInstall(); mod.addImport(dyn.name, dynmod.root_module); } return mod; } pub fn generateInstallStaticResources(self: *@This(), programName: []const u8, shadersPath: []const u8) !*std.Build.Module { const run = self.b.addRunArtifact(self.shaderEmbedGen); const pfile = self.b.fmt("{s}ShaderGen.zig", .{programName}); const output = run.addOutputFileArg(pfile); run.addArg(shadersPath); const b = self.b; const mod = self.b.addModule(pfile, .{ .target = self.target, .optimize = self.optimize, .root_source_file = output, }); mod.addImport("core", self.nwdep.module("core")); var dir = try std.fs.cwd().openDir("content/_shaders", .{ .iterate = true }); defer dir.close(); var walker = dir.iterate(); while (try walker.next()) |shaderType| { if (shaderType.kind == .directory) { var d2 = try dir.openDir(shaderType.name, .{ .iterate = true }); defer d2.close(); var w2 = d2.iterate(); while (try w2.next()) |shaderName| { const shaderPath = b.fmt("content/_shaders/{s}/{s}", .{ shaderType.name, shaderName.name }); mod.addAnonymousImport(shaderName.name, .{ .root_source_file = b.path(shaderPath), }); } } } return mod; }