From 14fb038a9c65afc03e4a7f99c8d906f52e6654ef Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 15 Aug 2023 08:35:56 +0200 Subject: [PATCH] Add per-thread shared Skia contextes on iOS (#1775) Offscreen and onscreen surfaces created on the same thread share the same Skia context. Code has been refactored to have somewhat of a symmetry with our OpenGL integration on Android. --- .../static/img/offscreen/multiple_circles.png | Bin 4273 -> 3864 bytes .../ios/RNSkia-iOS/RNSkMetalCanvasProvider.h | 19 +--- .../ios/RNSkia-iOS/RNSkMetalCanvasProvider.mm | 63 ++--------- .../ios/RNSkia-iOS/RNSkiOSPlatformContext.mm | 4 +- package/ios/RNSkia-iOS/SkiaMetalRenderer.h | 5 - package/ios/RNSkia-iOS/SkiaMetalRenderer.mm | 55 --------- .../ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h | 31 ++++++ .../ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm | 105 ++++++++++++++++++ .../renderer/__tests__/e2e/Offscreen.spec.tsx | 35 +++--- 9 files changed, 167 insertions(+), 150 deletions(-) delete mode 100644 package/ios/RNSkia-iOS/SkiaMetalRenderer.h delete mode 100644 package/ios/RNSkia-iOS/SkiaMetalRenderer.mm create mode 100644 package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h create mode 100644 package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm diff --git a/docs/static/img/offscreen/multiple_circles.png b/docs/static/img/offscreen/multiple_circles.png index 7b6175be35aed51f9da03d836e064cbefb8de35e..e47bb7c5630f2a2d06dc9007b1b642845bd233a3 100644 GIT binary patch literal 3864 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 diff --git a/package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.h b/package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.h index df6ac457d3..193a2736e8 100644 --- a/package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.h +++ b/package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.h @@ -6,13 +6,12 @@ #import #import -using MetalRenderContext = struct { - id commandQueue; - sk_sp skContext; -}; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#import -static std::unordered_map> - renderContexts; +#pragma clang diagnostic pop class RNSkMetalCanvasProvider : public RNSkia::RNSkCanvasProvider { public: @@ -27,17 +26,9 @@ class RNSkMetalCanvasProvider : public RNSkia::RNSkCanvasProvider { bool renderToCanvas(const std::function &cb) override; void setSize(int width, int height); - CALayer *getLayer(); private: - /** - * To be able to use static contexts (and avoid reloading the skia context for - * each new view, we track the Skia drawing context per thread. - * @return The drawing context for the current thread - */ - static std::shared_ptr getMetalRenderContext(); - std::shared_ptr _context; float _width = -1; float _height = -1; diff --git a/package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.mm b/package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.mm index 4cce2265b7..6c51a8dbce 100644 --- a/package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.mm +++ b/package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.mm @@ -1,5 +1,6 @@ #import #import +#import #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" @@ -14,19 +15,6 @@ #pragma clang diagnostic pop -/** Static members */ -std::shared_ptr -RNSkMetalCanvasProvider::getMetalRenderContext() { - auto threadId = std::this_thread::get_id(); - if (renderContexts.count(threadId) == 0) { - auto drawingContext = std::make_shared(); - drawingContext->commandQueue = nullptr; - drawingContext->skContext = nullptr; - renderContexts.emplace(threadId, drawingContext); - } - return renderContexts.at(threadId); -} - RNSkMetalCanvasProvider::RNSkMetalCanvasProvider( std::function requestRedraw, std::shared_ptr context) @@ -35,11 +23,8 @@ #pragma clang diagnostic ignored "-Wunguarded-availability-new" _layer = [CAMetalLayer layer]; #pragma clang diagnostic pop - - auto device = MTLCreateSystemDefaultDevice(); - _layer.framebufferOnly = NO; - _layer.device = device; + _layer.device = MTLCreateSystemDefaultDevice(); _layer.opaque = false; _layer.contentsScale = _context->getPixelDensity(); _layer.pixelFormat = MTLPixelFormatBGRA8Unorm; @@ -87,52 +72,18 @@ return false; } } - - // Get render context for current thread - auto renderContext = getMetalRenderContext(); - - if (renderContext->skContext == nullptr) { - auto device = MTLCreateSystemDefaultDevice(); - renderContext->commandQueue = - id(CFRetain((GrMTLHandle)[device newCommandQueue])); - renderContext->skContext = GrDirectContext::MakeMetal( - (__bridge void *)device, (__bridge void *)renderContext->commandQueue); - } - // Wrap in auto release pool since we want the system to clean up after // rendering and not wait until later - we've seen some example of memory // usage growing very fast in the simulator without this. @autoreleasepool { - - /* It is super important that we use the pattern of calling nextDrawable - inside this autoreleasepool and not depend on Skia's - SkSurface::MakeFromCAMetalLayer to encapsulate since we're seeing a lot of - drawables leaking if they're not done this way. - - This is now reverted from: - (https://github.com/Shopify/react-native-skia/commit/2e2290f8e6dfc6921f97b79f779d920fbc1acceb) - back to the original implementation. - */ id currentDrawable = [_layer nextDrawable]; if (currentDrawable == nullptr) { return false; } - GrMtlTextureInfo fbInfo; - fbInfo.fTexture.retain((__bridge void *)currentDrawable.texture); - - GrBackendRenderTarget backendRT(_layer.drawableSize.width, - _layer.drawableSize.height, 1, fbInfo); - - auto skSurface = SkSurfaces::WrapBackendRenderTarget( - renderContext->skContext.get(), backendRT, kTopLeft_GrSurfaceOrigin, - kBGRA_8888_SkColorType, nullptr, nullptr); - - if (skSurface == nullptr || skSurface->getCanvas() == nullptr) { - RNSkia::RNSkLogger::logToConsole( - "Skia surface could not be created from parameters."); - return false; - } + auto skSurface = SkiaMetalSurfaceFactory::makeWindowedSurface( + currentDrawable.texture, _layer.drawableSize.width, + _layer.drawableSize.height); SkCanvas *canvas = skSurface->getCanvas(); cb(canvas); @@ -140,11 +91,11 @@ GrBackendRenderTarget backendRT(_layer.drawableSize.width, skSurface->flushAndSubmit(); id commandBuffer( - [renderContext->commandQueue commandBuffer]); + [ThreadContextHolder::ThreadSkiaMetalContext + .commandQueue commandBuffer]); [commandBuffer presentDrawable:currentDrawable]; [commandBuffer commit]; } - return true; }; diff --git a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm index fc0df4571d..164af865a4 100644 --- a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm +++ b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm @@ -4,7 +4,7 @@ #include #include -#include +#include #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" @@ -59,7 +59,7 @@ sk_sp RNSkiOSPlatformContext::makeOffscreenSurface(int width, int height) { - return MakeOffscreenMetalSurface(width, height); + return SkiaMetalSurfaceFactory::makeOffscreenSurface(width, height); } void RNSkiOSPlatformContext::runOnMainThread(std::function func) { diff --git a/package/ios/RNSkia-iOS/SkiaMetalRenderer.h b/package/ios/RNSkia-iOS/SkiaMetalRenderer.h deleted file mode 100644 index 350c585aff..0000000000 --- a/package/ios/RNSkia-iOS/SkiaMetalRenderer.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "SkSurface.h" - -sk_sp MakeOffscreenMetalSurface(int width, int height); \ No newline at end of file diff --git a/package/ios/RNSkia-iOS/SkiaMetalRenderer.mm b/package/ios/RNSkia-iOS/SkiaMetalRenderer.mm deleted file mode 100644 index db32ab2ded..0000000000 --- a/package/ios/RNSkia-iOS/SkiaMetalRenderer.mm +++ /dev/null @@ -1,55 +0,0 @@ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdocumentation" - -#import "SkCanvas.h" -#import "SkColorSpace.h" -#import "SkSurface.h" - -#import -#import -#import - -#pragma clang diagnostic pop - -#import - -struct OffscreenRenderContext { - id device; - id commandQueue; - sk_sp skiaContext; - id texture; - - OffscreenRenderContext(int width, int height) { - device = MTLCreateSystemDefaultDevice(); - commandQueue = - id(CFRetain((GrMTLHandle)[device newCommandQueue])); - skiaContext = GrDirectContext::MakeMetal((__bridge void *)device, - (__bridge void *)commandQueue); - // Create a Metal texture descriptor - MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor - texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm - width:width - height:height - mipmapped:NO]; - textureDescriptor.usage = - MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; - texture = [device newTextureWithDescriptor:textureDescriptor]; - } -}; - -sk_sp MakeOffscreenMetalSurface(int width, int height) { - auto ctx = new OffscreenRenderContext(width, height); - - // Create a GrBackendTexture from the Metal texture - GrMtlTextureInfo info; - info.fTexture.retain((__bridge void *)ctx->texture); - GrBackendTexture backendTexture(width, height, GrMipMapped::kNo, info); - - // Create a SkSurface from the GrBackendTexture - auto surface = SkSurfaces::WrapBackendTexture( - ctx->skiaContext.get(), backendTexture, kTopLeft_GrSurfaceOrigin, 0, - kBGRA_8888_SkColorType, nullptr, nullptr, - [](void *addr) { delete (OffscreenRenderContext *)addr; }, ctx); - - return surface; -} diff --git a/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h b/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h new file mode 100644 index 0000000000..32fe1e814f --- /dev/null +++ b/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h @@ -0,0 +1,31 @@ +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#import "SkCanvas.h" +#import + +#pragma clang diagnostic pop + +using SkiaMetalContext = struct SkiaMetalContext { + id commandQueue = nullptr; + sk_sp skContext = nullptr; +}; + +class ThreadContextHolder { +public: + static thread_local SkiaMetalContext ThreadSkiaMetalContext; +}; + +class SkiaMetalSurfaceFactory { +public: + static sk_sp makeWindowedSurface(id texture, int width, + int height); + static sk_sp makeOffscreenSurface(int width, int height); + +private: + static id device; + static bool + createSkiaDirectContextIfNecessary(SkiaMetalContext *threadContext); +}; diff --git a/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm b/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm new file mode 100644 index 0000000000..19eb9ec728 --- /dev/null +++ b/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm @@ -0,0 +1,105 @@ +#import + +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#import "SkCanvas.h" +#import "SkColorSpace.h" +#import "SkSurface.h" + +#import +#import +#import + +#pragma clang diagnostic pop + +thread_local SkiaMetalContext ThreadContextHolder::ThreadSkiaMetalContext; + +struct OffscreenRenderContext { + id texture; + + OffscreenRenderContext(id device, + sk_sp skiaContext, + id commandQueue, int width, + int height) { + // Create a Metal texture descriptor + MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:width + height:height + mipmapped:NO]; + textureDescriptor.usage = + MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + texture = [device newTextureWithDescriptor:textureDescriptor]; + } +}; + +id SkiaMetalSurfaceFactory::device = MTLCreateSystemDefaultDevice(); + +bool SkiaMetalSurfaceFactory::createSkiaDirectContextIfNecessary( + SkiaMetalContext *skiaMetalContext) { + if (skiaMetalContext->skContext == nullptr) { + skiaMetalContext->commandQueue = + id(CFRetain((GrMTLHandle)[device newCommandQueue])); + skiaMetalContext->skContext = GrDirectContext::MakeMetal( + (__bridge void *)device, + (__bridge void *)skiaMetalContext->commandQueue); + if (skiaMetalContext->skContext == nullptr) { + RNSkia::RNSkLogger::logToConsole("Couldn't create a Skia Metal Context"); + return false; + } + } + return true; +} + +sk_sp +SkiaMetalSurfaceFactory::makeWindowedSurface(id texture, int width, + int height) { + // Get render context for current thread + if (!SkiaMetalSurfaceFactory::createSkiaDirectContextIfNecessary( + &ThreadContextHolder::ThreadSkiaMetalContext)) { + return nullptr; + } + GrMtlTextureInfo fbInfo; + fbInfo.fTexture.retain((__bridge void *)texture); + + GrBackendRenderTarget backendRT(width, height, 1, fbInfo); + + auto skSurface = SkSurfaces::WrapBackendRenderTarget( + ThreadContextHolder::ThreadSkiaMetalContext.skContext.get(), backendRT, + kTopLeft_GrSurfaceOrigin, kBGRA_8888_SkColorType, nullptr, nullptr); + + if (skSurface == nullptr || skSurface->getCanvas() == nullptr) { + RNSkia::RNSkLogger::logToConsole( + "Skia surface could not be created from parameters."); + return nullptr; + } + return skSurface; +} + +sk_sp SkiaMetalSurfaceFactory::makeOffscreenSurface(int width, + int height) { + if (!SkiaMetalSurfaceFactory::createSkiaDirectContextIfNecessary( + &ThreadContextHolder::ThreadSkiaMetalContext)) { + return nullptr; + } + auto ctx = new OffscreenRenderContext( + device, ThreadContextHolder::ThreadSkiaMetalContext.skContext, + ThreadContextHolder::ThreadSkiaMetalContext.commandQueue, width, height); + + // Create a GrBackendTexture from the Metal texture + GrMtlTextureInfo info; + info.fTexture.retain((__bridge void *)ctx->texture); + GrBackendTexture backendTexture(width, height, GrMipMapped::kNo, info); + + // Create a SkSurface from the GrBackendTexture + auto surface = SkSurfaces::WrapBackendTexture( + ThreadContextHolder::ThreadSkiaMetalContext.skContext.get(), + backendTexture, kTopLeft_GrSurfaceOrigin, 0, kBGRA_8888_SkColorType, + nullptr, nullptr, + [](void *addr) { delete (OffscreenRenderContext *)addr; }, ctx); + + return surface; +} diff --git a/package/src/renderer/__tests__/e2e/Offscreen.spec.tsx b/package/src/renderer/__tests__/e2e/Offscreen.spec.tsx index 68a23de891..dc14197f83 100644 --- a/package/src/renderer/__tests__/e2e/Offscreen.spec.tsx +++ b/package/src/renderer/__tests__/e2e/Offscreen.spec.tsx @@ -44,7 +44,7 @@ describe("Offscreen Drawings", () => { paint.setColor(Skia.Color("lightblue")); canvas.drawCircle(r, r, r, paint); backSurface.flush(); - const image = backSurface.makeImageSnapshot().makeNonTextureImage(); + const image = backSurface.makeImageSnapshot(); frontSurface.getCanvas().drawImage(image, 0, 0); return frontSurface.makeImageSnapshot().encodeToBase64(); }, @@ -56,18 +56,8 @@ describe("Offscreen Drawings", () => { expect(data).toBeDefined(); checkImage(image, docPath("offscreen/circle.png")); }); - it("Should use the React API to build an image", async () => { - const { width, height } = surface; - const { drawAsImage } = importSkia(); - const r = width / 2; - const image = drawAsImage( - , - width, - height - ); - checkImage(image, docPath("offscreen/circle.png")); - }); - it("Should render to multiple offscreen surfaces at once", async () => { + + it("Should render to multiple offscreen surfaces at once (1)", async () => { const { width, height } = surface; const raw = await surface.eval( (Skia, ctx) => { @@ -89,13 +79,11 @@ describe("Offscreen Drawings", () => { const canvas2 = backSurface2.getCanvas(); const paint2 = Skia.Paint(); paint2.setColor(Skia.Color("magenta")); - canvas2.drawCircle(r, r, r, paint2); + canvas2.drawCircle(r, r, r / 2, paint2); backSurface2.flush(); - // TODO: When we've implemented sharing on iOS we can remove makeNonTextureImage here: - const image1 = backSurface1.makeImageSnapshot().makeNonTextureImage(); - const image2 = backSurface2.makeImageSnapshot().makeNonTextureImage(); - + const image1 = backSurface1.makeImageSnapshot(); + const image2 = backSurface2.makeImageSnapshot(); frontSurface.getCanvas().drawImage(image1, 0, 0); frontSurface.getCanvas().drawImage(image2, ctx.width / 2, 0); return frontSurface.makeImageSnapshot().encodeToBase64(); @@ -108,4 +96,15 @@ describe("Offscreen Drawings", () => { expect(data).toBeDefined(); checkImage(image, docPath("offscreen/multiple_circles.png")); }); + it("Should use the React API to build an image", async () => { + const { width, height } = surface; + const { drawAsImage } = importSkia(); + const r = width / 2; + const image = drawAsImage( + , + width, + height + ); + checkImage(image, docPath("offscreen/circle.png")); + }); });