using Godot; using System; using System.IO.MemoryMappedFiles; using System.Runtime.InteropServices; /// /// Memory-mapped data structure to share with C++ code /// [StructLayout(LayoutKind.Sequential)] public struct SharedGazeData { public float LeftEyeX; public float LeftEyeY; public float LeftEyeZ; public float RightEyeX; public float RightEyeY; public float RightEyeZ; public float CombinedX; public float CombinedY; public float CombinedZ; public float Confidence; public long Timestamp; public int IsValid; public int LeftEyeFlagCondition; // 0 = false, 1 = true public int RightEyeFlagCondition; // 0 = false, 1 = true } /// /// Enum defining different data types for memory-mapped eye tracking data /// public enum MemmapDataTypes { EYES_VECTORS, EYES_ANGLES, DEBUG_INFO } /// /// Consumer for sending eye tracking data to a memory-mapped file for C++ interop /// public partial class MemmapConsumer : EyeTrackingConsumer { private MemoryMappedFile _sharedMem; private MemoryMappedViewAccessor _accessor; private bool _running; private bool _debug; private const string SharedMemoryName = "EyeTrackingMemmapData"; protected FrustumLoader _frustumLoader; /// /// Initializes a new instance of the MemmapConsumer class /// /// The name of this consumer public MemmapConsumer(string name = "Memmap Consumer") : base(name) { // Initialize frustum loader _frustumLoader = new FrustumLoader(); if (!_frustumLoader.LoadFrustumData()) { GD.PrintErr("Failed to load frustum data. Eye tracking will not function without frustum data."); throw new InvalidOperationException("Frustum data is required for MemmapConsumer to function properly."); } _debug = false; InitializeMemmap(); } /// /// Initialize the memory-mapped file /// private void InitializeMemmap() { try { Cleanup(); // Create a memory-mapped file of the size of SharedGazeData _sharedMem = MemoryMappedFile.CreateOrOpen( SharedMemoryName, Marshal.SizeOf(), MemoryMappedFileAccess.ReadWrite); // Create a view accessor for the memory-mapped file _accessor = _sharedMem.CreateViewAccessor(0, Marshal.SizeOf()); // Initialize with zeros and mark as invalid var data = new SharedGazeData { IsValid = 0 }; _accessor.Write(0, ref data); GD.Print($"{Name}: Successfully created shared memory"); _running = true; } catch (Exception ex) { GD.PrintErr($"{Name}: Failed to create shared memory: {ex.Message}"); GD.PrintErr(ex.StackTrace); _running = false; } } /// /// Cleanup mem resources /// private void Cleanup() { if (_accessor != null) { try { // Set data as invalid when stopping var data = new SharedGazeData { IsValid = 0 }; _accessor.Write(0, ref data); _accessor.Dispose(); _accessor = null; GD.Print($"{Name}: View accessor closed"); } catch (Exception ex) { GD.PrintErr($"{Name}: Error closing view accessor: {ex.Message}"); GD.PrintErr(ex.StackTrace); } } if (_sharedMem != null) { try { _sharedMem.Dispose(); _sharedMem = null; GD.Print($"{Name}: Shared memory closed"); } catch (Exception ex) { GD.PrintErr($"{Name}: Error closing shared memory: {ex.Message}"); GD.PrintErr(ex.StackTrace); } } } /// /// Process eye tracking data and send to the shared memory /// /// Eye tracking data public override void OnEyeDataUpdate(EyeTrackingData data) { if (Enabled) { ProcessData(data); } } /// /// Process eye tracking data and update shared memory /// /// Eye tracking data protected override void ProcessData(EyeTrackingData data) { if (!_running || _accessor == null) return; if (!_frustumLoader.IsLoaded) { GD.PrintErr($"{Name}: Frustum data is not loaded. Cannot compute gaze without frustum data."); return; } try { float leftX = data.LeftEyeX; float leftY = data.LeftEyeY; float rightX = data.RightEyeX; float rightY = data.RightEyeY; // If right eye data is invalid (0,0), use left eye data bool singleEyeTracking = rightX == 0 && rightY == 0; if (singleEyeTracking) { rightX = leftX; rightY = leftY; } // Convert eye positions to pitch and yaw using frustum loader // Convert normalized screen positions (0-1 range) to view angles Vector2 leftAngles = _frustumLoader.ScreenPositionToViewAngles(new Vector2(leftX, leftY), true); Vector2 rightAngles = _frustumLoader.ScreenPositionToViewAngles(new Vector2(rightX, rightY), false); // Extract pitch (vertical) and yaw (horizontal) angles float leftYaw = leftAngles.X; float leftPitch = leftAngles.Y; float rightYaw = rightAngles.X; float rightPitch = rightAngles.Y; if (_debug) { GD.Print($"Left eye - Pitch: {leftPitch}, Yaw: {leftYaw}"); GD.Print($"Right eye - Pitch: {rightPitch}, Yaw: {rightYaw}"); } // Convert angles to vectors Vector3 leftVector = AnglesToVector(leftPitch, leftYaw); Vector3 rightVector = AnglesToVector(rightPitch, rightYaw); // Calculate combined gaze direction (average of both eyes) Vector3 combinedVector = new Vector3( (leftVector.X + rightVector.X) / 2, (leftVector.Y + rightVector.Y) / 2, (leftVector.Z + rightVector.Z) / 2).Normalized(); // Calculate confidence float confidence = data.Confidence; // Create shared data structure var sharedData = new SharedGazeData { LeftEyeX = leftVector.X, LeftEyeY = leftVector.Y, LeftEyeZ = leftVector.Z, RightEyeX = rightVector.X, RightEyeY = rightVector.Y, RightEyeZ = rightVector.Z, CombinedX = combinedVector.X, CombinedY = combinedVector.Y, CombinedZ = combinedVector.Z, Confidence = confidence, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), IsValid = 1, LeftEyeFlagCondition = data.LeftEyeFlagCondition ? 1 : 0, RightEyeFlagCondition = data.RightEyeFlagCondition ? 1 : 0 }; // Write to shared memory _accessor.Write(0, ref sharedData); } catch (Exception ex) { GD.PrintErr($"{Name}: Error updating shared memory: {ex.Message}"); GD.PrintErr(ex.StackTrace); } } /// /// Convert pitch and yaw angles to a 3D direction vector /// /// Pitch angle in degrees /// Yaw angle in degrees /// Direction vector private Vector3 AnglesToVector(float pitch, float yaw) { // Convert degrees to radians float pitchRad = (float)(pitch * Math.PI / 180.0); float yawRad = (float)(yaw * Math.PI / 180.0); // Calculate direction vector float x = (float)(Math.Sin(yawRad) * Math.Cos(pitchRad)); float y = (float)Math.Sin(pitchRad); float z = (float)(Math.Cos(yawRad) * Math.Cos(pitchRad)); return new Vector3(x, y, -z); // Note: negating z as forward is -z in many 3D engines } /// /// Start the consumer and recreate resources /// public override void Enable() { base.Enable(); // Create memmap resources InitializeMemmap(); } /// /// Stop the consumer and clean up resources /// public override void Disable() { base.Disable(); // Call the base class Stop method first Cleanup(); _running = false; } }