blob: 7f4e0c0d9e6bcb2c858dc090760dc6e2d17b8f9b [file]
/*
* Copyright 2026 Rive
*/
#include "testing_window.hpp"
#if defined(RIVE_IOS) || defined(RIVE_IOS_SIMULATOR)
#include "common/rive_ios_app.hpp"
#include "rive/renderer/metal/render_context_metal_impl.h"
#include "rive/renderer/rive_renderer.hpp"
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
namespace rive::gpu
{
// Metal requires an aligned row stride for texture-to-buffer blits; 256
// satisfies every format's minimumLinearTextureAlignment on Apple GPUs.
static constexpr size_t kBlitRowAlignment = 256;
class TestingWindowMetalLayer : public TestingWindow
{
public:
TestingWindowMetalLayer(const BackendParams& backendParams,
void* platformWindow) :
m_layer((__bridge CAMetalLayer*)platformWindow)
{
RenderContextMetalImpl::ContextOptions metalOptions;
metalOptions.shaderCompilationMode =
backendParams.shaderCompilationMode;
metalOptions.disableFramebufferReads = backendParams.atomic;
m_renderContext =
RenderContextMetalImpl::MakeContext(m_gpu, metalOptions);
m_renderContext->static_impl_cast<RenderContextMetalImpl>()
->setCommandQueue(m_queue);
m_layer.device = m_gpu;
m_layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
m_layer.framebufferOnly = NO;
syncSizeFromLayer();
printf("==== MTLDevice: %s (%ux%u) ====\n",
m_gpu.name.UTF8String,
m_width,
m_height);
}
rive::Factory* factory() override { return m_renderContext.get(); }
rive::gpu::RenderContext* renderContext() const override
{
return m_renderContext.get();
}
void* getOreContext() const override
{
return m_renderContext->getOreContext();
}
void beginOreFrame() override
{
auto oreContext =
static_cast<rive::ore::Context*>(m_renderContext->getOreContext());
oreContext->beginFrame({});
}
void endOreFrame() override
{
auto oreContext =
static_cast<rive::ore::Context*>(m_renderContext->getOreContext());
oreContext->endFrame();
}
void resize(int width, int height) override
{
TestingWindow::resize(width, height);
m_layer.drawableSize = CGSizeMake(width, height);
}
bool consumeInputEvent(InputEventData& eventData) override
{
return rive_ios_app_poll_input_event(eventData);
}
bool shouldQuit() const override { return rive_ios_app_should_quit(); }
std::unique_ptr<rive::Renderer> beginFrame(
const FrameOptions& options) override
{
rive_ios_app_wait_while_inactive();
syncSizeFromLayer();
m_drawable = [m_layer nextDrawable];
rive::gpu::RenderContext::FrameDescriptor frameDescriptor = {
.renderTargetWidth = m_width,
.renderTargetHeight = m_height,
.loadAction = options.doClear
? rive::gpu::LoadAction::clear
: rive::gpu::LoadAction::preserveRenderTarget,
.clearColor = options.clearColor,
.disableRasterOrdering = options.disableRasterOrdering,
.triangulationThresholds = options.triangulationThresholds,
.wireframe = options.wireframe,
.fillsDisabled = options.fillsDisabled,
.strokesDisabled = options.strokesDisabled,
.clockwiseFillOverride = options.clockwiseFillOverride,
.synthesizedFailureType = options.synthesizedFailureType,
};
m_renderContext->beginFrame(frameDescriptor);
m_flushCommandBuffer = [m_queue commandBuffer];
return std::make_unique<RiveRenderer>(m_renderContext.get());
}
void flushPLSContext(RenderTarget* offscreenRenderTarget) final
{
if (!m_renderTarget || m_renderTarget->width() != m_width ||
m_renderTarget->height() != m_height)
{
m_renderTarget =
m_renderContext->static_impl_cast<RenderContextMetalImpl>()
->makeRenderTarget(
MTLPixelFormatBGRA8Unorm, m_width, m_height);
}
m_renderTarget->setTargetTexture(m_drawable != nil ? m_drawable.texture
: nil);
m_renderContext->flush({
.renderTarget = offscreenRenderTarget != nullptr
? offscreenRenderTarget
: m_renderTarget.get(),
.externalCommandBuffer = (__bridge void*)m_flushCommandBuffer,
});
[m_flushCommandBuffer commit];
m_flushCommandBuffer = [m_queue commandBuffer];
}
void endFrame(std::vector<uint8_t>* pixelData) override
{
flushPLSContext(nullptr);
const bool readPixels = pixelData != nullptr && m_drawable != nil;
const size_t tightRowBytes = m_width * 4;
const size_t alignedRowBytes =
(tightRowBytes + kBlitRowAlignment - 1) & ~(kBlitRowAlignment - 1);
const size_t readBuffLength = alignedRowBytes * m_height;
if (readPixels)
{
if (!m_pixelReadBuff || m_pixelReadBuff.length != readBuffLength)
{
m_pixelReadBuff =
[m_gpu newBufferWithLength:readBuffLength
options:MTLResourceStorageModeShared];
}
id<MTLBlitCommandEncoder> blitEncoder =
[m_flushCommandBuffer blitCommandEncoder];
[blitEncoder copyFromTexture:m_drawable.texture
sourceSlice:0
sourceLevel:0
sourceOrigin:MTLOriginMake(0, 0, 0)
sourceSize:MTLSizeMake(m_width, m_height, 1)
toBuffer:m_pixelReadBuff
destinationOffset:0
destinationBytesPerRow:alignedRowBytes
destinationBytesPerImage:readBuffLength];
[blitEncoder endEncoding];
}
if (m_drawable != nil)
{
[m_flushCommandBuffer presentDrawable:m_drawable];
}
[m_flushCommandBuffer commit];
if (readPixels)
{
[m_flushCommandBuffer waitUntilCompleted];
pixelData->resize(tightRowBytes * m_height);
const uint8_t* contents =
reinterpret_cast<const uint8_t*>(m_pixelReadBuff.contents);
for (size_t y = 0; y < m_height; ++y)
{
const uint8_t* src =
&contents[(m_height - y - 1) * alignedRowBytes];
uint8_t* dst = &(*pixelData)[y * tightRowBytes];
for (size_t x = 0; x < tightRowBytes; x += 4)
{
dst[x + 0] = src[x + 2];
dst[x + 1] = src[x + 1];
dst[x + 2] = src[x + 0];
dst[x + 3] = src[x + 3];
}
}
}
m_drawable = nil;
m_flushCommandBuffer = nil;
}
void* metalQueue() const override { return (__bridge void*)m_queue; }
void* getCurrentCommandBuffer() const override
{
return (__bridge void*)m_queue;
}
private:
void syncSizeFromLayer()
{
CGSize size = m_layer.drawableSize;
m_width = static_cast<uint32_t>(size.width);
m_height = static_cast<uint32_t>(size.height);
}
CAMetalLayer* m_layer;
id<MTLDevice> m_gpu = MTLCreateSystemDefaultDevice();
id<MTLCommandQueue> m_queue = [m_gpu newCommandQueue];
std::unique_ptr<RenderContext> m_renderContext;
rcp<RenderTargetMetal> m_renderTarget;
id<CAMetalDrawable> m_drawable = nil;
id<MTLBuffer> m_pixelReadBuff;
id<MTLCommandBuffer> m_flushCommandBuffer;
};
}; // namespace rive::gpu
TestingWindow* TestingWindow::MakeMetalLayer(const BackendParams& backendParams,
void* platformWindow)
{
if (platformWindow == nullptr)
{
fprintf(stderr, "MakeMetalLayer requires a CAMetalLayer.\n");
return nullptr;
}
return new rive::gpu::TestingWindowMetalLayer(backendParams,
platformWindow);
}
#endif