CGBitmapContext比CALayer的draw()慢2倍

问题描述 投票:0回答:1

我有一些无法更改的代码,希望可以随时绘制。它是下面BackgroundThread中的main()函数-假定无法以任何方式对其进行修改。运行此命令将使用70-80%的CPU。

如果不是运行线程,而是在View :: draw()中复制它的操作(即在随机位置绘制5000个白色矩形),则将使用大约30%的CPU。

区别在哪里?从Instruments来看,尽管从CGContextFillRect开始调用堆栈是相同的,但是View :: draw()版本仅花费16%的时间用于memset(),而线程版本则花费80%的时间。

下面的代码是FAST版本。注释掉FAST行,取消注释SLOW行以切换到SLOW(线程)版本。用swiftc test.swift -otest && ./test编译。如果重要的话,我使用的是macOS 10.13,集成显卡。

我可以做些什么来使线程版本与View :: draw()版本一样快吗?

import Cocoa

let NSApp = NSApplication.shared,
  vwaitSem = DispatchSemaphore(value: 0)

var
    mainWindow: NSWindow?,
    screen: CGContext?,
    link: CVDisplayLink?

  class View: NSView, CALayerDelegate {
    var lastTime: CFTimeInterval = 0
    override var acceptsFirstResponder: Bool {return true}
    required init(coder aDecoder: NSCoder) {fatalError("This class does not support NSCoding")}
    override func makeBackingLayer() -> CALayer {return CALayer()}

    override init(frame: CGRect) {
      super.init(frame: frame)
      self.wantsLayer = true
      self.layer?.contentsScale = 2.0
      self.layer?.backgroundColor = CGColor(red:0, green:0, blue:0, alpha: 1)
      self.layerContentsRedrawPolicy = NSView.LayerContentsRedrawPolicy.onSetNeedsDisplay  // FAST
    }

    func draw(_ layer: CALayer, in ctx: CGContext) {
      let now = CACurrentMediaTime(), timePassed = ((now-lastTime)*1000).rounded()
      // NSLog("\(timePassed)")
      lastTime = now

      ctx.setFillColor(CGColor.white)
      ctx.setStrokeColor(CGColor.white)
      for _ in 0...5000 {
        let rect = CGRect(x: CGFloat(arc4random_uniform(640)+1), y: CGFloat(arc4random_uniform(480)+1), width:6, height:6)
        ctx.setFillColor(CGColor.white)
        ctx.fill(rect)
      }
    }
  }

  func displayLinkOutputCallback(_ displayLink: CVDisplayLink, _ nowPtr: UnsafePointer<CVTimeStamp>,
  _ outputTimePtr: UnsafePointer<CVTimeStamp>, _ flagsIn: CVOptionFlags, _ flagsOut: UnsafeMutablePointer<CVOptionFlags>,
  _ displayLinkContext: UnsafeMutableRawPointer?) -> CVReturn {
    DispatchQueue.main.async {
      // mainWindow!.contentView!.layer!.contents = screen!.makeImage()        // SLOW
      mainWindow!.contentView!.display()                                 // FAST
      vwaitSem.signal()
    }
    return kCVReturnSuccess
  }

class BackgroundThread: Thread {
  var lastTime: CFTimeInterval = 0
  override func main() {
    while true {
      let now = CACurrentMediaTime(), timePassed = ((now-lastTime)*1000).rounded()
      // NSLog("\(timePassed)")
      lastTime = now

      screen?.clear(CGRect(x:0, y:0, width:640*2, height:480*2))
      for _ in 0...5000 {
        screen?.setFillColor(CGColor.white)
        screen?.setStrokeColor(CGColor.white)
        screen?.fill(CGRect(x: CGFloat(arc4random_uniform(640*2)+1), y: CGFloat(arc4random_uniform(480*2)+1), width: 6*2, height: 6*2))
      }
      vwaitSem.wait()
    }
  }
}

