Path: blob/a-new-beginning/Folium-iOS/Controllers/EmulationControllers/GrapeController.swift
2 views
//
// GrapeController.swift
// Folium-iOS
//
// Created by Jarrod Norwell on 5/3/2026.
//
/*
MARK: NOTES
**** Async UIAction ****
- [ ] Incomplete, rewrite all UIAction instances as .async(...)
*/
import CombinedCores
import GameController
import MetalKit
import UIKit
class GrapeController : UIViewController {
var imageView: MTKView? = nil,
blurredImageView: MTKView? = nil,
secondaryImageView: MTKView? = nil,
secondaryBlurredImageView: MTKView? = 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 aButton: UIButton? = nil,
bButton: UIButton? = nil,
yButton: UIButton? = nil,
xButton: UIButton? = nil
var lButton: UIButton? = nil,
rButton: UIButton? = nil
var portraitConstraints: [NSLayoutConstraint] = [],
landscapeConstraints: [NSLayoutConstraint] = []
var portraitBottomConstraint: NSLayoutConstraint? = nil
var topLandscapeLeadingConstraint: NSLayoutConstraint? = nil,
topLandscapeTrailingConstraint: NSLayoutConstraint? = nil,
bottomLandscapeLeadingConstraint: NSLayoutConstraint? = nil,
bottomLandscapeTrailingConstraint: NSLayoutConstraint? = nil
var constraints: (portrait: [NSLayoutConstraint], landscape: [NSLayoutConstraint]) = ([], [])
var commandQueue: MTLCommandQueue!
var pipelineState: MTLRenderPipelineState!
var samplerState: MTLSamplerState!
var vertexBuffer: MTLBuffer!
var device: MTLDevice? = nil
var topTexture: MTLTexture? = nil
var bottomTexture: MTLTexture? = nil
var game: NewGrapeGame
var grapeCore: CombinedCores.GrapeCore
init(_ game: NewGrapeGame, _ grapeCore: CombinedCores.GrapeCore) {
self.game = game
self.grapeCore = grapeCore
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)
device = MTLCreateSystemDefaultDevice()
blurredImageView = MTKView(frame: .zero, device: device)
guard let blurredImageView else {
return
}
blurredImageView.translatesAutoresizingMaskIntoConstraints = false
blurredImageView.colorPixelFormat = .bgra8Unorm
blurredImageView.contentMode = .scaleAspectFill
blurredImageView.delegate = self
blurredImageView.framebufferOnly = false
view.addSubview(blurredImageView)
secondaryBlurredImageView = MTKView(frame: .zero, device: device)
guard let secondaryBlurredImageView else {
return
}
secondaryBlurredImageView.translatesAutoresizingMaskIntoConstraints = false
secondaryBlurredImageView.colorPixelFormat = .bgra8Unorm
secondaryBlurredImageView.contentMode = .scaleAspectFill
secondaryBlurredImageView.delegate = self
secondaryBlurredImageView.framebufferOnly = false
view.addSubview(secondaryBlurredImageView)
visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
guard let visualEffectView else {
return
}
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(visualEffectView)
imageView = MTKView(frame: .zero, device: device)
guard let imageView else {
return
}
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .secondaryLabel
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
}
imageView.colorPixelFormat = .bgra8Unorm
imageView.contentMode = .scaleAspectFill
imageView.delegate = self
imageView.framebufferOnly = false
visualEffectView.contentView.addSubview(imageView)
secondaryImageView = MTKView(frame: .zero, device: device)
guard let secondaryImageView else {
return
}
secondaryImageView.translatesAutoresizingMaskIntoConstraints = false
secondaryImageView.backgroundColor = .secondaryLabel
if #available(iOS 26, *) {
secondaryImageView.clipsToBounds = true
secondaryImageView.cornerConfiguration = .corners(radius: .fixed(15.0))
} else {
secondaryImageView.clipsToBounds = true
secondaryImageView.layer.cornerCurve = .continuous
secondaryImageView.layer.cornerRadius = 15.0
}
secondaryImageView.colorPixelFormat = .bgra8Unorm
secondaryImageView.contentMode = .scaleAspectFill
secondaryImageView.delegate = self
secondaryImageView.framebufferOnly = false
secondaryImageView.isUserInteractionEnabled = true
visualEffectView.contentView.addSubview(secondaryImageView)
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.async(title: "Stop & Exit", image: UIImage(systemName: "stop"), attributes: .destructive) { action in
await self.grapeCore.stop()
self.dismiss(animated: true)
},
UIAction.async(title: await self.grapeCore.paused ? "Resume" : "Pause",
image: UIImage(systemName: await self.grapeCore.paused ? "play" : "pause")) { action in
if await self.grapeCore.paused {
await self.grapeCore.unpause()
} else {
await self.grapeCore.pause()
}
}
])
}
}
]),
UIMenu(options: .displayInline, children: [
UIMenu(title: "Delete State", image: UIImage(systemName: "minus.circle"),
options: .destructive, preferredElementSize: .small, children: [
UIDeferredMenuElement.uncached { completion in
if let grapeDirectory: URL = .grapeDirectory {
let statesDirectory: URL = grapeDirectory.appending(component: "states")
let states: [Int] = [0, 1, 2]
completion(states.map { state in
let url: URL = statesDirectory.appending(component: "\(self.game.details.name)_\(state).state")
let exists: Bool = FileManager.default.fileExists(atPath: url.path(percentEncoded: false))
return UIAction.throwing(image: UIImage(systemName: "\(state + 1).circle"),
attributes: exists ? .destructive : .disabled) { action in
try FileManager.default.removeItem(at: url)
}
})
} else {
completion([])
}
}
]),
UIMenu(title: "Save State", image: UIImage(systemName: "arrow.down.circle"),
preferredElementSize: .small, children: [
UIDeferredMenuElement.uncached { completion in
if let grapeDirectory: URL = .grapeDirectory {
let statesDirectory: URL = grapeDirectory.appending(component: "states")
let states: [Int] = [0, 1, 2]
completion(states.map { state in
let url: URL = statesDirectory.appending(component: "\(self.game.details.name)_\(state).state")
let exists: Bool = FileManager.default.fileExists(atPath: url.path(percentEncoded: false))
return UIAction.async(image: UIImage(systemName: "\(state + 1).circle")) { action in
if exists {
await self.grapeCore.save(state: url)
} else {
await self.grapeCore.save(state: url)
}
}
})
} else {
completion([])
}
}
]),
UIMenu(title: "Load State", image: UIImage(systemName: "arrow.up.circle"),
preferredElementSize: .small, children: [
UIDeferredMenuElement.uncached { completion in
if let grapeDirectory: URL = .grapeDirectory {
let statesDirectory: URL = grapeDirectory.appending(component: "states")
let states: [Int] = [0, 1, 2]
completion(states.map { state in
let url: URL = statesDirectory.appending(component: "\(self.game.details.name)_\(state).state")
let exists: Bool = FileManager.default.fileExists(atPath: url.path(percentEncoded: false))
print(state, url.path(), exists)
return UIAction.async(image: UIImage(systemName: "\(state + 1).circle"),
attributes: exists ? [] : .disabled) { action in
await self.grapeCore.load(state: url)
}
})
} else {
completion([])
}
}
])
])
]))
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.grapeCore.press(button: .select)
}, { _ in
await self.grapeCore.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.grapeCore.press(button: .start)
}, { _ in
await self.grapeCore.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.grapeCore.press(button: .up)
}, { _ in
await self.grapeCore.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.grapeCore.press(button: .down)
}, { _ in
await self.grapeCore.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.grapeCore.press(button: .left)
}, { _ in
await self.grapeCore.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.grapeCore.press(button: .right)
}, { _ in
await self.grapeCore.release(button: .right)
}))
guard let rightButton else {
return
}
visualEffectView.contentView.addSubview(rightButton)
let aConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "a.circle"))
aButton = .button(with: aConfiguration,
actions: ({ _ in
await self.grapeCore.press(button: .a)
}, { _ in
await self.grapeCore.release(button: .a)
}))
guard let aButton else {
return
}
visualEffectView.contentView.addSubview(aButton)
let bConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "b.circle"))
bButton = .button(with: bConfiguration,
actions: ({ _ in
await self.grapeCore.press(button: .b)
}, { _ in
await self.grapeCore.release(button: .b)
}))
guard let bButton else {
return
}
visualEffectView.contentView.addSubview(bButton)
let yConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "y.circle"))
yButton = .button(with: yConfiguration,
actions: ({ _ in
await self.grapeCore.press(button: .y)
}, { _ in
await self.grapeCore.release(button: .y)
}))
guard let yButton else {
return
}
visualEffectView.contentView.addSubview(yButton)
let xConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "x.circle"))
xButton = .button(with: xConfiguration,
actions: ({ _ in
await self.grapeCore.press(button: .x)
}, { _ in
await self.grapeCore.release(button: .x)
}))
guard let xButton else {
return
}
visualEffectView.contentView.addSubview(xButton)
let lConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "left"))
lButton = .button(with: lConfiguration,
actions: ({ _ in
await self.grapeCore.press(button: .l)
}, { _ in
await self.grapeCore.release(button: .l)
}))
guard let lButton else {
return
}
visualEffectView.contentView.addSubview(lButton)
let rConfiguration: UIButton.Configuration = .configuration(.large, .capsule, UIImage(systemName: "right"))
rButton = .button(with: rConfiguration,
actions: ({ _ in
await self.grapeCore.press(button: .r)
}, { _ in
await self.grapeCore.release(button: .r)
}))
guard let rButton else {
return
}
visualEffectView.contentView.addSubview(rButton)
portraitBottomConstraint = secondaryBlurredImageView.bottomAnchor.constraint(equalTo: secondaryImageView.bottomAnchor)
guard let portraitBottomConstraint else {
return
}
if iPhone {
constraints.portrait.append(contentsOf: [
// blurred
blurredImageView.topAnchor.constraint(equalTo: view.topAnchor),
blurredImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
blurredImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
blurredImageView.heightAnchor.constraint(equalTo: blurredImageView.safeAreaLayoutGuide.widthAnchor, multiplier: 3 / 4),
secondaryBlurredImageView.topAnchor.constraint(equalTo: blurredImageView.safeAreaLayoutGuide.bottomAnchor),
secondaryBlurredImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
secondaryBlurredImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
secondaryBlurredImageView.heightAnchor.constraint(equalTo: secondaryBlurredImageView.safeAreaLayoutGuide.widthAnchor, multiplier: 3 / 4),
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// unblurred
imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
imageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
imageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
imageView.heightAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.widthAnchor, multiplier: 3 / 4),
secondaryImageView.topAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor, constant: 20),
secondaryImageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40),
secondaryImageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40),
secondaryImageView.heightAnchor.constraint(equalTo: secondaryImageView.safeAreaLayoutGuide.widthAnchor, multiplier: 3 / 4),
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.0),
bButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor),
aButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
aButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
yButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
yButton.trailingAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.leadingAnchor),
xButton.bottomAnchor.constraint(equalTo: yButton.safeAreaLayoutGuide.topAnchor),
xButton.leadingAnchor.constraint(equalTo: yButton.safeAreaLayoutGuide.trailingAnchor),
upButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
upButton.bottomAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
leftButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
leftButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
downButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
downButton.bottomAnchor.constraint(equalTo: selectButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
rightButton.leadingAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.trailingAnchor),
rightButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
lButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
lButton.bottomAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
rButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
rButton.bottomAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0)
])
constraints.landscape.append(contentsOf: [
// blurred
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// unblurred
startButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
startButton.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
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: 8.0 / 7.0),
bButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
bButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor),
aButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
aButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
yButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
yButton.trailingAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.leadingAnchor),
xButton.bottomAnchor.constraint(equalTo: yButton.safeAreaLayoutGuide.topAnchor),
xButton.leadingAnchor.constraint(equalTo: yButton.safeAreaLayoutGuide.trailingAnchor),
upButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
upButton.bottomAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
leftButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
leftButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
downButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
downButton.bottomAnchor.constraint(equalTo: selectButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
rightButton.leadingAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.trailingAnchor),
rightButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
lButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
lButton.bottomAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
rButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
rButton.bottomAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0)
])
} else {
constraints.portrait.append(contentsOf: [
blurredImageView.topAnchor.constraint(equalTo: view.topAnchor),
blurredImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
blurredImageView.bottomAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor,
constant: 20),
blurredImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
secondaryBlurredImageView.topAnchor.constraint(equalTo: blurredImageView.safeAreaLayoutGuide.bottomAnchor),
secondaryBlurredImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
secondaryBlurredImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
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: view.safeAreaLayoutGuide.topAnchor, constant: 20),
imageView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
imageView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 4 / 5),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 3 / 4),
secondaryImageView.topAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor,
constant: 20),
secondaryImageView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
secondaryImageView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 4 / 5, constant: -80),
secondaryImageView.heightAnchor.constraint(equalTo: secondaryImageView.widthAnchor, multiplier: 3 / 4),
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.0),
bButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor),
aButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
aButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
yButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
yButton.trailingAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.leadingAnchor),
xButton.bottomAnchor.constraint(equalTo: yButton.safeAreaLayoutGuide.topAnchor),
xButton.leadingAnchor.constraint(equalTo: yButton.safeAreaLayoutGuide.trailingAnchor),
upButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
upButton.bottomAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
leftButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
leftButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
downButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
downButton.bottomAnchor.constraint(equalTo: selectButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
rightButton.leadingAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.trailingAnchor),
rightButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
lButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
lButton.bottomAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
rButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
rButton.bottomAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
portraitBottomConstraint
])
topLandscapeLeadingConstraint = blurredImageView.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor,
constant: -20)
topLandscapeTrailingConstraint = blurredImageView.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20)
bottomLandscapeLeadingConstraint = secondaryBlurredImageView.leadingAnchor.constraint(equalTo: secondaryImageView.safeAreaLayoutGuide.leadingAnchor,
constant: -20)
bottomLandscapeTrailingConstraint = secondaryBlurredImageView.trailingAnchor.constraint(equalTo: secondaryImageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20)
guard let topLandscapeLeadingConstraint, let topLandscapeTrailingConstraint,
let bottomLandscapeLeadingConstraint, let bottomLandscapeTrailingConstraint else {
return
}
constraints.landscape.append(contentsOf: [
blurredImageView.topAnchor.constraint(equalTo: view.topAnchor),
blurredImageView.bottomAnchor.constraint(equalTo: view.centerYAnchor),
blurredImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
secondaryBlurredImageView.topAnchor.constraint(equalTo: view.centerYAnchor),
secondaryBlurredImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
secondaryBlurredImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
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: view.topAnchor,
constant: 20),
imageView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
imageView.bottomAnchor.constraint(equalTo: view.centerYAnchor,
constant: -10),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor,
multiplier: 4 / 3),
secondaryImageView.topAnchor.constraint(equalTo: view.centerYAnchor,
constant: 10),
secondaryImageView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
secondaryImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor,
constant: -20),
secondaryImageView.widthAnchor.constraint(equalTo: secondaryImageView.heightAnchor,
multiplier: 4 / 3),
startButton.bottomAnchor.constraint(equalTo: visualEffectView.contentView.bottomAnchor,
constant: -20.0),
startButton.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor,
constant: 20.0),
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),
bButton.bottomAnchor.constraint(equalTo: startButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
bButton.trailingAnchor.constraint(equalTo: aButton.safeAreaLayoutGuide.leadingAnchor),
aButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
aButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
yButton.bottomAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.topAnchor),
yButton.trailingAnchor.constraint(equalTo: bButton.safeAreaLayoutGuide.leadingAnchor),
xButton.bottomAnchor.constraint(equalTo: yButton.safeAreaLayoutGuide.topAnchor),
xButton.leadingAnchor.constraint(equalTo: yButton.safeAreaLayoutGuide.trailingAnchor),
upButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
upButton.bottomAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.topAnchor),
leftButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
leftButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
downButton.leadingAnchor.constraint(equalTo: leftButton.safeAreaLayoutGuide.trailingAnchor),
downButton.bottomAnchor.constraint(equalTo: selectButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
rightButton.leadingAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.trailingAnchor),
rightButton.bottomAnchor.constraint(equalTo: downButton.safeAreaLayoutGuide.topAnchor),
lButton.leadingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.leadingAnchor,
constant: 20.0),
lButton.bottomAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
rButton.trailingAnchor.constraint(equalTo: visualEffectView.contentView.safeAreaLayoutGuide.trailingAnchor,
constant: -20.0),
rButton.bottomAnchor.constraint(equalTo: upButton.safeAreaLayoutGuide.topAnchor,
constant: -20.0),
topLandscapeLeadingConstraint,
topLandscapeTrailingConstraint,
bottomLandscapeLeadingConstraint,
bottomLandscapeTrailingConstraint
])
}
switch interfaceOrientation() {
case .portrait:
view.addConstraints(constraints.portrait)
case .landscapeLeft, .landscapeRight:
view.addConstraints(constraints.landscape)
default:
break
}
if let device {
commandQueue = device.makeCommandQueue()
guard let library = device.makeDefaultLibrary() else {
return
}
let vertexFunction = library.makeFunction(name: "grape_vertex_main")
let fragmentFunction = library.makeFunction(name: "grape_fragment_main")
let pipelineDescriptor: MTLRenderPipelineDescriptor = .init()
pipelineDescriptor.colorAttachments[0].pixelFormat = imageView.colorPixelFormat
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.vertexFunction = vertexFunction
do {
pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
} catch {
fatalError("Failed to create pipeline state: \(error)")
}
let vertices: [Float] = [
-1, 1, 0, 0, // top-left
-1, -1, 0, 1, // bottom-left
1, 1, 1, 0, // top-right
1, -1, 1, 1 // bottom-right
]
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Float>.size, options: [])
}
/* */
Task {
guard let device else {
return
}
await grapeCore.insert(cartridge: game.details.url)
await grapeCore.videoCallback { primaryBuffer, secondaryBuffer in
Task { @MainActor in
if self.topTexture == nil {
let descriptor: MTLTextureDescriptor = .texture2DDescriptor(pixelFormat: .bgra8Unorm,
width: 256,
height: 192,
mipmapped: false)
descriptor.storageMode = .shared
descriptor.usage = [.shaderRead, .shaderWrite]
self.topTexture = device.makeTexture(descriptor: descriptor)
self.bottomTexture = device.makeTexture(descriptor: descriptor)
let samplerDescriptor = MTLSamplerDescriptor()
samplerDescriptor.minFilter = .nearest
samplerDescriptor.magFilter = .nearest
samplerDescriptor.sAddressMode = .clampToEdge
samplerDescriptor.tAddressMode = .clampToEdge
self.samplerState = device.makeSamplerState(descriptor: samplerDescriptor)!
}
let bytesPerRow = 256 * 4
let region: MTLRegion = MTLRegionMake2D(0, 0, 256, 192)
if let texture = self.topTexture {
texture.replace(region: region, mipmapLevel: 0, withBytes: primaryBuffer, bytesPerRow: bytesPerRow)
}
blurredImageView.setNeedsDisplay()
imageView.setNeedsDisplay()
if let texture = self.bottomTexture {
texture.replace(region: region, mipmapLevel: 0, withBytes: secondaryBuffer, bytesPerRow: bytesPerRow)
}
secondaryBlurredImageView.setNeedsDisplay()
secondaryImageView.setNeedsDisplay()
}
}
await grapeCore.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.grapeCore.pause() : self.grapeCore.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.GrapeCore.GrapeButton, pressed: Bool) {
Task {
if pressed {
await self.grapeCore.press(button: button)
} else {
await self.grapeCore.release(button: button)
}
}
}
extendedGamepad.buttonA.pressedChangedHandler = { element, value, pressed in
input(button: .b, pressed: pressed)
}
extendedGamepad.buttonB.pressedChangedHandler = { element, value, pressed in
input(button: .a, pressed: pressed)
}
extendedGamepad.buttonX.pressedChangedHandler = { element, value, pressed in
input(button: .y, pressed: pressed)
}
extendedGamepad.buttonY.pressedChangedHandler = { element, value, pressed in
input(button: .y, 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)
}
extendedGamepad.leftTrigger.pressedChangedHandler = { element, value, pressed in
input(button: .l, pressed: pressed)
}
extendedGamepad.rightTrigger.pressedChangedHandler = { element, value, pressed in
input(button: .r, 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 preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { .portrait }
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .portrait }
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
switch interfaceOrientation() {
case .portrait:
guard let portraitBottomConstraint else {
return
}
portraitBottomConstraint.constant = view.safeAreaInsets.top
case .portraitUpsideDown:
guard let portraitBottomConstraint else {
return
}
portraitBottomConstraint.constant = view.safeAreaInsets.bottom
case .landscapeLeft:
guard let topLandscapeLeadingConstraint, let topLandscapeTrailingConstraint,
let bottomLandscapeLeadingConstraint, let bottomLandscapeTrailingConstraint else {
return
}
topLandscapeLeadingConstraint.constant = -view.safeAreaInsets.right
topLandscapeTrailingConstraint.constant = view.safeAreaInsets.right
bottomLandscapeLeadingConstraint.constant = -view.safeAreaInsets.right
bottomLandscapeTrailingConstraint.constant = view.safeAreaInsets.right
case .landscapeRight:
guard let topLandscapeLeadingConstraint, let topLandscapeTrailingConstraint,
let bottomLandscapeLeadingConstraint, let bottomLandscapeTrailingConstraint else {
return
}
topLandscapeLeadingConstraint.constant = -view.safeAreaInsets.left
topLandscapeTrailingConstraint.constant = view.safeAreaInsets.left
bottomLandscapeLeadingConstraint.constant = -view.safeAreaInsets.left
bottomLandscapeTrailingConstraint.constant = view.safeAreaInsets.left
default:
break
}
Task {
view.setNeedsUpdateConstraints()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
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 GrapeController {
@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)
}
}
}
extension GrapeController {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let secondaryImageView, let touch = touches.first, touch.view == secondaryImageView else {
return
}
let locationInView = touch.location(in: secondaryImageView)
let viewSize = secondaryImageView.frame.size
let mappedX = (locationInView.x / viewSize.width) * 256
let mappedY = (locationInView.y / viewSize.height) * 192
let finalPoint = CGPoint(x: mappedX, y: mappedY)
Task {
await grapeCore.touchBegan(point: finalPoint)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
Task {
await grapeCore.touchEnded()
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let secondaryImageView, let touch = touches.first, touch.view == secondaryImageView else {
return
}
let locationInView = touch.location(in: secondaryImageView)
let viewSize = secondaryImageView.frame.size
let mappedX = (locationInView.x / viewSize.width) * 256
let mappedY = (locationInView.y / viewSize.height) * 192
let finalPoint = CGPoint(x: mappedX, y: mappedY)
Task {
await grapeCore.touchMoved(point: finalPoint)
}
}
}
extension GrapeController : MTKViewDelegate {
func draw(in view: MTKView) {
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let topTexture = self.topTexture,
let bottomTexture = self.bottomTexture else { return }
// --- Second Pass: draw to screen ---
guard let viewPass = view.currentRenderPassDescriptor,
let drawable = view.currentDrawable else { return }
func pass(_ texture: MTLTexture) {
if let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: viewPass) {
encoder.setRenderPipelineState(pipelineState)
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
encoder.setFragmentTexture(texture, index: 0)
encoder.setFragmentSamplerState(samplerState, index: 0)
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
encoder.endEncoding()
}
}
if view == imageView || view == blurredImageView {
pass(topTexture)
}
if view == secondaryImageView || view == secondaryBlurredImageView {
pass(bottomTexture)
}
commandBuffer.present(drawable)
commandBuffer.commit()
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
}