blob: 97a407e1bcbef9655af5c385ad00c5b6ac088771 [file]
/*
* Copyright 2023 Rive
*/
#include "rive/pls/metal/pls_render_context_metal.h"
#include "buffer_ring_metal.h"
#include "rive/pls/metal/pls_render_target_metal.h"
#include <sstream>
#include "../out/obj/generated/color_ramp.exports.h"
#include "../out/obj/generated/draw.exports.h"
#include "../out/obj/generated/tessellate.exports.h"
namespace rive::pls
{
#ifdef RIVE_IOS
#include "../out/obj/generated/rive_pls_ios.metallib.c"
#else
#include "../out/obj/generated/rive_pls_macosx.metallib.c"
#endif
static id<MTLRenderPipelineState> make_pipeline_state(id<MTLDevice> gpu,
MTLRenderPipelineDescriptor* desc)
{
NSError* err = [NSError errorWithDomain:@"pls_pipeline_create" code:201 userInfo:nil];
id<MTLRenderPipelineState> state = [gpu newRenderPipelineStateWithDescriptor:desc error:&err];
if (!state)
{
printf("%s\n", err.localizedDescription.UTF8String);
exit(-1);
}
return state;
}
// Renders color ramps to the gradient texture.
class PLSRenderContextMetal::ColorRampPipeline
{
public:
ColorRampPipeline(id<MTLDevice> gpu, id<MTLLibrary> plsLibrary)
{
MTLRenderPipelineDescriptor* desc = [[MTLRenderPipelineDescriptor alloc] init];
desc.vertexFunction = [plsLibrary newFunctionWithName:@GLSL_colorRampVertexMain];
desc.fragmentFunction = [plsLibrary newFunctionWithName:@GLSL_colorRampFragmentMain];
desc.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm;
m_pipelineState = make_pipeline_state(gpu, desc);
}
id<MTLRenderPipelineState> pipelineState() const { return m_pipelineState; }
private:
id<MTLRenderPipelineState> m_pipelineState;
};
// Renders tessellated vertices to the tessellation texture.
class PLSRenderContextMetal::TessellatePipeline
{
public:
TessellatePipeline(id<MTLDevice> gpu, id<MTLLibrary> plsLibrary)
{
MTLRenderPipelineDescriptor* desc = [[MTLRenderPipelineDescriptor alloc] init];
desc.vertexFunction = [plsLibrary newFunctionWithName:@GLSL_tessellateVertexMain];
desc.fragmentFunction = [plsLibrary newFunctionWithName:@GLSL_tessellateFragmentMain];
desc.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA32Uint;
m_pipelineState = make_pipeline_state(gpu, desc);
}
id<MTLRenderPipelineState> pipelineState() const { return m_pipelineState; }
private:
id<MTLRenderPipelineState> m_pipelineState;
};
// Renders paths to the main render target.
class PLSRenderContextMetal::DrawPipeline
{
public:
DrawPipeline(PLSRenderContextMetal* context,
DrawType drawType,
const ShaderFeatures& shaderFeatures)
{
id<MTLFunction> vertexMain =
GetMainFunction(context, drawType, SourceType::vertexOnly, shaderFeatures);
id<MTLFunction> fragmentMain =
GetMainFunction(context, drawType, SourceType::wholeProgram, shaderFeatures);
constexpr static auto makePipelineState = [](id<MTLDevice> gpu,
id<MTLFunction> vertexMain,
id<MTLFunction> fragmentMain,
MTLPixelFormat pixelFormat) {
MTLRenderPipelineDescriptor* desc = [[MTLRenderPipelineDescriptor alloc] init];
desc.vertexFunction = vertexMain;
desc.fragmentFunction = fragmentMain;
desc.colorAttachments[0].pixelFormat = pixelFormat;
desc.colorAttachments[1].pixelFormat = MTLPixelFormatRG16Float;
desc.colorAttachments[2].pixelFormat = pixelFormat;
desc.colorAttachments[3].pixelFormat = MTLPixelFormatRG16Float;
return make_pipeline_state(gpu, desc);
};
m_pipelineStateRGBA8 =
makePipelineState(context->m_gpu, vertexMain, fragmentMain, MTLPixelFormatRGBA8Unorm);
m_pipelineStateBGRA8 =
makePipelineState(context->m_gpu, vertexMain, fragmentMain, MTLPixelFormatBGRA8Unorm);
}
id<MTLRenderPipelineState> pipelineState(MTLPixelFormat pixelFormat) const
{
assert(pixelFormat == MTLPixelFormatRGBA8Unorm || pixelFormat == MTLPixelFormatBGRA8Unorm);
return pixelFormat == MTLPixelFormatRGBA8Unorm ? m_pipelineStateRGBA8
: m_pipelineStateBGRA8;
}
private:
id<MTLFunction> GetMainFunction(PLSRenderContextMetal* context,
DrawType drawType,
SourceType sourceType,
const ShaderFeatures& shaderFeatures)
{
// Namespaces beginning in 'r' indicate the normal Rive renderer.
char namespaceName[] = "r0000";
if (drawType == DrawType::interiorTriangulation)
{
// Namespaces beginning in 't' indicate the special case when we draw non-overlapping
// interior triangles.
namespaceName[0] = 't';
}
// draw.metal uses the following bits in the namespace names for each flag:
// ENABLE_ADVANCED_BLEND: r0001 / t0001
// ENABLE_PATH_CLIPPING: r0010 / t0010
// ENABLE_EVEN_ODD: r0100 / t0100
// ENABLE_HSL_BLEND_MODES: r1000 / t1000
uint64_t shaderFeatureDefines = shaderFeatures.getPreprocessorDefines(sourceType);
if (shaderFeatureDefines & ShaderFeatures::PreprocessorDefines::ENABLE_ADVANCED_BLEND)
{
namespaceName[4] = '1';
}
if (shaderFeatureDefines & ShaderFeatures::PreprocessorDefines::ENABLE_PATH_CLIPPING)
{
namespaceName[3] = '1';
}
if (shaderFeatureDefines & ShaderFeatures::PreprocessorDefines::ENABLE_EVEN_ODD)
{
namespaceName[2] = '1';
}
if (shaderFeatureDefines & ShaderFeatures::PreprocessorDefines::ENABLE_HSL_BLEND_MODES)
{
namespaceName[1] = '1';
}
const char* mainFunctionName =
sourceType == SourceType::vertexOnly ? GLSL_drawVertexMain : GLSL_drawFragmentMain;
NSString* fullyQualifiedName =
[NSString stringWithFormat:@"%s::%s", namespaceName, mainFunctionName];
return [context->m_plsLibrary newFunctionWithName:fullyQualifiedName];
}
id<MTLRenderPipelineState> m_pipelineStateRGBA8;
id<MTLRenderPipelineState> m_pipelineStateBGRA8;
};
#ifdef RIVE_IOS
static bool is_apple_ios_silicon(id<MTLDevice> gpu)
{
if (@available(iOS 13, *))
{
return [gpu supportsFamily:MTLGPUFamilyApple4];
}
return false;
}
#endif
std::unique_ptr<PLSRenderContextMetal> PLSRenderContextMetal::Make(id<MTLDevice> gpu,
id<MTLCommandQueue> queue)
{
if (![gpu supportsFamily:MTLGPUFamilyApple1])
{
printf("error: GPU is not Apple family.");
return nullptr;
}
PlatformFeatures platformFeatures;
#ifdef RIVE_IOS
if (!is_apple_ios_silicon(gpu))
{
// The PowerVR GPU, at least on A10, has fp16 precision issues. We can't use the the bottom
// 3 bits of the path and clip IDs in order for our equality testing to work.
platformFeatures.pathIDGranularity = 8;
}
#endif
// It appears, so far, that we don't need to use flat interpolation for path IDs on any Apple
// device, and it's faster not to.
platformFeatures.avoidFlatVaryings = true;
platformFeatures.invertOffscreenY = true;
return std::unique_ptr<PLSRenderContextMetal>(
new PLSRenderContextMetal(platformFeatures, gpu, queue));
}
PLSRenderContextMetal::PLSRenderContextMetal(const PlatformFeatures& platformFeatures,
id<MTLDevice> gpu,
id<MTLCommandQueue> queue) :
PLSRenderContext(platformFeatures), m_gpu(gpu), m_queue(queue)
{
// Load the precompiled shaders.
dispatch_data_t metallibData = dispatch_data_create(
#ifdef RIVE_IOS
rive_pls_ios_metallib,
rive_pls_ios_metallib_len,
#else
rive_pls_macosx_metallib,
rive_pls_macosx_metallib_len,
#endif
nil,
nil);
NSError* err = [NSError errorWithDomain:@"pls_metallib_load" code:200 userInfo:nil];
m_plsLibrary = [m_gpu newLibraryWithData:metallibData error:&err];
if (m_plsLibrary == nil)
{
printf("Failed to load pls metallib.\n");
printf("%s\n", err.localizedDescription.UTF8String);
exit(-1);
}
m_colorRampPipeline = std::make_unique<ColorRampPipeline>(gpu, m_plsLibrary);
m_tessPipeline = std::make_unique<TessellatePipeline>(gpu, m_plsLibrary);
// Create vertex and index buffers for the different PLS patches.
m_pathPatchVertexBuffer = [gpu newBufferWithLength:kPatchVertexBufferCount * sizeof(PatchVertex)
options:MTLResourceStorageModeShared];
m_pathPatchIndexBuffer = [gpu newBufferWithLength:kPatchIndexBufferCount * sizeof(uint16_t)
options:MTLResourceStorageModeShared];
GeneratePatchBufferData(reinterpret_cast<PatchVertex*>(m_pathPatchVertexBuffer.contents),
reinterpret_cast<uint16_t*>(m_pathPatchIndexBuffer.contents));
}
PLSRenderContextMetal::~PLSRenderContextMetal() {}
rcp<PLSRenderTargetMetal> PLSRenderContextMetal::makeRenderTarget(MTLPixelFormat pixelFormat,
size_t width,
size_t height)
{
return rcp<PLSRenderTargetMetal>(
new PLSRenderTargetMetal(m_gpu, pixelFormat, width, height, m_platformFeatures));
}
std::unique_ptr<BufferRingImpl> PLSRenderContextMetal::makeVertexBufferRing(size_t capacity,
size_t itemSizeInBytes)
{
return std::make_unique<BufferMetal>(m_gpu, capacity, itemSizeInBytes);
}
std::unique_ptr<TexelBufferRing> PLSRenderContextMetal::makeTexelBufferRing(
TexelBufferRing::Format format,
Renderable,
size_t widthInItems,
size_t height,
size_t texelsPerItem,
int textureIdx,
TexelBufferRing::Filter)
{
return std::make_unique<TexelBufferMetal>(m_gpu, format, widthInItems, height, texelsPerItem);
}
std::unique_ptr<BufferRingImpl> PLSRenderContextMetal::makeUniformBufferRing(size_t capacity,
size_t itemSizeInBytes)
{
return std::make_unique<BufferMetal>(m_gpu, capacity, itemSizeInBytes);
}
void PLSRenderContextMetal::allocateTessellationTexture(size_t height)
{
MTLTextureDescriptor* desc = [[MTLTextureDescriptor alloc] init];
desc.pixelFormat = MTLPixelFormatRGBA32Uint;
desc.width = kTessTextureWidth;
desc.height = height;
desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
desc.textureType = MTLTextureType2D;
desc.mipmapLevelCount = 1;
desc.storageMode = MTLStorageModePrivate;
m_tessVertexTexture = [m_gpu newTextureWithDescriptor:desc];
}
void PLSRenderContextMetal::lockNextBufferRingIndex()
{
// Wait until the GPU finishes rendering flush "N + 1 - kBufferRingSize". This ensures it
// is safe for the CPU to begin modifying the next buffers in our rings.
m_bufferRingIdx = (m_bufferRingIdx + 1) % kBufferRingSize;
m_bufferRingLocks[m_bufferRingIdx].lock();
}
static id<MTLBuffer> mtl_buffer(const BufferRingImpl* bufferRing)
{
return static_cast<const BufferMetal*>(bufferRing)->submittedBuffer();
}
static id<MTLTexture> mtl_texture(const TexelBufferRing* texelBufferRing)
{
return static_cast<const TexelBufferMetal*>(texelBufferRing)->submittedTexture();
}
void PLSRenderContextMetal::onFlush(FlushType flushType,
LoadAction loadAction,
size_t gradSpanCount,
size_t gradSpansHeight,
size_t tessVertexSpanCount,
size_t tessDataHeight,
bool needsClipBuffer)
{
auto* renderTarget =
static_cast<const PLSRenderTargetMetal*>(frameDescriptor().renderTarget.get());
id<MTLCommandBuffer> commandBuffer = [m_queue commandBuffer];
// Render the complex color ramps to the gradient texture.
if (gradSpanCount > 0)
{
MTLRenderPassDescriptor* gradPass = [MTLRenderPassDescriptor renderPassDescriptor];
gradPass.renderTargetWidth = kGradTextureWidth;
gradPass.renderTargetHeight = gradTextureRowsForSimpleRamps() + gradSpansHeight;
// TODO: Uploading the "simple" gradient texels directly to this texture requires us to use
// MTLLoadActionLoad here, in addition to triple buffering the entire gradient texture. We
// should consider different approaches:
//
// * Upload the simple texels from the CPU to a separate buffer, and then transfer.
// * Just render the simple ramps too.
// * Upload the simple texels to the bottom of the texture, and then use
// "gradPass.renderTargetHeight" in combination with MTLLoadActionDontCare. (Still
// requires triple buffering of the gradient texture.)
//
gradPass.colorAttachments[0].loadAction = MTLLoadActionLoad;
gradPass.colorAttachments[0].storeAction = MTLStoreActionStore;
gradPass.colorAttachments[0].texture = mtl_texture(gradTexelBufferRing());
id<MTLRenderCommandEncoder> gradEncoder =
[commandBuffer renderCommandEncoderWithDescriptor:gradPass];
[gradEncoder setViewport:(MTLViewport){0.f,
static_cast<double>(gradTextureRowsForSimpleRamps()),
kGradTextureWidth,
static_cast<float>(gradSpansHeight),
0.0,
1.0}];
[gradEncoder setRenderPipelineState:m_colorRampPipeline->pipelineState()];
[gradEncoder setVertexBuffer:mtl_buffer(uniformBufferRing()) offset:0 atIndex:0];
[gradEncoder setVertexBuffer:mtl_buffer(gradSpanBufferRing()) offset:0 atIndex:1];
[gradEncoder setCullMode:MTLCullModeBack];
[gradEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip
vertexStart:0
vertexCount:4
instanceCount:gradSpanCount];
[gradEncoder endEncoding];
}
// Tessellate all curves into vertices in the tessellation texture.
if (tessVertexSpanCount > 0)
{
MTLRenderPassDescriptor* tessPass = [MTLRenderPassDescriptor renderPassDescriptor];
tessPass.renderTargetWidth = kTessTextureWidth;
tessPass.renderTargetHeight = tessDataHeight;
tessPass.colorAttachments[0].loadAction = MTLLoadActionDontCare;
tessPass.colorAttachments[0].storeAction = MTLStoreActionStore;
tessPass.colorAttachments[0].texture = m_tessVertexTexture;
id<MTLRenderCommandEncoder> tessEncoder =
[commandBuffer renderCommandEncoderWithDescriptor:tessPass];
[tessEncoder setViewport:(MTLViewport){0.f,
0.f,
kTessTextureWidth,
static_cast<float>(tessDataHeight),
0.0,
1.0}];
[tessEncoder setRenderPipelineState:m_tessPipeline->pipelineState()];
[tessEncoder setVertexBuffer:mtl_buffer(uniformBufferRing()) offset:0 atIndex:0];
[tessEncoder setVertexBuffer:mtl_buffer(tessSpanBufferRing()) offset:0 atIndex:1];
[tessEncoder setVertexTexture:mtl_texture(pathBufferRing()) atIndex:kPathTextureIdx];
[tessEncoder setVertexTexture:mtl_texture(contourBufferRing()) atIndex:kContourTextureIdx];
[tessEncoder setCullMode:MTLCullModeBack];
// Draw two instances per TessVertexSpan: one normal and one optional reflection.
[tessEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip
vertexStart:0
vertexCount:4
instanceCount:tessVertexSpanCount * 2];
[tessEncoder endEncoding];
}
// Set up the render pass that draws path patches and triangles.
MTLRenderPassDescriptor* pass = [MTLRenderPassDescriptor renderPassDescriptor];
pass.colorAttachments[0].texture = renderTarget->targetTexture();
if (loadAction == LoadAction::clear)
{
float cc[4];
UnpackColorToRGBA32F(frameDescriptor().clearColor, cc);
pass.colorAttachments[0].loadAction = MTLLoadActionClear;
pass.colorAttachments[0].clearColor = MTLClearColorMake(cc[0], cc[1], cc[2], cc[3]);
}
else
{
pass.colorAttachments[0].loadAction = MTLLoadActionLoad;
}
pass.colorAttachments[0].storeAction = MTLStoreActionStore;
pass.colorAttachments[1].texture = renderTarget->m_coverageMemorylessTexture;
pass.colorAttachments[1].loadAction = MTLLoadActionClear;
pass.colorAttachments[1].clearColor = MTLClearColorMake(0, 0, 0, 0);
pass.colorAttachments[1].storeAction = MTLStoreActionDontCare;
pass.colorAttachments[2].texture = renderTarget->m_originalDstColorMemorylessTexture;
pass.colorAttachments[2].loadAction = MTLLoadActionDontCare;
pass.colorAttachments[2].storeAction = MTLStoreActionDontCare;
pass.colorAttachments[3].texture = renderTarget->m_clipMemorylessTexture;
pass.colorAttachments[3].loadAction = MTLLoadActionClear;
pass.colorAttachments[3].clearColor = MTLClearColorMake(0, 0, 0, 0);
pass.colorAttachments[3].storeAction = MTLStoreActionDontCare;
id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:pass];
[encoder setViewport:(MTLViewport){0.f,
0.f,
static_cast<float>(renderTarget->width()),
static_cast<float>(renderTarget->height()),
0.0,
1.0}];
[encoder setVertexBuffer:mtl_buffer(uniformBufferRing()) offset:0 atIndex:0];
[encoder setVertexTexture:m_tessVertexTexture atIndex:kTessVertexTextureIdx];
[encoder setVertexTexture:mtl_texture(pathBufferRing()) atIndex:kPathTextureIdx];
[encoder setVertexTexture:mtl_texture(contourBufferRing()) atIndex:kContourTextureIdx];
[encoder setFragmentTexture:mtl_texture(gradTexelBufferRing()) atIndex:kGradTextureIdx];
[encoder setCullMode:MTLCullModeBack];
if (frameDescriptor().wireframe)
{
[encoder setTriangleFillMode:MTLTriangleFillModeLines];
}
// Execute the DrawList.
for (const Draw& draw : m_drawList)
{
if (draw.vertexOrInstanceCount == 0)
{
continue;
}
DrawType drawType = draw.drawType;
// Setup the pipeline for this specific drawType and shaderFeatures.
uint32_t pipelineKey =
ShaderUniqueKey(SourceType::wholeProgram, drawType, draw.shaderFeatures);
const DrawPipeline& drawPipeline =
m_drawPipelines.try_emplace(pipelineKey, this, drawType, draw.shaderFeatures)
.first->second;
[encoder setRenderPipelineState:drawPipeline.pipelineState(renderTarget->pixelFormat())];
switch (drawType)
{
case DrawType::midpointFanPatches:
case DrawType::outerCurvePatches:
{
// Draw PLS patches that connect the tessellation vertices.
[encoder setVertexBuffer:m_pathPatchVertexBuffer offset:0 atIndex:1];
[encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
indexCount:PatchIndexCount(drawType)
indexType:MTLIndexTypeUInt16
indexBuffer:m_pathPatchIndexBuffer
indexBufferOffset:PatchBaseIndex(drawType) * sizeof(uint16_t)
instanceCount:draw.vertexOrInstanceCount
baseVertex:0
baseInstance:draw.baseVertexOrInstance];
break;
}
case DrawType::interiorTriangulation:
// Draw generic triangles.
[encoder setVertexBuffer:mtl_buffer(triangleBufferRing()) offset:0 atIndex:1];
[encoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:draw.baseVertexOrInstance
vertexCount:draw.vertexOrInstanceCount];
break;
}
}
[encoder endEncoding];
// Schedule a callback that will unlock the buffers used by this flush, after the GPU has
// finished rendering with them. This unblocks the CPU from reusing them in a future flush.
std::mutex& thisFlushLock = m_bufferRingLocks[m_bufferRingIdx];
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer>) {
assert(!thisFlushLock.try_lock()); // The mutex should already be locked.
thisFlushLock.unlock();
}];
if (flushType == FlushType::intermediate)
{
// The frame isn't complete yet. The caller will begin preparing a new flush immediately
// after this method returns, so lock buffers for the next flush now.
lockNextBufferRingIndex();
}
[commandBuffer commit];
}
} // namespace rive::pls