mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-16 06:46:09 +00:00
renderer: create explicit sampler state for custom shaders
The GLSL to MSL conversion process uses a passed-in sampler state for the `iChannel0` parameter and we weren't providing it. This magically worked on Apple Silicon for unknown reasons but failed on Intel GPUs. In normal, hand-written MSL, we'd explicitly create the sampler state as a normal variable (we do this in `shaders.metal` already!), but the Shadertoy conversion stuff doesn't do this, probably because the exact sampler parameters can't be safely known. This fixes a Metal validation error when using custom shaders: ``` -[MTLDebugRenderCommandEncoder validateCommonDrawErrors:]:5970: failed assertion `Draw Errors Validation Fragment Function(main0): missing Sampler binding at index 0 for iChannel0Smplr[0]. ```
This commit is contained in:
43
pkg/opengl/Sampler.zig
Normal file
43
pkg/opengl/Sampler.zig
Normal file
@@ -0,0 +1,43 @@
|
||||
const Sampler = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const errors = @import("errors.zig");
|
||||
const glad = @import("glad.zig");
|
||||
const Texture = @import("Texture.zig");
|
||||
|
||||
id: c.GLuint,
|
||||
|
||||
/// Create a single sampler.
|
||||
pub fn create() errors.Error!Sampler {
|
||||
var id: c.GLuint = undefined;
|
||||
glad.context.GenSamplers.?(1, &id);
|
||||
try errors.getError();
|
||||
return .{ .id = id };
|
||||
}
|
||||
|
||||
/// glBindSampler
|
||||
pub fn bind(v: Sampler, index: c_uint) !void {
|
||||
glad.context.BindSampler.?(index, v.id);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn parameter(
|
||||
self: Sampler,
|
||||
name: Texture.Parameter,
|
||||
value: anytype,
|
||||
) errors.Error!void {
|
||||
switch (@TypeOf(value)) {
|
||||
c.GLint => glad.context.SamplerParameteri.?(
|
||||
self.id,
|
||||
@intFromEnum(name),
|
||||
value,
|
||||
),
|
||||
else => unreachable,
|
||||
}
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn destroy(v: Sampler) void {
|
||||
glad.context.DeleteSamplers.?(1, &v.id);
|
||||
}
|
@@ -18,6 +18,7 @@ pub const Buffer = @import("Buffer.zig");
|
||||
pub const Framebuffer = @import("Framebuffer.zig");
|
||||
pub const Renderbuffer = @import("Renderbuffer.zig");
|
||||
pub const Program = @import("Program.zig");
|
||||
pub const Sampler = @import("Sampler.zig");
|
||||
pub const Shader = @import("Shader.zig");
|
||||
pub const Texture = @import("Texture.zig");
|
||||
pub const VertexArray = @import("VertexArray.zig");
|
||||
|
@@ -25,6 +25,7 @@ pub const RenderPass = @import("metal/RenderPass.zig");
|
||||
pub const Pipeline = @import("metal/Pipeline.zig");
|
||||
const bufferpkg = @import("metal/buffer.zig");
|
||||
pub const Buffer = bufferpkg.Buffer;
|
||||
pub const Sampler = @import("metal/Sampler.zig");
|
||||
pub const Texture = @import("metal/Texture.zig");
|
||||
pub const shaders = @import("metal/shaders.zig");
|
||||
|
||||
@@ -285,6 +286,18 @@ pub inline fn textureOptions(self: Metal) Texture.Options {
|
||||
};
|
||||
}
|
||||
|
||||
pub inline fn samplerOptions(self: Metal) Sampler.Options {
|
||||
return .{
|
||||
.device = self.device,
|
||||
|
||||
// These parameters match Shadertoy behaviors.
|
||||
.min_filter = .linear,
|
||||
.mag_filter = .linear,
|
||||
.s_address_mode = .clamp_to_edge,
|
||||
.t_address_mode = .clamp_to_edge,
|
||||
};
|
||||
}
|
||||
|
||||
/// Pixel format for image texture options.
|
||||
pub const ImageTextureFormat = enum {
|
||||
/// 1 byte per pixel grayscale.
|
||||
|
@@ -20,6 +20,7 @@ pub const RenderPass = @import("opengl/RenderPass.zig");
|
||||
pub const Pipeline = @import("opengl/Pipeline.zig");
|
||||
const bufferpkg = @import("opengl/buffer.zig");
|
||||
pub const Buffer = bufferpkg.Buffer;
|
||||
pub const Sampler = @import("opengl/Sampler.zig");
|
||||
pub const Texture = @import("opengl/Texture.zig");
|
||||
pub const shaders = @import("opengl/shaders.zig");
|
||||
|
||||
@@ -364,6 +365,17 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing samplers.
|
||||
pub inline fn samplerOptions(self: OpenGL) Sampler.Options {
|
||||
_ = self;
|
||||
return .{
|
||||
.min_filter = .linear,
|
||||
.mag_filter = .linear,
|
||||
.wrap_s = .clamp_to_edge,
|
||||
.wrap_t = .clamp_to_edge,
|
||||
};
|
||||
}
|
||||
|
||||
/// Pixel format for image texture options.
|
||||
pub const ImageTextureFormat = enum {
|
||||
/// 1 byte per pixel grayscale.
|
||||
|
@@ -85,6 +85,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
const Target = GraphicsAPI.Target;
|
||||
const Buffer = GraphicsAPI.Buffer;
|
||||
const Sampler = GraphicsAPI.Sampler;
|
||||
const Texture = GraphicsAPI.Texture;
|
||||
const RenderPass = GraphicsAPI.RenderPass;
|
||||
|
||||
@@ -428,6 +429,19 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
front_texture: Texture,
|
||||
back_texture: Texture,
|
||||
|
||||
/// Shadertoy uses a sampler for accessing the various channel
|
||||
/// textures. In Metal, we need to explicitly create these since
|
||||
/// the glslang-to-msl compiler doesn't do it for us (as we
|
||||
/// normally would in hand-written MSL). To keep it clean and
|
||||
/// consistent, we just force all rendering APIs to provide an
|
||||
/// explicit sampler.
|
||||
///
|
||||
/// Samplers are immutable and describe sampling properties so
|
||||
/// we can share the sampler across front/back textures (although
|
||||
/// we only need it for the source texture at a time, we don't
|
||||
/// need to "swap" it).
|
||||
sampler: Sampler,
|
||||
|
||||
uniforms: UniformBuffer,
|
||||
|
||||
const UniformBuffer = Buffer(shadertoy.Uniforms);
|
||||
@@ -459,9 +473,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
);
|
||||
errdefer back_texture.deinit();
|
||||
|
||||
const sampler = try Sampler.init(api.samplerOptions());
|
||||
errdefer sampler.deinit();
|
||||
|
||||
return .{
|
||||
.front_texture = front_texture,
|
||||
.back_texture = back_texture,
|
||||
.sampler = sampler,
|
||||
.uniforms = uniforms,
|
||||
};
|
||||
}
|
||||
@@ -469,6 +487,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
pub fn deinit(self: *CustomShaderState) void {
|
||||
self.front_texture.deinit();
|
||||
self.back_texture.deinit();
|
||||
self.sampler.deinit();
|
||||
self.uniforms.deinit();
|
||||
}
|
||||
|
||||
@@ -1509,6 +1528,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
.pipeline = pipeline,
|
||||
.uniforms = state.uniforms.buffer,
|
||||
.textures = &.{state.back_texture},
|
||||
.samplers = &.{state.sampler},
|
||||
.draw = .{
|
||||
.type = .triangle,
|
||||
.vertex_count = 3,
|
||||
|
@@ -9,6 +9,7 @@ const objc = @import("objc");
|
||||
|
||||
const mtl = @import("api.zig");
|
||||
const Pipeline = @import("Pipeline.zig");
|
||||
const Sampler = @import("Sampler.zig");
|
||||
const Texture = @import("Texture.zig");
|
||||
const Target = @import("Target.zig");
|
||||
const Metal = @import("../Metal.zig");
|
||||
@@ -41,6 +42,9 @@ pub const Step = struct {
|
||||
/// MTLBuffer
|
||||
buffers: []const ?objc.Object = &.{},
|
||||
textures: []const ?Texture = &.{},
|
||||
/// Set of samplers to use for this step. The index maps to an index
|
||||
/// of a fragment texture, set via setFragmentSamplerState(_:index:).
|
||||
samplers: []const ?Sampler = &.{},
|
||||
draw: Draw,
|
||||
|
||||
/// Describes the draw call for this step.
|
||||
@@ -200,6 +204,15 @@ pub fn step(self: *const Self, s: Step) void {
|
||||
);
|
||||
};
|
||||
|
||||
// Set samplers.
|
||||
for (s.samplers, 0..) |samp, i| if (samp) |sampler| {
|
||||
self.encoder.msgSend(
|
||||
void,
|
||||
objc.sel("setFragmentSamplerState:atIndex:"),
|
||||
.{ sampler.sampler.value, @as(c_ulong, i) },
|
||||
);
|
||||
};
|
||||
|
||||
// Draw!
|
||||
self.encoder.msgSend(
|
||||
void,
|
||||
|
66
src/renderer/metal/Sampler.zig
Normal file
66
src/renderer/metal/Sampler.zig
Normal file
@@ -0,0 +1,66 @@
|
||||
//! Wrapper for handling samplers.
|
||||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
const objc = @import("objc");
|
||||
|
||||
const mtl = @import("api.zig");
|
||||
const Metal = @import("../Metal.zig");
|
||||
|
||||
const log = std.log.scoped(.metal);
|
||||
|
||||
/// Options for initializing a sampler.
|
||||
pub const Options = struct {
|
||||
/// MTLDevice
|
||||
device: objc.Object,
|
||||
min_filter: mtl.MTLSamplerMinMagFilter,
|
||||
mag_filter: mtl.MTLSamplerMinMagFilter,
|
||||
s_address_mode: mtl.MTLSamplerAddressMode,
|
||||
t_address_mode: mtl.MTLSamplerAddressMode,
|
||||
};
|
||||
|
||||
/// The underlying MTLSamplerState Object.
|
||||
sampler: objc.Object,
|
||||
|
||||
pub const Error = error{
|
||||
/// A Metal API call failed.
|
||||
MetalFailed,
|
||||
};
|
||||
|
||||
/// Initialize a sampler
|
||||
pub fn init(
|
||||
opts: Options,
|
||||
) Error!Self {
|
||||
// Create our descriptor
|
||||
const desc = init: {
|
||||
const Class = objc.getClass("MTLSamplerDescriptor").?;
|
||||
const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
|
||||
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
|
||||
break :init id_init;
|
||||
};
|
||||
defer desc.release();
|
||||
|
||||
// Properties
|
||||
desc.setProperty("minFilter", opts.min_filter);
|
||||
desc.setProperty("magFilter", opts.mag_filter);
|
||||
desc.setProperty("sAddressMode", opts.s_address_mode);
|
||||
desc.setProperty("tAddressMode", opts.t_address_mode);
|
||||
|
||||
// Create the sampler state
|
||||
const id = opts.device.msgSend(
|
||||
?*anyopaque,
|
||||
objc.sel("newSamplerStateWithDescriptor:"),
|
||||
.{desc},
|
||||
) orelse return error.MetalFailed;
|
||||
|
||||
return .{
|
||||
.sampler = objc.Object.fromId(id),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
self.sampler.release();
|
||||
}
|
@@ -8,6 +8,7 @@ const builtin = @import("builtin");
|
||||
const gl = @import("opengl");
|
||||
|
||||
const OpenGL = @import("../OpenGL.zig");
|
||||
const Sampler = @import("Sampler.zig");
|
||||
const Target = @import("Target.zig");
|
||||
const Texture = @import("Texture.zig");
|
||||
const Pipeline = @import("Pipeline.zig");
|
||||
@@ -35,6 +36,7 @@ pub const Step = struct {
|
||||
uniforms: ?gl.Buffer = null,
|
||||
buffers: []const ?gl.Buffer = &.{},
|
||||
textures: []const ?Texture = &.{},
|
||||
samplers: []const ?Sampler = &.{},
|
||||
draw: Draw,
|
||||
|
||||
/// Describes the draw call for this step.
|
||||
@@ -103,6 +105,11 @@ pub fn step(self: *Self, s: Step) void {
|
||||
_ = tex.texture.bind(tex.target) catch return;
|
||||
};
|
||||
|
||||
// Bind relevant samplers.
|
||||
for (s.samplers, 0..) |s_, i| if (s_) |sampler| {
|
||||
_ = sampler.sampler.bind(@intCast(i)) catch return;
|
||||
};
|
||||
|
||||
// Bind 0th buffer as the vertex buffer,
|
||||
// and bind the rest as storage buffers.
|
||||
if (s.buffers.len > 0) {
|
||||
|
47
src/renderer/opengl/Sampler.zig
Normal file
47
src/renderer/opengl/Sampler.zig
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Wrapper for handling samplers.
|
||||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
const gl = @import("opengl");
|
||||
|
||||
const OpenGL = @import("../OpenGL.zig");
|
||||
|
||||
const log = std.log.scoped(.opengl);
|
||||
|
||||
/// Options for initializing a sampler.
|
||||
pub const Options = struct {
|
||||
min_filter: gl.Texture.MinFilter,
|
||||
mag_filter: gl.Texture.MagFilter,
|
||||
wrap_s: gl.Texture.Wrap,
|
||||
wrap_t: gl.Texture.Wrap,
|
||||
};
|
||||
|
||||
sampler: gl.Sampler,
|
||||
|
||||
pub const Error = error{
|
||||
/// An OpenGL API call failed.
|
||||
OpenGLFailed,
|
||||
};
|
||||
|
||||
/// Initialize a sampler
|
||||
pub fn init(
|
||||
opts: Options,
|
||||
) Error!Self {
|
||||
const sampler = gl.Sampler.create() catch return error.OpenGLFailed;
|
||||
errdefer sampler.destroy();
|
||||
sampler.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed;
|
||||
sampler.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed;
|
||||
sampler.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed;
|
||||
sampler.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed;
|
||||
|
||||
return .{
|
||||
.sampler = sampler,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
self.sampler.destroy();
|
||||
}
|
Reference in New Issue
Block a user