shadertoy: decouple from vulkan/shaders.zig via LoadOptions hooks
Removes the cross-backend reach where `src/renderer/shadertoy.zig`
imported `vulkan/shaders.zig` directly to call `vulkanizeGlsl` and
hard-coded `target == .spv` checks for the `GHASTTY_VULKAN` define.
Backend-agnostic file no longer touches anything backend-specific.
Replaces the `target` parameter with a `LoadOptions` struct:
pub const LoadOptions = struct {
target: Target,
extra_defines: []const []const u8 = &.{},
rewrite: ?Rewriter = null,
};
`generic.zig` builds it per-call from comptime decls on the
`GraphicsAPI` type:
.extra_defines = if (@hasDecl(GraphicsAPI, "custom_shader_extra_defines"))
GraphicsAPI.custom_shader_extra_defines else &.{},
.rewrite = if (@hasDecl(GraphicsAPI, "rewriteCustomShaderSource"))
GraphicsAPI.rewriteCustomShaderSource else null,
Vulkan.zig declares both:
pub const custom_shader_extra_defines = &.{"GHASTTY_VULKAN 1"};
pub const rewriteCustomShaderSource = shaders.vulkanizeGlsl;
OpenGL and Metal omit the decls entirely → zero-cost for backends
that don't need them. Same pattern existing comptime hooks already
follow (`supports_custom_shaders`, `custom_shader_target`).
Verified via Docker (zig 0.15.2 linux-arm64):
zig build -Drenderer=vulkan -Dapp-runtime=none → clean
zig build -Drenderer=opengl -Dapp-runtime=none → clean
Step 4 of 6 in the PR-17 review refactor.
Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
parent
c2f7b6c395
commit
6ca24b7b4a
|
|
@ -100,6 +100,23 @@ pub const supports_custom_shaders: bool = true;
|
|||
/// Vulkan's clip-space Y axis points down (unlike OpenGL).
|
||||
pub const custom_shader_y_is_down = true;
|
||||
|
||||
/// Extra `#define` lines `shadertoy.loadFromFile` injects into the
|
||||
/// prefix between `#version` and the rest. `GHASTTY_VULKAN`
|
||||
/// activates the Vulkan-side `gl_FragCoord` flip + `texture()`
|
||||
/// upper-left wrap so `mainImage` sees shadertoy-convention coords
|
||||
/// even though Vulkan rasterizes Y-down. OpenGL/MSL backends omit
|
||||
/// this decl entirely and pass `&.{}` from `generic.zig`.
|
||||
pub const custom_shader_extra_defines: []const []const u8 = &.{"GHASTTY_VULKAN 1"};
|
||||
|
||||
/// GLSL → GLSL rewriter `shadertoy.loadFromFile` runs after the
|
||||
/// prefix splice and before the SPIR-V compile. Plugs the
|
||||
/// `vulkanizeGlsl` pass that rewrites `layout(binding = N)` into
|
||||
/// `layout(set = S, binding = N)` so the resulting SPIR-V matches
|
||||
/// the renderer's multi-set descriptor layout. Without this, the
|
||||
/// shader's `iChannel0` lands at set 0 binding 0 while the post
|
||||
/// pipeline binds it at set 1 binding 0 → sampler returns garbage.
|
||||
pub const rewriteCustomShaderSource = shaders.vulkanizeGlsl;
|
||||
|
||||
/// Single-buffered for v1; fence-paced submit-then-wait means there's
|
||||
/// only ever one frame in flight.
|
||||
pub const swap_chain_count = 1;
|
||||
|
|
|
|||
|
|
@ -856,7 +856,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
(shadertoy.loadFromFiles(
|
||||
arena_alloc,
|
||||
self.config.custom_shaders,
|
||||
GraphicsAPI.custom_shader_target,
|
||||
.{
|
||||
.target = GraphicsAPI.custom_shader_target,
|
||||
// Optional per-backend hooks. Resolved at
|
||||
// comptime via `@hasDecl`, so backends that
|
||||
// don't need them stay free of extra-define /
|
||||
// GLSL-rewrite logic.
|
||||
.extra_defines = if (@hasDecl(GraphicsAPI, "custom_shader_extra_defines"))
|
||||
GraphicsAPI.custom_shader_extra_defines
|
||||
else
|
||||
&.{},
|
||||
.rewrite = if (@hasDecl(GraphicsAPI, "rewriteCustomShaderSource"))
|
||||
GraphicsAPI.rewriteCustomShaderSource
|
||||
else
|
||||
null,
|
||||
},
|
||||
) catch |err| err: {
|
||||
log.warn("error loading custom shaders err={}", .{err});
|
||||
break :err &.{};
|
||||
|
|
|
|||
|
|
@ -54,18 +54,53 @@ pub const Uniforms = extern struct {
|
|||
/// spirv-cross-emitted main() didn't match the upstream prefix).
|
||||
pub const Target = enum { glsl, msl, spv };
|
||||
|
||||
/// Optional GLSL → GLSL rewriter applied between the prefix splice
|
||||
/// and the SPIR-V compile. Vulkan plugs in `vulkanizeGlsl` here so
|
||||
/// SPIR-V output uses the renderer's multi-set descriptor layout;
|
||||
/// other backends pass `null`. Owns its allocation under the
|
||||
/// caller's allocator (`shadertoy.loadFromFile` runs it inside an
|
||||
/// arena that's torn down at function exit, so the rewriter's
|
||||
/// returned slice may be arena-owned).
|
||||
pub const Rewriter = *const fn (
|
||||
alloc: Allocator,
|
||||
src: []const u8,
|
||||
) Allocator.Error![:0]const u8;
|
||||
|
||||
/// What `loadFromFile`/`loadFromFiles` need beyond the path itself.
|
||||
/// Keeps the function decoupled from any specific backend — every
|
||||
/// backend-flavored knob becomes an explicit field, and `shadertoy`
|
||||
/// itself reaches into no other backend's submodules.
|
||||
pub const LoadOptions = struct {
|
||||
/// Output language / format. See `Target` for the per-variant
|
||||
/// rationale.
|
||||
target: Target,
|
||||
|
||||
/// `#define <body>` lines injected after the prefix's
|
||||
/// `#version` directive. Vulkan passes
|
||||
/// `&.{"GHASTTY_VULKAN 1"}` so the prefix's `main()` flips
|
||||
/// `gl_FragCoord.y` and wraps `texture()` for upper-left
|
||||
/// sampling; OpenGL/MSL pass `&.{}`.
|
||||
extra_defines: []const []const u8 = &.{},
|
||||
|
||||
/// Optional second-pass GLSL transform run between the prefix
|
||||
/// splice and the SPIR-V compile. Vulkan installs
|
||||
/// `vulkan/shaders.zig:vulkanizeGlsl` here for the multi-set
|
||||
/// descriptor layout rewrite; other backends leave it null.
|
||||
rewrite: ?Rewriter = null,
|
||||
};
|
||||
|
||||
/// Load a set of shaders from files and convert them to the target
|
||||
/// format. The shader order is preserved.
|
||||
///
|
||||
/// Result element type depends on `target`: `.glsl`/`.msl` produce
|
||||
/// null-terminated UTF-8 source strings; `.spv` produces SPIR-V
|
||||
/// binary bytes (4-byte-aligned, no trailing null). We unify the
|
||||
/// return type as `[]const []const u8` and have the caller cast/
|
||||
/// Result element type depends on `opts.target`: `.glsl`/`.msl`
|
||||
/// produce null-terminated UTF-8 source strings; `.spv` produces
|
||||
/// SPIR-V binary bytes (4-byte-aligned, no trailing null). We unify
|
||||
/// the return type as `[]const []const u8` and have the caller cast/
|
||||
/// reinterpret as needed.
|
||||
pub fn loadFromFiles(
|
||||
alloc_gpa: Allocator,
|
||||
paths: configpkg.RepeatablePath,
|
||||
target: Target,
|
||||
opts: LoadOptions,
|
||||
) ![]const []const u8 {
|
||||
var list: std.ArrayList([]const u8) = .empty;
|
||||
defer list.deinit(alloc_gpa);
|
||||
|
|
@ -77,7 +112,7 @@ pub fn loadFromFiles(
|
|||
.required => |path| .{ path, false },
|
||||
};
|
||||
|
||||
const shader = loadFromFile(alloc_gpa, path, target) catch |err| {
|
||||
const shader = loadFromFile(alloc_gpa, path, opts) catch |err| {
|
||||
if (err == error.FileNotFound and optional) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -101,7 +136,7 @@ pub fn loadFromFiles(
|
|||
pub fn loadFromFile(
|
||||
alloc_gpa: Allocator,
|
||||
path: []const u8,
|
||||
target: Target,
|
||||
opts: LoadOptions,
|
||||
) ![]const u8 {
|
||||
var arena = ArenaAllocator.init(alloc_gpa);
|
||||
defer arena.deinit();
|
||||
|
|
@ -120,38 +155,32 @@ pub fn loadFromFile(
|
|||
);
|
||||
};
|
||||
|
||||
// Convert to full GLSL. For `.spv` we inject
|
||||
// `#define GHASTTY_VULKAN 1` so the prefix's `main()` mirrors
|
||||
// `gl_FragCoord.y` AND wraps `texture()` to flip uv.y. Together
|
||||
// those make `mainImage` see a shadertoy-convention fragCoord
|
||||
// (lower-left origin) AND sample `iChannel0` correctly even
|
||||
// though Vulkan natively uses upper-left for both. OpenGL/MSL
|
||||
// builds don't get the define and use the GL-native paths
|
||||
// unchanged.
|
||||
// Convert to full GLSL. `opts.extra_defines` lets a backend
|
||||
// inject `#define <body>` lines after the prefix's `#version`
|
||||
// directive — Vulkan uses this to flip `gl_FragCoord.y` and
|
||||
// wrap `texture()` for upper-left sampling so `mainImage` sees
|
||||
// shadertoy-convention coords; OpenGL/MSL pass `&.{}` and use
|
||||
// the GL-native paths unchanged.
|
||||
const glsl_raw: [:0]const u8 = glsl: {
|
||||
var stream: std.Io.Writer.Allocating = .init(alloc);
|
||||
const defines: []const []const u8 = if (target == .spv)
|
||||
&.{"GHASTTY_VULKAN 1"}
|
||||
else
|
||||
&.{};
|
||||
try glslFromShader(&stream.writer, src, defines);
|
||||
try glslFromShader(&stream.writer, src, opts.extra_defines);
|
||||
try stream.writer.writeByte(0);
|
||||
break :glsl stream.written()[0 .. stream.written().len - 1 :0];
|
||||
};
|
||||
|
||||
// For `.spv` we also run `vulkanizeGlsl` on the source so the
|
||||
// resulting SPIR-V uses the renderer's multi-set descriptor
|
||||
// layout (UBO=set 0, samplers=set 1, storage=set 2). Without
|
||||
// this, glslang assigns everything to `set 0` and our post
|
||||
// pipeline's descriptor set layout (one set per resource type)
|
||||
// would point at the wrong slots — the shader's `iChannel0` ends
|
||||
// up at set 0 binding 0 while our pipeline binds it at set 1
|
||||
// binding 0, sampling returns garbage / zero, output is
|
||||
// transparent.
|
||||
const glsl: [:0]const u8 = if (target == .spv) blk: {
|
||||
const vshaders = @import("vulkan/shaders.zig");
|
||||
break :blk try vshaders.vulkanizeGlsl(alloc, glsl_raw);
|
||||
} else glsl_raw;
|
||||
// Optional second-pass GLSL transform. Vulkan installs
|
||||
// `vulkanizeGlsl` here so the resulting SPIR-V uses the
|
||||
// renderer's multi-set descriptor layout (UBO=set 0,
|
||||
// samplers=set 1, storage=set 2). Without that rewrite,
|
||||
// glslang assigns everything to `set 0` and the post pipeline's
|
||||
// descriptor set layout points at the wrong slots — the
|
||||
// shader's `iChannel0` ends up at set 0 binding 0 while the
|
||||
// pipeline binds it at set 1 binding 0, sampling returns
|
||||
// garbage / zero, output is transparent.
|
||||
const glsl: [:0]const u8 = if (opts.rewrite) |f|
|
||||
try f(alloc, glsl_raw)
|
||||
else
|
||||
glsl_raw;
|
||||
|
||||
// Convert to SPIR-V
|
||||
const spirv: []const u8 = spirv: {
|
||||
|
|
@ -180,7 +209,7 @@ pub fn loadFromFile(
|
|||
// Important: using the alloc_gpa here on purpose because this is
|
||||
// the final result that will be returned to the caller (the arena
|
||||
// gets torn down on function exit).
|
||||
return switch (target) {
|
||||
return switch (opts.target) {
|
||||
.glsl => try glslFromSpv(alloc_gpa, spirv),
|
||||
.msl => try mslFromSpv(alloc_gpa, spirv),
|
||||
.spv => spv: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue