SpriteKit: performance hit while preloading SKTextureAtlas
Asked Answered
M

1

8

I'm experiencing a performance hit when preloading SKTextureAtlas:

let textureAtlas = SKTextureAtlas(named: atlasName)
textureAtlas.preload(completionHandler: {
    ...
})

By performance hit, I mean FPS dropping to ~50 for a short amounts of time.

I tested it with Time Profiler in Instruments and verified that this work is indeed being done on a worker thread, like stated in documentation.

The image bellow shows a Time Profiler capture of the spike, caused by preloading atlas. As you can see, most of the spike is caused by 2 worker threads, which all seem to be loading image data, as far as I can understand. However, this should not cause a performance hit on the main thread IMHO.

enter image description here

Note 1: The .spriteatlas I'm preloading is not that big: 4 assets with approx. 1000x1000 size.

Note 2: I'm testing with iPhone 6, iOS 10, Xcode 8.

Note 3: There is no other substantial work being done at the same time; CPU is hovering at ~30% all the time. Same goes for GPU.

Note 4: The atlas preload is requested way before any of the textures from that atlas is needed, so it should have more than enough time to preload.

Any help/direction greatly appreciated!


UPDATE

Complete code block where the preload happens:

let updateGroup = DispatchGroup()

for assetDefinition in assetContainmentDefinitions {

    let assetName = assetDefinition.assetName

    // Check if asset is not needed anymore and clear the cache with it
    if progress >= assetDefinition.range.to {
        if cachedAssets[assetName] != nil {
            cachedAssets[assetName] = nil
        }
    }
    // Check if asset is needed and if it's not already loading then preload and cache it
    else if progress >= assetDefinition.range.from {

        if currentlyLoadingAssets.contains(assetName) == false &&
            cachedAssets[assetName] == nil {

            currentlyLoadingAssets.append(assetName)

            // Enter dispatch group
            updateGroup.enter()

            switch assetDefinition.assetType {
            case .textureAtlas:

                let textureAtlas = SKTextureAtlas(named: assetName)
                textureAtlas.preload(completionHandler: {

                    DispatchQueue.main.async { [weak self] in

                        self?.cachedAssets[assetName] = textureAtlas
                        self?.currentlyLoadingAssets.remove(object: assetName)

                        // Leave dispatch group after preload is done
                        updateGroup.leave()
                    }
                })

            case .texture:

                let texture = SKTexture(imageNamed: assetName)
                texture.preload(completionHandler: {

                    DispatchQueue.main.async { [weak self] in

                        self?.cachedAssets[assetName] = texture
                        self?.currentlyLoadingAssets.remove(object: assetName)

                        // Leave dispatch group after preload is done
                        updateGroup.leave()
                    }
                })
            }
        }
    }
}

// Call completion after the dispatch group is fully completed
if let completion = completion {
    updateGroup.notify(queue: DispatchQueue.main, execute: completion)
}

UPDATE 2

I've created an empty project with nothing but atlas preload block. The performance drop still occurs. I've tried with multiple atlases, even with atlas with only one asset.

I've also tried what @Sez suggested (see bellow), in this empty new project, but in that case the completion block didn't even get called, which seems like another bug in SKTextureAtlas class. (?!)

let atlas = SKTextureAtlas(dictionary: ["texture1": UIImage(named: "texture1")!])

atlas.preload(completionHandler: { [weak self] in
    print("COMPLETION BLOCK NOT CALLED")
    self?.cachedAtlas = atlas
})

UPDATE 3

I tried deleting the .spriteatlas and creating the .atlas with the same textures and there is (almost) no performance hit. However, .atlas doesn't support slicing which is why I want to use .spriteatlas in the first place.

Mimosaceous answered 27/11, 2016 at 22:22 Comment(10)
I do not think it is your atlases that are the problem, you have something else going on, code will be neededKaikaia
@Kaikaia please see update above. There really isn't anything much to it. I call this block from a main thread, wait for a completion to be fired and that's it.Mimosaceous
you need to diagnose mutliple things. First, start a new project with literally nothing else going on... Then, try running your thread and see if performance drops. Then, take out the multithreading call in your main project and see if the performance drops. Then, make the multithreading call, but with no actions (remove the atlases). (see second comment due to length)Googly
We don't see anything particularly damaging in your function above. The only other idea I have is to overload all of the built-in steps in SK cycle and check delta-time for each (to see which one is lagging / taking the longest) developer.apple.com/library/content/documentation/…Googly
If you can run those 3 tests, or post more of your code, then we can maybe have enough info to give a definitive answer. Hope this helps!Googly
@Googly I created a new, empty SpriteKit project with just atlas.preload(completionHandler:) block and I still experience the same issue. Tried with multiple different atlases.Mimosaceous
Ouch. This is looking like a bug. Perhaps a workaround would be to just use textures eep... or you could create functions that create atlases on the fly from a list of textures, instead of trying to load premade atlases. I would hope that there would be a better workaround for this. Sometimes typecasting works, or using specifics.. I'm trying to think if any of this could apply here...Googly
spitballing, what if you wrapped all of this up into a function, then threaded the function, with the atlas load as a third thread? Or does your main-thread NEED this info right away? Also, aren't there other types/patterns of threading? Perhaps one of those could work.Googly
@Googly See my update 2, where I explain that creating texture atlas on the fly did not work. Can you be more exact with your last comment? I already tried putting everything on a background queue and it doesn't help.Mimosaceous
@Googly See my update 3.Mimosaceous
F
6

There are issues plaguing SKTextureAtlas (Apple Dev forums post).

Try creating your texture atlas using the SKTextureAtlas:withDictionary: supplying a dictionary of [String:UIImage], a method described on another StackOverflow post and see if that helps.

UPDATE: If you want that preload of the SKTexture to happen in a background thread, I'd write that specifically rather than relying on the preload convenience function.

let atlas: SKTextureAtlas
let myImages: [String: UIImage] = getImages()
let globalQueue = DispatchQueue.global()
globalQueue.async {
    let atlas = SKTextureAtlas(withDictionary:myImages)
    for (k,v) in myImages {
        let tex = atlas.textureNamed(k)
        print(tex.size()) // force load
    }
    DispatchQueue.main.async {
        completionHandler(atlas)
    }
}

Regarding app slicing, as long as you place assets in Asset catalogs they should be sliced. After you upload a build to iTunesConnect you can see a report of the App Store sizes reflecting this.

iTunesConnect app store sizes

I have a video I recorded of a live stream where I show this import process. Sorry its a bit agonisingly long (2hrs+), but the link here goes right to the moment where the import into the sprite sheet happens.

https://youtu.be/Ic9Wnux8vd8?t=1h17m

Doing it the way I show here you get a sprite atlas, with sliced images. My scripts (which I demonstrate earlier in the clip) are on my GitHub if you want them.

Forgat answered 30/11, 2016 at 6:20 Comment(2)
Seriously, Apple still hasn't sorted out sprite textures... for something called SPRITEkit?Arianism
You should see the mess that On-Demand Resources is in! Sprite textures are a dream by comparison. :-)Forgat

© 2022 - 2024 — McMap. All rights reserved.