| /* | |
| * 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); | |
| 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); | |
| return std::make_unique<RiveRenderer>(m_renderContext.get()); | |
| } | |
| rive::gpu::RenderTarget* renderTarget() const override | |
| { | |
| return m_renderTarget.get(); | |
| } | |
| OreTarget oreTarget() const override { return {true, true}; } | |
| void flushPLSContext(RenderTarget* offscreenRenderTarget) final | |
| { | |
| // A deferred replay flushes canvases before beginFrame, so the buffer | |
| // may not exist yet. | |
| if (m_flushCommandBuffer == nil) | |
| { | |
| m_flushCommandBuffer = [m_queue commandBuffer]; | |
| } | |
| 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 |