Passing a float or color into a Metal fragment shader from Swift
Asked Answered
M

1

8

I'm trying to write a fragment shader in Metal but can't get my head around how to pass in single values (e.g. float, float4 or half4). My shaders are as follows:

#include <metal_stdlib>
using namespace metal;

typedef struct {
    float4 renderedCoordinate [[position]];
} FullscreenQuadVertex;

vertex FullscreenQuadVertex fullscreenQuad(unsigned int vertex_id [[ vertex_id ]]) {
    float4x4 renderedCoordinates = float4x4(float4( -1.0, -1.0, 0.0, 1.0 ),
                                            float4(  1.0, -1.0, 0.0, 1.0 ),
                                            float4( -1.0,  1.0, 0.0, 1.0 ),
                                            float4(  1.0,  1.0, 0.0, 1.0 ));

    FullscreenQuadVertex outVertex;
    outVertex.renderedCoordinate = renderedCoordinates[vertex_id];

    return outVertex;
}

fragment float4 displayColor(device float4 *color [[ buffer(0) ]]) {
//    return float4(0.2, 0.5, 0.8, 1);
    return *color;
}

And I'm passing the color in from an MTKView subclass like this:

import MetalKit

class MetalView: MTKView {

    var color = NSColor(deviceRed: 0.2, green: 0.4, blue: 0.8, alpha: 1)
    var pipeline: MTLRenderPipelineState!
    var colorBuffer: MTLBuffer!

    init() {
        super.init(frame: CGRect.zero, device: nil)
        setup()
    }

    required init(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    func setup() {
        device = MTLCreateSystemDefaultDevice()
        colorPixelFormat = .bgra8Unorm

        // setup render pipeline for displaying the off screen buffer
        guard let library = device?.makeDefaultLibrary() else {
            fatalError("Failed to make Metal library")
        }

        let pipelineDescriptor = MTLRenderPipelineDescriptor()
        pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        pipelineDescriptor.colorAttachments[0].isBlendingEnabled = false
        pipelineDescriptor.vertexFunction = library.makeFunction(name: "fullscreenQuad")
        pipelineDescriptor.fragmentFunction = library.makeFunction(name: "displayColor")

        do {
            pipeline = try device?.makeRenderPipelineState(descriptor: pipelineDescriptor)
        } catch {
            fatalError("Failed to make render pipeline state")
        }

        colorBuffer = device?.makeBuffer(length: MemoryLayout<float4>.stride, options: .storageModeManaged)
        updateBackgroundColor()
    }

    func updateBackgroundColor() {
        var colorArray = [color.blueComponent, color.greenComponent, color.redComponent, color.alphaComponent].map { Float($0) }
        var data = Data(buffer: UnsafeBufferPointer(start: &colorArray, count: colorArray.count))

        colorBuffer.contents().copyMemory(from: &data, byteCount: data.count)
        colorBuffer.didModifyRange(0..<data.count)
    }

    override func draw(_ dirtyRect: NSRect) {
        drawColor()
    }

    func drawColor() {
        guard let commandQueue = device?.makeCommandQueue() else { return }
        guard let commandBuffer = commandQueue.makeCommandBuffer() else { return }
        guard let renderPassDescriptor = currentRenderPassDescriptor else { return }
        guard let currentDrawable = currentDrawable else { return }

        guard let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return }

        commandEncoder.setRenderPipelineState(pipeline)
        commandEncoder.setFragmentBuffer(colorBuffer, offset: 0, index: 0)
        commandEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
        commandEncoder.endEncoding()

        commandBuffer.present(currentDrawable)
        commandBuffer.commit()
    }

}

Despite passing in what I’d expect to be a shade of blue, all I see is black. Testing a hard coded colour returned from the the fragment shader works fine.

I'm not the most fluent in the use use UnsafePointers with Data types, so not sure if the problem is in setting the buffer data, or the way I'm actually trying to pass the buffer data into the shader. I did try the shader with an attribute type of [[ color(0) ]], however as far as I can see these aren't supported on macOS (or I was doing it wrong πŸ€·πŸ»β€β™‚οΈ).

Mistymisunderstand answered 6/1, 2019 at 1:27 Comment(2)
You don't actually explain what's going wrong. Why do you think anything is? And, and attribute of [[color(0)]] on an input doesn't mean it takes an app-provided color. It means the color that is in the render target that you're about to overwrite. It's to allow for custom blending. And, correct, it's not available for macOS. – Manassas
Damn, I was tired last night! Added. (It’s outputting black not blue). Thanks for explanation of [[color(0)]], that makes sense. – Mistymisunderstand
F
11

Rather than using a buffer for a single color, why not just send the color directly using setFragmentBytes()? Here's how it would look:

var fragmentColor = vector_float4(Float(color.redComponent), Float(color.greenComponent), Float(color.blueComponent), Float(color.alphaComponent))
commandEncoder.setFragmentBytes(&fragmentColor, length: MemoryLayout.size(ofValue: fragmentColor), index: 0)

And your shader would still use:

fragment float4 displayColor(constant float4 &color [[ buffer(0) ]])
Fungus answered 6/1, 2019 at 3:14 Comment(5)
For a uniform buffer like this, it's better to use the constant address space. And, since it's not actually an array of float4, but just a single one, better to make it a reference and not a pointer. So: constant float4 &color [[buffer(0)]] – Manassas
Damn, so simple! I had to make a couple of tweaks to make it work (var for inout value and casting to Floats). var fragmentColor = vector_float4(Float(color.redComponent), Float(color.greenComponent), Float(color.blueComponent), Float(color.alphaComponent)). Also it passes in as RGBA rather than BGRA which is the pixel format set on the pipeline, so I flipped the component order around. – Mistymisunderstand
@Mistymisunderstand The pixel format of the textures is not relevant for how the color should be passed in. In shader code, the components of a color float4 are always in RGBA order (little-endian; that is, red is first in memory). If a color value comes from reading/sampling a texture whose components are in a different order, Metal swizzles them around automatically. Likewise, if you return a color from a fragment function or directly write to a texture, Metal will swizzle the components from RGBA order to the texture's for you. – Manassas
Does it matter performance-wise that this will be re-uploading the colour to the GPU every frame even if it's not changing that regularly? Seems slightly out of sync with how I've always thought about using the GPU. In GLSL I'm used to holding onto the shader so it doesn't need to be changed, but I guess Metal's architecture differs here? – Mistymisunderstand
Uploading a single four-component float will have no effect on performance in either OpenGL or Metal. I have a model with 1/2 a million vertexes and I upload the projection and view matrix on every frame (so 16 * 4 * 2 = 128 bytes), and it runs at 60 fps without issue. (The geometry stays on the card, though.) – Fungus

© 2022 - 2024 β€” McMap. All rights reserved.