Merge remote-tracking branch 'upstream/main' into jacob/uucode
commit
4fc8faa01e
|
|
@ -143,6 +143,8 @@
|
|||
A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */; };
|
||||
A5E408452E0483FD0035FEAC /* KeybindIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408442E0483F80035FEAC /* KeybindIntent.swift */; };
|
||||
A5E408472E04852B0035FEAC /* InputIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408462E0485270035FEAC /* InputIntent.swift */; };
|
||||
A5F9A1F22E7C7301005AFACE /* SurfaceProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F9A1F12E7C7301005AFACE /* SurfaceProgressBar.swift */; };
|
||||
A5F9A1F32E7C7D59005AFACE /* SurfaceProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F9A1F12E7C7301005AFACE /* SurfaceProgressBar.swift */; };
|
||||
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; };
|
||||
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; };
|
||||
C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
|
||||
|
|
@ -293,6 +295,7 @@
|
|||
A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteIntent.swift; sourceTree = "<group>"; };
|
||||
A5E408442E0483F80035FEAC /* KeybindIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindIntent.swift; sourceTree = "<group>"; };
|
||||
A5E408462E0485270035FEAC /* InputIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputIntent.swift; sourceTree = "<group>"; };
|
||||
A5F9A1F12E7C7301005AFACE /* SurfaceProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceProgressBar.swift; sourceTree = "<group>"; };
|
||||
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = "<group>"; };
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -492,6 +495,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
A55B7BB729B6F53A0055DE60 /* Package.swift */,
|
||||
A5F9A1F12E7C7301005AFACE /* SurfaceProgressBar.swift */,
|
||||
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
|
||||
A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */,
|
||||
A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */,
|
||||
|
|
@ -892,6 +896,7 @@
|
|||
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
|
||||
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
|
||||
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
|
||||
A5F9A1F22E7C7301005AFACE /* SurfaceProgressBar.swift in Sources */,
|
||||
A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */,
|
||||
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
|
||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
|
||||
|
|
@ -986,6 +991,7 @@
|
|||
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
|
||||
A5F9A1F32E7C7D59005AFACE /* SurfaceProgressBar.swift in Sources */,
|
||||
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */,
|
||||
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||
A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -99,10 +99,13 @@ extension Ghostty.Action {
|
|||
|
||||
let state: State
|
||||
let progress: UInt8?
|
||||
|
||||
init(c: ghostty_action_progress_report_s) {
|
||||
self.state = State(c.state)
|
||||
self.progress = c.progress >= 0 ? UInt8(c.progress) : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Putting the initializer in an extension preserves the automatic one.
|
||||
extension Ghostty.Action.ProgressReport {
|
||||
init(c: ghostty_action_progress_report_s) {
|
||||
self.state = State(c.state)
|
||||
self.progress = c.progress >= 0 ? UInt8(c.progress) : nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import SwiftUI
|
||||
|
||||
/// The progress bar to show a surface progress report. We implement this from scratch because the
|
||||
/// standard ProgressView is broken on macOS 26 and this is simple anyways and gives us a ton of
|
||||
/// control.
|
||||
struct SurfaceProgressBar: View {
|
||||
let report: Ghostty.Action.ProgressReport
|
||||
|
||||
private var color: Color {
|
||||
switch report.state {
|
||||
case .error: return .red
|
||||
case .pause: return .orange
|
||||
default: return .accentColor
|
||||
}
|
||||
}
|
||||
|
||||
private var progress: UInt8? {
|
||||
// If we have an explicit progress use that.
|
||||
if let v = report.progress { return v }
|
||||
|
||||
// Otherwise, if we're in the pause state, we act as if we're at 100%.
|
||||
if report.state == .pause { return 100 }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private var accessibilityLabel: String {
|
||||
switch report.state {
|
||||
case .error: return "Terminal progress - Error"
|
||||
case .pause: return "Terminal progress - Paused"
|
||||
case .indeterminate: return "Terminal progress - In progress"
|
||||
default: return "Terminal progress"
|
||||
}
|
||||
}
|
||||
|
||||
private var accessibilityValue: String {
|
||||
if let progress {
|
||||
return "\(progress) percent complete"
|
||||
} else {
|
||||
switch report.state {
|
||||
case .error: return "Operation failed"
|
||||
case .pause: return "Operation paused at completion"
|
||||
case .indeterminate: return "Operation in progress"
|
||||
default: return "Indeterminate progress"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
if let progress {
|
||||
// Determinate progress bar with specific percentage
|
||||
Rectangle()
|
||||
.fill(color)
|
||||
.frame(
|
||||
width: geometry.size.width * CGFloat(progress) / 100,
|
||||
height: geometry.size.height
|
||||
)
|
||||
.animation(.easeInOut(duration: 0.2), value: progress)
|
||||
} else {
|
||||
// Indeterminate states without specific progress - all use bouncing animation
|
||||
BouncingProgressBar(color: color)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 2)
|
||||
.clipped()
|
||||
.allowsHitTesting(false)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityAddTraits(.updatesFrequently)
|
||||
.accessibilityLabel(accessibilityLabel)
|
||||
.accessibilityValue(accessibilityValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bouncing progress bar for indeterminate states
|
||||
private struct BouncingProgressBar: View {
|
||||
let color: Color
|
||||
@State private var position: CGFloat = 0
|
||||
|
||||
private let barWidthRatio: CGFloat = 0.25
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
Rectangle()
|
||||
.fill(color.opacity(0.3))
|
||||
|
||||
Rectangle()
|
||||
.fill(color)
|
||||
.frame(
|
||||
width: geometry.size.width * barWidthRatio,
|
||||
height: geometry.size.height
|
||||
)
|
||||
.offset(x: position * (geometry.size.width * (1 - barWidthRatio)))
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(
|
||||
.easeInOut(duration: 1.2)
|
||||
.repeatForever(autoreverses: true)
|
||||
) {
|
||||
position = 1
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
position = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -114,11 +114,17 @@ extension Ghostty {
|
|||
}
|
||||
.ghosttySurfaceView(surfaceView)
|
||||
|
||||
// Progress report overlay
|
||||
if let progressReport = surfaceView.progressReport {
|
||||
ProgressReportOverlay(report: progressReport)
|
||||
// Progress report
|
||||
if let progressReport = surfaceView.progressReport, progressReport.state != .remove {
|
||||
VStack(spacing: 0) {
|
||||
SurfaceProgressBar(report: progressReport)
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.allowsHitTesting(false)
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
|
||||
#if canImport(AppKit)
|
||||
// If we are in the middle of a key sequence, then we show a visual element. We only
|
||||
// support this on macOS currently although in theory we can support mobile with keyboards!
|
||||
|
|
@ -272,48 +278,7 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
// Progress report overlay that shows a progress bar at the top of the terminal
|
||||
struct ProgressReportOverlay: View {
|
||||
let report: Action.ProgressReport
|
||||
|
||||
@ViewBuilder
|
||||
private var progressBar: some View {
|
||||
if let progress = report.progress {
|
||||
// Determinate progress bar
|
||||
ProgressView(value: Double(progress), total: 100)
|
||||
.progressViewStyle(.linear)
|
||||
.tint(report.state == .error ? .red : report.state == .pause ? .orange : nil)
|
||||
.animation(.easeInOut(duration: 0.2), value: progress)
|
||||
} else {
|
||||
// Indeterminate states
|
||||
switch report.state {
|
||||
case .indeterminate:
|
||||
ProgressView()
|
||||
.progressViewStyle(.linear)
|
||||
case .error:
|
||||
ProgressView()
|
||||
.progressViewStyle(.linear)
|
||||
.tint(.red)
|
||||
case .pause:
|
||||
Rectangle().fill(Color.orange)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
progressBar
|
||||
.scaleEffect(x: 1, y: 0.5, anchor: .center)
|
||||
.frame(height: 2)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This is the resize overlay that shows on top of a surface to show the current
|
||||
// size during a resize operation.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
@ -273,6 +274,27 @@ pub inline fn textureOptions(self: Metal) Texture.Options {
|
|||
.cpu_cache_mode = .write_combined,
|
||||
.storage_mode = self.default_storage_mode,
|
||||
},
|
||||
.usage = .{
|
||||
// textureOptions is currently only used for custom shaders,
|
||||
// which require both the shader read (for when multiple shaders
|
||||
// are chained) and render target (for the final output) usage.
|
||||
// Disabling either of these will lead to metal validation
|
||||
// errors in Xcode.
|
||||
.shader_read = true,
|
||||
.render_target = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -311,6 +333,10 @@ pub inline fn imageTextureOptions(
|
|||
.cpu_cache_mode = .write_combined,
|
||||
.storage_mode = self.default_storage_mode,
|
||||
},
|
||||
.usage = .{
|
||||
// We only need to read from this texture from a shader.
|
||||
.shader_read = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -334,6 +360,10 @@ pub fn initAtlasTexture(
|
|||
.cpu_cache_mode = .write_combined,
|
||||
.storage_mode = self.default_storage_mode,
|
||||
},
|
||||
.usage = .{
|
||||
// We only need to read from this texture from a shader.
|
||||
.shader_read = true,
|
||||
},
|
||||
},
|
||||
atlas.size,
|
||||
atlas.size,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ pub const Options = struct {
|
|||
device: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
resource_options: mtl.MTLResourceOptions,
|
||||
usage: mtl.MTLTextureUsage,
|
||||
};
|
||||
|
||||
/// The underlying MTLTexture Object.
|
||||
|
|
@ -57,6 +58,7 @@ pub fn init(
|
|||
desc.setProperty("width", @as(c_ulong, width));
|
||||
desc.setProperty("height", @as(c_ulong, height));
|
||||
desc.setProperty("resourceOptions", opts.resource_options);
|
||||
desc.setProperty("usage", opts.usage);
|
||||
|
||||
// Initialize
|
||||
const id = opts.device.msgSend(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
Loading…
Reference in New Issue