Asynchronously run Apple APIs on the main thread

These operations now run on the main thread and not on the Replay thread
* Updating the NSOpenGLContext
* Setting the view property on the NSOpenGLContext
* Getting the window size from the NSView

The specific methods that are run on the main thread are:
* NSOpenGLContext setView
* NSOpenGLContext update
* NSView frame
* NSView convertSizeToBacking
* NSView setWantsBestResolutionOpenGLSurface

These methods are asynchronously dispatched to the main thread using the Apple NSApplication main thread dispatch queue i.e.

dispatch_async(dispatch_get_main_queue(), <method>);

The threading synchronization intent is:
* the Replay does not block waiting for a deferred main thread API to complete
* mutex locks are used to protect containers shared between the main and Replay threads
* the main thread uses a TryLock approach when wanting to acquire the context lock. If the context lock is held by the Replay thread then the main thread reschedules the requested context operation i.e.

if (TryToGetContectLock(context))
{
  [context update];
}
else
{
  scheduleContextUpdate(context);
}

* When scheduling requests to the main thread the NSOpenGLContext and NSView objects are referred to by an index into an array and not by pointer value.

There are two debugging modes which are disabled by default
* RD_THREAD_RANDOM_SLEEP
* RD_USE_CONTEXT_LOCK_COUNTS

Enabling RD_THREAD_RANDOM_SLEEP performs sleeps of random times on both the Replay and main thread when calling the different context and view methods.
RD_THREAD_RANDOM_SLEEP is useful to expose race conditions.

Enabling RD_USE_CONTEXT_LOCK_COUNTS tracks the lock count on the context objects in the "s_ContextLocksCount" container.

The low-level context locking is handled in NSGL_makeCurrentContext which is called from CGLPlatform::MakeContextCurrent which in turn is called from GLReplay::MakeCurrentReplayContext.

