你如何使用顶点缓冲区将动态且大小可变的数据传递给Metal?
背景:
我正在学习Metal,以及如何在SwiftUI中使用MTKView。
我在为一个面向对象的绘图程序打基础,该程序将允许用户使用Catmull-Rom样条来绘制羽化笔触。
我一开始通过 setVertexBytes 将我的顶点传给Metal,以便把顶点传给我的顶点着色器。
我已经写了辅助函数来绘制粗线、圆弧、开圆(甜甜圈)、正方形,以及其他一些我从示例代码中抽出来简化的其它类型。
当我尝试绘制一个大圆时,你会看到它像一个多边形而不是圆。我把分段数提高到360,但后来发现你只能通过 setVertexBytes 传输大约4k的数据。
我更新了代码,改为使用顶点缓冲区来传递顶点列表,而不是用 setVertexBytes 传递,这样我就可以传入更大的顶点数组。然而,在我看来,顶点缓冲区像是附着在Metal设备上的一个固定大小的持久性资源。我不确定如何通过顶点缓冲区向着色器传递可变长度的数据。(因为我在做绘图程序,顶点列表会随着用户的绘图而增长。)
问题:
当我使用 setVertexBytes 绘制时,我可以通过对我的形状绘制函数的调用来组成一个绘图,一切都按预期工作。下面的绘图代码,我稍后再详细解释:
let limit: Float = 0.9
// drawCircle(center: simd_float2(-0.75, -0.75), color: blue, radius: 30, lineThickness: 6)
// drawCircle(center: simd_float2(-0.75, -0.75), color: black, radius: 20, lineThickness: 6)
// drawCircle(center: simd_float2(-0.75, -0.75), color: blue, radius: 10, lineThickness: 6)
// drawCircle(center: simd_float2(-0.75, -0.75), color: black, radius: 2, lineThickness: 4)
drawCircle(center: simd_float2(0, 0), color: blue, radius: 280, steps: 120, lineThickness: 6)
// drawSquare(center: simd_float2(0.7, 0.7), color: red, width: 58, orthoMatrix: orthoMatrix)
drawThickLine(
p1: simd_float2(-limit,limit * drawingInfo.wrappedValue.linePlacement),
p2: simd_float2(limit, -limit * drawingInfo.wrappedValue.linePlacement),
color: black,
thickness: 20,
)
会得到这张图片:

