3
votes

This is my first post here, so please forgive any unintentional breach of protocol or etiquette, thanks!

BASIC PROBLEM: MetalKt doesn't seem to be drawing all the lines I'm trying to display.

DETAILS: I'm into about week 3 of learning the Metal Frameworks (mostly thru MetalKit on OS X). So far I've managed to put together a MetalView displaying an audio wave from a file on disk, with a swiper bar that travels across the screen as the audio is playing back.

The audio wave is just a set of points representing sound levels, each pair of which is connected by a line, which eventually looks like something one would see in GarageBand or Logic etc.

The problem I am having is Metal doesn't draw all the points I believe I am asking it to. Thru trial and error I discovered it stops after drawing 2048 points (computer number!). I can verify that I'm feeding the data in correctly - that is, I'm gathering enough points to draw the wave fully, with the correct coordinates that draw the entire wave, but somewhere between creating the buffer and asking Metal to draw it, it gets clipped to 2048. The rest of the audio just doesn’t show up.

So I'm wondering if there is some buffer data limit of my creation, or in Metal itself, that would cause this. I've gotten around it by using multiple buffers, but this feels like a band-aid fix, and it troubles me that I don't understand the cause.

The setup is fairly barebones, no textures or scaling (that I'm aware of ... like I said I'm just starting)

Here are my classes:

// Shaders.metal
#include <metal_stdlib>
using namespace metal;

struct Vertex {
    float4 position [[position]];
    float4 color;
};

struct Uniforms {
    float4x4 modelMatrix;
};

vertex Vertex vertex_func(constant  Vertex *vertices    [[buffer(0)]],
                          constant  Uniforms &uniforms  [[buffer(1)]],
                          uint      vid                 [[vertex_id]]) {
    float4x4 matrix = uniforms.modelMatrix;
    Vertex in       = vertices[vid];
    Vertex out;
    out.position    = matrix * float4(in.position);
    out.color       = in.color;
    return out;
}

fragment float4 fragment_func(Vertex vert [[stage_in]]) {
    return vert.color;
}

Here's some matrix utils (mostly for future use, currently returning unity) - adapted from online Metal tutorial by Marius Horga:

//  MathUtils.swift
//  chapter07
//
//  Created by Marius on 3/1/16.
//  Copyright © 2016 Marius Horga. All rights reserved.

// adapted for personal use

import simd

struct Vertex {
    var position    : vector_float4
    var color       : vector_float4
    init(pos: vector_float4, col: vector_float4) {
        position    = pos
        color       = col
    }
}

struct Matrix {
    var m: [Float]

    init() {
        m = [1, 0, 0, 0,
             0, 1, 0, 0,
             0, 0, 1, 0,
             0, 0, 0, 1
        ]
    }

    func modelMatrix(var matrix: Matrix) -> Matrix {
        return matrix   // for now, just unity
     }
}

Here's the view:

// MetalView
import MetalKit

public class TesterMetalView: MTKView {

    var vert_audio_buffer   : MTLBuffer!
    var uniform_buffer      : MTLBuffer!

    var rps : MTLRenderPipelineState! = nil

    required public init(coder: NSCoder) {
        super.init(coder: coder)

        createBuffers()
        registerShaders()
    }
    override public init(frame frameRect: CGRect, device: MTLDevice?) {
        super.init(frame: frameRect, device: device)
        createBuffers()
        registerShaders()
    }

    override public func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)
        if let rpd = currentRenderPassDescriptor,
            drawable = currentDrawable {

            rpd.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1.0)
            let command_buffer = device!.newCommandQueue().commandBuffer()
            let command_encoder = command_buffer.renderCommandEncoderWithDescriptor(rpd)
            command_encoder.setRenderPipelineState(rps)

            command_encoder.setVertexBuffer(vert_audio_buffer,  offset: 0, atIndex: 0)
            command_encoder.setVertexBuffer(uniform_buffer,     offset: 0, atIndex: 1)

            let numVerts = (vert_audio_buffer.length / sizeof(Vertex))
            command_encoder.drawPrimitives(.Line, vertexStart: 0, vertexCount: numVerts)

            command_encoder.endEncoding()
            command_buffer.presentDrawable(drawable)
            command_buffer.commit()
        }
    }

    func createBuffers() {
        if device == nil { self.device = MTLCreateSystemDefaultDevice() }

        // rotation + scaling
        uniform_buffer      = device!.newBufferWithLength(sizeof(Float) * 16, options: [])
        let bufferPointer   = uniform_buffer.contents()
        memcpy(bufferPointer, Matrix().modelMatrix(Matrix()).m, sizeof(Float) * 16)
    }

    func registerShaders() {
        if device == nil { self.device = MTLCreateSystemDefaultDevice() }
        if let library  = device!.newDefaultLibrary() {
            let vertex_func = library.newFunctionWithName("vertex_func")
            let frag_func   = library.newFunctionWithName("fragment_func")

            let rpld = MTLRenderPipelineDescriptor()
            rpld.vertexFunction     = vertex_func
            rpld.fragmentFunction   = frag_func
            rpld.colorAttachments[0].pixelFormat = .BGRA8Unorm

            do {
                try rps = device!.newRenderPipelineStateWithDescriptor(rpld)
            } catch {
                Swift.print("***ERROR: newRenderPipelineStateWithDescriptor failed ...\r\t\(error)")
            }
        }
    }
}

And here's how the metal buffers are created:

// within Metal View Controller
var verts_audio = [Vertex]()

// ... create Vertex's from audio data
// I can confirm verts_audio contains valid data
// however, Metal draws only the first 2048 of them

let bufferLength = sizeof(Vertex) * verts_audio.count

// metalV() gets a typed reference to the view
metalV().vert_audio_buffer = metalV().device!
    .newBufferWithBytes(verts_audio,
                        length  : bufferLength,
                        options : [])

So, if I try to draw a wave with vertices containing 2838 points, and put them all in a single buffer using the above code, I get this:

screen shot - clipped drawing

If I spread the save vertices amongst multiple buffers, each containing 2048 Vertex's (code not shown), I get the full wave (lighter lines show extra buffers):

screen shot - full wave

I'm probably doing something bone-headed or obvious. I'd sure appreciate someone smarter than me shedding some light on this. Thanks!

1
Out of curiosity, what hardware configuration and OS X version are you using? Might this be happening on an nVidia GPU?warrenm
@warrenm: So far, this has been running only on a late 2013 iMac 27", El Capitan 10.11.5.Yes, according to About This Mac:NVIDIA GeForce GTX 775M 2048 MB.. It makes sense that the video card could be part of the answer, and I never would've thought of that - thank you! I'm slowly making progress on the app and learning a lot along the way.paulcrescendo

1 Answers

5
votes

I think I've been fighting this today also. While searching for an answer I found " Is there a size limit to newBufferWithBytes()? ".

In it, the answer mentions the Apple Documentation: Metal Feature Set Tables.

In there it says that the " Maximum memory allocation for a shader or compute function variable in the constant address space" is unlimited for the iOS devices but only 64KB for OS X.

So in the vertex shader, I believe, "constant Vertex *vertices" should be "device Vertex *vertices" on OS X to use the device address space.