diff --git a/tools/compileShaders/build.zig b/tools/compileShaders/build.zig new file mode 100644 index 0000000..432ccd8 --- /dev/null +++ b/tools/compileShaders/build.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "compileShaders", + .root_source_file = b.path("compileShaders.zig"), + .target = target, + .optimize = optimize, + }); + + b.installArtifact(exe); + + 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 compileShaders tool"); + run_step.dependOn(&run_cmd.step); +} \ No newline at end of file diff --git a/tools/compileShaders/compileShaders.zig b/tools/compileShaders/compileShaders.zig new file mode 100644 index 0000000..0846bf8 --- /dev/null +++ b/tools/compileShaders/compileShaders.zig @@ -0,0 +1,292 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +var stringsArena: std.heap.ArenaAllocator = undefined; + +fn printLog(comptime fmt: []const u8, args: anytype) void { + std.debug.print(fmt ++ "\n", args); +} + +fn printErr(comptime fmt: []const u8, args: anytype) void { + std.debug.print("ERROR: " ++ fmt ++ "\n", args); +} + +const OutputFormat = struct { + extension: []const u8, + args: []const []const u8, +}; + +const ShaderFile = struct { + path: []const u8, + base_name: []const u8, + source_dir: []const u8, +}; + +fn findContentDir(allocator: std.mem.Allocator) ![]const u8 { + // First try local content directory + var content_dir = std.fs.cwd().openDir("content", .{}) catch |err| switch (err) { + error.FileNotFound => { + // Walk up directories looking for content.txt + var current_dir = try std.fs.cwd().realpathAlloc(allocator, "."); + defer allocator.free(current_dir); + + var last_path: ?[]const u8 = null; + while (last_path == null or !std.mem.eql(u8, current_dir, last_path.?)) { + const content_txt_path = try std.fs.path.join(allocator, &.{ current_dir, "content.txt" }); + defer allocator.free(content_txt_path); + + if (std.fs.cwd().openFile(content_txt_path, .{})) |file| { + defer file.close(); + + const content = try file.readToEndAlloc(allocator, 1024); + defer allocator.free(content); + + const rel_path = std.mem.trim(u8, content, " \t\n\r"); + const abs_path = try std.fs.path.join(allocator, &.{ current_dir, rel_path }); + return try stringsArena.allocator().dupe(u8, abs_path); + } else |_| { + last_path = current_dir; + const parent = std.fs.path.dirname(current_dir) orelse break; + current_dir = try allocator.dupe(u8, parent); + if (last_path) |lp| allocator.free(lp); + } + } + + return error.ContentDirNotFound; + }, + else => return err, + }; + defer content_dir.close(); + + const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); + defer allocator.free(cwd_path); + + const content_path = try std.fs.path.join(allocator, &.{ cwd_path, "content" }); + return try stringsArena.allocator().dupe(u8, content_path); +} + +fn getShadercrossPath(allocator: std.mem.Allocator) ![]const u8 { + const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); + defer allocator.free(cwd_path); + + const lib_path = try std.fs.path.join(allocator, &.{ cwd_path, "..", "..", "lib", "sdl3", "shadercross", "bin" }); + defer allocator.free(lib_path); + + const platform_dir = switch (builtin.os.tag) { + .linux => "linux", + .macos => "osx", + .windows => "win64", + else => return error.UnsupportedPlatform, + }; + + const platform_path = try std.fs.path.join(allocator, &.{ lib_path, platform_dir }); + defer allocator.free(platform_path); + + const exe_name = if (builtin.os.tag == .windows) "shadercross.exe" else "shadercross"; + const exe_path = try std.fs.path.join(allocator, &.{ platform_path, exe_name }); + + return try stringsArena.allocator().dupe(u8, exe_path); +} + +fn discoverShaders(allocator: std.mem.Allocator) !std.ArrayList(ShaderFile) { + var result = std.ArrayList(ShaderFile).init(allocator); + + const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); + defer allocator.free(cwd_path); + + const engine_path = try std.fs.path.join(allocator, &.{ cwd_path, "..", "..", "engine" }); + defer allocator.free(engine_path); + + var engine_dir = std.fs.cwd().openDir(engine_path, .{ .iterate = true }) catch |err| { + printErr("Failed to open engine directory: {}", .{err}); + return result; + }; + defer engine_dir.close(); + + var engine_iter = engine_dir.iterate(); + while (try engine_iter.next()) |entry| { + if (entry.kind != .directory) continue; + + const module_path = try std.fs.path.join(allocator, &.{ engine_path, entry.name }); + defer allocator.free(module_path); + + const shaders_path = try std.fs.path.join(allocator, &.{ module_path, "shaders" }); + defer allocator.free(shaders_path); + + var shaders_dir = std.fs.cwd().openDir(shaders_path, .{ .iterate = true }) catch continue; + defer shaders_dir.close(); + + var shader_iter = shaders_dir.iterate(); + while (try shader_iter.next()) |shader_entry| { + if (shader_entry.kind != .file) continue; + if (!std.mem.endsWith(u8, shader_entry.name, ".hlsl")) continue; + + const full_path = try std.fs.path.join(allocator, &.{ shaders_path, shader_entry.name }); + const base_name = shader_entry.name[0 .. shader_entry.name.len - 5]; // Remove .hlsl + + try result.append(.{ + .path = try stringsArena.allocator().dupe(u8, full_path), + .base_name = try stringsArena.allocator().dupe(u8, base_name), + .source_dir = try stringsArena.allocator().dupe(u8, shaders_path), + }); + } + } + + return result; +} + +fn runCommand(allocator: std.mem.Allocator, argv: []const []const u8) !void { + printLog("Running command: {s}", .{try std.mem.join(allocator, " ", argv)}); + + var child = std.process.Child.init(argv, allocator); + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + const term = try child.spawnAndWait(); + switch (term) { + .Exited => |code| { + if (code != 0) { + return error.CommandFailed; + } + }, + else => return error.CommandFailed, + } +} + +fn ensureDir(path: []const u8) !void { + std.fs.cwd().makePath(path) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; +} + +fn cookShaders(allocator: std.mem.Allocator, shader_files: []const ShaderFile, content_dir: []const u8, shadercross_path: []const u8) !void { + const cooked_root = try std.fs.path.join(allocator, &.{ content_dir, "_shaders" }); + defer allocator.free(cooked_root); + + const output_formats = [_]OutputFormat{ + .{ .extension = "dxil", .args = &.{} }, + .{ .extension = "spv", .args = &.{} }, + .{ .extension = "msl", .args = &.{} }, + }; + + var spv_files = std.ArrayList(struct { spv_path: []const u8, source_dir: []const u8 }).init(allocator); + defer spv_files.deinit(); + + // Compile shaders to all formats + for (output_formats) |format| { + const format_dir = try std.fs.path.join(allocator, &.{ cooked_root, format.extension }); + defer allocator.free(format_dir); + + try ensureDir(format_dir); + + for (shader_files) |shader| { + const output_name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ shader.base_name, format.extension }); + defer allocator.free(output_name); + + const output_path = try std.fs.path.join(allocator, &.{ format_dir, output_name }); + defer allocator.free(output_path); + + var cmd_args = std.ArrayList([]const u8).init(allocator); + defer cmd_args.deinit(); + + try cmd_args.append(shadercross_path); + try cmd_args.append(shader.path); + for (format.args) |arg| { + try cmd_args.append(arg); + } + try cmd_args.append("-o"); + try cmd_args.append(output_path); + + try runCommand(allocator, cmd_args.items); + + // Store SPV files for reflection processing + if (std.mem.eql(u8, format.extension, "spv")) { + try spv_files.append(.{ + .spv_path = try stringsArena.allocator().dupe(u8, output_path), + .source_dir = shader.source_dir, + }); + } + } + } + + // Generate reflection data for SPV files + for (spv_files.items) |spv_file| { + // Get file size to ensure it's not empty + const file = std.fs.cwd().openFile(spv_file.spv_path, .{}) catch continue; + defer file.close(); + + const file_size = try file.getEndPos(); + if (file_size <= 1) { + printErr("SPV file is empty or too small: {s}", .{spv_file.spv_path}); + continue; + } + + const base_name = std.fs.path.basename(spv_file.spv_path); + const name_no_ext = base_name[0 .. base_name.len - 4]; // Remove .spv + + const json_name = try std.fmt.allocPrint(allocator, "{s}.json", .{name_no_ext}); + defer allocator.free(json_name); + + const json_path = try std.fs.path.join(allocator, &.{ spv_file.source_dir, json_name }); + defer allocator.free(json_path); + + const cmd_args = [_][]const u8{ + "spirv-cross", + spv_file.spv_path, + "--reflect", + "--output", + json_path, + }; + + try runCommand(allocator, &cmd_args); + } +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + + var arena = std.heap.ArenaAllocator.init(gpa.allocator()); + defer arena.deinit(); + + const allocator = arena.allocator(); + stringsArena = std.heap.ArenaAllocator.init(gpa.allocator()); + defer stringsArena.deinit(); + + printLog("Starting shader compilation...", .{}); + + // Find content directory + const content_dir = findContentDir(allocator) catch |err| { + printErr("Failed to find content directory: {}", .{err}); + return; + }; + printLog("Content target dir: {s}", .{content_dir}); + + // Get shadercross path + const shadercross_path = getShadercrossPath(allocator) catch |err| { + printErr("Failed to get shadercross path: {}", .{err}); + return; + }; + printLog("Shadercross path: {s}", .{shadercross_path}); + + // Discover shader files + var shader_files = discoverShaders(allocator) catch |err| { + printErr("Failed to discover shaders: {}", .{err}); + return; + }; + defer shader_files.deinit(); + + printLog("Found {} shader files:", .{shader_files.items.len}); + for (shader_files.items) |shader| { + printLog(" {s}", .{shader.path}); + } + + // Cook shaders + cookShaders(allocator, shader_files.items, content_dir, shadercross_path) catch |err| { + printErr("Failed to cook shaders: {}", .{err}); + return; + }; + + printLog("Shader compilation completed successfully!", .{}); +}