renderer/vulkan: custom-shader post pipeline (SPV-direct, Y-aware)
Custom shaders (`custom-shader = ...` in config) now work on the
Vulkan backend with the same visual result as OpenGL. Five pieces
land together:
1. `shadertoy.Target` gains `.spv`. `loadFromFile` for `.spv`
returns the raw SPIR-V binary (4-byte-aligned) emitted by
glslang, skipping the spirv-cross GLSL roundtrip that the
`.glsl` target uses. Vulkan consumes SPIR-V natively — feeding
the user shader through GLSL→SPIR-V→GLSL→SPIR-V was double
compile work AND lost the upstream source structure for any
text-level rewrites. Return type is unified as
`[]const []const u8`; OpenGL's `initShaders` keeps its existing
`[:0]const u8` signature by reinterpreting (the underlying bytes
are still null-terminated for the GLSL path).
2. `Vulkan.custom_shader_target = .spv`. The post pipeline now
loads SPIR-V directly into a `Module.initFromSpirv`, skipping
the second glslang compile inside `Module.init` that the
built-in shaders go through.
3. `vulkanizeGlsl` becomes `pub` so `shadertoy.zig` can call it on
the GLSL before `spirvFromGlsl`. Without that, the SPIR-V comes
out with every binding at `set 0` (glslang's default for
unannotated bindings), but our post pipeline's descriptor set
layout splits UBO into set 0 and the sampler into set 1 — the
shader's `iChannel0` would read from the wrong slot and the
window goes transparent. Running `vulkanizeGlsl` on the
shadertoy GLSL first rewrites `layout(binding = N)` →
`layout(set = S, binding = N)` with the same set/binding scheme
the renderer pipelines wire up.
4. Y-flip: shadertoy expects `gl_FragCoord` lower-left, Vulkan's
is upper-left. `shadertoy.zig` injects `#define GHASTTY_VULKAN 1`
before the prefix's `main()` (placed after `#version` since GLSL
requires it first), and the prefix's main branches:
#ifdef GHASTTY_VULKAN
mainImage(_fragColor, vec2(gl_FragCoord.x,
iResolution.y - gl_FragCoord.y));
#else
mainImage(_fragColor, gl_FragCoord.xy);
#endif
5. `RenderPass.begin`'s viewport Y-flip is now conditional on the
attachment kind: enabled for `.target` (the dmabuf Qt mmaps and
paints with origin-upper-left), disabled for `.texture` (the
custom-shader back_texture). The flipped fragCoord from (4) and
the un-flipped back_texture orientation cancel — the shadertoy
`uv = fragCoord/iResolution` sampling reads the right row, the
terminal content inside the custom shader paints upright. The
final post pass writes to `frame.target` so it keeps the
Y-flipped viewport.
`vulkanizeGlsl`'s `texture()` → `textureLod(..., 0.0)` rewrite is
now restricted to known unnormalized samplers (the two atlas
samplers `atlas_grayscale` / `atlas_color`) instead of every
`texture()` call. The implicit-LOD opcode is only forbidden for
unnormalized samplers — forcing every sampler through
`textureLod` made the driver work harder per call across the
whole custom shader.
Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
parent
6ef36d9934
commit
3cdda1ec9b
|
|
@ -301,12 +301,24 @@ pub fn drawFrameEnd(self: *OpenGL) void {
|
|||
pub fn initShaders(
|
||||
self: *const OpenGL,
|
||||
alloc: Allocator,
|
||||
custom_shaders: []const [:0]const u8,
|
||||
custom_shaders: []const []const u8,
|
||||
) !shaders.Shaders {
|
||||
_ = alloc;
|
||||
// `loadFromFiles` returns `[]const []const u8` so the SPV-target
|
||||
// Vulkan path can share the loader, but for `.glsl` the underlying
|
||||
// allocation IS null-terminated (`glslFromSpv` returns
|
||||
// `[:0]const u8` and writes a trailing null one past `.len`).
|
||||
// Cast each entry back to `[:0]const u8` so the downstream
|
||||
// `Pipeline.init` calls that expect a sentinel-terminated string
|
||||
// keep working without changing their signatures.
|
||||
const z_shaders = try self.alloc.alloc([:0]const u8, custom_shaders.len);
|
||||
defer self.alloc.free(z_shaders);
|
||||
for (custom_shaders, z_shaders) |bytes, *out| {
|
||||
out.* = @ptrCast(bytes);
|
||||
}
|
||||
return try shaders.Shaders.init(
|
||||
self.alloc,
|
||||
custom_shaders,
|
||||
z_shaders,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,18 +77,25 @@ pub const Buffer = bufferpkg.Buffer;
|
|||
|
||||
// ---- comptime contract --------------------------------------------------
|
||||
|
||||
/// Custom user shaders (`shadertoy.zig`) target GLSL — same as OpenGL.
|
||||
pub const custom_shader_target: shadertoy.Target = .glsl;
|
||||
/// Custom user shaders compile to SPIR-V directly — skip the
|
||||
/// GLSL → SPIR-V → GLSL roundtrip that `.glsl` would do. The
|
||||
/// roundtrip exists for backends that consume GLSL (OpenGL, Metal
|
||||
/// via MSL), but Vulkan ingests SPIR-V natively and we already have
|
||||
/// a glslang shim for the renderer's built-in shaders. Bypassing
|
||||
/// the roundtrip halves the per-shader compile cost AND avoids the
|
||||
/// spirv-cross-emitted main() losing the upstream `gl_FragCoord.xy`
|
||||
/// pattern we hook for the Y-flip fix.
|
||||
pub const custom_shader_target: shadertoy.Target = .spv;
|
||||
|
||||
/// Custom shaders are not yet supported on the Vulkan backend. The
|
||||
/// renderer's first pass draws into `CustomShaderState.back_texture`
|
||||
/// when custom shaders are configured, and a second "post" pass is
|
||||
/// expected to composite back_texture → frame.target through the
|
||||
/// user's shader. We haven't built that second pass for Vulkan yet,
|
||||
/// so enabling custom shaders here would leave `frame.target` empty
|
||||
/// and the window blank. Until the post pipeline lands, the generic
|
||||
/// renderer skips loading custom shaders for Vulkan and warns once.
|
||||
pub const supports_custom_shaders: bool = false;
|
||||
/// Custom shaders ARE now supported on the Vulkan backend.
|
||||
/// `shaders.Shaders.init` builds one post pipeline per user shader
|
||||
/// (UBO at set 0 binding 1, iChannel0 sampler at set 1 binding 0,
|
||||
/// matching `shadertoy_prefix.glsl` after `vulkanizeGlsl` rewrites
|
||||
/// the layouts). The renderer's post pass at the end of `drawFrame`
|
||||
/// chains them — first pipeline samples `back_texture` and writes
|
||||
/// `front_texture`, swap, repeat; the last one writes
|
||||
/// `frame.target` instead.
|
||||
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;
|
||||
|
|
@ -257,7 +264,10 @@ pub fn drawFrameEnd(self: *Vulkan) void {
|
|||
pub fn initShaders(
|
||||
self: *const Vulkan,
|
||||
alloc: Allocator,
|
||||
custom_shaders: []const [:0]const u8,
|
||||
/// For Vulkan these are SPIR-V binaries (loaded with
|
||||
/// `shadertoy.Target = .spv`), not GLSL strings — see
|
||||
/// `custom_shader_target` above.
|
||||
custom_shaders: []const []const u8,
|
||||
) !shaders.Shaders {
|
||||
_ = self;
|
||||
return try shaders.Shaders.init(alloc, devicePtr(), custom_shaders);
|
||||
|
|
|
|||
|
|
@ -852,7 +852,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
// ignored.
|
||||
const can_use_custom = !@hasDecl(GraphicsAPI, "supports_custom_shaders") or
|
||||
GraphicsAPI.supports_custom_shaders;
|
||||
const custom_shaders: []const [:0]const u8 = if (can_use_custom)
|
||||
const custom_shaders: []const []const u8 = if (can_use_custom)
|
||||
(shadertoy.loadFromFiles(
|
||||
arena_alloc,
|
||||
self.config.custom_shaders,
|
||||
|
|
|
|||
|
|
@ -49,4 +49,14 @@ layout(location = 0) out vec4 _fragColor;
|
|||
#define texture2D texture
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord );
|
||||
void main() { mainImage (_fragColor, gl_FragCoord.xy); }
|
||||
void main() {
|
||||
// Vulkan's `gl_FragCoord` origin is upper-left, OpenGL's is
|
||||
// lower-left; ShaderToy convention is lower-left, so on Vulkan
|
||||
// we mirror y. The backend (`renderer/shadertoy.zig`) injects
|
||||
// `#define GHASTTY_VULKAN 1` only for `.spv` targets.
|
||||
#ifdef GHASTTY_VULKAN
|
||||
mainImage(_fragColor, vec2(gl_FragCoord.x, iResolution.y - gl_FragCoord.y));
|
||||
#else
|
||||
mainImage(_fragColor, gl_FragCoord.xy);
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,16 +40,34 @@ pub const Uniforms = extern struct {
|
|||
};
|
||||
|
||||
/// The target to load shaders for.
|
||||
pub const Target = enum { glsl, msl };
|
||||
///
|
||||
/// - `.glsl`: roundtripped through SPIR-V back to GLSL via
|
||||
/// spirv-cross. Normalizes/validates the source. The OpenGL
|
||||
/// backend consumes this.
|
||||
/// - `.msl`: spirv-cross translation to Metal Shading Language.
|
||||
/// - `.spv`: raw SPIR-V binary (no spirv-cross roundtrip). The
|
||||
/// Vulkan backend consumes this — Vulkan compiles GLSL → SPIR-V
|
||||
/// itself via glslang for its built-in shaders, and feeding
|
||||
/// the user shader through GLSL→SPIR-V→GLSL→SPIR-V again costs
|
||||
/// 2× the compile work AND loses the original source structure
|
||||
/// (which broke our `gl_FragCoord` Y-flip rewrite when the
|
||||
/// spirv-cross-emitted main() didn't match the upstream prefix).
|
||||
pub const Target = enum { glsl, msl, spv };
|
||||
|
||||
/// 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/
|
||||
/// reinterpret as needed.
|
||||
pub fn loadFromFiles(
|
||||
alloc_gpa: Allocator,
|
||||
paths: configpkg.RepeatablePath,
|
||||
target: Target,
|
||||
) ![]const [:0]const u8 {
|
||||
var list: std.ArrayList([:0]const u8) = .empty;
|
||||
) ![]const []const u8 {
|
||||
var list: std.ArrayList([]const u8) = .empty;
|
||||
defer list.deinit(alloc_gpa);
|
||||
errdefer for (list.items) |shader| alloc_gpa.free(shader);
|
||||
|
||||
|
|
@ -75,11 +93,16 @@ pub fn loadFromFiles(
|
|||
|
||||
/// Load a single shader from a file and convert it to the target language
|
||||
/// ready to be used with renderers.
|
||||
///
|
||||
/// For `.glsl` / `.msl` the returned slice is a null-terminated UTF-8
|
||||
/// source string; the underlying allocation is `[:0]const u8` and
|
||||
/// callers that need the sentinel may safely cast. For `.spv` the
|
||||
/// returned slice is raw SPIR-V bytes — no terminator, 4-byte aligned.
|
||||
pub fn loadFromFile(
|
||||
alloc_gpa: Allocator,
|
||||
path: []const u8,
|
||||
target: Target,
|
||||
) ![:0]const u8 {
|
||||
) ![]const u8 {
|
||||
var arena = ArenaAllocator.init(alloc_gpa);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
|
@ -97,14 +120,36 @@ pub fn loadFromFile(
|
|||
);
|
||||
};
|
||||
|
||||
// Convert to full GLSL
|
||||
const glsl: [:0]const u8 = glsl: {
|
||||
// Convert to full GLSL. For `.spv` we inject a
|
||||
// `#define GHASTTY_VULKAN 1` so the prefix's `main()` can flip
|
||||
// `gl_FragCoord.y` (Vulkan's origin is upper-left vs OpenGL's
|
||||
// lower-left, which would otherwise paint custom shaders upside
|
||||
// down).
|
||||
const glsl_raw: [:0]const u8 = glsl: {
|
||||
var stream: std.Io.Writer.Allocating = .init(alloc);
|
||||
try glslFromShader(&stream.writer, src);
|
||||
const defines: []const []const u8 = if (target == .spv)
|
||||
&.{"GHASTTY_VULKAN 1"}
|
||||
else
|
||||
&.{};
|
||||
try glslFromShader(&stream.writer, src, 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;
|
||||
|
||||
// Convert to SPIR-V
|
||||
const spirv: []const u8 = spirv: {
|
||||
var stream: std.Io.Writer.Allocating = .init(alloc);
|
||||
|
|
@ -129,12 +174,22 @@ pub fn loadFromFile(
|
|||
break :spirv list.items;
|
||||
};
|
||||
|
||||
// Convert to MSL
|
||||
// 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) {
|
||||
// Important: using the alloc_gpa here on purpose because this
|
||||
// is the final result that will be returned to the caller.
|
||||
.glsl => try glslFromSpv(alloc_gpa, spirv),
|
||||
.msl => try mslFromSpv(alloc_gpa, spirv),
|
||||
.spv => spv: {
|
||||
// Copy the SPIR-V binary out of the arena into a
|
||||
// 4-byte-aligned allocation under `alloc_gpa`. Vulkan
|
||||
// expects `pCode: []const u32`, so over-aligning is safe;
|
||||
// we return as `[]const u8` to share the unified return
|
||||
// type with the GLSL/MSL paths.
|
||||
const dst = try alloc_gpa.alignedAlloc(u8, .of(u32), spirv.len);
|
||||
@memcpy(dst, spirv);
|
||||
break :spv dst;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -144,9 +199,33 @@ pub fn loadFromFile(
|
|||
/// mainImage function and don't define any of the uniforms. This function
|
||||
/// will convert the ShaderToy shader into a valid GLSL shader that can be
|
||||
/// compiled and linked.
|
||||
pub fn glslFromShader(writer: *std.Io.Writer, src: []const u8) !void {
|
||||
pub fn glslFromShader(
|
||||
writer: *std.Io.Writer,
|
||||
src: []const u8,
|
||||
/// Macros to inject as `#define <body>` lines after the prefix's
|
||||
/// `#version` directive (GLSL requires `#version` first, so we
|
||||
/// can't simply prepend). Empty for the default OpenGL/MSL paths;
|
||||
/// the Vulkan SPV path uses this to flag the prefix's `main()`
|
||||
/// to Y-flip `gl_FragCoord`.
|
||||
defines: []const []const u8,
|
||||
) !void {
|
||||
const prefix = @embedFile("shaders/shadertoy_prefix.glsl");
|
||||
try writer.writeAll(prefix);
|
||||
if (defines.len == 0) {
|
||||
try writer.writeAll(prefix);
|
||||
} else {
|
||||
// Find the first newline after `#version ...` and inject the
|
||||
// defines on the following line. We assume the prefix begins
|
||||
// with a `#version` directive on its own line (true today;
|
||||
// the comptime split below would crash loudly otherwise).
|
||||
const first_nl = std.mem.indexOfScalar(u8, prefix, '\n').?;
|
||||
try writer.writeAll(prefix[0 .. first_nl + 1]);
|
||||
for (defines) |def| {
|
||||
try writer.writeAll("#define ");
|
||||
try writer.writeAll(def);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
try writer.writeAll(prefix[first_nl + 1 ..]);
|
||||
}
|
||||
try writer.writeAll("\n\n");
|
||||
try writer.writeAll(src);
|
||||
}
|
||||
|
|
@ -348,7 +427,7 @@ fn spvCross(
|
|||
fn testGlslZ(alloc: Allocator, src: []const u8) ![:0]const u8 {
|
||||
var buf: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer buf.deinit();
|
||||
try glslFromShader(&buf.writer, src);
|
||||
try glslFromShader(&buf.writer, src, &.{});
|
||||
return try buf.toOwnedSliceSentinel(0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,21 @@ pub fn begin(opts: Options) Self {
|
|||
.texture => |t| .{ t.view, t.image, @intCast(t.width), @intCast(t.height) },
|
||||
.target => |t| .{ t.view, t.image, t.width, t.height },
|
||||
};
|
||||
// Y-flip only when writing to a final `Target` (the dmabuf that
|
||||
// Qt mmaps and paints with origin-upper-left). Intermediate
|
||||
// `Texture` targets (the custom-shader back_texture) stay in
|
||||
// OpenGL-style Y-up orientation so the shadertoy `mainImage`'s
|
||||
// `uv = fragCoord/iResolution` sampling lands on the right row
|
||||
// — the shader's flipped `fragCoord` (set by the
|
||||
// `GHASTTY_VULKAN` define in the shadertoy prefix) cancels with
|
||||
// the un-flipped texture orientation. Without this distinction
|
||||
// the terminal CONTENT inside the custom shader shows
|
||||
// upside-down because the back_texture was already y-flipped at
|
||||
// render time AND the shader then samples with a flipped uv.
|
||||
const y_flip_viewport: bool = switch (attach.target) {
|
||||
.target => true,
|
||||
.texture => false,
|
||||
};
|
||||
|
||||
// Transition to COLOR_ATTACHMENT_OPTIMAL. Sources from
|
||||
// UNDEFINED (fresh target) or whatever — we always discard
|
||||
|
|
@ -202,13 +217,23 @@ pub fn begin(opts: Options) Self {
|
|||
// top of the window appears at the bottom. `gl_FragCoord` still
|
||||
// reports origin-upper-left, matching `cell_bg.f.glsl`'s
|
||||
// `layout(origin_upper_left)` request.
|
||||
const viewport: vk.VkViewport = .{
|
||||
//
|
||||
// See `y_flip_viewport` above for why intermediate textures
|
||||
// (custom-shader back_texture) opt out of the flip.
|
||||
const viewport: vk.VkViewport = if (y_flip_viewport) .{
|
||||
.x = 0,
|
||||
.y = @floatFromInt(height),
|
||||
.width = @floatFromInt(width),
|
||||
.height = -@as(f32, @floatFromInt(height)),
|
||||
.minDepth = 0,
|
||||
.maxDepth = 1,
|
||||
} else .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = @floatFromInt(width),
|
||||
.height = @floatFromInt(height),
|
||||
.minDepth = 0,
|
||||
.maxDepth = 1,
|
||||
};
|
||||
opts.device.dispatch.cmdSetViewport(opts.cb, 0, 1, &viewport);
|
||||
const scissor: vk.VkRect2D = .{
|
||||
|
|
|
|||
|
|
@ -153,7 +153,12 @@ const ResourceSet = enum(u8) {
|
|||
/// authoring.
|
||||
///
|
||||
/// Caller frees the returned buffer with the same allocator.
|
||||
fn vulkanizeGlsl(
|
||||
///
|
||||
/// Also called from `shadertoy.zig` when building SPIR-V for the
|
||||
/// Vulkan backend's custom-shader path, so the binding layouts in the
|
||||
/// user's shader come out in the same set/binding scheme the
|
||||
/// renderer's pipelines wire up.
|
||||
pub fn vulkanizeGlsl(
|
||||
alloc: std.mem.Allocator,
|
||||
src: []const u8,
|
||||
) std.mem.Allocator.Error![:0]const u8 {
|
||||
|
|
@ -193,14 +198,20 @@ fn vulkanizeGlsl(
|
|||
} else if (std.mem.eql(u8, ident, "sampler2DRect")) {
|
||||
try out.appendSlice(alloc, "sampler2D");
|
||||
} else if (std.mem.eql(u8, ident, "texture") and
|
||||
nextNonSpaceIsOpenParen(src, i))
|
||||
nextSamplerIsUnnormalized(src, i))
|
||||
{
|
||||
// Replace `texture(args)` with `textureLod(args, 0.0)`.
|
||||
// Replace `texture(args)` with `textureLod(args, 0.0)`
|
||||
// ONLY when the sampler argument is one we created
|
||||
// with `unnormalized_coordinates = true` (the atlas
|
||||
// samplers in cell_text.f). Vulkan forbids implicit-LOD
|
||||
// sampling for those — see VUID-vkCmdDraw-None-08610.
|
||||
// For every other sampler (cell_bg.f's storage, the
|
||||
// shadertoy `iChannel0`, etc.) leave `texture()` alone:
|
||||
// it's the faster opcode the driver wants for normal
|
||||
// mipmapped or LOD-derivative sampling.
|
||||
try out.appendSlice(alloc, "textureLod(");
|
||||
// Skip past the `(`.
|
||||
while (i < src.len and src[i] != '(') : (i += 1) {}
|
||||
i += 1; // consume the '('
|
||||
// Copy the args verbatim until the matching `)`.
|
||||
var depth: i32 = 1;
|
||||
while (i < src.len and depth > 0) {
|
||||
const cc = src[i];
|
||||
|
|
@ -212,7 +223,6 @@ fn vulkanizeGlsl(
|
|||
try out.append(alloc, cc);
|
||||
i += 1;
|
||||
}
|
||||
// Insert the explicit LOD argument and the closing `)`.
|
||||
try out.appendSlice(alloc, ", 0.0)");
|
||||
if (i < src.len) i += 1; // consume the closing `)`
|
||||
} else {
|
||||
|
|
@ -313,6 +323,39 @@ fn nextNonSpaceIsOpenParen(src: []const u8, i: usize) bool {
|
|||
return p < src.len and src[p] == '(';
|
||||
}
|
||||
|
||||
/// Names of samplers we create with `unnormalized_coordinates =
|
||||
/// VK_TRUE`. The shaders here all use only the two atlas samplers
|
||||
/// for cell_text; if more get added (or renamed) update this list.
|
||||
/// The fragment shader `cell_text.f.glsl` is the only renderer
|
||||
/// shader that references either name, so this list is intentionally
|
||||
/// tiny — broader matching would force `textureLod` on the custom
|
||||
/// shader's `iChannel0`, which is normalized, and bypassing the
|
||||
/// implicit-LOD opcode path makes the driver work harder per call.
|
||||
const unnormalized_sampler_names = [_][]const u8{
|
||||
"atlas_grayscale",
|
||||
"atlas_color",
|
||||
};
|
||||
|
||||
/// True when `texture(IDENT, ...)` at position `i` (positioned right
|
||||
/// after the `texture` identifier) names an unnormalized sampler.
|
||||
/// Walks past whitespace and the `(`, then reads the next identifier
|
||||
/// and matches it against `unnormalized_sampler_names`.
|
||||
fn nextSamplerIsUnnormalized(src: []const u8, i: usize) bool {
|
||||
var p = i;
|
||||
while (p < src.len and isAnySpace(src[p])) : (p += 1) {}
|
||||
if (p >= src.len or src[p] != '(') return false;
|
||||
p += 1;
|
||||
while (p < src.len and isAnySpace(src[p])) : (p += 1) {}
|
||||
if (p >= src.len or !isIdentChar(src[p])) return false;
|
||||
const start = p;
|
||||
while (p < src.len and isIdentChar(src[p])) : (p += 1) {}
|
||||
const name = src[start..p];
|
||||
for (unnormalized_sampler_names) |needle| {
|
||||
if (std.mem.eql(u8, name, needle)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn isHorizSpace(c: u8) bool {
|
||||
return c == ' ' or c == '\t';
|
||||
}
|
||||
|
|
@ -632,7 +675,16 @@ const empty_pipeline: Pipeline = .{
|
|||
/// follow-up commit once the rest of the integration is wired.
|
||||
pub const Shaders = struct {
|
||||
pipelines: PipelineCollection,
|
||||
post_pipelines: []const Pipeline,
|
||||
/// One per user-supplied custom shader. Built by `Shaders.init`
|
||||
/// from the `post_shaders` arg — empty when no custom shaders.
|
||||
/// Owned by `Shaders` (deinit destroys each).
|
||||
post_pipelines: []Pipeline,
|
||||
/// Allocator used to allocate `post_pipelines`; held so deinit
|
||||
/// can free the slice.
|
||||
post_alloc: ?Allocator = null,
|
||||
/// Compiled `VkShaderModule`s for each user shader, parallel to
|
||||
/// `post_pipelines`. Owned by `Shaders` (deinit destroys each).
|
||||
post_modules: []Module = &.{},
|
||||
modules: Modules,
|
||||
|
||||
/// Process-wide descriptor pool. Sized for one set per pipeline
|
||||
|
|
@ -685,10 +737,13 @@ pub const Shaders = struct {
|
|||
pub fn init(
|
||||
alloc: Allocator,
|
||||
device: *const @import("Device.zig"),
|
||||
post_shaders: []const [:0]const u8,
|
||||
// SPIR-V binaries (4-byte-aligned) from
|
||||
// `shadertoy.loadFromFiles` with `target = .spv`. The Vulkan
|
||||
// backend bypasses the spirv-cross GLSL roundtrip the other
|
||||
// backends use, so each entry here is the SPIR-V the
|
||||
// built-in glslang shim would have produced.
|
||||
post_shaders: []const []const u8,
|
||||
) !Shaders {
|
||||
_ = post_shaders;
|
||||
|
||||
// Compile each built-in shader. Errors are fatal — the
|
||||
// renderer can't run without these. The `errdefer` chain
|
||||
// tears down any successfully-compiled modules if a later
|
||||
|
|
@ -987,9 +1042,92 @@ pub const Shaders = struct {
|
|||
pipelines.cell_bg = cell_bg_pipeline;
|
||||
pipelines.cell_text = cell_text_pipeline;
|
||||
|
||||
// ---- post (custom shader) pipelines ----------------------
|
||||
//
|
||||
// One pipeline per user shader source in `post_shaders`. Each
|
||||
// pipeline is the same shape:
|
||||
//
|
||||
// set 0 binding 1 Globals UBO (shadertoy uniforms)
|
||||
// set 1 binding 0 iChannel0 combined image sampler
|
||||
// (the prior pass's back_texture +
|
||||
// `state.sampler` from CustomShaderState)
|
||||
//
|
||||
// The vertex shader is the same `full_screen_vert` triangle
|
||||
// generator we use for bg_color; the user-supplied source IS
|
||||
// the fragment shader (run through `vulkanizeGlsl` and the
|
||||
// glslang shim — same path the built-in shaders take).
|
||||
// Color format matches `textureOptions()` and `initTarget`
|
||||
// (BGRA SRGB) since post passes write to either a
|
||||
// back_texture or `frame.target` and both use that format.
|
||||
//
|
||||
// Shadertoy shaders sample with normalized coordinates so the
|
||||
// post pipeline's sampler is the normalized `state.sampler`,
|
||||
// not the atlas sampler.
|
||||
var post_pipelines: []Pipeline = &.{};
|
||||
var post_modules: []Module = &.{};
|
||||
if (post_shaders.len > 0) {
|
||||
post_pipelines = try alloc.alloc(Pipeline, post_shaders.len);
|
||||
errdefer alloc.free(post_pipelines);
|
||||
post_modules = try alloc.alloc(Module, post_shaders.len);
|
||||
errdefer alloc.free(post_modules);
|
||||
|
||||
// Init counter so partial failures can deinit only what
|
||||
// was built.
|
||||
var built: usize = 0;
|
||||
errdefer {
|
||||
for (post_pipelines[0..built]) |p| p.deinit();
|
||||
for (post_modules[0..built]) |m| m.deinit();
|
||||
}
|
||||
|
||||
// Shared descriptor set layouts across post pipelines.
|
||||
// Tracked in `set_layouts` so deinit destroys once.
|
||||
const post_ubo_dsl = try createSingleBindingDsl(
|
||||
device,
|
||||
1,
|
||||
vk.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
|
||||
vk.VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
);
|
||||
tracker.track(post_ubo_dsl);
|
||||
const post_sampler_dsl = try createSingleBindingDsl(
|
||||
device,
|
||||
0,
|
||||
vk.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
vk.VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
);
|
||||
tracker.track(post_sampler_dsl);
|
||||
|
||||
for (post_shaders, 0..) |spv_bytes, i| {
|
||||
// Reinterpret the binary as the 32-bit word slice
|
||||
// Vulkan's VkShaderModuleCreateInfo wants. The
|
||||
// allocation is over-aligned to `u32` in shadertoy.zig
|
||||
// so this cast is safe.
|
||||
if (spv_bytes.len % 4 != 0) {
|
||||
log.err("custom shader SPIR-V size {} not a multiple of 4", .{spv_bytes.len});
|
||||
return error.VulkanFailed;
|
||||
}
|
||||
const spv_words: []const u32 = std.mem.bytesAsSlice(u32, @as([]align(@alignOf(u32)) const u8, @alignCast(spv_bytes)));
|
||||
post_modules[i] = try Module.initFromSpirv(device, spv_words, .fragment);
|
||||
post_pipelines[i] = try Pipeline.init(.{
|
||||
.device = device,
|
||||
.descriptor_pool = &pool,
|
||||
.vertex_module = modules.full_screen_vert.handle,
|
||||
.fragment_module = post_modules[i].handle,
|
||||
.vertex_input = null,
|
||||
.descriptor_set_layouts = &.{ post_ubo_dsl, post_sampler_dsl },
|
||||
.empty_set_layout = empty_dsl,
|
||||
.color_format = vk.VK_FORMAT_B8G8R8A8_SRGB,
|
||||
.blending_enabled = false,
|
||||
.topology = vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
|
||||
});
|
||||
built = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.pipelines = pipelines,
|
||||
.post_pipelines = &.{},
|
||||
.post_pipelines = post_pipelines,
|
||||
.post_alloc = if (post_shaders.len > 0) alloc else null,
|
||||
.post_modules = post_modules,
|
||||
.modules = modules,
|
||||
.descriptor_pool = pool,
|
||||
.set_layouts = set_layouts,
|
||||
|
|
@ -1053,6 +1191,16 @@ pub const Shaders = struct {
|
|||
if (p_ptr.pipeline != null) p_ptr.deinit();
|
||||
}
|
||||
|
||||
// Post (custom shader) pipelines + their fragment modules.
|
||||
// Same teardown order as the built-in pipelines/modules:
|
||||
// pipeline first (holds VkPipelineLayout), then shader module.
|
||||
for (self.post_pipelines) |p| p.deinit();
|
||||
for (self.post_modules) |m| m.deinit();
|
||||
if (self.post_alloc) |a| {
|
||||
a.free(self.post_pipelines);
|
||||
a.free(self.post_modules);
|
||||
}
|
||||
|
||||
// Atlas sampler held by `Shaders` for the cell_text pipeline's
|
||||
// texture bindings.
|
||||
if (self.atlas_sampler) |samp| samp.deinit();
|
||||
|
|
|
|||
Loading…
Reference in New Issue