alexiscreuzot / SwiftyGif

High performance GIF engine
MIT License
1.99k stars 210 forks source link

Performance Issues on the iPhone Pro Motion Display #164

Closed KlemensStrasser closed 2 years ago

KlemensStrasser commented 2 years ago

Version: 5.3.2 Dependancy manager: SPM Xcode version: Version 13.2.1 (13C100)

Hi there,

So I recently got a new iPhone 13 Pro, which has the Pro Motion display. But when running my app on it, I notice that the performance of the GIF rendering has drastically worsen compared to my previous phone (iPhone X). The delay between the frames is too big, making the gifs run too slow and thus, feeling sluggish.

Some observations from the iOS Example Application:

Anyone else seeing the issue and any ideas how to solve it? Happy to help digging into it.

Best, Klemens

4tuneTeller commented 2 years ago

I have the same issue. The playback is exactly 2 times slower on iPhone 13 Pro than on older devices. I've also checked if the same issue would occur if I use the Kingfisher library and I can confirm that it does not.

yushdotkapoor commented 2 years ago

Same Issue here. I was a bit confused as maybe I had set the level of integrity to a low value, but that was not the case. I tested it on my iPhone X and it works as it should.

yushdotkapoor commented 2 years ago

Ok it seems like I found a workaround by tinkering a bit. I believe the issue to arise from the calculateFrameDelay function in the UIImage+SwiftyGif.swift file. It seemed to have incorrectly taken into account the 120Hz refresh rate of the ProMotion Displays. The correct function code is listed below.

ORIGINAL CODE:

    private func calculateFrameDelay(_ delaysArray: [Float], levelOfIntegrity: GifLevelOfIntegrity) {
        let levelOfIntegrity = max(0, min(1, levelOfIntegrity))
        var delays = delaysArray

        var displayRefreshFactors = [Int]()

        if #available(iOS 10.3, tvOS 10.3, *) {
          // Will be 120 on devices with ProMotion display, 60 otherwise.
          displayRefreshFactors.append(UIScreen.main.maximumFramesPerSecond)
        }

        if let first = displayRefreshFactors.first, first != 60 {
          // Append 60 if needed.
          displayRefreshFactors.append(60)
        }

        displayRefreshFactors.append(contentsOf: [30, 20, 15, 12, 10, 6, 5, 4, 3, 2, 1])

        // maxFramePerSecond,default is 60
        let maxFramePerSecond = displayRefreshFactors[0]

        // frame numbers per second
        let displayRefreshRates = displayRefreshFactors.map { maxFramePerSecond / $0 }

        // time interval per frame
        let displayRefreshDelayTime = displayRefreshRates.map { 1 / Float($0) }

        // caclulate the time when each frame should be displayed at(start at 0)
        for i in delays.indices.dropFirst() {
            delays[i] += delays[i - 1]
        }

        //find the appropriate Factors then BREAK
        for (i, delayTime) in displayRefreshDelayTime.enumerated() {
            let displayPosition = delays.map { Int($0 / delayTime) }
            var frameLoseCount: Float = 0

            for j in displayPosition.indices.dropFirst() where displayPosition[j] == displayPosition[j - 1] {
                frameLoseCount += 1
            }

            if displayPosition.first == 0 {
                frameLoseCount += 1
            }

            if frameLoseCount <= Float(displayPosition.count) * (1 - levelOfIntegrity) || i == displayRefreshDelayTime.count - 1 {
                imageCount = displayPosition.last
                displayRefreshFactor = displayRefreshFactors[i]
                displayOrder = []
                var oldIndex = 0
                var newIndex = 1
                let imageCount = self.imageCount ?? 0

                while newIndex <= imageCount && oldIndex < displayPosition.count {
                    if newIndex <= displayPosition[oldIndex] {
                        displayOrder?.append(oldIndex)
                        newIndex += 1
                    } else {
                        oldIndex += 1
                    }
                }

                break
            }
        }
    }

EDITED CODE:

     private func calculateFrameDelay(_ delaysArray: [Float], levelOfIntegrity: GifLevelOfIntegrity) {
        let levelOfIntegrity = max(0, min(1, levelOfIntegrity))
        var delays = delaysArray

        var displayRefreshFactors = [Int]()

        displayRefreshFactors.append(contentsOf: [60, 30, 20, 15, 12, 10, 6, 5, 4, 3, 2, 1])

        // maxFramePerSecond,default is 60
        let maxFramePerSecond = displayRefreshFactors[0]

        // frame numbers per second
        var displayRefreshRates = displayRefreshFactors.map { maxFramePerSecond / $0 }

        if #available(iOS 10.3, *) {
          // Will be 120 on devices with ProMotion display, 60 otherwise.
            let maximumFramesPerSecond = UIScreen.main.maximumFramesPerSecond
            if maximumFramesPerSecond == 120 {
                displayRefreshRates.append(UIScreen.main.maximumFramesPerSecond)
            }
        }
        // time interval per frame
        let displayRefreshDelayTime = displayRefreshRates.map { 1 / Float($0) }

        // caclulate the time when each frame should be displayed at(start at 0)
        for i in delays.indices.dropFirst() {
            delays[i] += delays[i - 1]
        }

        //find the appropriate Factors then BREAK
        for (i, delayTime) in displayRefreshDelayTime.enumerated() {
            let displayPosition = delays.map { Int($0 / delayTime) }

            var frameLoseCount: Float = 0

            for j in displayPosition.indices.dropFirst() where displayPosition[j] == displayPosition[j - 1] {
                frameLoseCount += 1
            }

            if displayPosition.first == 0 {
                frameLoseCount += 1
            }

            if frameLoseCount <= Float(displayPosition.count) * (1 - levelOfIntegrity) || i == displayRefreshDelayTime.count - 1 {
                imageCount = displayPosition.last
                displayRefreshFactor = displayRefreshFactors[i]
                displayOrder = []
                var oldIndex = 0
                var newIndex = 1
                let imageCount = self.imageCount ?? 0

                while newIndex <= imageCount && oldIndex < displayPosition.count {
                    if newIndex <= displayPosition[oldIndex] {
                        displayOrder?.append(oldIndex)
                        newIndex += 1
                    } else {
                        oldIndex += 1
                    }
                }
                break
            }
        }
    }

I believe the issue was a mathematical error which caused the displayRefreshRates to have incorrect values. The 120 value should have been inserted after the displayRefreshFactors mapping onto the displayRefreshRates value.

For those with SPM, I don't think you can edit the code directly, but I think with cocoapods you can.

Hope this issue is fixed globally!