The currently locked context is tracked in TLS storage and the currently locked context is unlocked before locking the new context and then setting the current context TLS value.
This commit is contained in:
Jake Turner
2021-03-06 05:51:06 +00:00
committed by Baldur Karlsson
parent 2c63b24cbc
commit b37ed43ba3
3 changed files with 468 additions and 59 deletions
+12 -17
View File
@@ -30,19 +30,14 @@
#include "apple_gl_hook_defs.h"
// helpers defined in cgl_platform.mm
extern "C" int NSGL_getLayerWidth(void *layer);
extern "C" int NSGL_getLayerHeight(void *layer);
extern "C" void *NSGL_createContext(void *view, void *shareContext);
extern "C" void NSGL_makeCurrentContext(void *context);
extern "C" void NSGL_update(void *context);
extern "C" void NSGL_flushBuffer(void *context);
extern "C" void NSGL_destroyContext(void *context);
// helper for cgl_platform.mm
extern "C" void NSGL_LogText(const char *text)
{
RDCLOG("CGL: %s", text);
}
void Apple_getWindowSize(void *view, int &width, int &height);
void Apple_stopTrackingWindowSize(void *view);
void NSGL_init();
void *NSGL_createContext(void *view, void *shareContext);
void NSGL_makeCurrentContext(void *context);
void NSGL_update(void *context);
void NSGL_flushBuffer(void *context);
void NSGL_destroyContext(void *context);
// gl functions (used for quad rendering on legacy contexts)
extern "C" void glPushMatrix();
@@ -161,15 +156,15 @@ class CGLPlatform : public GLPlatform
{
RDCASSERT(context.nsgl_ctx);
NSGL_destroyContext(context.nsgl_ctx);
Apple_stopTrackingWindowSize(context.wnd);
}
void SwapBuffers(GLWindowingData context) { NSGL_flushBuffer(context.nsgl_ctx); }
void WindowResized(GLWindowingData context) { NSGL_update(context.nsgl_ctx); }
void GetOutputWindowDimensions(GLWindowingData context, int32_t &w, int32_t &h)
{
if(context.layer)
if(context.wnd)
{
w = NSGL_getLayerWidth(context.layer);
h = NSGL_getLayerHeight(context.layer);
Apple_getWindowSize(context.wnd, w, h);
}
else
{
@@ -201,7 +196,6 @@ class CGLPlatform : public GLPlatform
ret.nsgl_ctx = NSGL_createContext(window.macOS.view, share_context.nsgl_ctx);
ret.wnd = window.macOS.view;
ret.layer = window.macOS.layer;
return ret;
}
@@ -223,6 +217,7 @@ class CGLPlatform : public GLPlatform
{
RDCASSERT(api == RDCDriver::OpenGL);
NSGL_init();
replayContext.nsgl_ctx = NSGL_createContext(NULL, NULL);
return ReplayStatus::Succeeded;
+456 -38
View File
@@ -1,27 +1,412 @@
/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019-2021 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include "api/replay/rdcstr.h"
#include "common/common.h"
#include "common/threading.h"
#include "os/os_specific.h"
#import <Cocoa/Cocoa.h>
extern "C" void NSGL_LogText(const char *text);
#define RD_THREAD_RANDOM_SLEEP (0)
#define RD_USE_CONTEXT_LOCK_COUNTS (0)
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
extern "C" int NSGL_getLayerWidth(void *layer)
{
CALayer *caLayer = (CALayer *)layer;
assert([caLayer isKindOfClass:[CALayer class]]);
static uint64_t s_currentContextTLSkey;
return caLayer.bounds.size.width;
static Threading::CriticalSection s_WindowPtrsArrayLock;
static rdcarray<NSView *> s_WindowPtrs;
static rdcarray<int> s_WindowWidths;
static rdcarray<int> s_WindowHeights;
static int s_ReplaySalt = 0;
static Threading::CriticalSection s_ContextPtrsArrayLock;
static rdcarray<NSOpenGLContext *> s_ContextPtrs;
static rdcarray<Threading::CriticalSection *> s_ContextLocks;
#if RD_USE_CONTEXT_LOCK_COUNTS
static rdcarray<int32_t> s_ContextLocksCount;
#endif // #if RD_USE_CONTEXT_LOCK_COUNTS
static void scheduleContextSetView(int contextIndex, int replaySalt, NSView *view);
static void scheduleContextUpdate(int contextIndex, int replaySalt);
static void RandomSleep()
{
#if RD_THREAD_RANDOM_SLEEP
usleep(rand() % 1000);
#endif // #if RD_THREAD_RANDOM_SLEEP
}
extern "C" int NSGL_getLayerHeight(void *layer)
static NSOpenGLContext *GetNSOpenGLContext(void *context)
{
CALayer *caLayer = (CALayer *)layer;
assert([caLayer isKindOfClass:[CALayer class]]);
return caLayer.bounds.size.height;
NSOpenGLContext *nsglContext = (NSOpenGLContext *)context;
RDCASSERT([nsglContext isKindOfClass:[NSOpenGLContext class]]);
return nsglContext;
}
extern "C" void *NSGL_createContext(void *view, void *shareContext)
static NSView *GetNSView(void *view)
{
NSView *nsView = (NSView *)view;
RDCASSERT([nsView isKindOfClass:[NSView class]]);
return nsView;
}
static void IncrementContextLockCount(int contextIndex)
{
#if RD_USE_CONTEXT_LOCK_COUNTS
Atomic::Inc32(s_ContextLocksCount.data() + contextIndex);
#endif //#if RD_USE_CONTEXT_LOCK_COUNTS
}
static void DecrementContextLockCount(int contextIndex)
{
#if RD_USE_CONTEXT_LOCK_COUNTS
Atomic::Dec32(s_ContextLocksCount.data() + contextIndex);
#endif //#if RD_USE_CONTEXT_LOCK_COUNTS
}
static void LockContext(int contextIndex)
{
s_ContextLocks[contextIndex]->Lock();
IncrementContextLockCount(contextIndex);
}
static bool TryLockContext(int contextIndex)
{
if(s_ContextLocks[contextIndex]->Trylock())
{
IncrementContextLockCount(contextIndex);
return true;
}
return false;
}
static void UnLockContext(int contextIndex)
{
DecrementContextLockCount(contextIndex);
s_ContextLocks[contextIndex]->Unlock();
}
// s_WindowPtrsArrayLock must be locked by the caller
static bool findWindowIndex(NSView *nsView, int &index)
{
const int windowCount = s_WindowPtrs.count();
int firstFreeIndex = windowCount;
for(int i = 0; i < windowCount; ++i)
{
if(s_WindowPtrs[i] == nsView)
{
index = i;
return true;
}
if((s_WindowPtrs[i] == nil) && (i < firstFreeIndex))
{
firstFreeIndex = i;
}
}
index = firstFreeIndex;
return false;
}
static int getWindowIndex(NSView *nsView)
{
SCOPED_LOCK(s_WindowPtrsArrayLock);
int index = -1;
if(findWindowIndex(nsView, index))
{
return index;
}
const int windowCount = s_WindowPtrs.count();
if(index < windowCount)
{
RDCASSERT(index >= 0);
RDCASSERT(index < s_WindowWidths.count());
RDCASSERT(index < s_WindowHeights.count());
s_WindowPtrs[index] = nsView;
s_WindowWidths[index] = 0;
s_WindowHeights[index] = 0;
}
else
{
RDCASSERT(windowCount == s_WindowWidths.count());
RDCASSERT(windowCount == s_WindowHeights.count());
s_WindowPtrs.push_back(nsView);
s_WindowWidths.push_back(0);
s_WindowHeights.push_back(0);
}
return index;
}
static void SetCurrentContextIndexTLS(int contextIndex)
{
Threading::SetTLSValue(s_currentContextTLSkey, (void *)(uintptr_t)contextIndex);
}
static int GetCurrentContextIndexTLS()
{
return (int)(uintptr_t)Threading::GetTLSValue(s_currentContextTLSkey);
}
static int getContextIndex(NSOpenGLContext *nsglContext)
{
SCOPED_LOCK(s_ContextPtrsArrayLock);
const int contextCount = s_ContextPtrs.count();
int firstFreeIndex = contextCount;
for(int i = 0; i < contextCount; ++i)
{
if(s_ContextPtrs[i] == nsglContext)
{
return i;
}
if((s_ContextPtrs[i] == nil) && (i < firstFreeIndex))
{
firstFreeIndex = i;
}
}
int index = firstFreeIndex;
if(index < contextCount)
{
RDCASSERT(index >= 0);
RDCASSERT(index < s_ContextLocks.count());
s_ContextPtrs[index] = nsglContext;
RDCASSERT(s_ContextLocks[index]);
#if RD_USE_CONTEXT_LOCK_COUNTS
RDCASSERT(0 == s_ContextLocksCount[index]);
RDCASSERT(index < s_ContextLocksCount.count());
s_ContextLocksCount[index] = 0;
#endif // #if RD_USE_CONTEXT_LOCK_COUNTS
}
else
{
RDCASSERT(contextCount == s_ContextLocks.count());
#if RD_USE_CONTEXT_LOCK_COUNTS
RDCASSERT(contextCount == s_ContextLocksCount.count());
#endif // #if RD_USE_CONTEXT_LOCK_COUNTS
s_ContextPtrs.push_back(nsglContext);
s_ContextLocks.push_back(new Threading::CriticalSection);
#if RD_USE_CONTEXT_LOCK_COUNTS
s_ContextLocksCount.push_back(0);
#endif // #if RD_USE_CONTEXT_LOCK_COUNTS
}
return index;
}
static NSOpenGLContext *getLockedContext(int contextIndex)
{
SCOPED_LOCK(s_ContextPtrsArrayLock);
RDCASSERT(contextIndex >= 0 && contextIndex < s_ContextPtrs.count());
if(contextIndex >= 0 && contextIndex < s_ContextPtrs.count())
{
NSOpenGLContext *context = s_ContextPtrs[contextIndex];
if(context && TryLockContext(contextIndex))
{
return context;
}
}
return nil;
}
static void viewSetWantBestResolutionMT(NSView *view)
{
RandomSleep();
[view setWantsBestResolutionOpenGLSurface:true];
}
static void viewGetWindowSizeMT(int windowIndex, int replaySalt)
{
RandomSleep();
const int currentReplaySalt = s_ReplaySalt;
if(replaySalt != currentReplaySalt)
{
return;
}
SCOPED_LOCK(s_WindowPtrsArrayLock);
RDCASSERT(windowIndex >= 0 && windowIndex < s_WindowPtrs.count());
if(windowIndex >= 0 && windowIndex < s_WindowPtrs.count())
{
NSView *view = s_WindowPtrs[windowIndex];
if(view)
{
const NSRect contentRect = [view frame];
CGSize viewSize = [view convertSizeToBacking:contentRect.size];
s_WindowWidths[windowIndex] = viewSize.width;
s_WindowHeights[windowIndex] = viewSize.height;
}
}
}
static void contextUpdateMT(int contextIndex, int replaySalt)
{
RandomSleep();
const int currentReplaySalt = s_ReplaySalt;
if(replaySalt != currentReplaySalt)
{
return;
}
NSOpenGLContext *lockedContext = getLockedContext(contextIndex);
if(lockedContext)
{
[lockedContext update];
UnLockContext(contextIndex);
}
else
{
scheduleContextUpdate(contextIndex, replaySalt);
}
}
static void contextSetViewMT(int contextIndex, int replaySalt, NSView *view)
{
RandomSleep();
const int currentReplaySalt = s_ReplaySalt;
if(replaySalt != currentReplaySalt)
{
return;
}
NSOpenGLContext *lockedContext = getLockedContext(contextIndex);
if(lockedContext)
{
[lockedContext setView:view];
[lockedContext update];
UnLockContext(contextIndex);
}
else
{
scheduleContextSetView(contextIndex, replaySalt, view);
}
}
static void scheduleContextSetView(int contextIndex, int replaySalt, NSView *view)
{
RandomSleep();
dispatch_async(dispatch_get_main_queue(), ^(void) {
contextSetViewMT(contextIndex, replaySalt, view);
});
}
static void scheduleContextUpdate(int contextIndex, int replaySalt)
{
RandomSleep();
dispatch_async(dispatch_get_main_queue(), ^(void) {
contextUpdateMT(contextIndex, replaySalt);
});
}
static void scheduleViewSetWantBestResolution(NSView *view)
{
RandomSleep();
dispatch_async(dispatch_get_main_queue(), ^(void) {
viewSetWantBestResolutionMT(view);
});
}
static void scheduleViewGetWindowSizeMT(int windowIndex, int replaySalt)
{
RandomSleep();
dispatch_async(dispatch_get_main_queue(), ^(void) {
viewGetWindowSizeMT(windowIndex, replaySalt);
});
}
void Apple_getWindowSize(void *view, int &width, int &height)
{
RandomSleep();
if(!view)
{
width = 0;
height = 0;
return;
}
NSView *nsView = GetNSView(view);
const int windowIndex = getWindowIndex(nsView);
width = s_WindowWidths[windowIndex];
height = s_WindowHeights[windowIndex];
scheduleViewGetWindowSizeMT(windowIndex, s_ReplaySalt);
}
void Apple_stopTrackingWindowSize(void *view)
{
RandomSleep();
if(!view)
return;
NSView *nsView = (NSView *)view;
{
SCOPED_LOCK(s_WindowPtrsArrayLock);
int windowIndex = -1;
if(findWindowIndex(nsView, windowIndex))
{
s_WindowPtrs[windowIndex] = nil;
s_WindowWidths[windowIndex] = 0;
s_WindowHeights[windowIndex] = 0;
}
}
}
void NSGL_init()
{
RandomSleep();
static bool s_allocatedTLSKey = false;
if(!s_allocatedTLSKey)
{
s_ReplaySalt = 0;
s_allocatedTLSKey = true;
s_currentContextTLSkey = Threading::AllocateTLSSlot();
const int initialWindowCountMax = 8;
s_WindowPtrs.reserve(initialWindowCountMax);
s_WindowWidths.reserve(initialWindowCountMax);
s_WindowHeights.reserve(initialWindowCountMax);
const int initialContextCountMax = 8;
s_ContextPtrs.reserve(initialContextCountMax);
s_ContextLocks.reserve(initialContextCountMax);
#if RD_USE_CONTEXT_LOCK_COUNTS
s_ContextLocksCount.reserve(initialContextCountMax);
#endif // #if RD_USE_CONTEXT_LOCK_COUNTS
}
Atomic::Inc32(&s_ReplaySalt);
SetCurrentContextIndexTLS(-1);
{
SCOPED_LOCK(s_WindowPtrsArrayLock);
s_WindowPtrs.resize(0);
s_WindowWidths.resize(0);
s_WindowHeights.resize(0);
}
{
SCOPED_LOCK(s_ContextPtrsArrayLock);
s_ContextPtrs.resize(0);
s_ContextLocks.resize(0);
#if RD_USE_CONTEXT_LOCK_COUNTS
s_ContextLocksCount.resize(0);
#endif // #if RD_USE_CONTEXT_LOCK_COUNTS
}
}
void *NSGL_createContext(void *view, void *shareContext)
{
RandomSleep();
NSView *nsView = (NSView *)view;
assert(nsView == nil || [nsView isKindOfClass:[NSView class]]);
@@ -29,17 +414,21 @@ extern "C" void *NSGL_createContext(void *view, void *shareContext)
assert(share == nil || [share isKindOfClass:[NSOpenGLContext class]]);
NSOpenGLPixelFormatAttribute attr[] = {
NSOpenGLPFANoRecovery,
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAAccelerated,
NSOpenGLPFAAllowOfflineRenderers,
NSOpenGLPFAClosestPolicy,
NSOpenGLPFAOpenGLProfile,
NSOpenGLProfileVersion4_1Core,
NSOpenGLPFAColorSize,
32,
24,
NSOpenGLPFAAlphaSize,
8,
NSOpenGLPFADepthSize,
24,
NSOpenGLPFAStencilSize,
8,
NSOpenGLPFADoubleBuffer,
NSOpenGLPFASampleBuffers,
0,
0,
};
@@ -47,7 +436,7 @@ extern "C" void *NSGL_createContext(void *view, void *shareContext)
if(pix == nil)
{
NSGL_LogText("Failed to create NSOpenGLPixelFormat");
RDCERR("CGL: Failed to create NSOpenGLPixelFormat");
return nil;
}
@@ -56,53 +445,82 @@ extern "C" void *NSGL_createContext(void *view, void *shareContext)
if(context == nil)
{
NSGL_LogText("Failed to create NSOpenGLContext");
RDCERR("CGL: Failed to create NSOpenGLContext");
return nil;
}
GLint aboveWindow = 1;
[context setValues:&aboveWindow forParameter:NSOpenGLCPSurfaceOrder];
[context setView:nsView];
[context update];
scheduleViewSetWantBestResolution(nsView);
NSOpenGLContext *nsglContext = GetNSOpenGLContext(context);
const int contextIndex = getContextIndex(nsglContext);
scheduleContextSetView(contextIndex, s_ReplaySalt, nsView);
return context;
}
extern "C" void NSGL_makeCurrentContext(void *context)
void NSGL_makeCurrentContext(void *context)
{
NSOpenGLContext *nsglContext = (NSOpenGLContext *)context;
assert([nsglContext isKindOfClass:[NSOpenGLContext class]]);
RandomSleep();
const int currentThreadContextIndex = GetCurrentContextIndexTLS();
if(currentThreadContextIndex >= 0)
UnLockContext(currentThreadContextIndex);
NSOpenGLContext *nsglContext = GetNSOpenGLContext(context);
const int contextIndex = getContextIndex(nsglContext);
SetCurrentContextIndexTLS(contextIndex);
LockContext(contextIndex);
[nsglContext makeCurrentContext];
scheduleContextUpdate(contextIndex, s_ReplaySalt);
}
extern "C" void NSGL_update(void *context)
void NSGL_update(void *context)
{
NSOpenGLContext *nsglContext = (NSOpenGLContext *)context;
assert([nsglContext isKindOfClass:[NSOpenGLContext class]]);
[nsglContext update];
RandomSleep();
NSOpenGLContext *nsglContext = GetNSOpenGLContext(context);
const int contextIndex = getContextIndex(nsglContext);
scheduleContextUpdate(contextIndex, s_ReplaySalt);
}
extern "C" void NSGL_flushBuffer(void *context)
void NSGL_flushBuffer(void *context)
{
NSOpenGLContext *nsglContext = (NSOpenGLContext *)context;
assert([nsglContext isKindOfClass:[NSOpenGLContext class]]);
RandomSleep();
NSOpenGLContext *nsglContext = GetNSOpenGLContext(context);
const int contextIndex = getContextIndex(nsglContext);
LockContext(contextIndex);
[nsglContext flushBuffer];
UnLockContext(contextIndex);
}
extern "C" void NSGL_destroyContext(void *context)
void NSGL_destroyContext(void *context)
{
RandomSleep();
@autoreleasepool
{
NSOpenGLContext *nsglContext = (NSOpenGLContext *)context;
assert([nsglContext isKindOfClass:[NSOpenGLContext class]]);
NSOpenGLContext *nsglContext = GetNSOpenGLContext(context);
const int contextIndex = getContextIndex(nsglContext);
const int currentThreadContextIndex = GetCurrentContextIndexTLS();
if(currentThreadContextIndex == contextIndex)
{
SetCurrentContextIndexTLS(-1);
UnLockContext(contextIndex);
}
LockContext(contextIndex);
[nsglContext makeCurrentContext];
[nsglContext clearDrawable];
[nsglContext update];
[nsglContext release];
UnLockContext(contextIndex);
{
SCOPED_LOCK(s_ContextPtrsArrayLock);
s_ContextPtrs[contextIndex] = nil;
#if RD_USE_CONTEXT_LOCK_COUNTS
RDCASSERT(0 == s_ContextLocksCount[contextIndex]);
#endif //#if RD_USE_CONTEXT_LOCK_COUNTS
}
}
}
-4
View File
@@ -198,8 +198,6 @@ struct GLWindowingData
ctx = NULL;
wnd = NULL;
pix = NULL;
layer = NULL;
}
union
@@ -210,8 +208,6 @@ struct GLWindowingData
void *wnd; // during capture, this is the CGL window ID. During replay, it's the NSView
CGLPixelFormatObj pix;
void *layer; // during replay only, this is the CALayer
};
#define DECL_HOOK_EXPORT(function) \