iOS: Keep the TV's layer until its output's removal has been handled

The add request holds the layer unretained, and the swapchain only takes
a reference once it's handled. If the TV disconnected (or the setting
changed) before that, the window and the layer were freed with the add
still queued. The view controller now keeps a reference for the requests,
released after Native_RemoveOutputAndWait, off the main thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-22 23:02:36 +02:00
1 parent be4bdf3590
commit 22aa12d047
1 file changed
+14 -3
+14 -3
View File
@@ -42,6 +42,8 @@ static bool g_launched = false;
// Adds the view's layer as a Game output once it has a size, and keeps the output's size up to date.
@interface ExternalDisplayViewController : UIViewController
@property (nonatomic) int outputId;
// A reference to the layer for the output's requests, which hold it unretained. Released once it's removed.
@property (nonatomic) void *retainedLayer;
@end
@implementation ExternalDisplayViewController {
@@ -73,7 +75,8 @@ static bool g_launched = false;
}
Draw::SwapchainDesc desc{};
desc.winsys = WINDOWSYSTEM_METAL_EXT;
desc.data1 = (__bridge void *)self.view.layer;
self.retainedLayer = (void *)CFBridgingRetain(self.view.layer);
desc.data1 = self.retainedLayer;
desc.width = width;
desc.height = height;
desc.tag = "ExternalDisplay";
@@ -137,8 +140,16 @@ static bool g_launched = false;
}
ExternalDisplayViewController *viewController = (ExternalDisplayViewController *)self.window.rootViewController;
if (viewController.outputId) {
// The swapchain keeps the layer alive until the removal has gone through.
Native_RemoveOutput(viewController.outputId);
// The window and its layer go now, but the add may still be queued, so our reference to the layer has to
// stay until the removal has been handled. Waited for off the main thread, which the render thread may
// be waiting on. If that times out, the layer is kept, as the add may still come.
const int outputId = viewController.outputId;
void *layer = viewController.retainedLayer;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
if (Native_RemoveOutputAndWait(outputId)) {
CFRelease(layer);
}
});
}
self.window = nil;
}