Looking for larger OpenGL applications to test with I chose the Unity Editor application. Had a short detour to work out how to get Unity Editor to run in OpenGL mode instead of Metal, turns out the Apple Silicon version of Unity Editor (and standalone) only support the Metal API. Running the Unity Editor on an Intel Mac supports the OpenGL API. Not much to say except it worked first time, capturing Unity Editor from RenderDoc Mac and then replaying it on Intel or Apple Silicon RenderDoc UI. Here are some screenshots of it replaying in RenderDoc UI:
I have not experienced any crashes or problems during the capturing or replay.
The next steps are to review the changes to support running Apple UI calls on the main thread and open a PR to upstream them.
Whilst using the remote server feature for renderdoccmd, I tried out a few more of the command line tool options to see if they worked on Apple.
The thumb option would generate an image preview from the replay of a capture. It just worked ie.
Next was the replay option which should display a window showing the preview from the replay of the capture. This required platform specific implementation of this method
I had various bits of Cocoa window handling code from the nuklear_cocoa implementation that was done to support the RenderDoc Mac automated tests. This requirement was simpler, it only needed a window, the GL context and drawing would all be handled by the renderdoc library code.
I started from scratch and ended up implementing these APIs
The RenderDoc library has keyboard support ie. F12 to trigger a capture in the hooked application. This had not been implemented in the Mac version. Locally I had code on an old branch with RenderDoc Mac capture key support. This implementation was very simple and queried the keyboard directly which worked but it meant pressing F12 on a different application would also trigger a capture ie. the keyboard events were not focussed to the application. Did some Apple API searching using the Dash tool and found something that looked like it would do what I wanted, which was to add an event listener to an existing application/windowaddLocalMonitorForEventsMatchingMask.
It did what it said on the tin and just worked. The Apple specific code ended up being straightforward and is now in PR review for upstreaming.
The non-Apple specific code is more lines of code because have to translate between the RenderDoc key codes and the Apple virtual key codes which means lines of code like this:
switch(key)
{
case eRENDERDOC_Key_A: appleKeyCode = kVK_ANSI_A; break;
case eRENDERDOC_Key_B: appleKeyCode = kVK_ANSI_B; break;
case eRENDERDOC_Key_C: appleKeyCode = kVK_ANSI_C; break;
case eRENDERDOC_Key_D: appleKeyCode = kVK_ANSI_D; break;
case eRENDERDOC_Key_E: appleKeyCode = kVK_ANSI_E; break;
case eRENDERDOC_Key_F: appleKeyCode = kVK_ANSI_F; break;
case eRENDERDOC_Key_G: appleKeyCode = kVK_ANSI_G; break;
case eRENDERDOC_Key_H: appleKeyCode = kVK_ANSI_H; break;
case eRENDERDOC_Key_I: appleKeyCode = kVK_ANSI_I; break;
case eRENDERDOC_Key_J: appleKeyCode = kVK_ANSI_J; break;
case eRENDERDOC_Key_K: appleKeyCode = kVK_ANSI_K; break;
It would be nice if OS’s could agree on keyboard scancodes and give applications a unified cross-platform value to test against.
I wanted to test RenderDoc Mac GL with a larger project than the small samples I had been using A Unity application made sense to me. I am familar with using Unity on Mac and OpenGL. Hit a stumbling block the Apple Silicon support in Unity does not support OpenGL (it is Metal only). This meant I needed to use an Intel version of the RenderDoc library for capturing and replay. This turned out to be an opportunity to test/use/verify on Mac the RenderDoc remote server feature. I had never needed to use this feature on desktop (only used it when using RenderDoc on Android platform).
I switched to using an Intel Mac. For the Unity test project I downloaded the Unity Learn Karting Microgame. Changed the project to use the OpenGL API and built the standalone application. On the Intel Mac compiled the RenderDoc Mac GL code from the development branch. Starting the remote server on the Intel Mac was straight forward “renderdoccmd remoteserver“.
On the ARM Mac in the RenderDoc UI connected to the remote server via its IP address ie. Tools menu, Manage Remote Servers. From the UI could then launch applications remotely as if they were local (had to accept some Apple dialogs on the Intel machine about granting access to certain folders) and then it all just worked
I also made captures locally on the Intel Mac using RenderDoc UI and transferred those capture files to the ARM Mac and replayed them locally on the ARM Mac without any problems.
Here is an example of replaying locally on ARM Mac (with the original capture being made on Intel Mac)
A couple of small Apple specific RenderDoc PRs have already been upstreamed (thanks to Baldur for reviewing and merging them). I wanted to do more testing before making a PR with the threading fixes for RenderDoc Mac GL. The change is currently isolated to Apple specific files in the RenderDoc platform code (the gl_common.h change is deleting some Apple specific code) ie.
To get some OpenGL test cases I used the OpenGL Samples Pack project. First hurdle was the FreeImage static library included in the project was compiled for Intel. I am working on ARM (M1 Mac Book Air [which is very nice BTW]). Solved this by getting the FreeImage source (at the correct version) and then modifying its makefiles to build an ARM64 target version of the static library and include that in the universal static library (Apple has a nice system for this using the lipo tool).
I tested RenderDoc Mac GL using about 30 different OpenGL samples. If the sample worked outside of RenderDoc (not all of the samples work on Apple OpenGL) then I was able to capture and replay the sample without significant errors/problems and the replay results matched the result of running the sample outside of RenderDoc. This process was about an hour long with a single launch of RenderDoc UI and then launching and capturing and replaying one GL sample after another.
During the testing I got errors from Qt when docking panes which is a specific Qt message and it seems harmless (I think this error is not specific to RenderDoc Mac version). I got one replay error where the shader did not bind for the postvs and I also got one crash in Apple dispatch_async method which did not make any sense. I was not able to reproduce either of these crashes or replay errors.
These OpenGL samples are unit test focussed and not very demanding. Some of the samples wrote to the depth buffer and it was good to see the depth buffer working during replay and visible in the UI.
During testing and using RenderDoc Mac UI with Vulkan captures I discovered a 100% crash which happened the second time a Vulkan capture was opened. The message in Xcode was
2021-03-25 06:22:15.603812+0000 qrenderdoc[62009:956064] *** -[CaptureMTLDepthStencilState retain]: message sent to deallocated instance 0x6008ae823fd0
I could not recreate this outside of Xcode and I traced it down to having "GPU Frame Capture" enabled in Xcode. Changing that option to Disabled and now the crashes have gone away.
I have seen various changes in behaviour/crashes/asserts/errors when having Xcode GPU Frame Capture enabled when running with the MoltenVk driver.
On RenderDoc Mac GL the layer bounds do not get automatically updated when the view resizes. This means have to get the window size from the view object, which is not allowed on the Replay thread.
To solve this implemented a simple caching system with the Replay thread reading the width and height from a cache keyed by the NSView pointer address. The cache is updated on the main thread via a dispatch_async call. The data ownership between the main thread and the Replay thread is clear with the Replay thread only ever reading from the cache and the main thread only ever writing to the cache. In addition the Replay thread is responsible for choosing which cache entry that the main thread should update. This simplicity and clarity of data ownership per thread is intended to avoid any threading problems ie. race conditions. The code ended up being nice and simple:
static void viewGetWindowSizeMT(NSView *view, int index)
{
CALayer *layer = view.layer;
assert([layer isKindOfClass:[CALayer class]]);
const NSRect contentRect = [view frame];
const CGFloat scaleFactor = layer.contentsScale;
assert(index >= 0 && index < s_WindowCount);
int width = contentRect.size.width * scaleFactor;
int height = contentRect.size.height * scaleFactor;
s_WindowWidths[index] = width;
s_WindowHeights[index] = height;
}
void getAppleWindowSize(void *viewHandle, int &width, int &height)
{
NSView *view = (NSView *)viewHandle;
assert([view isKindOfClass:[NSView class]]);
int index = getWindowIndex(view);
if (index == -1)
{
index = s_WindowCount;
assert(index >= 0 && index < MAX_WINDOWS_COUNT);
s_WindowIndexes[index] = view;
s_WindowWidths[index] = 0;
s_WindowHeights[index] = 0;
++s_WindowCount;
}
width = s_WindowWidths[index];
height = s_WindowHeights[index];
dispatch_async(dispatch_get_main_queue(), ^(void)
{
viewGetWindowSizeMT(view, index);
});
}
As an unexpected bonus this has fixed the problem on RenderDoc Mac GL where resizing the thumbnail pane did not update the rendering views to the correct aspect ratio.
With both of this changes I have not been able to trigger the Xcode main thread checker asserts when using RenderDoc Mac with GL or Vulkan replays.
My setup for debugging RenderDoc Mac using Xcode is to have Xcode launch qrenderdoc and automatically attach to it. When I first started doing this I had a problem where the process would quit with an error about unknown command line argument ie.
QTRD 057176: [04:33:05] qrenderdoc.cpp( 197) - Error - "Unknown options: N, S, D, o, c, u, m, e, n, t, R, e, i, s, i, o, n, s, D, e, b, u, g, M, o, d, e."
Until today I had worked around this problem by commenting out the code which would quit if an unknown command line arugment was found.
I was a bit confused about why Xcode was adding extra command lines which were not set in the project and looking at the argc, argv parameters passed to main there are three arguments, the extra two arguments being passed are
-NSDocumentRevisionsDebugMode
YES
WIth a bit of internet searching tracked down the source of the extra command line arguments to the "Allow debugging when browsing versions" option in the Xcode Run options for the qrenderdoc target
Disabling that option prevents the extra command line arugments being added and makes the debugging process much smoother without an interuption on every launch of qrenderdoc from Xcode.
After long and fruitful discussions with Baldur it was clear the previous approach or any approaches which required changes to the RenderDoc interfaces or RenderDoc common code is not desired and would not be able to be upstreamed. Today I tried a new approach which is entirely in the RenderDoc CGL Platform layer.
To recap, the challenge is from Replay thread in the RenderDoc CGL Platform layer there are some Apple UI Object function calls which must be made on the main thread i.e.
The challenge was the data context and view are private to and owned by the RenderDoc CGL Platform layer and there is no way to expose that to the main thread without changing the RenderDoc interfaces (which is undesirable).
The Apple main thread dispatch offers a solution to this using dispatch_async ie.
The problem is the context object can be in use on the Replay thread and there is a race condition here if the main thread calls methods on this object whilst the Replay thread is using it.
The current solution is to track the active context object on the Replay thread and then on the main thread if the dequeued context object is in use by the Replay thread, the main thread command is rescheduled. This idea of rescheduling the main thread command is a polling type approach instead of locking approach which is intended to avoid a potential deadlock between the main and Replay thread.
The current simplistic approach uses a mutex lock around the tracking of the active context object to prevent the Replay thread switching to the same context that the main thread is operating on.
The breakthrough which made this happen was the consistency of the RenderDoc Gl rendering implementation which calls a method to make the platform context the current context before doing any rendering calls (without that consistency this approach would not work).
The changed files are here and all restricted to RenderDoc CGL Platform specific code or Apple specific code in the RenderDoc command code.
Did a quick experiment using the Apple dispatch API by adding this code block to the RenderDoc CGL platform code in the NSGL_createContext objective-C method which is called on the Replay thread
The method worked first time and saw this output in the RenderDoc diagnostic log (MT is the result of NSThread.isMainThread on the execution thread and QT is the result of NSThread.isMainThread on the submission thread).
Next steps are to work out the relationship with ReplayManager::run to explore how to make this work. I am thinking about adding an API named something like “start rendering”, “end rendering” to IReplayController, which would be implemented in the platform layer and would be null operation on most platforms. The start and end calls would be called in the ReplayManager::run method and would prevent the replay manager loop from executing commands at the same time as the main thread is executing UI dispatch commands. The main thread dispatch blocks could do a Try Get Lock to guarantee executing commands only when the replay thread is idle. The one piece I need to get clear in my mind is how to reschedule the commands if the replay thread is not idle. I want to try and use a polling and reschedule approach to avoid deadlocks.
I still need to debug is renderdoccmd replay to see if it runs on the main thread or a helper thread. Right now renderdoccmd replay does not do much on Mac because the Mac window code has not been implemented in it.
As an aside I was looking at a drawio extension to Visual Studio Code which looks like it makes it quite simple to create diagrams and integrate them live into markdown documents in Visual Studio Code. I am going to give it a go to draw out the threading model design for RenderDoc main thread and replay thread.