I'm trying to make a simple software renderer for MacOS, but am having trouble displaying my bitmap continuously. The examples I find about setting this up is either using IOS or OpenGL/Metal views.
Here's my attempt in a mcve which compiles but stops updating the screen after a couple of iterations. I've made sure that the displayCallback is called correctly.
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!)
}
}
The result:
So the problem seem to be that the view isn't updated properly. I thought that @State var game would make it so the view got updated whenever it was modified, but I guess I need to modify the field directly for it to register.
I've tried using Timer in ContentView to call a function that updates it, but I can't capture self in its init (Escaping closure captures mutating 'self' parameter) or have a @objc function in the view (@objc can only be used with members of classes, @objc protocols, and concrete extensions of classes).
I'm not sure how to approach this problem. I've tried to pass ContentView to the CVDisplayLinkOutputCallback, but I got an error stating that Generic struct 'Unmanaged' requires that 'ContentView' be a class type.
So, how do I continuously render a software bitmap with SwiftUI?