let width = 640, height = 480,
  appMenuItem = NSMenuItem(),
  quitMenuItem = NSMenuItem(title:"Quit",
    action:#selector(NSApplication.terminate), keyEquivalent:"q"),
  window = NSWindow(contentRect:NSMakeRect(0,0, CGFloat(width), CGFloat(height)),
      styleMask:[.closable,.titled], backing:.buffered, defer:false),
  colorProfile = ColorSyncProfileCreateWithDisplayID(0),
  colorSpace = CGColorSpace(platformColorSpaceRef: colorProfile!.toOpaque()),
  screen_ = CGContext(data: nil, width: Int(width)*2, height:Int(height)*2, bitsPerComponent:8, bytesPerRow: 0,
    space: colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue),
  backgroundThread = BackgroundThread()

NSApp.setActivationPolicy(NSApplication.ActivationPolicy.regular)
NSApp.mainMenu = NSMenu()
NSApp.mainMenu?.addItem(appMenuItem)
appMenuItem.submenu = NSMenu()
appMenuItem.submenu?.addItem(quitMenuItem)
window.cascadeTopLeft(from:NSMakePoint(20,20))
window.makeKeyAndOrderFront(nil)
window.contentView = View()
window.makeFirstResponder(window.contentView)
NSApp.activate(ignoringOtherApps:true)

mainWindow = window
screen = screen_

CVDisplayLinkCreateWithCGDisplay(CGMainDisplayID(), &link)
CVDisplayLinkSetOutputCallback(link!, displayLinkOutputCallback, UnsafeMutableRawPointer(Unmanaged.passUnretained(window).toOpaque()))
CVDisplayLinkStart(link!)

// backgroundThread.start()   // SLOW
NSApp.run()
swift macos
1个回答
0
投票

我误读了makeImage()文档中的注释,并认为除非确实需要,否则它不会复制数据。好吧,仪器显示它确实可以复制数据。每帧。

所以我切换到了Metal,现在我可以从后台线程以与CGContext相同的性能/ CPU使用率进行绘制,据我所知,没有副本。

以下是一些工作代码:

import Cocoa
import MetalKit

class View: MTKView {
  var screen: CGContext?
  var commandQueue: MTLCommandQueue?
  var buffer: MTLBuffer?
  var texture: MTLTexture?
  var vwaitSem = DispatchSemaphore(value: 0)
  var backgroundThread: Thread?
  var allocationSize = 0

  func alignUp(size: Int, align: Int) -> Int {return (size+(align-1)) & ~(align-1)}
  override var acceptsFirstResponder: Bool {return true}
  required init(coder aDecoder: NSCoder) {fatalError("This class does not support NSCoding")}
  init() {super.init(frame: CGRect(x:0, y:0, width:0, height: 0), device: MTLCreateSystemDefaultDevice())}

  override func viewDidMoveToWindow() {
    layer?.contentsScale = NSScreen.main!.backingScaleFactor
    let metalLayer = layer as! CAMetalLayer
    let pixelRowAlignment = metalLayer.device!.minimumLinearTextureAlignment(for: metalLayer.pixelFormat)
    let bytesPerRow = alignUp(size: Int(layer!.frame.width)*Int(layer!.contentsScale)*4, align: pixelRowAlignment)
    let pagesize = Int(getpagesize())
    var data: UnsafeMutableRawPointer? = nil

    allocationSize = alignUp(size: bytesPerRow*Int(layer!.frame.height)*Int(layer!.contentsScale), align: pagesize)
    posix_memalign(&data, pagesize, allocationSize)

    let colorProfile = ColorSyncProfileCreateWithDisplayID(0),
      colorSpace = CGColorSpace(platformColorSpaceRef: colorProfile!.toOpaque()),
      screen_ = CGContext(data: data,
        width: Int(layer!.frame.width)*Int(layer!.contentsScale),
        height: Int(layer!.frame.height)*Int(layer!.contentsScale),
        bitsPerComponent:8, bytesPerRow: bytesPerRow,
        space: colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!,
      buffer_ = metalLayer.device!.makeBuffer(bytesNoCopy: data!, length: allocationSize, options: .storageModeManaged,
        deallocator: { pointer, length in free(self.screen!.data!) })!,
      textureDescriptor = MTLTextureDescriptor()

    textureDescriptor.pixelFormat = metalLayer.pixelFormat
    textureDescriptor.width = screen_.width
    textureDescriptor.height = screen_.height
    textureDescriptor.storageMode = buffer_.storageMode
    textureDescriptor.usage = MTLTextureUsage(rawValue: MTLTextureUsage.shaderRead.rawValue)
    texture = buffer_.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: screen_.bytesPerRow)
    commandQueue = device?.makeCommandQueue()

    screen = screen_
    buffer = buffer_
    backgroundThread = BackgroundThread(screen: screen!, vwaitSem: vwaitSem)
    backgroundThread!.start()
  }

  override func draw(_ dirtyRect: NSRect) {
    if let drawable = currentDrawable {
      buffer!.didModifyRange(0..<allocationSize)
      texture!.replace(region: MTLRegionMake2D(0,0, screen!.width, screen!.height),
        mipmapLevel:0, slice:0, withBytes: screen!.data!, bytesPerRow: screen!.bytesPerRow, bytesPerImage: 0)

      let commandBuffer = commandQueue!.makeCommandBuffer()!
      let blitPass = commandBuffer.makeBlitCommandEncoder()!
      blitPass.copy(from: texture!, sourceSlice:0, sourceLevel:0, sourceOrigin: MTLOrigin(x:0,y:0,z:0),
        sourceSize: MTLSize(width:screen!.width, height:screen!.height, depth: 1),
        to: drawable.texture, destinationSlice:0, destinationLevel:0, destinationOrigin: MTLOrigin(x:0,y:0,z:0))
      blitPass.endEncoding()

      if let renderPass = currentRenderPassDescriptor {
        renderPass.colorAttachments[0].texture = drawable.texture
        renderPass.colorAttachments[0].loadAction = .load  
        commandBuffer.makeRenderCommandEncoder(descriptor: renderPass)!.endEncoding()

        commandBuffer.addCompletedHandler {cb in self.vwaitSem.signal()}
        commandBuffer.present(drawable)
        commandBuffer.commit()
      }
    }
  }
}

class BackgroundThread: Thread {
  var screen: CGContext
  var vwaitSem: DispatchSemaphore
  var x = 0

  init(screen:CGContext, vwaitSem:DispatchSemaphore) {
    self.screen = screen
    self.vwaitSem = vwaitSem
  }

  override func main() {
    while true {
      // screen.clear(CGRect(x:0,y:0, width:screen.width, height:screen.height))
      // screen.setFillColor(CGColor.white)
      // screen.fill(CGRect(x:x, y:0, width:100, height:100))
      // x += 1

      screen.clear(CGRect(x:0,y:0, width:screen.width, height:screen.height))
      screen.setFillColor(CGColor.white)
      let screenWidth = UInt32(screen.width), screenHeight = UInt32(screen.height)
      for _ in 0...5000 {
        let rect = CGRect(x: CGFloat(arc4random_uniform(screenWidth+1)),
          y: CGFloat(arc4random_uniform(screenHeight+1)), width:6, height:6)
        screen.fill(rect)
      }
      vwaitSem.wait()
    }
  }
}

let width = 640, height = 480,
  appMenuItem = NSMenuItem(),
  quitMenuItem = NSMenuItem(title:"Quit",
    action:#selector(NSApplication.terminate), keyEquivalent:"q"),
  window = NSWindow(contentRect:NSMakeRect(0,0, CGFloat(width), CGFloat(height)),
      styleMask:[.closable,.titled], backing:.buffered, defer:false)

NSApp.setActivationPolicy(NSApplication.ActivationPolicy.regular)
NSApp.mainMenu = NSMenu()
NSApp.mainMenu?.addItem(appMenuItem)
appMenuItem.submenu = NSMenu()
appMenuItem.submenu?.addItem(quitMenuItem)
window.cascadeTopLeft(from:NSMakePoint(20,20))
window.makeKeyAndOrderFront(nil)
window.contentView = View()
window.makeFirstResponder(window.contentView)
NSApp.activate(ignoringOtherApps:true)
NSApp.run()
© www.soinside.com 2019 - 2024. All rights reserved.