From 441c5c3fcef53f6c23946ba141c1ba1a6a5cc994 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 16 Dec 2025 20:10:32 -0800 Subject: [PATCH] chore: more work on shaders --- ScreenShader/effect_view.swift | 21 ++++++++ ScreenShader/renderer.swift | 80 +++++++++++++++++++++++-------- ScreenShader/slang_compiler.swift | 9 ++++ 3 files changed, 90 insertions(+), 20 deletions(-) diff --git a/ScreenShader/effect_view.swift b/ScreenShader/effect_view.swift index a1b179d..6bcb12e 100644 --- a/ScreenShader/effect_view.swift +++ b/ScreenShader/effect_view.swift @@ -4,6 +4,27 @@ class SourceTextView: NSTextView { override func insertTab(_ sender: Any?) { self.insertText(" ", replacementRange: self.selectedRange()) } + + // Disable the context menu to prevent Metal library loading crash + override func menu(for event: NSEvent) -> NSMenu? { + return nil + } + + // Override performKeyEquivalent to handle Cmd key events without triggering system UI + override func performKeyEquivalent(with event: NSEvent) -> Bool { + // Let standard text editing shortcuts through (Cmd+C, Cmd+V, etc.) + if event.modifierFlags.contains(.command) { + let key = event.charactersIgnoringModifiers ?? "" + switch key { + case "c", "v", "x", "a", "z", "s": + return super.performKeyEquivalent(with: event) + default: + // Consume other Cmd key events to prevent system UI crashes + return true + } + } + return super.performKeyEquivalent(with: event) + } } class EffectViewController: NSViewController, NSTextFieldDelegate, NSTextViewDelegate { diff --git a/ScreenShader/renderer.swift b/ScreenShader/renderer.swift index d577e67..e057da3 100644 --- a/ScreenShader/renderer.swift +++ b/ScreenShader/renderer.swift @@ -11,12 +11,22 @@ class MetalView: MTKView { } } +// Uniforms struct that matches Slang's expected layout +struct SlangUniforms { + var screenSize: vector_float2 + var mousePosition: vector_float2 + var time: Float + var _padding: Float = 0 // Alignment padding +} + class MetalRenderer { private let device: MTLDevice private let commandQueue: MTLCommandQueue private var textureCache: CVMetalTextureCache! private var activeEffectSource: String? = nil private var renderPipeline: MTLRenderPipelineState? = nil + private var activeShaderLanguage: ShaderLanguage = .metal + private var samplerState: MTLSamplerState! private let baseTime = ProcessInfo.processInfo.systemUptime init(metalLayer: CAMetalLayer) { @@ -44,6 +54,14 @@ class MetalRenderer { } else { fatalError("Could not create CVMetalTextureCache.") } + + // Create a sampler state for Slang shaders + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .linear + samplerDescriptor.magFilter = .linear + samplerDescriptor.sAddressMode = .clampToEdge + samplerDescriptor.tAddressMode = .clampToEdge + self.samplerState = self.device.makeSamplerState(descriptor: samplerDescriptor) } /// Build a render pipeline from Metal shader effect source @@ -177,20 +195,23 @@ class MetalRenderer { ]) } - // Build the complete Metal library source with the Slang-generated fragment shader - // and our standard vertex shader + // Debug: print the generated Metal code + print("=== Slang-generated Metal code ===") + print(metalFragmentSource) + print("=== End Slang-generated Metal code ===") + + // Slang generates a complete Metal file with its own includes. + // We need to add our vertex shader to it, but avoid duplicate includes. + // Strip the Slang includes and add our vertex shader after. + + // The Slang output already has the fragment shader, we just need to add vertex shader let librarySource = """ - #include - using namespace metal; - - // ========== Slang-generated fragment shader code ========== \(metalFragmentSource) - // ========== End Slang-generated code ========== - // Vertex shader (always Metal, as Slang only generates the fragment shader) + // ========== ScreenShader Vertex Shader ========== struct VertexOut { float4 position [[position]]; - float2 texCoord; + float2 texCoord [[user(TEXCOORD)]]; }; vertex VertexOut vertex_main(uint vertexId [[vertex_id]]) { @@ -212,15 +233,19 @@ class MetalRenderer { } """ - let library = try device.makeLibrary(source: librarySource, options: nil) + let library: MTLLibrary + do { + library = try device.makeLibrary(source: librarySource, options: nil) + } catch { + throw NSError( + domain: "MetalRenderer", code: 4, + userInfo: [ + NSLocalizedDescriptionKey: "Metal compilation failed: \(error.localizedDescription)" + ]) + } let vertexFunction = library.makeFunction(name: "vertex_main") - // The Slang-generated fragment function - look for fragmentMain (Slang naming) - var fragmentFunction = library.makeFunction(name: "fragmentMain") - // Fallback to main if fragmentMain not found (some Slang versions) - if fragmentFunction == nil { - fragmentFunction = library.makeFunction(name: "main") - } + let fragmentFunction = library.makeFunction(name: "fragmentMain") guard vertexFunction != nil && fragmentFunction != nil else { throw NSError( @@ -242,9 +267,11 @@ class MetalRenderer { guard let effectSource = effectSource else { self.activeEffectSource = nil self.renderPipeline = nil + self.activeShaderLanguage = .metal return } self.activeEffectSource = effectSource + self.activeShaderLanguage = language do { self.renderPipeline = try Self.buildRenderPipeline( device: self.device, effectSource: effectSource, language: language) @@ -313,10 +340,23 @@ class MetalRenderer { encoder.setRenderPipelineState(renderPipeline) encoder.setFragmentTexture(texture, index: 0) - encoder.setFragmentBytes(&screenSize, length: MemoryLayout.stride, index: 0) - encoder.setFragmentBytes( - &mousePosition, length: MemoryLayout.stride, index: 1) - encoder.setFragmentBytes(&time, length: MemoryLayout.stride, index: 2) + + if self.activeShaderLanguage == .slang { + // Slang shaders expect a Uniforms struct at buffer(0) and a sampler + var uniforms = SlangUniforms( + screenSize: screenSize, + mousePosition: mousePosition, + time: time + ) + encoder.setFragmentBytes(&uniforms, length: MemoryLayout.stride, index: 0) + encoder.setFragmentSamplerState(self.samplerState, index: 0) + } else { + // Metal shaders use separate buffer bindings + encoder.setFragmentBytes(&screenSize, length: MemoryLayout.stride, index: 0) + encoder.setFragmentBytes( + &mousePosition, length: MemoryLayout.stride, index: 1) + encoder.setFragmentBytes(&time, length: MemoryLayout.stride, index: 2) + } encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) } diff --git a/ScreenShader/slang_compiler.swift b/ScreenShader/slang_compiler.swift index a35abca..9c740a1 100644 --- a/ScreenShader/slang_compiler.swift +++ b/ScreenShader/slang_compiler.swift @@ -144,11 +144,18 @@ class SlangCompiler { // Shader input structure matching ScreenShader's ShaderInput struct ShaderInput { + Texture2D inputTexture; + SamplerState sampler; float2 texCoord; float2 screenPosition; float2 screenSize; float2 mousePosition; float time; + + // Method to sample the input texture + float4 sample(float2 uv) { + return inputTexture.Sample(sampler, uv); + } }; // Utility functions @@ -184,6 +191,8 @@ class SlangCompiler { [shader("fragment")] FragmentOutput fragmentMain(FragmentInput input) { ShaderInput shaderInput; + shaderInput.inputTexture = inputTexture; + shaderInput.sampler = textureSampler; shaderInput.texCoord = input.texCoord; shaderInput.screenSize = uniforms.screenSize; shaderInput.screenPosition = texToScreen(input.texCoord, uniforms.screenSize); -- 2.51.2