Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[iOS, v2/Ganesh] MetalContext over-releases the system MTLDevice once per exited thread, then Metal crashes

Open Beginner friendly
#4,130 0 comments 0 reactions 0 assignees View on GitHub

@wu-hongjun is already working on this.

Since Oct 5, 2026.

  • #4131 by @wu-hongjun — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
cpp, ios, objective-c
Domain
backend, mobile

Research direction

Start with packages/skia/apple/MetalContext.mm on the 2.x branch and check the fDevice.reset call against the ownership semantics documented in include/ports/SkCFObject.h; compare with the command queue handling nearby. The issue identifies backendContext.fDevice.retain as the fix. Verify that short-lived threads no longer reduce the device retain count or crash in the provided simulator reproduction.

Written by the indexing model from the issue text.

Description

Description

On the v2 line (Ganesh), every MetalContext that is destroyed releases the system MTLDevice once more than it retained it. MetalContext is thread_local (MetalContext::getInstance()), so this happens each time a thread that drew with Skia exits. After a few such exits the device is freed while Metal still uses it, and the app crashes inside Metal (-[_MTLDevice dealloc], -[_MTLFunction dealloc], -[MTLRenderPipelineDescriptorInternal dealloc], or an XPC misuse trap under the simulator driver).

Apps whose Skia threads come and go are exposed: worklet runtimes that are torn down, frame-processor or worker threads, a JS thread recreated on reload.

Expected: the device's retain count is the same after a thread exits as before it started. Actual: it drops by one per exited thread.

Cause. In packages/skia/apple/MetalContext.mm (2.x):

_device = MTLCreateSystemDefaultDevice();                     // ARC strong ivar
...
GrMtlBackendContext backendContext = {};
backendContext.fDevice.reset((__bridge void *)_device);       // adopts a reference, no CFRetain
backendContext.fQueue.reset((__bridge void *)_commandQueue);  // balanced by the CFRetain above

sk_cfp::reset adopts the pointer without retaining it ("No call to CFRetain() will be made", include/ports/SkCFObject.h) and releases it when backendContext goes out of scope at the end of the constructor. The __bridge cast transfers no ownership, so that release is one too many. The command queue on the next line is balanced by its explicit CFRetain; the device is not.

main (v3) has no MetalContext, so this is v2 only.

React Native Skia Version

2.14.0 and the 2.x branch (f24fc55). The line is unchanged since at least 2.6.2.

React Native Version

0.83.1 (the repo's example app). Also seen with 0.86.

Using New Architecture
  • Enabled
Steps to Reproduce
  1. Draw with Skia on a background thread (anything that calls MetalContext::getInstance(): an offscreen surface, a texture), then let the thread exit.
  2. Repeat on new threads.
  3. After a handful of exits, the process crashes in Metal.

A standalone program reproduces it deterministically on the iOS simulator. It compiles the 2.x branch's own apple/MetalContext.mm unchanged, links the m154 prebuilt libskia.a from react-native-skia-apple-ios, and draws one circle on each of N short-lived threads (code below):

MetalContext.mm 1 thread 12 threads 64 threads
as on 2.x (fDevice.reset) OK, retain count 4 -> 3 SIGTRAP after 4-5 threads (count 4 -> 1, then -[_MTLDevice dealloc] from MetalContext::~MetalContext()) SIGTRAP
with fDevice.retain OK, 4 -> 5 OK, stays 5 OK, stays 5

Crashing stack (iOS 27 simulator):

libxpc.dylib        _xpc_api_misuse
MTLSimDriver        -[MTLSimBuffer dealloc]
Metal               -[_MTLDevice dealloc]
MTLSimDriver        -[MTLSimDevice dealloc]
metalctx-baseline   MetalContext::~MetalContext()
libdyld.dylib       dyld::ThreadLocalVariables::finalizeList(void*)
libsystem_pthread   _pthread_tsd_cleanup

In an app with a small pool of worker threads that each made a Skia context and exited, a Release build on the iOS simulator crashed on 9 of 10 launches with the line as it is, and on 0 of 10 with retain.

Snack, Code Example, Screenshot, or Link to Repository
main.mm (built against packages/skia/apple/MetalContext.mm and the m154 libskia.a, iOS simulator, ARC)
// N threads each draw with their thread_local MetalContext and exit.
// Prints the system MTLDevice's retain count. Usage: metalctx <threads>
#import <Metal/Metal.h>
#include <cstdio>
#include <cstdlib>
#include <thread>
#include "MetalContext.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkPaint.h"

static void drawOnce() {
  @autoreleasepool {
    auto &ctx = MetalContext::getInstance();
    auto surface = ctx.MakeOffscreen(64, 64);
    SkPaint p; p.setColor(SK_ColorRED);
    surface->getCanvas()->drawCircle(32, 32, 20, p);
    ctx.getDirectContext()->flushAndSubmit(GrSyncCpu::kYes);
  }
}

int main(int argc, char **argv) {
  const int threads = argc > 1 ? std::atoi(argv[1]) : 8;
  id<MTLDevice> device = MTLCreateSystemDefaultDevice();
  CFTypeRef d = (__bridge CFTypeRef)device;
  std::printf("start: device retain count %ld\n", (long)CFGetRetainCount(d));
  for (int i = 1; i <= threads; i++) {
    std::thread t(drawOnce);
    t.join(); // the thread exits: its thread_local MetalContext is destroyed
    std::printf("after %3d threads: device retain count %ld\n", i,
                (long)CFGetRetainCount(d));
  }
  std::thread t(drawOnce); t.join();
  std::printf("OK threads=%d\n", threads);
  return 0;
}

Built with clang++ -target arm64-apple-ios17.0-simulator -std=c++20 -fobjc-arc -x objective-c++ -DSK_METAL=1 -DSK_GANESH=1 -I packages/skia/{apple,cpp,cpp/utils,cpp/skia} -I <react-native>/ReactCommon/jsi main.mm packages/skia/apple/MetalContext.mm <libskia.a> -framework Metal -framework MetalKit -framework UIKit ... and run with xcrun simctl spawn <device> ./metalctx 12.

Fix: backendContext.fDevice.retain((__bridge void *)_device);. PR to follow.

Dominant language
TypeScript
Stars
8.6k
Forks
653
PR merge metrics
No merged PRs in 30d

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Shopify/react-native-skia

All issues in Shopify/react-native-skia

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.