How can I programmatically switch to openGL in Sprite Kit to adopt to older devices like iPad2
Asked Answered
E

1

5

I am developing a game using Sprite Kit. Since iOS9 SpriteKit uses Metal as shader backend. SK and the shaders work great. But if I test it on an iPad2, it's not working anymore. I've read about this problem and know that iPad2 is not supporting Metal. Now I would like to fall back to open GL to provide GLES shaders in this case.

I can programmatically test if "Metal" is available like this (Swift 2):

    /**
     Returns true if the executing device supports metal.
     */
    var metalAvailable:Bool {

        get {

            struct Static {

                static var metalAvailable : Bool = false
                static var metalNeedsToBeTested : Bool = true
            }

            if Static.metalNeedsToBeTested {

                let device = MTLCreateSystemDefaultDevice()
                Static.metalAvailable = (device != nil)
            }
            return Static.metalAvailable
        }
    }

I know that it's possible to set a compatibility mode in the plist of the application:

  • Edit your app's Info.plist
  • Add the PrefersOpenGL key with a bool value of YES

In this case SpriteKit always uses openGL. This is not what I would like to use. I want my application to always use metal and just fall back to openGL if no Metal device was detected.

Is there any option in SpriteKit or UIKit or somewhere in the APIs where I can switch to the "PrefersOpenGL"-option programmatically?

Thanks in advance,

Jack

Eiten answered 29/2, 2016 at 12:39 Comment(6)
Hmm good question, you would think that it would do it by default since Metal is not supported on anything less than A7 CPUMultiphase
Not sure, but maybe you can test for Metal in a precompiler, which might be able to adjust the plist? Out of curiosity, what does "it's not working anymore" mean? Are you referring to shaders, or is nothing being displayed? Note: These are just ideas, I don't have experience with this.Introspect
Some symbols, like vec2 uv = v_tex_coord; (Metal) and vec2 uv = gl_FragColor.xy) / size.xy; are different. I did some rendering in my Metal shaders which generates smooth plasma. On iPad2 just an opaque background color is being shown.Eiten
An another example is: gl_FragColor = vec4(1.0, 1.0, 1,0, 1.0); Is just ignored. There is no shader output on iPad2.Eiten
I would expect the GL fallback to just work. Please file a radar at bugreport.apple.com with as much detail as possible.Asepsis
Wait! My mistake. gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); works! I forgot to clean the project before compiling. It was still the old shader in the built bundle. So that means it works. after testing I found out that SpriteKit indeed fell back to openGL automatically. That means that the error was in my shader code. I am trying to figure this out exactly. If I found the problem I will post a complete answer how to treat old devices.Eiten
E
8

Summary

I found the solution. At the end it was my mistake. SpriteKit is definitely automatically falling back to openGL. The GLES shader language is less forgiving than Metal. This is where the problem came from. In openGL-shaders you MUST set the decimal point in each number. Unfortunately the shader compiler did't told me that after compiling. Another issue was that sometimes old shader builds stick with the bundle. Perform "clean" before testing a shader.

So this is how to deal with both kinds of shaders and detecting Metal / openGL:

Detect if Metal is available

This small helper can be placed anywhere in your code. It helps you to detect Metal at the first usage and gives you the opportunity to execute custom code depending on the configuration once.

Headers:

#import SpriteKit
#import Metal

Code:

/**
 Detect Metal. Returns true if the device supports metal.
 */
var metalAvailable:Bool {

    get {

        struct Static {

            static var metalAvailable : Bool = false
            static var metalNeedsToBeTested : Bool = true
        }

        if Static.metalNeedsToBeTested {

            Static.metalNeedsToBeTested = false
            let device = MTLCreateSystemDefaultDevice()
            Static.metalAvailable = (device != nil)
            if Static.metalAvailable {

               // Do sth. to init Metal code, if needed
            } else {

               // Do sth. to init openGL code, if needed
            }
        }
        return Static.metalAvailable
    }
}

