SpriteKit Repeat Shader

Jun 11, 2016

SpriteKit is an OpenGL rendering environment that composes multiple rectangular regions into a single framebuffer. SpriteKit provides a high performance render path and does many other things well. But, SpriteKit has some missing features and performance issues related to shaders. See previous blog post for background info on using a shader in SpriteKit. This post presents GLSL source code to tile fill a node using a custom shader. In addition, a SpriteKit performance issue discovered under iOS 9 is examined.

Repeated Textures

A repeating texture can be used in a number of different applications. In a 3D rendering environment, a black and white tiled image can be stretched over the surface of a globe.

    Tiled Image

In a 2D rendering environment, a single small image can be repeated in a rectangular area. For example, assume this cat image is used as a tile source:

    Tiled Cat Image

When a 10 x 8 tiled rectangle is filled, the output would look like:

    Tiled Cat Image

CoreGraphics Implementation

Before discussing an effective implementation, let's examine a bad approach and show why it is not useful.

The approach described above will generate an image that properly shows the tiled image at native resolution. This approach has been implemented using SpriteKit and the CGContextDrawTiledImage() API. Working source code can be found at this github project.

The problem with an approach based on using the CGContextDrawTiledImage() API is excessive memory usage. The example running on a retina iPad at 2048 x 1536 will result in a texture that consumes about 12 megs of memory.

SpriteKit Shader Implementation

A custom shader can be used to address the excessive memory use in the previous implementation. The shader can repeatedly render the tile pattern into a SpriteKit node before the composition stage. The repeat shader GLSL source code is the following:

precision highp float;
void main(void) {
  vec2 oneOutputPixel =
    outSampleHalfPixelOffset * 2.0;
  vec2 oneInputPixel =
    inSampleHalfPixelOffset * 2.0;
  vec2 outNumPixels =
    (v_tex_coord - outSampleHalfPixelOffset) /
    oneOutputPixel;
  outNumPixels = floor(outNumPixels + 0.5);
  vec2 modNumPixels =
    mod(outNumPixels, tileSize);
  modNumPixels = floor(modNumPixels + 0.5);
  vec2 lookupCoord = inSampleHalfPixelOffset +
    (modNumPixels * oneInputPixel);
  vec4 px = texture2D(u_texture, lookupCoord);
  gl_FragColor = px;
}

Source code for a complete working repeat shader can be found in the following github project:

A new feature in SpriteKit under iOS 9 is both an OpenGL mode and a Metal mode. Which mode is better? Let's compare the runtime performance of this tile shader under each mode.

OpenGL Performance

The OpenGL mode is fully compatible with the iOS 8 shader implementation. In the OpenGL mode, the repeat shader fills a full screen node at the full frame rate of 60 FPS.

Metal Performance

Setting PrefersOpenGL=NO in Info.plist will switch the project into Metal render mode. Note that a strange thing happens in Metal render mode. Take a look at the SpriteKit FPS output that appears in the lower right hand corner. When running in Metal mode, the shader is only able to render at 39 FPS! Yikes! This is not a complex shader and there should not be a performance problem related to accessing a small tile in GPU memory.

To investigate further, the shader project was executed in profile mode and then Metal System Trace was selected as the profiling tool. One hypothesis is that perhaps the Metal commands are being scheduled as a compute shader and that might account for the performance problems. After looking at the trace output, it seems that both vertex and fragment shader logic was being run. The compute pipeline hypothesis was not correct and it is not clear what the problem actually is.

After looking into the performance problem in Metal mode, no explanation was forthcoming. The issue is not caused by this specific shader source code as it can also been seen with other shaders. This appears to be a bug with the new Metal mode in SpriteKit under iOS 9. The short term fix is to make sure PrefersOpenGL=YES is set in Info.plist. Use of the PrefersOpenGL=YES setting assures that the shader executes with maximum performance in OpenGL mode.

Effective Implementation

The custom SpriteKit tile shader implementation provided is basically the same functionality provided by the GL_REPEAT flag in normal OpenGL code. This shader implementation shows impressive performance and uses only minimal memory at runtime. This implementation is provided under BSD license terms, so that anyone can make use of it in their own projects.