‹ projects

reverb-m1

Native wired VR on Apple silicon: Reverb G1 display bring-up and stereo demo
git clone https://umhau.org/code/reverb-m1.git
Log | Files | Refs

OpticsControls.swift (6621B)


      1 import AppKit
      2 
      3 struct OpticalSettings: Equatable, Codable {
      4     enum Pattern: Int, Codable { case scene, whiteGrid, greenGrid }
      5     var pattern: Pattern = .scene
      6     var redGain: Float = 1
      7     var blueHorizontalGain: Float = 1
      8     var blueVerticalGain: Float = 1
      9 
     10     static var cacheURL: URL { HeadsetOptics.cacheURL.deletingLastPathComponent().appendingPathComponent("optics-adjustments.json") }
     11     static func initial() -> OpticalSettings {
     12         var settings = (try? JSONDecoder().decode(OpticalSettings.self, from: Data(contentsOf: cacheURL))) ?? OpticalSettings()
     13         settings.redGain = settings.redGain.isFinite ? min(4, max(-1, settings.redGain)) : 1
     14         settings.blueHorizontalGain = settings.blueHorizontalGain.isFinite ? min(4, max(-1, settings.blueHorizontalGain)) : 1
     15         settings.blueVerticalGain = settings.blueVerticalGain.isFinite ? min(4, max(-1, settings.blueVerticalGain)) : 1
     16         if CommandLine.arguments.contains("--green-grid") { settings.pattern = .greenGrid }
     17         else if CommandLine.arguments.contains("--optics-grid") { settings.pattern = .whiteGrid }
     18         return settings
     19     }
     20 
     21     func save() {
     22         do { try JSONEncoder().encode(self).write(to: Self.cacheURL, options: .atomic) }
     23         catch { log("cannot save optical adjustments: \(error)") }
     24     }
     25 }
     26 
     27 /// Main-screen controls; the render worker receives settings through Renderer's lock.
     28 final class OpticsControls: NSObject {
     29     private let renderer: Renderer
     30     private let window: NSWindow
     31     private let pattern = NSPopUpButton()
     32     private let red = NSSlider(value: 1, minValue: -1, maxValue: 4, target: nil, action: nil)
     33     private let blueHorizontal = NSSlider(value: 1, minValue: -1, maxValue: 4, target: nil, action: nil)
     34     private let blueVertical = NSSlider(value: 1, minValue: -1, maxValue: 4, target: nil, action: nil)
     35     private let redLabel = NSTextField(labelWithString: "")
     36     private var saveTimer: Timer?
     37     private let blueHorizontalLabel = NSTextField(labelWithString: "")
     38 
     39     private let blueVerticalLabel = NSTextField(labelWithString: "")
     40 
     41     init(renderer: Renderer) {
     42         self.renderer = renderer
     43         window = NSWindow(contentRect: NSRect(x: 60, y: 100, width: 460, height: 340),
     44             styleMask: [.titled, .miniaturizable], backing: .buffered, defer: false)
     45         super.init()
     46         window.title = "Headset optics"
     47         window.isReleasedWhenClosed = false
     48         let stack = NSStackView()
     49         stack.orientation = .vertical; stack.alignment = .leading; stack.spacing = 12
     50         stack.edgeInsets = NSEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
     51         stack.translatesAutoresizingMaskIntoConstraints = false
     52         window.contentView!.addSubview(stack)
     53         NSLayoutConstraint.activate([
     54             stack.leadingAnchor.constraint(equalTo: window.contentView!.leadingAnchor),
     55             stack.trailingAnchor.constraint(equalTo: window.contentView!.trailingAnchor),
     56             stack.topAnchor.constraint(equalTo: window.contentView!.topAnchor)
     57         ])
     58         pattern.addItems(withTitles: ["3D scene", "White grid — sharpness and color", "Green grid — sharpness only"])
     59         pattern.target = self; pattern.action = #selector(changed)
     60         stack.addArrangedSubview(pattern)
     61         let help = NSTextField(wrappingLabelWithString: "Compare both line directions on the stationary grids. Try one eye at a time; the green cross marks straight ahead.")
     62         stack.addArrangedSubview(help)
     63         for (slider, label) in [(red, redLabel), (blueHorizontal, blueHorizontalLabel), (blueVertical, blueVerticalLabel)] {
     64             slider.target = self; slider.action = #selector(changed)
     65             slider.isContinuous = true
     66             slider.widthAnchor.constraint(equalToConstant: 410).isActive = true
     67             stack.addArrangedSubview(label); stack.addArrangedSubview(slider)
     68         }
     69         stack.addArrangedSubview(NSTextField(labelWithString: "Color correction: 0% = off, 100% = factory"))
     70         let reset = NSButton(title: "Reset colors to factory", target: self, action: #selector(resetColors))
     71         let buttons = NSStackView(views: [reset, NSButton(title: "Quit", target: NSApp, action: #selector(NSApplication.terminate(_:)))])
     72         buttons.spacing = 12
     73         stack.addArrangedSubview(buttons)
     74         let settings = renderer.opticalSettings()
     75         pattern.selectItem(at: settings.pattern.rawValue)
     76         red.floatValue = settings.redGain
     77         blueHorizontal.floatValue = settings.blueHorizontalGain
     78         blueVertical.floatValue = settings.blueVerticalGain
     79         refreshLabels()
     80         window.setContentSize(NSSize(width: 460, height: max(340, stack.fittingSize.height)))
     81         if let screen = NSScreen.screens.first(where: { ($0.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value == CGMainDisplayID() }) {
     82             window.setFrameOrigin(NSPoint(x: screen.visibleFrame.minX + 60, y: screen.visibleFrame.maxY - window.frame.height - 60))
     83         }
     84         window.makeKeyAndOrderFront(nil)
     85     }
     86 
     87     @objc private func changed() {
     88         var settings = renderer.opticalSettings()
     89         settings.pattern = OpticalSettings.Pattern(rawValue: pattern.indexOfSelectedItem) ?? .scene
     90         settings.redGain = (red.floatValue * 100).rounded() / 100
     91         settings.blueHorizontalGain = (blueHorizontal.floatValue * 100).rounded() / 100
     92         settings.blueVerticalGain = (blueVertical.floatValue * 100).rounded() / 100
     93         renderer.setOpticalSettings(settings)
     94         saveTimer?.invalidate()
     95         saveTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in settings.save() }
     96         refreshLabels()
     97     }
     98 
     99     private func refreshLabels() {
    100         let colorsVisible = pattern.indexOfSelectedItem != OpticalSettings.Pattern.greenGrid.rawValue
    101         red.isEnabled = colorsVisible; blueHorizontal.isEnabled = colorsVisible; blueVertical.isEnabled = colorsVisible
    102         redLabel.stringValue = String(format: "Red correction: %.0f%%", red.floatValue * 100)
    103         blueHorizontalLabel.stringValue = String(format: "Blue horizontal correction (sides): %.0f%%", blueHorizontal.floatValue * 100)
    104         blueVerticalLabel.stringValue = String(format: "Blue vertical correction (top/bottom): %.0f%%", blueVertical.floatValue * 100)
    105     }
    106 
    107     @objc private func resetColors() { red.floatValue = 1; blueHorizontal.floatValue = 1; blueVertical.floatValue = 1; changed() }
    108     func close() {
    109         saveTimer?.invalidate(); saveTimer = nil
    110         renderer.opticalSettings().save()
    111         window.orderOut(nil)
    112     }
    113 }