diff --git a/pkg/opengl/Sampler.zig b/pkg/opengl/Sampler.zig new file mode 100644 index 000000000..f5c15699f --- /dev/null +++ b/pkg/opengl/Sampler.zig @@ -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); +} diff --git a/pkg/opengl/main.zig b/pkg/opengl/main.zig index 7165ad3ab..2f22154c6 100644 --- a/pkg/opengl/main.zig +++ b/pkg/opengl/main.zig @@ -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"); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index b42b83276..f4201edcc 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.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. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index e572806d1..673f79501 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -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. diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index c6cfca87b..802c769a6 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -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, diff --git a/src/renderer/metal/RenderPass.zig b/src/renderer/metal/RenderPass.zig index e48bc4c00..d42d9fa21 100644 --- a/src/renderer/metal/RenderPass.zig +++ b/src/renderer/metal/RenderPass.zig @@ -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, diff --git a/src/renderer/metal/Sampler.zig b/src/renderer/metal/Sampler.zig new file mode 100644 index 000000000..0f4de8848 --- /dev/null +++ b/src/renderer/metal/Sampler.zig @@ -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(); +} diff --git a/src/renderer/opengl/RenderPass.zig b/src/renderer/opengl/RenderPass.zig index 0f5bd89e7..7a9365d88 100644 --- a/src/renderer/opengl/RenderPass.zig +++ b/src/renderer/opengl/RenderPass.zig @@ -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) { diff --git a/src/renderer/opengl/Sampler.zig b/src/renderer/opengl/Sampler.zig new file mode 100644 index 000000000..98d4b35fe --- /dev/null +++ b/src/renderer/opengl/Sampler.zig @@ -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(); +}