2

I have been struggling with a performance related issue for a simple Cocoa based visualization program, that is continually drawing a heat map as a bitmap and displaing the bitmap on screen. Broadly, the heat map is a continually scrolling source of realtime information. At each redraw, new pixels are added at the right side of the bitmap and then the bitmap is shifted to the left, creating an on-going flow of information.

My current approach is implemented in a custom NVSView, with a circular buffer for the underlying pixels. The image is actually drawn rotated 90 degrees, so adding new data is simply a matter of appending the buffer (adding rows to the image). I then use a CGDataProviderCreateWithData to create a CoreGraphics image and draw that to a rotated context. (The code is below, although is less important to the question.)

My hope is to figure out a more performant way of achieving this. It seems like redrawing the full bitmap each time is excessive, and in my attempts, it uses a surprisingly high amount of CPU (~20-30%). I feel like there should be a way to somehow instruct the GPU to cache, but shift the existing pixels and then append new data.

I am considering an approach that will use two CGBitmapContextCreate, one context for what is currently on the screen and one context for modification. Prior pixels would be copied from one context to the other context, but I am not sure that will improve performance significantly. Before I proceed too far down that rabbit hole, are there better ways to handle such updating?

Here is the relevant code from my current implementation, although I think I am more in need of higher level guidance:

class PlotView: NSView
{
    private let bytesPerPixel = 4
    private let height = 512
    private let bufferLength = 5242880
    private var buffer: TPCircularBuffer

    // ... Other code that appends data to buffer and set needsDisplay ...

    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)

        // get context
        guard let context = NSGraphicsContext.currentContext() else {
            return
        }

        // get colorspace
        guard let colorSpace = CGColorSpaceCreateDeviceRGB() else {
            return
        }

        // number of sampels to draw
        let maxSamples = Int(frame.width) * height
        let maxBytes = maxSamples * bytesPerPixel

        // bytes for reading
        var availableBytes: Int32 = 0
        let bufferStart = UnsafeMutablePointer<UInt8>(TPCircularBufferTail(&buffer, &availableBytes))
        let samplesStart: UnsafeMutablePointer<UInt8>
        let samplesCount: Int
        let samplesBytes: Int

        // buffer management
        if Int(availableBytes) > maxBytes {
            // advance buffer start
            let bufferOffset = Int(availableBytes) - maxBytes

            // set sample start
            samplesStart = bufferStart.advancedBy(bufferOffset)

            // consume values
            TPCircularBufferConsume(&buffer, Int32(bufferOffset))

            // number of samples
            samplesCount = maxSamples
            samplesBytes = maxBytes
        }
        else {
            // set to start
            samplesStart = bufferStart

            // number of samples
            samplesBytes = Int(availableBytes)
            samplesCount = samplesBytes / bytesPerPixel
        }

        // get dimensions
        let rows = height
        let columns = samplesCount / rows

        // bitmap info
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.NoneSkipFirst.rawValue)

        // counts
        let bytesPerComponent = sizeof(UInt8)
        let bytesPerPixel = sizeof(UInt8) * bytesPerPixel

        // prepare image
        let provider = CGDataProviderCreateWithData(nil, samplesStart, samplesBytes, nil)
        let image = CGImageCreate(rows, columns, 8 * bytesPerComponent, 8 * bytesPerPixel, rows * bytesPerPixel, colorSpace, bitmapInfo, provider, nil, false, .RenderingIntentDefault)

        // make rotated size
        let rotatedSize = CGSize(width: frame.width, height: frame.height)
        let rotatedCenterX = rotatedSize.width / 2, rotatedCenterY = rotatedSize.height / 2

        // rotate, from: http://stackoverflow.com/questions/10544887/rotating-a-cgimage
        CGContextTranslateCTM(context.CGContext, rotatedCenterX, rotatedCenterY)
        CGContextRotateCTM(context.CGContext, CGFloat(0 - M_PI_2))
        CGContextScaleCTM(context.CGContext, 1, -1)
        CGContextTranslateCTM(context.CGContext, -rotatedCenterY, -rotatedCenterX)

        // draw
        CGContextDrawImage(context.CGContext, NSRect(origin: NSPoint(x: rotatedSize.height - CGFloat(rows), y: 0), size: NSSize(width: rows, height: columns)), image)
    }
}
Nathan P
  • 976
  • 1
  • 7
  • 14
  • Well, the idea of using two `CGBitmapContext` for holding the buffer is even more resource intensive. So much for that idea. I did play with the profiling tools and the function that is accounting for almost 80% of the usage is `CGContextDrawImage`, for what that is worth. – Nathan P Feb 19 '16 at 22:03
  • Core Graphics really isn't designed to interface with the GPU at all... it's all done on the CPU. Have you considered using Open GL to display this data? `glDrawPixels()` sounds like exactly what you want - just passing in a bitmap to be rendered onto the screen. I can't immediately think of any Core Graphics functionality that'll let you 'shift' the pixels over, rather than re-drawing. – Hamish Feb 20 '16 at 22:48
  • Thanks for the suggestion. Since `glDrawPixels` has been removed from OpenGL, I tried a variation on what you described by using Metal to draw a texture to the screen, but it is actually slightly slower than using CoreGraphics (probably just ignorance of Metal). Overall, the idea that I am getting is that I need to somehow copy around data in the frame buffer rather than fully redrawing the plot. More learning required... – Nathan P Feb 21 '16 at 21:13
  • Oh, I wasn't aware that it had been deprecated. I've only done a bit of OpenGL ES, so I was going off a quick google search there! I guess the next best way is to render 2 squares (6 vertices in a triangle strip), with a texture per square. Create an empty texture with `glTexImage2D` and then add a new line to it with `glTexSubImage2D` when your data updates. Then you just have to translate your vertices over to account for the offset when the first texture fills up, and repeat with the second texture. That way you should only have to send off *new* data to the GPU, along with an offset value. – Hamish Feb 21 '16 at 21:48
  • I started going down that route, but realized that there is a lot of complexity in updating the textures. I do some to be having a promising approach using `CGLayer`. – Nathan P Feb 23 '16 at 14:32

0 Answers0