Skip to content

Commit

Permalink
Add per-thread shared Skia contextes on iOS (#1775)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
wcandillon authored Aug 15, 2023
1 parent c6b9fd7 commit 14fb038
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 150 deletions.
Binary file modified docs/static/img/offscreen/multiple_circles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 5 additions & 14 deletions package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
#import <MetalKit/MetalKit.h>
#import <QuartzCore/CAMetalLayer.h>

using MetalRenderContext = struct {
id<MTLCommandQueue> commandQueue;
sk_sp<GrDirectContext> skContext;
};
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"

#import <include/gpu/GrDirectContext.h>

static std::unordered_map<std::thread::id, std::shared_ptr<MetalRenderContext>>
renderContexts;
#pragma clang diagnostic pop

class RNSkMetalCanvasProvider : public RNSkia::RNSkCanvasProvider {
public:
Expand All @@ -27,17 +26,9 @@ class RNSkMetalCanvasProvider : public RNSkia::RNSkCanvasProvider {
bool renderToCanvas(const std::function<void(SkCanvas *)> &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<MetalRenderContext> getMetalRenderContext();

std::shared_ptr<RNSkia::RNSkPlatformContext> _context;
float _width = -1;
float _height = -1;
Expand Down
63 changes: 7 additions & 56 deletions package/ios/RNSkia-iOS/RNSkMetalCanvasProvider.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import <RNSkLog.h>
#import <RNSkMetalCanvasProvider.h>
#import <SkiaMetalSurfaceFactory.h>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"
Expand All @@ -14,19 +15,6 @@

#pragma clang diagnostic pop

/** Static members */
std::shared_ptr<MetalRenderContext>
RNSkMetalCanvasProvider::getMetalRenderContext() {
auto threadId = std::this_thread::get_id();
if (renderContexts.count(threadId) == 0) {
auto drawingContext = std::make_shared<MetalRenderContext>();
drawingContext->commandQueue = nullptr;
drawingContext->skContext = nullptr;
renderContexts.emplace(threadId, drawingContext);
}
return renderContexts.at(threadId);
}

RNSkMetalCanvasProvider::RNSkMetalCanvasProvider(
std::function<void()> requestRedraw,
std::shared_ptr<RNSkia::RNSkPlatformContext> context)
Expand All @@ -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;
Expand Down Expand Up @@ -87,64 +72,30 @@
return false;
}
}

// Get render context for current thread
auto renderContext = getMetalRenderContext();

if (renderContext->skContext == nullptr) {
auto device = MTLCreateSystemDefaultDevice();
renderContext->commandQueue =
id<MTLCommandQueue>(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<CAMetalDrawable> 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);

skSurface->flushAndSubmit();

id<MTLCommandBuffer> commandBuffer(
[renderContext->commandQueue commandBuffer]);
[ThreadContextHolder::ThreadSkiaMetalContext
.commandQueue commandBuffer]);
[commandBuffer presentDrawable:currentDrawable];
[commandBuffer commit];
}

return true;
};

Expand Down
4 changes: 2 additions & 2 deletions package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include <thread>
#include <utility>

#include <SkiaMetalRenderer.h>
#include <SkiaMetalSurfaceFactory.h>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"
Expand Down Expand Up @@ -59,7 +59,7 @@

sk_sp<SkSurface> RNSkiOSPlatformContext::makeOffscreenSurface(int width,
int height) {
return MakeOffscreenMetalSurface(width, height);
return SkiaMetalSurfaceFactory::makeOffscreenSurface(width, height);
}

void RNSkiOSPlatformContext::runOnMainThread(std::function<void()> func) {
Expand Down
5 changes: 0 additions & 5 deletions package/ios/RNSkia-iOS/SkiaMetalRenderer.h

This file was deleted.

55 changes: 0 additions & 55 deletions package/ios/RNSkia-iOS/SkiaMetalRenderer.mm

This file was deleted.

31 changes: 31 additions & 0 deletions package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#import <MetalKit/MetalKit.h>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"

#import "SkCanvas.h"
#import <include/gpu/GrDirectContext.h>

#pragma clang diagnostic pop

using SkiaMetalContext = struct SkiaMetalContext {
id<MTLCommandQueue> commandQueue = nullptr;
sk_sp<GrDirectContext> skContext = nullptr;
};

class ThreadContextHolder {
public:
static thread_local SkiaMetalContext ThreadSkiaMetalContext;
};

class SkiaMetalSurfaceFactory {
public:
static sk_sp<SkSurface> makeWindowedSurface(id<MTLTexture> texture, int width,
int height);
static sk_sp<SkSurface> makeOffscreenSurface(int width, int height);

private:
static id<MTLDevice> device;
static bool
createSkiaDirectContextIfNecessary(SkiaMetalContext *threadContext);
};
105 changes: 105 additions & 0 deletions package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#import <RNSkLog.h>

#include <SkiaMetalSurfaceFactory.h>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"

#import "SkCanvas.h"
#import "SkColorSpace.h"
#import "SkSurface.h"

#import <include/gpu/GrBackendSurface.h>
#import <include/gpu/GrDirectContext.h>
#import <include/gpu/ganesh/SkSurfaceGanesh.h>

#pragma clang diagnostic pop

thread_local SkiaMetalContext ThreadContextHolder::ThreadSkiaMetalContext;

struct OffscreenRenderContext {
id<MTLTexture> texture;

OffscreenRenderContext(id<MTLDevice> device,
sk_sp<GrDirectContext> skiaContext,
id<MTLCommandQueue> 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<MTLDevice> SkiaMetalSurfaceFactory::device = MTLCreateSystemDefaultDevice();

bool SkiaMetalSurfaceFactory::createSkiaDirectContextIfNecessary(
SkiaMetalContext *skiaMetalContext) {
if (skiaMetalContext->skContext == nullptr) {
skiaMetalContext->commandQueue =
id<MTLCommandQueue>(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<SkSurface>
SkiaMetalSurfaceFactory::makeWindowedSurface(id<MTLTexture> 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<SkSurface> 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;
}
Loading

0 comments on commit 14fb038

Please sign in to comment.