forked from mirror/vulkan-zig
The build root and cache root paths were being concatanated which resulted in the wrong path being calculated for the shaders.
225 lines
9.1 KiB
Zig
225 lines
9.1 KiB
Zig
const std = @import("std");
|
|
const path = std.fs.path;
|
|
const Builder = std.build.Builder;
|
|
const Step = std.build.Step;
|
|
const GeneratedFile = std.build.GeneratedFile;
|
|
|
|
/// Utility functionality to help with compiling shaders from build.zig.
|
|
/// Invokes a shader compile command (e.g., glslc ...) for each shader
|
|
/// added via `addShader`.
|
|
pub const ShaderCompileStep = struct {
|
|
/// The directory within the zig-cache directory that is used to store
|
|
/// shader artifacts.
|
|
pub const cache_dir = "shaders";
|
|
|
|
/// This structure contains additional options that pertain to specific shaders only.
|
|
pub const ShaderOptions = struct {
|
|
/// Additional arguments that should be passed to the shader compiler.
|
|
args: []const []const u8 = &.{},
|
|
|
|
/// Paths of additional files that should be watched for changes to
|
|
/// trigger recompilation.
|
|
watched_files: []const []const u8 = &.{},
|
|
|
|
/// To ensure that if compilation options change, the shader is recompiled
|
|
/// properly.
|
|
fn hash(self: ShaderOptions, b: *Builder, hasher: anytype) !void {
|
|
for (self.args) |arg| {
|
|
hasher.update(arg);
|
|
}
|
|
for (self.watched_files) |file_path| {
|
|
const full_path = b.build_root.join(b.allocator, &.{file_path}) catch unreachable;
|
|
|
|
const source = std.fs.cwd().readFileAlloc(
|
|
b.allocator,
|
|
full_path,
|
|
std.math.maxInt(usize),
|
|
) catch |err| switch (err) {
|
|
error.FileNotFound => {
|
|
std.log.err("could not open file '{s}'", .{file_path});
|
|
return error.FileNotFound;
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
hasher.update(source);
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Structure representing a shader to be compiled.
|
|
const Shader = struct {
|
|
/// The name of the shader in the generated file.
|
|
/// Must be unique for all shaders added to this ShaderCompileStep.
|
|
name: []const u8,
|
|
|
|
/// The path to the shader, relative to the current build root.
|
|
source_path: []const u8,
|
|
|
|
/// Miscellaneous options to pass when compiling the shader.
|
|
options: ShaderOptions,
|
|
};
|
|
|
|
step: Step,
|
|
b: *Builder,
|
|
|
|
/// The command and optional arguments used to invoke the shader compiler.
|
|
compile_command: []const []const u8,
|
|
|
|
/// The compiler flag used to specify the output path, `-o` most of the time
|
|
output_flag: []u8,
|
|
|
|
/// List of shaders that are to be compiled.
|
|
shaders: std.ArrayList(Shader),
|
|
|
|
/// The main Zig file that contains all the shaders. Each shader is included as
|
|
/// `pub const ${name} align(@alignOf(u32))= @embedFile("${path").*;`
|
|
generated_file: GeneratedFile,
|
|
|
|
/// Create a ShaderCompileStep for `builder`. When this step is invoked by the build
|
|
/// system, `<compile_command...> <shader_source> <output_flag> <path>` is invoked for each shader.
|
|
/// For example, if one calls this with `create(b, "glslc", "-o")` and then
|
|
/// `c.addShader("vertex", "vertex.glsl", .{})`, the command will be `glslc vertex.glsl -o <path>`
|
|
pub fn create(builder: *Builder, compile_command: []const []const u8, output_flag: []const u8) *ShaderCompileStep {
|
|
const self = builder.allocator.create(ShaderCompileStep) catch unreachable;
|
|
self.* = .{
|
|
.step = Step.init(.custom, "shaders", builder.allocator, make),
|
|
.b = builder,
|
|
.compile_command = builder.dupeStrings(compile_command),
|
|
.output_flag = builder.dupe(output_flag),
|
|
.shaders = std.ArrayList(Shader).init(builder.allocator),
|
|
.generated_file = undefined,
|
|
};
|
|
self.generated_file = .{ .step = &self.step };
|
|
return self;
|
|
}
|
|
|
|
/// Returns the shaders module with name.
|
|
pub fn getModule(self: *ShaderCompileStep) *std.build.Module {
|
|
return self.b.createModule(.{
|
|
.source_file = self.getSource(),
|
|
});
|
|
}
|
|
|
|
/// Returns the file source for the generated shader resource code.
|
|
pub fn getSource(self: *ShaderCompileStep) std.build.FileSource {
|
|
return .{ .generated = &self.generated_file };
|
|
}
|
|
|
|
/// Add a shader to be compiled. `src` is shader source path, relative to the project root.
|
|
/// Returns the full path where the compiled binary will be stored upon successful compilation.
|
|
/// This path can then be used to include the binary into an executable, for example by passing it
|
|
/// to @embedFile via an additional generated file.
|
|
pub fn add(self: *ShaderCompileStep, name: []const u8, src: []const u8, options: ShaderOptions) void {
|
|
const full_source_path = self.b.build_root.join(self.b.allocator, &.{src}) catch unreachable;
|
|
self.shaders.append(.{
|
|
.name = name,
|
|
.source_path = full_source_path,
|
|
.options = options,
|
|
}) catch unreachable;
|
|
}
|
|
|
|
/// Create a hash of a shader's source contents.
|
|
fn hashShaderToFileName(self: *ShaderCompileStep, shader: Shader) ![64]u8 {
|
|
const source = std.fs.cwd().readFileAlloc(
|
|
self.b.allocator,
|
|
shader.source_path,
|
|
std.math.maxInt(usize),
|
|
) catch |err| switch (err) {
|
|
error.FileNotFound => {
|
|
std.log.err("could not open shader '{s}'", .{shader.source_path});
|
|
return error.FileNotFound;
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
|
|
var hasher = std.crypto.hash.blake2.Blake2b384.init(.{});
|
|
// Random bytes to make ShaderCompileStep unique. Refresh with new random
|
|
// bytes when the implementation is changed in a non-backwards-compatible way.
|
|
hasher.update("Pw7Z*9Q8r!fLY8&!");
|
|
// Make sure that there is no cache hit if the shader's source has changed.
|
|
hasher.update(source);
|
|
// Not only the shader source must be the same to ensure uniqueness -
|
|
// the compilation options must be the same as well!
|
|
try shader.options.hash(self.b, &hasher);
|
|
// And the compile command, too.
|
|
for (self.compile_command) |cmd| {
|
|
hasher.update(cmd);
|
|
}
|
|
|
|
return digest(&hasher);
|
|
}
|
|
|
|
/// Create a base-64 hash digest from a hasher, which we can use as file name.
|
|
fn digest(hasher: anytype) [64]u8 {
|
|
var hash_digest: [48]u8 = undefined;
|
|
hasher.final(&hash_digest);
|
|
var hash: [64]u8 = undefined;
|
|
_ = std.fs.base64_encoder.encode(&hash, &hash_digest);
|
|
return hash;
|
|
}
|
|
|
|
/// Internal build function.
|
|
fn make(step: *Step) !void {
|
|
const self = @fieldParentPtr(ShaderCompileStep, "step", step);
|
|
const cwd = std.fs.cwd();
|
|
|
|
var cmd = std.ArrayList([]const u8).init(self.b.allocator);
|
|
try cmd.appendSlice(self.compile_command);
|
|
const base_cmd_len = cmd.items.len;
|
|
|
|
var shaders_file_contents = std.ArrayList(u8).init(self.b.allocator);
|
|
const shaders_out = shaders_file_contents.writer();
|
|
|
|
const shaders_dir = try self.b.cache_root.join(
|
|
self.b.allocator,
|
|
&.{cache_dir},
|
|
);
|
|
try cwd.makePath(shaders_dir);
|
|
|
|
for (self.shaders.items) |shader| {
|
|
const shader_basename = try self.hashShaderToFileName(shader);
|
|
const shader_out_path = try std.fs.path.join(self.b.allocator, &.{
|
|
shaders_dir,
|
|
&shader_basename,
|
|
});
|
|
|
|
// This path must be relative to the shaders zig file - which is in the same directory
|
|
try shaders_out.print("pub const {s} align(@alignOf(u32)) = @embedFile(\"{s}\").*;\n", .{
|
|
shader.name,
|
|
&shader_basename,
|
|
});
|
|
|
|
// If we have a cache hit, we can save some compile time by not invoking the compile command.
|
|
compile_shader: {
|
|
std.fs.accessAbsolute(shader_out_path, .{}) catch |err| switch (err) {
|
|
error.FileNotFound => break :compile_shader,
|
|
else => |e| return e,
|
|
};
|
|
|
|
continue;
|
|
}
|
|
|
|
cmd.items.len = base_cmd_len;
|
|
|
|
try cmd.appendSlice(shader.options.args);
|
|
try cmd.appendSlice(&.{ shader.source_path, self.output_flag, shader_out_path });
|
|
try self.b.spawnChild(cmd.items);
|
|
}
|
|
|
|
// Generate a file name for the shaders zig source based on the contents of shaders_file_contents.
|
|
// In this case we don't need to omit writing the file - Zig does this check already for us.
|
|
var hasher = std.crypto.hash.blake2.Blake2b384.init(.{});
|
|
// Note: don't need to seed the hasher - it transitively contains the seed from
|
|
// hashShaderToFileName. Change that if the implementation changes.
|
|
hasher.update(shaders_file_contents.items);
|
|
|
|
const shaders_path = try std.fs.path.join(
|
|
self.b.allocator,
|
|
&.{ shaders_dir, &digest(&hasher) },
|
|
);
|
|
|
|
try cwd.writeFile(shaders_path, shaders_file_contents.items);
|
|
self.generated_file.path = shaders_path;
|
|
}
|
|
};
|