如何使用 SwiftUI 连续渲染软件位图?

Posted

技术标签:

【中文标题】如何使用 SwiftUI 连续渲染软件位图?【英文标题】:How to continuously render a software bitmap with SwiftUI? 【发布时间】:2021-03-16 23:09:51 【问题描述】:

我正在尝试为 MacOS 制作一个简单的软件渲染器,但无法连续显示我的位图。 The examples I 发现有关设置是使用 ios 或 OpenGL/Metal 视图。

这是我在 mcve 中的尝试,它编译但在几次迭代后停止更新屏幕。我已确保正确调用了 displayCallback

import SwiftUI

@main
struct PlatformApp: App 
    var body: some Scene 
        WindowGroup 
            ContentView()
        
    


public struct Color 
    public var r, g, b, a: UInt8
    
    public init(r: UInt8, g: UInt8, b: UInt8, a: UInt8 = 255) 
        self.r = r
        self.g = g
        self.b = b
        self.a = a
    


public extension Color 
    static let clear = Color(r: 0,   g: 0,   b: 0, a: 0)
    static let red   = Color(r: 255, g: 0,   b: 0)
    static let green = Color(r: 0,   g: 255, b: 0)
    static let blue  = Color(r: 0,   g: 0,   b: 255)


extension Image 
    init?(bitmap: Bitmap) 
        let alphaInfo = CGImageAlphaInfo.premultipliedLast
        let bytesPerPixel = MemoryLayout<Color>.size
        let bytesPerRow = bitmap.width * bytesPerPixel

        guard let providerRef = CGDataProvider(data: Data(
            bytes: bitmap.pixels, count: bitmap.height * bytesPerRow
        ) as CFData) else 
            return nil
        

        guard let cgImage = CGImage(
            width: bitmap.width,
            height: bitmap.height,
            bitsPerComponent: 8,
            bitsPerPixel: bytesPerPixel * 8,
            bytesPerRow: bytesPerRow,
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: CGBitmapInfo(rawValue: alphaInfo.rawValue),
            provider: providerRef,
            decode: nil,
            shouldInterpolate: true,
            intent: .defaultIntent
        ) else 
            return nil
        

        self.init(decorative: cgImage, scale: 1.0, orientation: .up)
    


public struct Bitmap 
    public private(set) var pixels: [Color]
    public let width: Int
    
    public init(width: Int, pixels: [Color]) 
        self.width  = width
        self.pixels = pixels
    


public extension Bitmap 
    var height: Int 
        return pixels.count / width
    
    
    subscript(x: Int, y: Int) -> Color 
        get  return pixels[y * width + x] 
        set  pixels[y * width + x] = newValue 
    

    init(width: Int, height: Int, color: Color) 
        self.pixels = Array(repeating: color, count: width * height)
        self.width  = width
    


public struct Renderer 
    public private(set) var bitmap: Bitmap
    private var i: Int = 0
    private var j: Int = 0
    public init(width: Int, height: Int, backgroundColor: Color = .clear) 
        self.bitmap = Bitmap(width: width, height: height, color: backgroundColor)
    


public extension Renderer 
    mutating func draw()   // Temporary for testing
        bitmap[i, j] = [Color.red, Color.green, Color.blue][i % 3]
    
        i = i + 1
        if (i >= bitmap.width) 
            i = 0
            j += 1
            if (j >= bitmap.height) 
                j = 0
            
        
    



struct ContentView: View 
    @State var game = Game(width: 10, height: 10)
    
    var body: some View 
        game.image?
            .resizable()
            .interpolation(.none)
            .frame(width: 200, height: 200, alignment: .center)
            .aspectRatio(contentMode: .fit)
    



class Game 
    public  var image: Image?
    private var renderer: Renderer
    private var displayLink: CVDisplayLink?
    
    let displayCallback: CVDisplayLinkOutputCallback =  displayLink, inNow, inOutputTime, flagsIn, flagsOut, displayLinkContext in
        let game = unsafeBitCast(displayLinkContext, to: Game.self)
        game.renderer.draw()
        game.image = Image(bitmap: game.renderer.bitmap)
        return kCVReturnSuccess
    
    
    init(width: Int, height: Int) 
        self.renderer = Renderer(width: width, height: height)

        let error = CVDisplayLinkCreateWithActiveCGDisplays(&self.displayLink)
        guard let link = self.displayLink, kCVReturnSuccess == error else 
            NSLog("Display Link created with error: %d", error)
            return
        

        CVDisplayLinkSetOutputCallback(link, displayCallback, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
        CVDisplayLinkStart(link)
    
    
    deinit 
        CVDisplayLinkStop(self.displayLink!)
    

结果:

所以问题似乎是视图没有正确更新。我认为@State var game 会做到这一点,因此只要修改了视图就会更新,但我想我需要直接修改该字段才能注册。

我尝试在ContentView 中使用Timer 来调用更新它的函数,但我无法在其init (Escaping closure captures mutating 'self' parameter) 中捕获self 或在其中包含@objc 函数视图 (@objc can only be used with members of classes, @objc protocols, and concrete extensions of classes)。

我不确定如何解决这个问题。我尝试将ContentView 传递给CVDisplayLinkOutputCallback,但我收到一条错误消息,指出Generic struct 'Unmanaged' requires that 'ContentView' be a class type

那么,如何使用 SwiftUI 持续渲染软件位图?

【问题讨论】:

【参考方案1】:

您正在使用@State,但您的游戏是引用类型类而不是值类型结构。使用类似observedobject的东西

例如:快速而肮脏的 hack:

struct ContentView: View 
    @ObservedObject
    var game = Game(width: 10, height: 10)
    
    var body: some View 
        game.image?
            .resizable()
            .interpolation(.none)
            .frame(width: 200, height: 200, alignment: .center)
            .aspectRatio(contentMode: .fit)
    



class Game: ObservableObject 
    @Published
    var toggle: Bool = false
    
    public  var image: Image? 
        didSet 
            DispatchQueue.main.async  [weak self] in
                self?.toggle.toggle()
            
        
    

【讨论】:

这将如何工作? ObservedObject 将发布更改,这是后台线程所不允许的。 我添加了一个示例 - 可能是一种糟糕的方法,但它似乎可以绘制

以上是关于如何使用 SwiftUI 连续渲染软件位图?的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 布局(Layout)基础

如何使用 Direct2D 从 BITMAPINFOHEADER 和 BYTE 渲染位图

如何使用 SwiftUI 连续呈现两个警报视图

如何使用 GDI 将方形位图渲染为任意四边形多边形?

如何在 SwiftUI 中实现 MVVM 模式?视图不会重新渲染

WPF 高性能位图渲染 WriteableBitmap 及其高性能用法示例