293 lines
10 KiB
Zig
293 lines
10 KiB
Zig
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!", .{});
|
|
}
|