diff --git a/Gaze/Services/EyeTracking/CameraSessionManager.swift b/Gaze/Services/EyeTracking/CameraSessionManager.swift index 6598709..e04dec5 100644 --- a/Gaze/Services/EyeTracking/CameraSessionManager.swift +++ b/Gaze/Services/EyeTracking/CameraSessionManager.swift @@ -10,16 +10,20 @@ import Combine import Foundation protocol CameraSessionDelegate: AnyObject { - nonisolated func cameraSession( + @MainActor func cameraSession( _ manager: CameraSessionManager, didOutput pixelBuffer: CVPixelBuffer, imageSize: CGSize ) } +private struct PixelBufferBox: @unchecked Sendable { + let buffer: CVPixelBuffer +} + final class CameraSessionManager: NSObject, ObservableObject { @Published private(set) var isRunning = false - weak var delegate: CameraSessionDelegate? + nonisolated(unsafe) weak var delegate: CameraSessionDelegate? private var captureSession: AVCaptureSession? private var videoOutput: AVCaptureVideoDataOutput? @@ -116,6 +120,11 @@ extension CameraSessionManager: AVCaptureVideoDataOutputSampleBufferDelegate { height: CVPixelBufferGetHeight(pixelBuffer) ) - delegate?.cameraSession(self, didOutput: pixelBuffer, imageSize: size) + let bufferBox = PixelBufferBox(buffer: pixelBuffer) + + DispatchQueue.main.async { [weak self, bufferBox] in + guard let self else { return } + self.delegate?.cameraSession(self, didOutput: bufferBox.buffer, imageSize: size) + } } } diff --git a/Gaze/Services/EyeTracking/EyeTrackingService.swift b/Gaze/Services/EyeTracking/EyeTrackingService.swift index 624ddbc..0ebd66c 100644 --- a/Gaze/Services/EyeTracking/EyeTrackingService.swift +++ b/Gaze/Services/EyeTracking/EyeTrackingService.swift @@ -161,7 +161,7 @@ class EyeTrackingService: NSObject, ObservableObject { } extension EyeTrackingService: CameraSessionDelegate { - nonisolated func cameraSession( + @MainActor func cameraSession( _ manager: CameraSessionManager, didOutput pixelBuffer: CVPixelBuffer, imageSize: CGSize @@ -174,28 +174,23 @@ extension EyeTrackingService: CameraSessionDelegate { if let leftRatio = result.leftPupilRatio, let rightRatio = result.rightPupilRatio, let faceWidth = result.faceWidthRatio { - Task { @MainActor in - guard CalibratorService.shared.isCalibrating else { return } - CalibratorService.shared.submitSampleToBridge( - leftRatio: leftRatio, - rightRatio: rightRatio, - leftVertical: result.leftVerticalRatio, - rightVertical: result.rightVerticalRatio, - faceWidthRatio: faceWidth - ) - } + guard CalibratorService.shared.isCalibrating else { return } + CalibratorService.shared.submitSampleToBridge( + leftRatio: leftRatio, + rightRatio: rightRatio, + leftVertical: result.leftVerticalRatio, + rightVertical: result.rightVerticalRatio, + faceWidthRatio: faceWidth + ) } - Task { @MainActor [weak self] in - guard let self else { return } - self.faceDetected = result.faceDetected - self.isEyesClosed = result.isEyesClosed - self.userLookingAtScreen = result.userLookingAtScreen - self.debugAdapter.update(from: result) - self.debugAdapter.updateEyeImages(from: PupilDetector.self) - self.syncDebugState() - self.updateGazeConfiguration() - } + self.faceDetected = result.faceDetected + self.isEyesClosed = result.isEyesClosed + self.userLookingAtScreen = result.userLookingAtScreen + self.debugAdapter.update(from: result) + self.debugAdapter.updateEyeImages(from: PupilDetector.self) + self.syncDebugState() + self.updateGazeConfiguration() } } diff --git a/Gaze/Services/EyeTracking/GazeDetector.swift b/Gaze/Services/EyeTracking/GazeDetector.swift index 889cbe4..04bcdfd 100644 --- a/Gaze/Services/EyeTracking/GazeDetector.swift +++ b/Gaze/Services/EyeTracking/GazeDetector.swift @@ -6,7 +6,7 @@ // import Foundation -import Vision +@preconcurrency import Vision import simd struct EyeTrackingProcessingResult: Sendable { @@ -54,19 +54,19 @@ final class GazeDetector: @unchecked Sendable { } private let lock = NSLock() - private var configuration: Configuration + private nonisolated(unsafe) var configuration: Configuration - init(configuration: Configuration) { + nonisolated init(configuration: Configuration) { self.configuration = configuration } - func updateConfiguration(_ configuration: Configuration) { + nonisolated func updateConfiguration(_ configuration: Configuration) { lock.lock() self.configuration = configuration lock.unlock() } - nonisolated func process( + func process( analysis: VisionPipeline.FaceAnalysis, pixelBuffer: CVPixelBuffer ) -> EyeTrackingProcessingResult { @@ -75,7 +75,7 @@ final class GazeDetector: @unchecked Sendable { config = configuration lock.unlock() - guard analysis.faceDetected, let face = analysis.face else { + guard analysis.faceDetected, let face = analysis.face?.value else { return EyeTrackingProcessingResult( faceDetected: false, isEyesClosed: false, diff --git a/Gaze/Services/EyeTracking/PupilDetector.swift b/Gaze/Services/EyeTracking/PupilDetector.swift index 2ef3349..59ca1ff 100644 --- a/Gaze/Services/EyeTracking/PupilDetector.swift +++ b/Gaze/Services/EyeTracking/PupilDetector.swift @@ -18,7 +18,7 @@ import Accelerate import CoreImage import ImageIO import UniformTypeIdentifiers -import Vision +@preconcurrency import Vision struct PupilPosition: Equatable, Sendable { let x: CGFloat diff --git a/Gaze/Services/EyeTracking/VisionPipeline.swift b/Gaze/Services/EyeTracking/VisionPipeline.swift index 3d5a8fe..70210a2 100644 --- a/Gaze/Services/EyeTracking/VisionPipeline.swift +++ b/Gaze/Services/EyeTracking/VisionPipeline.swift @@ -6,17 +6,21 @@ // import Foundation -import Vision +@preconcurrency import Vision final class VisionPipeline: @unchecked Sendable { struct FaceAnalysis: Sendable { let faceDetected: Bool - let face: VNFaceObservation? + let face: NonSendableFaceObservation? let imageSize: CGSize let debugYaw: Double? let debugPitch: Double? } + struct NonSendableFaceObservation: @unchecked Sendable { + nonisolated(unsafe) let value: VNFaceObservation + } + nonisolated func analyze( pixelBuffer: CVPixelBuffer, imageSize: CGSize @@ -46,7 +50,7 @@ final class VisionPipeline: @unchecked Sendable { ) } - guard let face = (request.results as? [VNFaceObservation])?.first else { + guard let face = request.results?.first else { return FaceAnalysis( faceDetected: false, face: nil, @@ -58,7 +62,7 @@ final class VisionPipeline: @unchecked Sendable { return FaceAnalysis( faceDetected: true, - face: face, + face: NonSendableFaceObservation(value: face), imageSize: imageSize, debugYaw: face.yaw?.doubleValue, debugPitch: face.pitch?.doubleValue diff --git a/Gaze/Views/Containers/MenuBarGuideOverlayView.swift b/Gaze/Views/Containers/MenuBarGuideOverlayView.swift index 656eddf..e637132 100644 --- a/Gaze/Views/Containers/MenuBarGuideOverlayView.swift +++ b/Gaze/Views/Containers/MenuBarGuideOverlayView.swift @@ -76,7 +76,10 @@ final class MenuBarGuideOverlayPresenter { private func startCheckTimer() { checkTimer?.invalidate() checkTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in - self?.checkWindowFrame() + guard let self else { return } + Task { @MainActor in + self.checkWindowFrame() + } } } @@ -129,7 +132,10 @@ final class MenuBarGuideOverlayPresenter { // Set up KVO for window frame changes onboardingWindowObserver = onboardingWindow.observe(\.frame, options: [.new, .old]) { [weak self] _, _ in - self?.checkWindowFrame() + guard let self else { return } + Task { @MainActor in + self.checkWindowFrame() + } } // Add observer for when the onboarding window is closed @@ -145,7 +151,10 @@ final class MenuBarGuideOverlayPresenter { } // Hide the overlay when onboarding window closes - self?.hide() + guard let self else { return } + Task { @MainActor in + self.hide() + } } } } diff --git a/Gaze/Views/Setup/LookAwaySetupView.swift b/Gaze/Views/Setup/LookAwaySetupView.swift index 1a0d6c4..78c7211 100644 --- a/Gaze/Views/Setup/LookAwaySetupView.swift +++ b/Gaze/Views/Setup/LookAwaySetupView.swift @@ -54,7 +54,6 @@ struct LookAwaySetupView: View { private func previewLookAway() { guard let screen = NSScreen.main else { return } - let sizePercentage = settingsManager.settings.subtleReminderSize.percentage let lookAwayIntervalMinutes = settingsManager.settings.lookAwayIntervalMinutes PreviewWindowHelper.showPreview(on: screen) { dismiss in LookAwayReminderView(countdownSeconds: lookAwayIntervalMinutes * 60, onDismiss: dismiss)