(我注释掉了部分代码以简化失败场景。
然而,当我改用 setVertexBytes 传递顶点数据时,事情就变得很糟。形状要么完全缺失,要么缺失部分,而且它们彼此之间会相互渗透。上面绘图代码的结果看起来像这样:


如果我只绘制大圆或粗线,它会正确显示。如果我只尝试绘制一个红色正方形,只有它的一个三角形被绘制出来。如果我只绘制那个大圆和粗线,结果如上所示。如果我取消注释上面的其他绘图代码,也会得到上面的图像。红色正方形和同心圆完全缺失。
以下是我的着色器代码,这段代码不需要根据顶点数据的传递方式而改变:
//
// Shaders.metal
// DrawingApp
//
// Created by Duncan Champney on 5/4/26.
//
#include <metal_stdlib>
#include "MetalStructs.h"
using namespace metal;
struct VertexOut {
float4 position [[position]];
float2 texCoord;
};
struct Uniforms {
float4 color;
bool drawWithTexture;
float4x4 orthoMatrix;
};
vertex VertexOut vertex_main(const device float2* position [[buffer(0)]],
constant Uniforms& uniforms [[buffer(1)]],
uint vid [[vertex_id]]) {
VertexOut out;
float2 pos = position[vid];
out.position = uniforms.orthoMatrix * float4(pos, 0, 1);
out.texCoord = pos * 0.5 + 0.5; // basic mapping
return out;
}
fragment float4 fragment_main(VertexOut in [[stage_in]],
texture2d<float> tex [[texture(0)]],
constant Uniforms& uniforms [[buffer(1)]]) {
constexpr sampler s(address::clamp_to_edge, filter::linear);
if (uniforms.drawWithTexture) {
float2 coord = in.texCoord;
return tex.sample(s, coord);
} else {
return uniforms.color;
}
}
我当前在创建渲染器时只创建一次顶点缓冲区,使其足够容纳我最大的顶点数组。然后在每次调用 drawPrimitives 之前把顶点数据拷贝到其中。
我的渲染器的init方法看起来像这样:
init(drawingInfo: Binding<DrawingInfo>) {
self.drawingInfo = drawingInfo
device = MTLCreateSystemDefaultDevice()
guard let vertBuffer = device.makeBuffer(
length: maxVerticiesSize,
options: .storageModeShared
) else {
fatalError("Could not create vertex buffer")
}
vertexBuffer = vertBuffer
super.init()
//MARK: Oversampling
if device.supportsTextureSampleCount(4) {
sampleCount = 4
} else if device.supportsTextureSampleCount(2) {
sampleCount = 2
}
commandQueue = device.makeCommandQueue()
makePipeline()
}
而我的渲染器的 draw() 方法看起来像这样:
func draw(in view: MTKView) {
enum ArrowHeadDirection {
case down
case left
}
guard let drawable = view.currentDrawable else {
print("[ScopeRenderer] currentDrawable is nil")
return
}
guard let descriptor = view.currentRenderPassDescriptor else {
print("[ScopeRenderer] currentRenderPassDescriptor is nil")
return
}
guard let pipeline = pipeline else {
print("[ScopeRenderer] pipeline is nil")
return
}
#if os(macOS)
scale = Float(mtkView?.window?.screen?.backingScaleFactor ?? 1.0)
#else
scale = Float(mtkView?.contentScaleFactor ?? 1)
#endif
let orthoMatrix = matrix_identity_float4x4
let commandBuffer = commandQueue.makeCommandBuffer()!
let colorComponents = drawingInfo.wrappedValue.backgroundColor.components()
descriptor.colorAttachments[0].clearColor = MTLClearColor(
red: colorComponents[0],
green: colorComponents[1],
blue: colorComponents[2],
alpha: colorComponents[3])
descriptor.colorAttachments[0].loadAction = MTLLoadAction.clear
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)!
encoder.setRenderPipelineState(pipeline)
// Drawing code goes here:
var uniforms = Uniforms(
color: black,
drawWithTetxure: false,
orthoMatrix: orthoMatrix
)
let limit: Float = 0.9
// drawCircle(center: simd_float2(-0.75, -0.75), color: blue, radius: 30, lineThickness: 6)
// drawCircle(center: simd_float2(-0.75, -0.75), color: black, radius: 20, lineThickness: 6)
// drawCircle(center: simd_float2(-0.75, -0.75), color: blue, radius: 10, lineThickness: 6)
// drawCircle(center: simd_float2(-0.75, -0.75), color: black, radius: 2, lineThickness: 4)
drawCircle(center: simd_float2(0, 0), color: blue, radius: 280, steps: 120, lineThickness: 6)
// drawSquare(center: simd_float2(0.7, 0.7), color: red, width: 58, orthoMatrix: orthoMatrix)
drawThickLine(
p1: simd_float2(-limit,limit * drawingInfo.wrappedValue.linePlacement),
p2: simd_float2(limit, -limit * drawingInfo.wrappedValue.linePlacement),
color: black,
thickness: 20,
)
encoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
// MARK: - nested drawing functions
func drawCircle(
center: simd_float2,
color: SIMD4<Float>,
radius: Float,
steps: Int = 24,
lineThickness: Float,
) {
drawArc(
center: center,
color: color,
radius: radius,
steps: steps,
lineThickness: lineThickness)
}
func drawArc(
center: simd_float2,
color: SIMD4<Float>,
radius: Float,
startAngle: Float = 0,
endAngle: Float = 360.0,
steps: Int = 24,
lineThickness: Float,
asDiamond: Bool = false) {
let aspect = drawingInfo.wrappedValue.imageAspectRatio
let landscape = aspect > 1
let adjustment: simd_float2 = landscape ? simd_float2(1, aspect) : simd_float2(1/aspect, 1)
let widthPerPixel: Float = scale / Float(max(drawableSize.width, drawableSize.height))
let startAngleRadians = startAngle.degreesToRadians
let arcDelta = endAngle.degreesToRadians - startAngleRadians
let notFullCircle = startAngle != 0.0 || endAngle != 360.0
var verticies = [simd_float2]()
verticies.reserveCapacity(steps * 2)
let radius = 2 * radius + lineThickness / 8
let loopSteps = notFullCircle ? steps - 1 : steps
for step in 0 ..< loopSteps {
let angle: Float = startAngleRadians + Float(step) / Float(steps) * arcDelta
let angle2 = startAngleRadians + Float((step+1) % steps) / Float(loopSteps) * arcDelta
var deltaX = cos(angle) * widthPerPixel * (radius - lineThickness) * adjustment.x
var deltaY = sin(angle) * widthPerPixel * (radius - lineThickness) * adjustment.y
let p1Inside = simd_float2(x: center.x + deltaX, y: center.y + deltaY)
deltaX = cos(angle) * widthPerPixel * (radius + lineThickness) * adjustment.x
deltaY = sin(angle) * widthPerPixel * (radius + lineThickness) * adjustment.y
let p1Outside = simd_float2(x: center.x + deltaX, y: center.y + deltaY)
deltaX = cos(angle2) * widthPerPixel * (radius - lineThickness) * adjustment.x
deltaY = sin(angle2) * widthPerPixel * (radius - lineThickness) * adjustment.y
let p2Inside = simd_float2(x: center.x + deltaX, y: center.y + deltaY)
deltaX = cos(angle2) * widthPerPixel * (radius + lineThickness) * adjustment.x
deltaY = sin(angle2) * widthPerPixel * (radius + lineThickness) * adjustment.y
let p2Outside = simd_float2(x: center.x + deltaX, y: center.y + deltaY)
verticies += [p1Inside, p1Outside, p2Inside, p2Outside]
}
uniforms = Uniforms(
color: color,
drawWithTetxure: false,
orthoMatrix: orthoMatrix
)
let verticiesSize = MemoryLayout<simd_float2>.stride * verticies.count
if maxVerticiesSize < verticiesSize {
maxVerticiesSize = verticiesSize
print("maxVerticiesSize = \(maxVerticiesSize). verticies.count = \(verticies.count)")
}
if useVertexBuffers {
vertexBuffer.contents().copyMemory(from: verticies, byteCount: verticiesSize)
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
} else {
encoder.setVertexBytes(verticies, length: verticiesSize, index: 0)
}
encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: verticies.count)
}
func drawThickLine(
p1: simd_float2,
p2: simd_float2,
color: SIMD4<Float>,
thickness: Float
) {
let aspect = drawingInfo.wrappedValue.imageAspectRatio
let landscape = aspect > 1
let adjustment: simd_float2 = landscape ? simd_float2(1, 1/aspect) : simd_float2(1*aspect, 1)
let p1Tweaked = p1 * adjustment
let p2Tweaked = p2 * adjustment
let thickness = thickness * scale / Float(max(drawableSize.width, drawableSize.height))
let dir = normalize(p2Tweaked - p1Tweaked)
let normal = simd_float2(-dir.y, dir.x) * thickness
let v0 = (p1Tweaked + normal) / adjustment
let v1 = (p1Tweaked - normal) / adjustment
let v2 = (p2Tweaked + normal) / adjustment
let v3 = (p2Tweaked - normal) / adjustment
var vertices = [v0, v1, v2, v3]
if useVertexBuffers {
let verticiesSize = MemoryLayout<simd_float2>.stride * 4
vertexBuffer.contents().copyMemory(from: vertices, byteCount: verticiesSize)
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
} else {
encoder.setVertexBytes(vertices, length: MemoryLayout<simd_float2>.stride * 4, index: 0)
}
uniforms = Uniforms(
color: color,
drawWithTetxure: false,
orthoMatrix: orthoMatrix
)
encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
}
}
大量 的代码,我知道。对不起。我不确定还能省略哪些部分,同时仍然能完整描述问题。
问题:
为什么我的代码不起作用?如果我只用 setVertexBuffer 绘制一个单独的圆或单独的粗线,它确实可以工作,
你能否将数据通过组合使用 setVertexBuffer 和setVertexBytes传给着色器?以及如何处理顶点数据会随时间变化并增长的图像的渲染?
你可以在Github上看到整个测试项目,地址是 https://github.com/DuncanMC/DrawingApp.git
解决方案
所以我问了ChatGPT,看看发生了什么,它识别出问题并给了我一个解决方案。
问题在于我在渲染一个帧时多次写入我的顶点缓冲区,然后一次性提交绘制。
Metal是异步运行的,在我用下一条绘制命令修改缓冲区之后,它会从我的顶点缓冲区获取内容。
ChatGPT给出的解决方案是将代码改为使用顶点缓冲区作循环缓冲区,在写入时改为在其中的某个偏移量处写入,而不是总是在起始位置,并在setVertexBuffer调用中传递该偏移量。
这需要一些额外的逻辑来强制让循环缓冲区中的偏移量达到256字节的页对齐,这是Metal要求在缓冲区使用偏移量时的对齐要求。
我会把更新后的代码推送到我的仓库以供参考。
以下是更新后的绘制方法:
func draw(in view: MTKView) {
enum ArrowHeadDirection {
case down
case left
}
guard let drawable = view.currentDrawable else {
print("[ScopeRenderer] currentDrawable is nil")
return
}
guard let descriptor = view.currentRenderPassDescriptor else {
print("[ScopeRenderer] currentRenderPassDescriptor is nil")
return
}
guard let pipeline = pipeline else {
print("[ScopeRenderer] pipeline is nil")
return
}
#if os(macOS)
scale = Float(mtkView?.window?.screen?.backingScaleFactor ?? 1.0)
#else
scale = Float(mtkView?.contentScaleFactor ?? 1)
#endif
let orthoMatrix = matrix_identity_float4x4
// Reset ring write offset at the start of a frame region
// Simple partitioning by frame without explicit GPU sync. For robust sync, use in-flight semaphores.
if ringWriteOffset >= ringBufferSize - ringBufferAlignment {
ringWriteOffset = 0
}
let commandBuffer = commandQueue.makeCommandBuffer()!
let colorComponents = drawingInfo.wrappedValue.backgroundColor.components()
descriptor.colorAttachments[0].clearColor = MTLClearColor(
red: colorComponents[0],
green: colorComponents[1],
blue: colorComponents[2],
alpha: colorComponents[3])
descriptor.colorAttachments[0].loadAction = MTLLoadAction.clear
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)!
encoder.setRenderPipelineState(pipeline)
// Drawing code goes here:
var uniforms = Uniforms(
color: black,
drawWithTetxure: false,
orthoMatrix: orthoMatrix
)
// MARK: Test drawing code.
let limit: Float = 0.9
drawCircle(center: simd_float2(0, 0), color: blue, radius: 280, steps: 120, lineThickness: 6)
drawCircle(center: simd_float2(-0.75, -0.75), color: blue, radius: 30, lineThickness: 6)
drawCircle(center: simd_float2(-0.75, -0.75), color: black, radius: 20, lineThickness: 6)
drawCircle(center: simd_float2(-0.75, -0.75), color: blue, radius: 10, lineThickness: 6)
drawCircle(center: simd_float2(-0.75, -0.75), color: black, radius: 2, lineThickness: 4)
drawThickLine(
p1: simd_float2(-limit,limit * drawingInfo.wrappedValue.linePlacement),
p2: simd_float2(limit, -limit * drawingInfo.wrappedValue.linePlacement),
color: black,
thickness: 20,
)
drawSquare(center: simd_float2(0.7, 0.7), color: red, width: 58, orthoMatrix: orthoMatrix)
encoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
// MARK: - nested drawing functions
func drawCircle(
center: simd_float2,
color: SIMD4<Float>,
radius: Float,
steps: Int = 24,
lineThickness: Float,
) {
drawArc(
center: center,
color: color,
radius: radius,
steps: steps,
lineThickness: lineThickness)
}
func drawArc(
center: simd_float2,
color: SIMD4<Float>,
radius: Float,
startAngle: Float = 0,
endAngle: Float = 360.0,
steps: Int = 24,
lineThickness: Float,
asDiamond: Bool = false) {
let aspect = drawingInfo.wrappedValue.imageAspectRatio
let landscape = aspect > 1
let adjustment: simd_float2 = landscape ? simd_float2(1, aspect) : simd_float2(1/aspect, 1)
let widthPerPixel: Float = scale / Float(max(drawableSize.width, drawableSize.height))
let startAngleRadians = startAngle.degreesToRadians
let arcDelta = endAngle.degreesToRadians - startAngleRadians
let notFullCircle = startAngle != 0.0 || endAngle != 360.0
var verticies = [simd_float2]()
verticies.reserveCapacity(steps * 2)
let radius = 2 * radius + lineThickness / 8
let loopSteps = notFullCircle ? steps - 1 : steps
for step in 0 ..< loopSteps {
let angle: Float = startAngleRadians + Float(step) / Float(steps) * arcDelta
let angle2 = startAngleRadians + Float((step+1) % steps) / Float(loopSteps) * arcDelta
var deltaX = cos(angle) * widthPerPixel * (radius - lineThickness) * adjustment.x
var deltaY = sin(angle) * widthPerPixel * (radius - lineThickness) * adjustment.y
let p1Inside = simd_float2(x: center.x + deltaX, y: center.y + deltaY)
deltaX = cos(angle) * widthPerPixel * (radius + lineThickness) * adjustment.x
deltaY = sin(angle) * widthPerPixel * (radius + lineThickness) * adjustment.y
let p1Outside = simd_float2(x: center.x + deltaX, y: center.y + deltaY)
deltaX = cos(angle2) * widthPerPixel * (radius - lineThickness) * adjustment.x
deltaY = sin(angle2) * widthPerPixel * (radius - lineThickness) * adjustment.y
let p2Inside = simd_float2(x: center.x + deltaX, y: center.y + deltaY)
deltaX = cos(angle2) * widthPerPixel * (radius + lineThickness) * adjustment.x
deltaY = sin(angle2) * widthPerPixel * (radius + lineThickness) * adjustment.y
let p2Outside = simd_float2(x: center.x + deltaX, y: center.y + deltaY)
verticies += [p1Inside, p1Outside, p2Inside, p2Outside]
}
uniforms = Uniforms(
color: color,
drawWithTetxure: false,
orthoMatrix: orthoMatrix
)
let verticiesSize = MemoryLayout<simd_float2>.stride * verticies.count
if maxVerticiesSize < verticiesSize {
maxVerticiesSize = verticiesSize
print("maxVerticiesSize = \(maxVerticiesSize). verticies.count = \(verticies.count)")
}
if useVertexBuffers {
let offset = allocateVerticesInRing(byteCount: verticiesSize)
let dst = vertexBuffer.contents().advanced(by: offset)
dst.copyMemory(from: verticies, byteCount: verticiesSize)
encoder.setVertexBuffer(vertexBuffer, offset: offset, index: 0)
} else {
encoder.setVertexBytes(verticies, length: verticiesSize, index: 0)
}
encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: verticies.count)
}
func drawSquare(
center: simd_float2,
color: SIMD4<Float>,
width: Float,
orthoMatrix: float4x4,
asDiamond: Bool = false) {
let aspect = drawingInfo.wrappedValue.imageAspectRatio
let landscape = aspect > 1
let adjustment: simd_float2 = landscape ? simd_float2(1, 1/aspect) : simd_float2(1*aspect, 1)
let width = width
let center: simd_float2 = simd_float2(x: center.x, y: center.y)
let widthPerPixel: Float = scale / Float(max(drawableSize.width, drawableSize.height))
let yOffset = (widthPerPixel * width) / adjustment.y
let xOffset = widthPerPixel * width / adjustment.x
let p1: simd_float2
let p2: simd_float2
let p3: simd_float2
let p4: simd_float2
if !asDiamond {
p1 = simd_float2(x: center.x - xOffset, y: center.y + yOffset)
p2 = simd_float2(x: center.x + xOffset, y: center.y + yOffset)
p3 = simd_float2(x: center.x + xOffset, y: center.y - yOffset)
p4 = simd_float2(x: center.x - xOffset, y: center.y - yOffset)
} else {
p1 = simd_float2(x: center.x, y: center.y + yOffset)
p2 = simd_float2(x: center.x + xOffset, y: center.y)
p3 = simd_float2(x: center.x, y: center.y - yOffset)
p4 = simd_float2(x: center.x - xOffset, y: center.y)
}
var verts: [simd_float2] = [p1, p2, p3]
var uniforms: Uniforms = Uniforms(
color: color,
drawWithTetxure: false,
orthoMatrix: orthoMatrix
)
var verticiesSize = MemoryLayout<simd_float2>.stride * verts.count
if useVertexBuffers {
let offset = allocateVerticesInRing(byteCount: verticiesSize)
let dst = vertexBuffer.contents().advanced(by: offset)
dst.copyMemory(from: verts, byteCount: verticiesSize)
encoder.setVertexBuffer(vertexBuffer, offset: offset, index: 0)
} else {
encoder.setVertexBytes(verts, length: MemoryLayout<simd_float2>.stride * 3, index: 0)
}
encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
verts = [p1, p3, p4]
verticiesSize = MemoryLayout<simd_float2>.stride * verts.count
if useVertexBuffers {
let offset2 = allocateVerticesInRing(byteCount: verticiesSize)
let dst2 = vertexBuffer.contents().advanced(by: offset2)
dst2.copyMemory(from: verts, byteCount: verticiesSize)
encoder.setVertexBuffer(vertexBuffer, offset: offset2, index: 0)
} else {
encoder.setVertexBytes(verts, length: MemoryLayout<simd_float2>.stride * 3, index: 0)
}
encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
}
func drawThickLine(
p1: simd_float2,
p2: simd_float2,
color: SIMD4<Float>,
thickness: Float
) {
let aspect = drawingInfo.wrappedValue.imageAspectRatio
let landscape = aspect > 1
let adjustment: simd_float2 = landscape ? simd_float2(1, 1/aspect) : simd_float2(1*aspect, 1)
let p1Tweaked = p1 * adjustment
let p2Tweaked = p2 * adjustment
let thickness = thickness * scale / Float(max(drawableSize.width, drawableSize.height))
let dir = normalize(p2Tweaked - p1Tweaked)
let normal = simd_float2(-dir.y, dir.x) * thickness
let v0 = (p1Tweaked + normal) / adjustment
let v1 = (p1Tweaked - normal) / adjustment
let v2 = (p2Tweaked + normal) / adjustment
let v3 = (p2Tweaked - normal) / adjustment
var vertices = [v0, v1, v2, v3]
if useVertexBuffers {
let verticiesSize = MemoryLayout<simd_float2>.stride * 4
let offset = allocateVerticesInRing(byteCount: verticiesSize)
let dst = vertexBuffer.contents().advanced(by: offset)
dst.copyMemory(from: vertices, byteCount: verticiesSize)
encoder.setVertexBuffer(vertexBuffer, offset: offset, index: 0)
} else {
encoder.setVertexBytes(vertices, length: MemoryLayout<simd_float2>.stride * 4, index: 0)
}
uniforms = Uniforms(
color: color,
drawWithTetxure: false,
orthoMatrix: orthoMatrix
)
encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1)
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
}
// MARK: Helper function for managing offsets into the ring buffer
@inline(__always)
func allocateVerticesInRing(byteCount: Int) -> Int {
let alignedSize = ((byteCount + ringBufferAlignment - 1) / ringBufferAlignment) * ringBufferAlignment
if ringWriteOffset + alignedSize > ringBufferSize {
// Wrap to start if not enough space
ringWriteOffset = 0
}
let offset = ringWriteOffset
ringWriteOffset += alignedSize
return offset
}
}