Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/skia/android/cpp/jni/include/JniSkiaManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
#include "RNSkLog.h"
#include "RNSkManager.h"

#if defined(SK_GRAPHITE)
#include "RNDawnContext.h"
#else
#include "OpenGLContext.h"
#endif

namespace RNSkia {

class RNSkManager;
Expand Down Expand Up @@ -55,6 +61,15 @@ class JniSkiaManager : public jni::HybridClass<JniSkiaManager> {
void invalidate() {
_skManager = nullptr;
_context = nullptr;
// Invalidate the OpenGL/Dawn context to ensure fresh GPU resources
// are created after a bridge reload (e.g., Expo OTA updates).
// Without this, the thread-local singleton would retain stale
// GPU context references causing crashes.
#if defined(SK_GRAPHITE)
DawnContext::getInstance().invalidate();
#else
OpenGLContext::getInstance().invalidate();
#endif
}

private:
Expand Down
37 changes: 34 additions & 3 deletions packages/skia/android/cpp/rnskia-android/OpenGLContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,28 @@ class OpenGLContext {
return instance;
}

// Invalidate the context - must be called before RN bridge reload
// to ensure fresh GPU resources are created after reload.
// This is critical for OTA updates (e.g., Expo Updates) where the
// main thread survives but the bridge is recreated.
void invalidate() {
_directContext = nullptr;
_glSurface = nullptr;
_glContext = nullptr;
}

// Check if the context is valid and ready to use
bool isValid() const { return _directContext != nullptr; }

// Ensure the context is initialized (creates new resources if invalidated)
void ensureValid() {
if (!isValid()) {
initialize();
}
}

sk_sp<SkSurface> MakeOffscreen(int width, int height) {
ensureValid();
auto colorType = kRGBA_8888_SkColorType;

SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
Expand Down Expand Up @@ -103,6 +124,7 @@ class OpenGLContext {

sk_sp<SkImage> MakeImageFromBuffer(void *buffer,
bool requireKnownFormat = false) {
ensureValid();
#if __ANDROID_API__ >= 26
const AHardwareBuffer *hardwareBuffer =
static_cast<AHardwareBuffer *>(buffer);
Expand Down Expand Up @@ -168,21 +190,30 @@ class OpenGLContext {

// TODO: remove width, height
std::unique_ptr<WindowContext> MakeWindow(ANativeWindow *window) {
ensureValid();
auto display = OpenGLSharedContext::getInstance().getDisplay();
return std::make_unique<OpenGLWindowContext>(
_directContext.get(), display, _glContext.get(), window,
OpenGLSharedContext::getInstance().getConfig());
}

GrDirectContext *getDirectContext() { return _directContext.get(); }
void makeCurrent() { _glContext->makeCurrent(_glSurface.get()); }
GrDirectContext *getDirectContext() {
ensureValid();
return _directContext.get();
}
void makeCurrent() {
ensureValid();
_glContext->makeCurrent(_glSurface.get());
}

private:
std::unique_ptr<gl::Context> _glContext;
std::unique_ptr<gl::Surface> _glSurface;
sk_sp<GrDirectContext> _directContext;

OpenGLContext() {
OpenGLContext() { initialize(); }

void initialize() {
auto display = OpenGLSharedContext::getInstance().getDisplay();
auto sharedContext = OpenGLSharedContext::getInstance().getContext();
auto glConfig = OpenGLSharedContext::getInstance().getConfig();
Expand Down
36 changes: 33 additions & 3 deletions packages/skia/apple/MetalContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,31 @@ class MetalContext {
return instance;
}

// Invalidate the context - must be called before RN bridge reload
// to ensure fresh GPU resources are created after reload.
// This is critical for OTA updates (e.g., Expo Updates) where the
// main thread survives but the bridge is recreated.
void invalidate() {
_directContext = nullptr;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_directContext is a shared pointer. It could be a problem if another owner retains a reference here. On closer inspection this doesn't seem to be shared, the pointer only escapes this file via get(). So that's not an issue right now.

However, the issue says it's important to clear out all the GPU resources here. If anyone actually shares ownership, it could cause a regression. It might be worthwhile to make this a unique ptr instead.

if (_commandQueue) {
CFRelease((GrMTLHandle)_commandQueue);
_commandQueue = nullptr;
}
_device = nullptr;
}

// Check if the context is valid and ready to use
bool isValid() const { return _directContext != nullptr && _device != nullptr; }

// Ensure the context is initialized (creates new resources if invalidated)
void ensureValid() {
if (!isValid()) {
initialize();
}
}

sk_sp<SkSurface> MakeOffscreen(int width, int height) {
ensureValid();
auto device = _device;
auto ctx = new OffscreenRenderContext(device, _directContext, _commandQueue,
width, height);
Expand All @@ -65,7 +89,7 @@ class MetalContext {
}

sk_sp<SkImage> MakeImageFromBuffer(void *buffer) {

ensureValid();
CVPixelBufferRef sampleBuffer = (CVPixelBufferRef)buffer;
SkiaCVPixelBufferUtils::CVPixelBufferBaseFormat format =
SkiaCVPixelBufferUtils::getCVPixelBufferBaseFormat(sampleBuffer);
Expand All @@ -92,18 +116,24 @@ class MetalContext {
std::unique_ptr<RNSkia::WindowContext>
MakeWindow(CALayer *window, int width, int height,
bool useP3ColorSpace = true) {
ensureValid();
auto device = _device;
return std::make_unique<MetalWindowContext>(_directContext.get(), device,
_commandQueue, window, width,
height, useP3ColorSpace);
}

GrDirectContext *getDirectContext() { return _directContext.get(); }
GrDirectContext *getDirectContext() {
ensureValid();
return _directContext.get();
}

private:
id<MTLDevice> _device = nullptr;
id<MTLCommandQueue> _commandQueue = nullptr;
sk_sp<GrDirectContext> _directContext = nullptr;

MetalContext();
MetalContext() { initialize(); }

void initialize();
};
2 changes: 1 addition & 1 deletion packages/skia/apple/MetalContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

#pragma clang diagnostic pop

MetalContext::MetalContext() {
void MetalContext::initialize() {
_device = MTLCreateSystemDefaultDevice();
if (!_device) {
throw std::runtime_error("Failed to create Metal device");
Expand Down
15 changes: 15 additions & 0 deletions packages/skia/apple/SkiaManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

#import "RNSkApplePlatformContext.h"

#if defined(SK_GRAPHITE)
#import "RNDawnContext.h"
#else
#import "MetalContext.h"
#endif

static __weak SkiaManager *sharedInstance = nil;

@implementation SkiaManager {
Expand All @@ -21,6 +27,15 @@ @implementation SkiaManager {

- (void)invalidate {
_skManager = nullptr;
// Invalidate the Metal/Dawn context to ensure fresh GPU resources
// are created after a bridge reload (e.g., Expo OTA updates).
// Without this, the thread-local singleton would retain stale
// GPU context references causing crashes.
#if defined(SK_GRAPHITE)
RNSkia::DawnContext::getInstance().invalidate();
#else
MetalContext::getInstance().invalidate();
#endif
}

- (instancetype)initWithBridge:(RCTBridge *)bridge
Expand Down
48 changes: 47 additions & 1 deletion packages/skia/cpp/rnskia/RNDawnContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,41 @@ class DawnContext {
return instance;
}

// Invalidate the context - must be called before RN bridge reload
// to ensure fresh GPU resources are created after reload.
// This is critical for OTA updates (e.g., Expo Updates) where the
// main thread survives but the bridge is recreated.
void invalidate() {
std::lock_guard<std::mutex> lock(_mutex);
_invalidated = true;
}

// Check if the context is valid
bool isValid() const { return !_invalidated && fGraphiteContext != nullptr; }

// Re-initialize the context if it was invalidated
void ensureValid() {
std::lock_guard<std::mutex> lock(_mutex);
ensureValidLocked();
}

private:
// Must be called with _mutex held
void ensureValidLocked() {
if (_invalidated) {
reinitialize();
_invalidated = false;
}
}

public:

sk_sp<SkImage> MakeRasterImage(sk_sp<SkImage> image) {
if (!image->isTextureBacked()) {
return image;
}
std::lock_guard<std::mutex> lock(_mutex);
ensureValidLocked();
AsyncContext asyncContext;
fGraphiteContext->asyncRescaleAndReadPixels(
image.get(), image->imageInfo(), image->imageInfo().bounds(),
Expand Down Expand Up @@ -96,13 +126,15 @@ class DawnContext {
skgpu::graphite::Recording *recording,
skgpu::graphite::SyncToCpu syncToCpu = skgpu::graphite::SyncToCpu::kNo) {
std::lock_guard<std::mutex> lock(_mutex);
ensureValidLocked();
skgpu::graphite::InsertRecordingInfo info;
info.fRecording = recording;
fGraphiteContext->insertRecording(info);
fGraphiteContext->submit(syncToCpu);
}

sk_sp<SkImage> MakeImageFromBuffer(void *buffer) {
ensureValid();
#ifdef __APPLE__
wgpu::SharedTextureMemoryIOSurfaceDescriptor platformDesc;
auto ioSurface = CVPixelBufferGetIOSurface((CVPixelBufferRef)buffer);
Expand Down Expand Up @@ -163,6 +195,7 @@ class DawnContext {

// Create offscreen surface
sk_sp<SkSurface> MakeOffscreen(int width, int height) {
ensureValid();
SkImageInfo info = SkImageInfo::Make(
width, height, DawnUtils::PreferedColorType, kPremul_SkAlphaType);
sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(getRecorder(), info);
Expand All @@ -177,6 +210,7 @@ class DawnContext {
// Create onscreen surface with window
std::unique_ptr<WindowContext> MakeWindow(void *window, int width,
int height) {
ensureValid();
// 1. Create Surface
wgpu::SurfaceDescriptor surfaceDescriptor;
#ifdef __APPLE__
Expand All @@ -199,8 +233,11 @@ class DawnContext {
std::unique_ptr<skgpu::graphite::Context> fGraphiteContext;
skgpu::graphite::DawnBackendContext backendContext;
std::mutex _mutex;
bool _invalidated = false;

DawnContext() {
DawnContext() { initialize(); }

void initialize() {
DawnProcTable backendProcs = dawn::native::GetProcs();
dawnProcSetProcs(&backendProcs);
static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
Expand All @@ -227,6 +264,15 @@ class DawnContext {
}
}

void reinitialize() {
// Clean up existing resources
fGraphiteContext = nullptr;
backendContext.fDevice = nullptr;
instance = nullptr;
// Re-initialize
initialize();
}

~DawnContext() {
backendContext.fDevice = nullptr;
tick();
Expand Down
Loading