Skip to content

Commit 1022ec7

Browse files
committed
fix(🍏🤖): Fix crash after Expo OTA updates by invalidating GPU context on bridge reload
1 parent cb611c8 commit 1022ec7

File tree

6 files changed

+145
-8
lines changed

6 files changed

+145
-8
lines changed

‎packages/skia/android/cpp/jni/include/JniSkiaManager.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
#include "RNSkLog.h"
1313
#include "RNSkManager.h"
1414

15+
#if defined(SK_GRAPHITE)
16+
#include "RNDawnContext.h"
17+
#else
18+
#include "OpenGLContext.h"
19+
#endif
20+
1521
namespace RNSkia {
1622

1723
class RNSkManager;
@@ -55,6 +61,15 @@ class JniSkiaManager : public jni::HybridClass<JniSkiaManager> {
5561
void invalidate() {
5662
_skManager = nullptr;
5763
_context = nullptr;
64+
// Invalidate the OpenGL/Dawn context to ensure fresh GPU resources
65+
// are created after a bridge reload (e.g., Expo OTA updates).
66+
// Without this, the thread-local singleton would retain stale
67+
// GPU context references causing crashes.
68+
#if defined(SK_GRAPHITE)
69+
DawnContext::getInstance().invalidate();
70+
#else
71+
OpenGLContext::getInstance().invalidate();
72+
#endif
5873
}
5974

6075
private:

‎packages/skia/android/cpp/rnskia-android/OpenGLContext.h‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,28 @@ class OpenGLContext {
5858
return instance;
5959
}
6060

61+
// Invalidate the context - must be called before RN bridge reload
62+
// to ensure fresh GPU resources are created after reload.
63+
// This is critical for OTA updates (e.g., Expo Updates) where the
64+
// main thread survives but the bridge is recreated.
65+
void invalidate() {
66+
_directContext = nullptr;
67+
_glSurface = nullptr;
68+
_glContext = nullptr;
69+
}
70+
71+
// Check if the context is valid and ready to use
72+
bool isValid() const { return _directContext != nullptr; }
73+
74+
// Ensure the context is initialized (creates new resources if invalidated)
75+
void ensureValid() {
76+
if (!isValid()) {
77+
initialize();
78+
}
79+
}
80+
6181
sk_sp<SkSurface> MakeOffscreen(int width, int height) {
82+
ensureValid();
6283
auto colorType = kRGBA_8888_SkColorType;
6384

6485
SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
@@ -103,6 +124,7 @@ class OpenGLContext {
103124

104125
sk_sp<SkImage> MakeImageFromBuffer(void *buffer,
105126
bool requireKnownFormat = false) {
127+
ensureValid();
106128
#if __ANDROID_API__ >= 26
107129
const AHardwareBuffer *hardwareBuffer =
108130
static_cast<AHardwareBuffer *>(buffer);
@@ -168,21 +190,30 @@ class OpenGLContext {
168190

169191
// TODO: remove width, height
170192
std::unique_ptr<WindowContext> MakeWindow(ANativeWindow *window) {
193+
ensureValid();
171194
auto display = OpenGLSharedContext::getInstance().getDisplay();
172195
return std::make_unique<OpenGLWindowContext>(
173196
_directContext.get(), display, _glContext.get(), window,
174197
OpenGLSharedContext::getInstance().getConfig());
175198
}
176199

177-
GrDirectContext *getDirectContext() { return _directContext.get(); }
178-
void makeCurrent() { _glContext->makeCurrent(_glSurface.get()); }
200+
GrDirectContext *getDirectContext() {
201+
ensureValid();
202+
return _directContext.get();
203+
}
204+
void makeCurrent() {
205+
ensureValid();
206+
_glContext->makeCurrent(_glSurface.get());
207+
}
179208

180209
private:
181210
std::unique_ptr<gl::Context> _glContext;
182211
std::unique_ptr<gl::Surface> _glSurface;
183212
sk_sp<GrDirectContext> _directContext;
184213

185-
OpenGLContext() {
214+
OpenGLContext() { initialize(); }
215+
216+
void initialize() {
186217
auto display = OpenGLSharedContext::getInstance().getDisplay();
187218
auto sharedContext = OpenGLSharedContext::getInstance().getContext();
188219
auto glConfig = OpenGLSharedContext::getInstance().getConfig();

‎packages/skia/apple/MetalContext.h‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,31 @@ class MetalContext {
4444
return instance;
4545
}
4646

47+
// Invalidate the context - must be called before RN bridge reload
48+
// to ensure fresh GPU resources are created after reload.
49+
// This is critical for OTA updates (e.g., Expo Updates) where the
50+
// main thread survives but the bridge is recreated.
51+
void invalidate() {
52+
_directContext = nullptr;
53+
if (_commandQueue) {
54+
CFRelease((GrMTLHandle)_commandQueue);
55+
_commandQueue = nullptr;
56+
}
57+
_device = nullptr;
58+
}
59+
60+
// Check if the context is valid and ready to use
61+
bool isValid() const { return _directContext != nullptr && _device != nullptr; }
62+
63+
// Ensure the context is initialized (creates new resources if invalidated)
64+
void ensureValid() {
65+
if (!isValid()) {
66+
initialize();
67+
}
68+
}
69+
4770
sk_sp<SkSurface> MakeOffscreen(int width, int height) {
71+
ensureValid();
4872
auto device = _device;
4973
auto ctx = new OffscreenRenderContext(device, _directContext, _commandQueue,
5074
width, height);
@@ -65,7 +89,7 @@ class MetalContext {
6589
}
6690

6791
sk_sp<SkImage> MakeImageFromBuffer(void *buffer) {
68-
92+
ensureValid();
6993
CVPixelBufferRef sampleBuffer = (CVPixelBufferRef)buffer;
7094
SkiaCVPixelBufferUtils::CVPixelBufferBaseFormat format =
7195
SkiaCVPixelBufferUtils::getCVPixelBufferBaseFormat(sampleBuffer);
@@ -92,18 +116,24 @@ class MetalContext {
92116
std::unique_ptr<RNSkia::WindowContext>
93117
MakeWindow(CALayer *window, int width, int height,
94118
bool useP3ColorSpace = true) {
119+
ensureValid();
95120
auto device = _device;
96121
return std::make_unique<MetalWindowContext>(_directContext.get(), device,
97122
_commandQueue, window, width,
98123
height, useP3ColorSpace);
99124
}
100125

101-
GrDirectContext *getDirectContext() { return _directContext.get(); }
126+
GrDirectContext *getDirectContext() {
127+
ensureValid();
128+
return _directContext.get();
129+
}
102130

103131
private:
104132
id<MTLDevice> _device = nullptr;
105133
id<MTLCommandQueue> _commandQueue = nullptr;
106134
sk_sp<GrDirectContext> _directContext = nullptr;
107135

108-
MetalContext();
136+
MetalContext() { initialize(); }
137+
138+
void initialize();
109139
};

‎packages/skia/apple/MetalContext.mm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
#pragma clang diagnostic pop
1919

20-
MetalContext::MetalContext() {
20+
void MetalContext::initialize() {
2121
_device = MTLCreateSystemDefaultDevice();
2222
if (!_device) {
2323
throw std::runtime_error("Failed to create Metal device");

‎packages/skia/apple/SkiaManager.mm‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88

99
#import "RNSkApplePlatformContext.h"
1010

11+
#if defined(SK_GRAPHITE)
12+
#import "RNDawnContext.h"
13+
#else
14+
#import "MetalContext.h"
15+
#endif
16+
1117
static __weak SkiaManager *sharedInstance = nil;
1218

1319
@implementation SkiaManager {
@@ -21,6 +27,15 @@ @implementation SkiaManager {
2127

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

2641
- (instancetype)initWithBridge:(RCTBridge *)bridge

‎packages/skia/cpp/rnskia/RNDawnContext.h‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,41 @@ class DawnContext {
6161
return instance;
6262
}
6363

64+
// Invalidate the context - must be called before RN bridge reload
65+
// to ensure fresh GPU resources are created after reload.
66+
// This is critical for OTA updates (e.g., Expo Updates) where the
67+
// main thread survives but the bridge is recreated.
68+
void invalidate() {
69+
std::lock_guard<std::mutex> lock(_mutex);
70+
_invalidated = true;
71+
}
72+
73+
// Check if the context is valid
74+
bool isValid() const { return !_invalidated && fGraphiteContext != nullptr; }
75+
76+
// Re-initialize the context if it was invalidated
77+
void ensureValid() {
78+
std::lock_guard<std::mutex> lock(_mutex);
79+
ensureValidLocked();
80+
}
81+
82+
private:
83+
// Must be called with _mutex held
84+
void ensureValidLocked() {
85+
if (_invalidated) {
86+
reinitialize();
87+
_invalidated = false;
88+
}
89+
}
90+
91+
public:
92+
6493
sk_sp<SkImage> MakeRasterImage(sk_sp<SkImage> image) {
6594
if (!image->isTextureBacked()) {
6695
return image;
6796
}
6897
std::lock_guard<std::mutex> lock(_mutex);
98+
ensureValidLocked();
6999
AsyncContext asyncContext;
70100
fGraphiteContext->asyncRescaleAndReadPixels(
71101
image.get(), image->imageInfo(), image->imageInfo().bounds(),
@@ -96,13 +126,15 @@ class DawnContext {
96126
skgpu::graphite::Recording *recording,
97127
skgpu::graphite::SyncToCpu syncToCpu = skgpu::graphite::SyncToCpu::kNo) {
98128
std::lock_guard<std::mutex> lock(_mutex);
129+
ensureValidLocked();
99130
skgpu::graphite::InsertRecordingInfo info;
100131
info.fRecording = recording;
101132
fGraphiteContext->insertRecording(info);
102133
fGraphiteContext->submit(syncToCpu);
103134
}
104135

105136
sk_sp<SkImage> MakeImageFromBuffer(void *buffer) {
137+
ensureValid();
106138
#ifdef __APPLE__
107139
wgpu::SharedTextureMemoryIOSurfaceDescriptor platformDesc;
108140
auto ioSurface = CVPixelBufferGetIOSurface((CVPixelBufferRef)buffer);
@@ -163,6 +195,7 @@ class DawnContext {
163195

164196
// Create offscreen surface
165197
sk_sp<SkSurface> MakeOffscreen(int width, int height) {
198+
ensureValid();
166199
SkImageInfo info = SkImageInfo::Make(
167200
width, height, DawnUtils::PreferedColorType, kPremul_SkAlphaType);
168201
sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(getRecorder(), info);
@@ -177,6 +210,7 @@ class DawnContext {
177210
// Create onscreen surface with window
178211
std::unique_ptr<WindowContext> MakeWindow(void *window, int width,
179212
int height) {
213+
ensureValid();
180214
// 1. Create Surface
181215
wgpu::SurfaceDescriptor surfaceDescriptor;
182216
#ifdef __APPLE__
@@ -199,8 +233,11 @@ class DawnContext {
199233
std::unique_ptr<skgpu::graphite::Context> fGraphiteContext;
200234
skgpu::graphite::DawnBackendContext backendContext;
201235
std::mutex _mutex;
236+
bool _invalidated = false;
202237

203-
DawnContext() {
238+
DawnContext() { initialize(); }
239+
240+
void initialize() {
204241
DawnProcTable backendProcs = dawn::native::GetProcs();
205242
dawnProcSetProcs(&backendProcs);
206243
static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
@@ -227,6 +264,15 @@ class DawnContext {
227264
}
228265
}
229266

267+
void reinitialize() {
268+
// Clean up existing resources
269+
fGraphiteContext = nullptr;
270+
backendContext.fDevice = nullptr;
271+
instance = nullptr;
272+
// Re-initialize
273+
initialize();
274+
}
275+
230276
~DawnContext() {
231277
backendContext.fDevice = nullptr;
232278
tick();

0 commit comments

Comments
 (0)