Create shader in Sprite Kit

Create the shader as usual using sprite kit.

let shaderContainer = SKSpriteNode()
shaderContainer.position = CGPoint(x:self.frame.size.width/2, y:self.frame.size.height/2)
shaderContainer.size = CGSize(width:self.frame.size.width, height:self.frame.size.height)
self.backgroundNode.addChild(shaderContainer)

let bgShader:SKShader
// Test if metal is available
if self.metalAvailable {

  bgShader = SKShader(fileNamed:"plasma.fsh")
} else {

  NSLog("Falling back to openGL")
  bgShader = SKShader(fileNamed:"plasmaGL.fsh")
}

// Add your uniforms. OpenGL needs the size of the frame to normalize
// The coordinates. This is why we always use the size uniform        
bgShader.uniforms = [
   SKUniform(name: "size", floatVector2:GLKVector2Make(1920.0, 1024.0))
]

shaderContainer.shader = bgShader

As you can see depending on the detected configuration another shader file is being loaded. The openGL shaders need an additional uniform for the size, because the symbol v_tex_coord is not available in openGL. If you don't use the size uniform in Metal, you can move the uniforms statement into the if block or just ignore it. Metal is not complaining if you don't use it.

Metal shader: plasma.fsh

#define  M_PI 3.1415926535897932384626433832795
#define frequency  1 // Metal is less sensitive to number types. 
#define colorDepth 2 // Numbers without decimal point make problems with openGL
void main(void) {

    vec2 uv = v_tex_coord; // Normalized coordinates in Metal shaders

    float red = ((sin((uv.x + u_time * 0.01) * M_PI * frequency) * cos((uv.y + u_time * 0.03) * M_PI * frequency) + 1) / colorDepth) + (colorDepth / 2.75) - (2 / 2.75);

    gl_FragColor = vec4(red, uv.x, u_time, 1.0);
}

In Metal shaders you can simply read normalized coordinates. You can use the size to reconstruct the image coordinates if you like. However Metal is more forgiving with decimal points. As you can see, some numbers don't have decimal points here.

Open GL shader: plasmaGL.fsh

// OPEN GL shaders NEED the decimal point in numbers. so never use 1 but 1. or 1.0
#define  M_PI 3.1415926535897932384626433832795
#define frequency  1.0  // This number must have a decimal point
#define colorDepth 2.0  // Same here.
void main(void) {

    vec2 uv = gl_FragCoord.xy / size.xy; // Frame coordinates in openGL

    // This formula is always using numbers with decimal points. 
    // Compare it to the metal shader. Two numbers of the metal
    // have no decimal point. If you cut copy paste the metal shader 
    // formula to the GL shader it will not work!
    float red = ((sin((uv.x + u_time * 0.01) * M_PI * frequency) * cos((uv.y + u_time * 0.03) * M_PI * frequency) + 1.0) / colorDepth) + (colorDepth / 2.75) - (2.0 / 2.75);

    gl_FragColor = vec4(red, uv.x, u_time, 1.0);
}

Outlook

It is more work to test for both systems and creating two shaders. But as long as we are transitioning from GL to Metal this is a good method to test which kind of shader should be used. The iOS simulator doesn't support Metal, too. That means you can test the openGL behavior with the iOS and tvOS simulator.

If you develop for AppleTV then this approach is really handy, because the openGL shaders always work with Metal. You just need to replace gl_FragCoord.xy / size.xy with v_tex_coord. If you run the code on the simulator, you will see the openGL code, if you run it on the AppleTV target you'll see the smooth Metal shaders.

And another hint to all swift developers: Never ever forget the semicolon at the end of the line with shaders ;-)

Another trap is casting.

Metal: int intVal = (int) uv.x; float a = (float) intVal;

Open GL: int intVal = int(uv.x); float a = float(intVal);

I hope I could help anyone.

Cheers,

Jack

Eiten answered 1/3, 2016 at 13:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.