Path: blob/a-new-beginning/Folium-iOS/Controllers/EmulationControllers/KiwiController.swift
2 views
//
// KiwiController.swift
// Folium
//
// Created by Jarrod Norwell on 27/11/2025.
//
import AVFoundation
import AudioToolbox
import CTPCircularBuffer
import CoreAudio
import CombinedCores
import GameController
import UIKit
fileprivate extension TPCircularBuffer {
init(length: UInt32) {
self.init()
_TPCircularBufferInit(&self, length, MemoryLayout<TPCircularBuffer>.size)
}
mutating func clear() {
TPCircularBufferClear(&self)
}
mutating func tail() -> (UnsafeMutableRawPointer?, UInt32) {
var availableBytes: UInt32 = 0
let tail = TPCircularBufferTail(&self, &availableBytes)
return (tail, availableBytes)
}
mutating func consume(bytes: UInt32) {
TPCircularBufferConsume(&self, bytes)
}
@discardableResult mutating func produceBytes(from source: [UInt32], count: UInt32) -> Bool {
return produceBytes(from: source, length: UInt32(MemoryLayout<UInt32>.size) * count)
}
@discardableResult mutating func produceBytes(from source: UnsafeRawPointer, length: UInt32) -> Bool {
return TPCircularBufferProduceBytes(&self, source, length)
}
@discardableResult
mutating func produceBytes(from source: [UInt8], count: UInt32) -> Bool {
return produceBytes(from: source, length: UInt32(MemoryLayout<UInt8>.size) * count)
}
@discardableResult
mutating func produceBytes(from source: UnsafePointer<UInt8>, count: UInt32) -> Bool {
return produceBytes(from: UnsafeRawPointer(source),
length: UInt32(MemoryLayout<UInt8>.size) * count)
}
mutating func cleanup() {
TPCircularBufferCleanup(&self)
}
}
fileprivate let bufferLength = 1024 * 1024
fileprivate final class AudioEngine : @unchecked Sendable {
private var audioUnit: AUAudioUnit
private var renderVars = RenderVars()
private final class RenderVars {
var lastSample: UInt32 = 0
var circularBuffer = TPCircularBuffer(length: 1024 * 1024)
}
init() throws {
let desc = AudioComponentDescription(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_RemoteIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0
)
self.audioUnit = try AUAudioUnit(componentDescription: desc)
}
func startAudio() throws {
let sampleRate = 35112 * (262144.0 / 4389.0)
guard let format = AVAudioFormat(
commonFormat: .pcmFormatInt16,
sampleRate: sampleRate,
channels: 2,
interleaved: true
) else {
throw NSError(domain: "AudioEngine", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to create audio format"])
}
try audioUnit.inputBusses[0].setFormat(format)
audioUnit.outputProvider = { [weak self] _, _, frameCount, _, inputData in
guard
let self,
let bufferPointer = inputData[0].mBuffers.mData?.assumingMemoryBound(to: UInt32.self)
else {
return noErr
}
let (data, bytes) = self.renderVars.circularBuffer.tail()
let framesToCopy = min(Int(bytes) / MemoryLayout<UInt32>.size, Int(frameCount))
let bytesToCopy = framesToCopy * MemoryLayout<UInt32>.size
if let data {
memcpy(bufferPointer, data, bytesToCopy)
self.renderVars.circularBuffer.consume(bytes: UInt32(bytesToCopy))
}
if framesToCopy > 0 {
self.renderVars.lastSample = bufferPointer[framesToCopy - 1]
}
for i in framesToCopy..<Int(frameCount) {
bufferPointer[i] = self.renderVars.lastSample
}
return noErr
}
restartAudio()
try audioUnit.allocateRenderResources()
try audioUnit.startHardware()
}
func stopAudio() {
audioUnit.stopHardware()
}
func restartAudio() {
renderVars.circularBuffer.clear()
}
func stream(_ data: [UInt32], _ count: Int) {
renderVars.circularBuffer.produceBytes(from: data, count: .init(count))
}
deinit {
stopAudio()
renderVars.circularBuffer.cleanup()
}
}
class KiwiController : UIViewController {
var imageView: UIImageView? = nil,
secondaryImageView: UIImageView? = nil
var visualEffectView: UIVisualEffectView? = nil
var settingsButton: UIButton? = nil,
selectButton: UIButton? = nil,
startButton: UIButton? = nil
var upButton: UIButton? = nil,
downButton: UIButton? = nil,
leftButton: UIButton? = nil,
rightButton: UIButton? = nil
var bButton: UIButton? = nil,
aButton: UIButton? = nil
var portraitBottomConstraint: NSLayoutConstraint? = nil
var landscapeLeftConstraint: NSLayoutConstraint? = nil,
landscapeRightConstraint: NSLayoutConstraint? = nil
var constraints: (portrait: [NSLayoutConstraint], landscape: [NSLayoutConstraint]) = ([], [])
fileprivate var engine: AudioEngine? = nil
var game: NewKiwiGame
var kiwiCore: CombinedCores.KiwiCore
init(_ game: NewKiwiGame, _ kiwiCore: CombinedCores.KiwiCore) {
self.game = game
self.kiwiCore = kiwiCore
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
secondaryImageView = UIImageView()
guard let secondaryImageView else {
return
}
secondaryImageView.translatesAutoresizingMaskIntoConstraints = false
secondaryImageView.backgroundColor = .label
view.addSubview(secondaryImageView)
visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
guard let visualEffectView else {
return
}
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(visualEffectView)
imageView = UIImageView()
guard let imageView else {
return
}
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .label
if #available(iOS 26, *) {
imageView.clipsToBounds = true
imageView.cornerConfiguration = .corners(radius: .fixed(15.0))
} else {
imageView.clipsToBounds = true
imageView.layer.cornerCurve = .continuous
imageView.layer.cornerRadius = 15.0
}
visualEffectView.contentView.addSubview(imageView)
let settingsConfiguration: UIButton.Configuration = .configuration(.medium, .capsule, UIImage(systemName: "ellipsis"), .medium)
settingsButton = .button(with: settingsConfiguration,
actions: ({ _ in }, { _ in }), UIMenu(children: [
UIMenu(options: .displayInline, children: [
UIDeferredMenuElement.uncached { completion in
Task {
completion([
UIAction(title: "Stop & Exit", image: UIImage(systemName: "stop"), attributes: .destructive) { _ in
let alertController: UIAlertController = UIAlertController(title: "Stop & Exit",
message: "Are you sure you want to stop & exit? Unsaved progress will be lost",
preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: .localized(for: .dismiss), style: .cancel))
alertController.addAction(UIAlertAction(title: "Stop & Exit", style: .destructive, handler: { _ in
Task {
await self.kiwiCore.stop()
self.dismiss(animated: true)
}
}))
self.present(alertController, animated: true)
},
UIAction(title: await self.kiwiCore.paused ? "Resume" : "Pause",
image: await self.kiwiCore.paused ? UIImage(systemName: "play") : UIImage(systemName: "pause")) { _ in
Task {
if await self.kiwiCore.paused {
await self.kiwiCore.unpause()
} else {
await self.kiwiCore.pause()
}
}
}
])
}
}
]),
UIMenu(options: .displayInline, children: [
UIMenu(title: "Delete State", image: UIImage(systemName: "minus.circle"), options: .destructive, preferredElementSize: .small, children: [
UIDeferredMenuElement.uncached { completion in
guard let documentDirectory: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion([])
return
}
let statesDirectory: URL = documentDirectory.appending(component: "Kiwi").appending(component: "states")
let stateNumbers: [Int] = [0, 1, 2]
let elements: [UIAction] = stateNumbers.map { state in
let url: URL = statesDirectory.appending(component: "\(self.game.details.name)_\(state).state")
let fileExists: Bool = FileManager.default.fileExists(atPath: url.path)
return .init(image: UIImage(systemName: "\(state + 1).circle"), attributes: fileExists ? .destructive : .disabled) { _ in
Task {
try FileManager.default.removeItem(at: url)
}
}
}
completion(elements)
}
]),
UIMenu(title: "Save State", image: .init(systemName: "arrow.down.circle"), preferredElementSize: .small, children: [
UIDeferredMenuElement.uncached { completion in
guard let documentDirectory: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion([])
return
}
let statesDirectory: URL = documentDirectory.appending(component: "Kiwi").appending(component: "states")
let stateNumbers: [Int] = [0, 1, 2]
let elements: [UIAction] = stateNumbers.map { state in
let url: URL = statesDirectory.appending(component: "\(self.game.details.name)_\(state).state")
let fileExists: Bool = FileManager.default.fileExists(atPath: url.path)
return .init(image: UIImage(systemName: "\(state + 1).circle")) { _ in
if fileExists {
let alertController: UIAlertController = UIAlertController(title: "Overwrite?",
message: "Are you sure you want to overwrite? This action is irreversible",
preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: .localized(for: .dismiss), style: .cancel))
alertController.addAction(UIAlertAction(title: "Overwrite", style: .destructive, handler: { _ in
Task {
await self.kiwiCore.save(state: url)
}
}))
self.present(alertController, animated: true)
} else {
Task {
await self.kiwiCore.save(state: url)
}
}
}
}
completion(elements)
}
]),
UIMenu(title: "Load State", image: .init(systemName: "arrow.up.circle"), preferredElementSize: .small, children: [
UIDeferredMenuElement.uncached { completion in
guard let documentDirectory: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion([])
return
}
let statesDirectory: URL = documentDirectory.appending(component: "Kiwi").appending(component: "states")
let stateNumbers: [Int] = [0, 1, 2]
let elements: [UIAction] = stateNumbers.map { state in
let url: URL = statesDirectory.appending(component: "\(self.game.details.name)_\(state).state")
let fileExists: Bool = FileManager.default.fileExists(atPath: url.path)
return .init(image: UIImage(systemName: "\(state + 1).circle"), attributes: fileExists ? [] : .disabled) { _ in
Task {
await self.kiwiCore.load(state: url)
}
}
}
completion(elements)
}
]),
UIMenu(options: .displayInline, children: [
UIAction(title: "Capture Still Image", image: UIImage(systemName: "photo.badge.arrow.down")) { _ in
Task {
await self.kiwiCore.pause()
guard let imageView: UIImageView = self.imageView, let image: UIImage = imageView.image else {
return
}
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
await self.kiwiCore.unpause()
}
}
])
])
]))
guard let settingsButton else {
return
}
visualEffectView.contentView.addSubview(settingsButton)
let selectConfiguration: UIButton.Configuration = .configuration(.medium, .capsule, UIImage(systemName: "minus"), .medium)
selectButton = .button(with: selectConfiguration,
actions: ({ _ in
await self.kiwiCore.press(button: .select)
}, { _ in
await self.kiwiCore.release(button: .select)
}))
guard let selectButton else {
return
}
visualEffectView.contentView.addSubview(selectButton)
let startConfiguration: UIButton.Configuration = .configuration(.medium, .capsule, UIImage(systemName: "plus"), .medium)
startButton = .button(with: startConfiguration,
actions: ({ _ in
await self.kiwiCore.press(button: .start)
}, { _ in
await self.kiwiCore.release(button: .start)
}))
guard let startButton else {
return
}
visualEffectView.contentView.addSubview(startButton)
let upConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "arrow.up"))
upButton = .button(with: upConfiguration,
actions: ({ _ in
await self.kiwiCore.press(button: .up)
}, { _ in
await self.kiwiCore.release(button: .up)
}))
guard let upButton else {
return
}
visualEffectView.contentView.addSubview(upButton)
let downConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "arrow.down"))
downButton = .button(with: downConfiguration,
actions: ({ _ in
await self.kiwiCore.press(button: .down)
}, { _ in
await self.kiwiCore.release(button: .down)
}))
guard let downButton else {
return
}
visualEffectView.contentView.addSubview(downButton)
let leftConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "arrow.left"))
leftButton = .button(with: leftConfiguration,
actions: ({ _ in
await self.kiwiCore.press(button: .left)
}, { _ in
await self.kiwiCore.release(button: .left)
}))
guard let leftButton else {
return
}
visualEffectView.contentView.addSubview(leftButton)
let rightConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "arrow.right"))
rightButton = .button(with: rightConfiguration,
actions: ({ _ in
await self.kiwiCore.press(button: .right)
}, { _ in
await self.kiwiCore.release(button: .right)
}))
guard let rightButton else {
return
}
visualEffectView.contentView.addSubview(rightButton)
let bConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "b.circle"))
bButton = .button(with: bConfiguration,
actions: ({ _ in
await self.kiwiCore.press(button: .b)
}, { _ in
await self.kiwiCore.release(button: .b)
}))
guard let bButton else {
return
}
visualEffectView.contentView.addSubview(bButton)
let aConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "a.circle"))
aButton = .button(with: aConfiguration,
actions: ({ _ in
await self.kiwiCore.press(button: .a)
}, { _ in
await self.kiwiCore.release(button: .a)
}))
guard let aButton else {
return
}
visualEffectView.contentView.addSubview(aButton)
if iPhone {
portraitBottomConstraint = secondaryImageView.bottomAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor)
guard let portraitBottomConstraint else {
return
}
constraints.portrait.append(contentsOf: [
secondaryImageView.topAnchor.constraint(equalTo: view.topAnchor),
secondaryImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
secondaryImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
portraitBottomConstraint,
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.topAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.topAnchor,
constant: 20.0),
imageView.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
imageView.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
imageView.heightAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.widthAnchor,
multiplier: 3.0 / 4.0),
settingsButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
settingsButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
selectButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
selectButton.trailingAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
startButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
startButton.leadingAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
bButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20),
bButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor),
aButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
aButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20),
upButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
upButton.bottomAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
leftButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20),
leftButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
downButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
downButton.bottomAnchor.constraint(equalTo: selectButton.safeAreaLayoutGuide.topAnchor,
constant: -20),
rightButton.leadingAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.trailingAnchor),
rightButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor)
])
landscapeLeftConstraint = secondaryImageView.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor)
landscapeRightConstraint = secondaryImageView.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor)
guard let landscapeLeftConstraint, let landscapeRightConstraint else {
return
}
constraints.landscape.append(contentsOf: [
secondaryImageView.topAnchor.constraint(equalTo: view.topAnchor),
secondaryImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
landscapeLeftConstraint,
landscapeRightConstraint,
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
startButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
startButton.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20),
selectButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
selectButton.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
settingsButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
settingsButton.leadingAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
imageView.topAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.topAnchor,
constant: 20.0),
imageView.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
imageView.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
imageView.widthAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.heightAnchor,
multiplier: 4.0 / 3.0),
bButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20),
bButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor),
aButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
aButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20),
upButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
upButton.bottomAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
leftButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20),
leftButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
downButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
downButton.bottomAnchor.constraint(equalTo: selectButton.safeAreaLayoutGuide.topAnchor,
constant: -20),
rightButton.leadingAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.trailingAnchor),
rightButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor)
])
} else {
portraitBottomConstraint = secondaryImageView.bottomAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor)
guard let portraitBottomConstraint else {
return
}
constraints.portrait.append(contentsOf: [
secondaryImageView.topAnchor.constraint(equalTo: view.topAnchor),
secondaryImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
secondaryImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
portraitBottomConstraint,
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.topAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.topAnchor,
constant: 20.0),
imageView.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
imageView.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
imageView.heightAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.widthAnchor,
multiplier: 3.0 / 4.0),
settingsButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
settingsButton.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
selectButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
selectButton.trailingAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
startButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
startButton.leadingAnchor.constraint(equalTo: settingsButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
bButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20),
bButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor),
aButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
aButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20),
upButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
upButton.bottomAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
leftButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20),
leftButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
downButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
downButton.bottomAnchor.constraint(equalTo: selectButton.safeAreaLayoutGuide.topAnchor,
constant: -20),
rightButton.leadingAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.trailingAnchor),
rightButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor)
])
landscapeLeftConstraint = secondaryImageView.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor)
landscapeRightConstraint = secondaryImageView.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor)
guard let landscapeLeftConstraint, let landscapeRightConstraint else {
return
}
constraints.landscape.append(contentsOf: [
secondaryImageView.topAnchor.constraint(equalTo: view.topAnchor),
secondaryImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
landscapeLeftConstraint,
landscapeRightConstraint,
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
startButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
startButton.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20),
selectButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
selectButton.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor,
constant: -20.0),
settingsButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
settingsButton.leadingAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
imageView.topAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.topAnchor,
constant: 20.0),
imageView.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
imageView.centerXAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.centerXAnchor),
imageView.widthAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.heightAnchor,
multiplier: 4.0 / 3.0),
bButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20),
bButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor),
aButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
aButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20),
upButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
upButton.bottomAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
leftButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20),
leftButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
downButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
downButton.bottomAnchor.constraint(equalTo: selectButton.safeAreaLayoutGuide.topAnchor,
constant: -20),
rightButton.leadingAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.trailingAnchor),
rightButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor)
])
}
switch interfaceOrientation() {
case .portrait:
view.addConstraints(constraints.portrait)
case .landscapeLeft, .landscapeRight:
view.addConstraints(constraints.landscape)
default:
break
}
Task {
engine = try AudioEngine()
guard let engine else {
return
}
try engine.startAudio()
await kiwiCore.insert(cartridge: game.details.url)
await kiwiCore.audioCallback { audio, samples in
engine.stream(.init(UnsafeBufferPointer(start: audio, count: samples)), samples)
}
await kiwiCore.videoCallback { framebuffer, width, height in
Task { @MainActor in
guard let cgImage = CGImage.gba(framebuffer, width, height) else {
return
}
imageView.image = .init(cgImage: cgImage)
secondaryImageView.image = .init(cgImage: cgImage)
}
}
await kiwiCore.start()
}
Task {
await GCController.startWirelessControllerDiscovery()
}
NotificationCenter.default.addObserver(forName: .applicationStateDidChange, object: nil, queue: .current) { notification in
guard let applicationState: ApplicationState = notification.object as? ApplicationState else {
return
}
Task {
await applicationState.shouldPause ? self.kiwiCore.pause() : self.kiwiCore.unpause()
}
}
NotificationCenter.default.addObserver(forName: .GCControllerDidConnect, object: nil, queue: .current) { notification in
guard let controller: GCController = notification.object as? GCController,
let extendedGamepad: GCExtendedGamepad = controller.extendedGamepad else {
return
}
visualEffectView.contentView.subviews.filter { subview in
subview.isKind(of: UIButton.classForCoder()) && subview != settingsButton
}.forEach { button in
UIView.animate(withDuration: 1 / 3) {
button.alpha = 1 / 3
}
}
func input(button: CombinedCores.KiwiCore.KiwiButton, pressed: Bool) {
Task {
if pressed {
await self.kiwiCore.press(button: button)
} else {
await self.kiwiCore.release(button: button)
}
}
}
extendedGamepad.buttonA.pressedChangedHandler = { element, value, pressed in
input(button: .a, pressed: pressed)
}
extendedGamepad.buttonB.pressedChangedHandler = { element, value, pressed in
input(button: .b, pressed: pressed)
}
extendedGamepad.dpad.up.pressedChangedHandler = { element, value, pressed in
input(button: .up, pressed: pressed)
}
extendedGamepad.dpad.down.pressedChangedHandler = { element, value, pressed in
input(button: .down, pressed: pressed)
}
extendedGamepad.dpad.left.pressedChangedHandler = { element, value, pressed in
input(button: .left, pressed: pressed)
}
extendedGamepad.dpad.right.pressedChangedHandler = { element, value, pressed in
input(button: .right, pressed: pressed)
}
if let buttonOptions = extendedGamepad.buttonOptions {
buttonOptions.pressedChangedHandler = { element, value, pressed in
input(button: .select, pressed: pressed)
}
}
extendedGamepad.buttonMenu.pressedChangedHandler = { element, value, pressed in
input(button: .start, pressed: pressed)
}
}
NotificationCenter.default.addObserver(forName: .GCControllerDidDisconnect, object: nil, queue: .current) { notification in
visualEffectView.contentView.subviews.filter { subview in
subview.isKind(of: UIButton.classForCoder()) && subview != settingsButton
}.forEach { button in
UIView.animate(withDuration: 1 / 3) {
button.alpha = 1
}
}
}
}
override var prefersHomeIndicatorAutoHidden: Bool { true }
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if iPhone {
.all
} else {
.portrait
}
}
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
switch interfaceOrientation() {
case .portrait:
guard let portraitBottomConstraint else {
return
}
portraitBottomConstraint.constant = view.safeAreaInsets.top
case .landscapeLeft:
guard let landscapeLeftConstraint, let landscapeRightConstraint else {
return
}
landscapeLeftConstraint.constant = -view.safeAreaInsets.right
landscapeRightConstraint.constant = view.safeAreaInsets.right
case .landscapeRight:
guard let landscapeLeftConstraint, let landscapeRightConstraint else {
return
}
landscapeLeftConstraint.constant = -view.safeAreaInsets.left
landscapeRightConstraint.constant = view.safeAreaInsets.left
default:
break
}
Task {
view.setNeedsUpdateConstraints()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let engine {
engine.stopAudio()
self.engine = nil
}
GCController.stopWirelessControllerDiscovery()
NotificationCenter.default.removeObserver(self)
}
override func viewWillTransition(to size: CGSize, with coordinator: any UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { context in } completion: { context in
switch self.interfaceOrientation() {
case .portrait:
self.view.removeConstraints(self.constraints.landscape)
self.view.addConstraints(self.constraints.portrait)
case .landscapeLeft, .landscapeRight:
self.view.removeConstraints(self.constraints.portrait)
self.view.addConstraints(self.constraints.landscape)
default:
break
}
self.view.setNeedsUpdateConstraints()
}
}
}
extension KiwiController {
@objc func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
if #available(iOS 17.5, *), let settingsButton {
UINotificationFeedbackGenerator(view: settingsButton).notificationOccurred(error == nil ? .success : .error)
} else {
UINotificationFeedbackGenerator().notificationOccurred(error == nil ? .success : .error)
}
}
}