Toggling iOS Render Debug Options Without a Computer
Have you ever been away from your computer, playing around with a feature on your app, and spot a performance issue that you want to get some insights into right at that moment?
Some of the tools that we depend on for debugging these issues, like Instruments, are sufficiently powerful that I don’t blame them for requiring a separate machine to use.
Others, though, don’t have that same excuse — like the incredibly useful Render Debug Options (such as Color Blended Layers or Color Offscreen-Rendered). Though these are enabled through Xcode, they’re essentially just toggling a flag on the device itself, so it seems reasonable that we can cut Xcode out of the picture entirely.
Note: The timing of this post might seem a bit off, as being away from a computer is less viable now than usual.
As a counterpoint, I’d like to mention that WWDC is in less than a week away, and I feel that posting this now slightly increases the chance that by sheer bad luck, this becomes officially supported and this post is made redundant — which would still be a fine outcome overall.
CARenderServerSetDebugOption
When you toggle one of the render debug options in Xcode,
the end result is a call to CARenderServerSetDebugOption
on your device or simulator.
This function takes in three parameters:
CARenderServerSetDebugOption(
0, /* render server mach port */
0x02, /* debug option (Color Blended Layers) */
1 /* new value */
)
CARenderServerSetDebugOption
works by communicating with iOS’ Render Server, a separate process
that handles… well, rendering.
The first parameter here is a mach port that allows other processes to communicate with that server. You can get
the appropriate port yourself by first calling CARenderServerGetServerPort
, but you can
also pass 0
to have CARenderServerSetDebugOption
look it up for you.
The remaining two parameters are fairly straightforward — we pass an integer value that
represents the option we want to update, and then its new value (either 0
or 1
).
Here are the options that Xcode allows you to toggle:
Color Blended Layers: 0x02
Color Hits Green and Misses Red: 0x13
Color Copied Images: 0x01
Color Layer Formats: 0x14
Color Immediately: 0x03
Color Misaligned Images: 0x0E
Color Offscreen-Rendered Yellow: 0x11
Color Compositing Fast-Path Blue: 0x12
Flash Updated Regions: 0x00
Setting Options from a Simulator
Since CARenderServerSetDebugOption
is not a public function,
we need to look it up at runtime. We can do this using
dlsym
,
which allows us to find the address of a symbol within a given
dynamic library (in this case, QuartzCore):
// The location of the QuartzCore
// framework on iOS
let quartzCorePath =
"/System/Library/Frameworks/" +
"QuartzCore.framework/QuartzCore"
// Use `dlopen` to get a handle
// for the QuartzCore framework
let quartzCoreHandle = dlopen(
quartzCorePath,
RTLD_NOW)
// Store the address of our function
let functionAddress = dlsym(
quartzCoreHandle,
"CARenderServerSetDebugOption")
// Create a typealias representing our
// function's param types / return type
typealias functionType = @convention(c) (
CInt, CInt, CInt
) -> Void
// Cast the address to the
// above function type
let CARenderServerSetDebugOption = unsafeBitCast(
functionAddress,
to: functionType.self)
// Call the function!
CARenderServerSetDebugOption(0, 0x2, 1)
If you call the above code in an app running on the Simulator, you’ll see the Color Blended Layers option take effect, with your app now painted in a sea of red & green.
Replacing the second parameter in the call to
CARenderServerSetDebugOption
with other values from the list above
allows you to toggle other options instead — including ones that are usually
only exposed for devices rather than simulators (though not all work perfectly).
Setting Options from a Device
Entitlements
The above code will run without any errors or logs on a physical device — but
it won’t actually have any effect. That’s because the binary calling this function
must include the com.apple.QuartzCore.debug
entitlement.
It’s easy to add this entitlement for use on a jailbroken device using ldid or jtool), but unfortunately, this does limit our ability to use this function on standard devices.
Note: It might also be possible to use an entitlement bypass here, but I haven’t had much luck with this particular entitlement. Part of the issue might be that it’s still unclear to me precisely where this entitlement is actually checked — a question for a different day.
Might As Well Build A Preference Bundle
Since we’re essentially limited to jailbroken devices here, we might as well switch to controlling this via Settings, since that feels a bit more natural than keeping some random app around to toggle render options with.
PreferenceLoader provides an easy way to do just that; it allows us to create our own preference pane with full control over the view controller it displays, which is perfect for our use case.
We can start by creating a subclass of
PSListController
,
provided by iOS’ Preferences.framework
.
This provides a lot of conveniences for us, including the ability to easily create new switch cells using
PSSpecifier
instances, each of which describes an individual preference.
As an example, the below setup will create a preference pane with a single switch, along with the ability to run arbitrary code when that switch is toggled:
#import <Preferences/PSListController.h>
#import <Preferences/PSSpecifier.h>
// Create a `PSListController` subclass
@interface ExampleListController: PSListController
@end
@implementation ExampleListController
// Override `viewDidLoad` to setup our specifiers,
// which describe each preference we support
- (void)viewDidLoad {
[super viewDidLoad];
// Specifier for a switch labeled "Basic Switch"
// that calls our setter below when toggled
PSSpecifier *specifier =
[PSSpecifier preferenceSpecifierNamed:@"Basic Switch"
target:self
set:@selector(valueChanged:forSpecifier:)
get:nil
detail:Nil
cell:PSSwitchCell
edit:Nil];
// Update our controller's specifiers list
self.specifiers = [NSMutableArray arrayWithObject:specifier];
}
// Setter called when our switch is toggled
- (void)valueChanged:(NSNumber *)value forSpecifier:(PSSpecifier *)specifier {
// Logs "Specifier 'Basic Switch' changed to 1" (or 0)
NSLog(@"Specifier '%@' changed to %i",
specifier.name,
value.boolValue);
}
@end
From here, we can easily create one PSSpecifier
for each render debugging option that
we want to expose, and then call CARenderServerSetDebugOption
in our setter to enable the option.
We can even add a getter that uses CARenderServerGetDebugOption
to ensure our preference pane always
reflects the current state when relaunched.
The End Result
With this setup, we can build our full preference pane, and I’m fairly happy with the end result:
If you’re interested in the resulting code, or want to try it on your own device, you can find the project on GitHub.
An idea for a Better End Result
One thing I wanted to try earlier on was creating an application that provided info & examples for each of the render debug options.
For example, you could have a detail view controller
with a paragraph or two about offscreen rendering, and then a scrollable list of examples
where it might come into play — like two side-by-side rectangles with a shadow applied, but one
using shadowPath
to avoid offscreen rendering — and a short blub about the difference. When running
on a simulator or jailbroken device, you could add switches to enable or disable the effect
and see the difference for yourself.
I thought that would be a fun app; both for educational purposes, and to have an easy reference for some common performance issues; but I realized that not only do I not know enough details about some of the newer options (like “Color Layer Formats”), I am also unable to find any good sources for learning about them!
If you’re aware of a good reference for these options, or know enough about them yourself and are willing to share — please let me know!
Say Hello!