using Godot; using System; using ImGuiNET; using BSEnroll; using System.IO; using ETPreferences; using AlignmentHelper; using System.Diagnostics; using System.Collections.Generic; using VRCFT; using System.Net; using System.Globalization; public partial class Ui : Node { public enum process_error { UsingCPU, CameraRevoked, NoFrustum } public enum auth_code { InvalidToken, ExpiredToken, Success } public enum xr_launch_type { Enrollment, Aligner } public enum window_pages { Home, Debug, Advanced, DFR } [Flags] public enum feature_announcements { None = 0, Blink = 1 << 0, DFR = 1 << 1, Wink = 1 << 2, // [ Add more with new announcements ] } public struct adv_toggle { public string ID; public Func Get; public Action Set; } public struct adv_dropdown { public string Label; public string ID; public string[] Options; public Func GetIndex; public Action SetIndex; } public struct dfr_app { public string title; public string desc; } public Main main; // Signal to notify when the model is selected [Signal] public delegate void ModelSelectedEventHandler(string modelPath); [Signal] public delegate void DisableCameraEventHandler(); [Signal] public delegate void EnableCameraEventHandler(); [Export] public Godot.Collections.Array model_names { get; set; } = new Godot.Collections.Array(); public bool hault_render { get; set; } = false; public Godot.Collections.Array consumers { get; set; } = new Godot.Collections.Array(); public GazeEstimation gazeEstimation { get; set; } private const bool ENABLE_POLICY_NOTICE = true; private const bool DEBUG_SETTINGS = false; // Track the currently selected model index (-1 means no selection) private int selectedModelIndex = -1; private string pending_popup; private window_pages activePage = window_pages.Home; private auth_code authentication_status; // Set UI element scale private float globalScale = 1; private const float windowHeight = 470; private const float windowWidth = 600; private float windowScale { get { return globalScale; } } private float checkboxScale { get { return (1.5f * globalScale); } } private float modelUIScale { get { return (1.5f * globalScale); } } private float sectionFontScale { get { return (0.85f * globalScale); } } private const float popup_buttonWidth = 70.0f; private const float popup_spacing = 10.0f; private const float popup_totalWidth = (popup_buttonWidth * 2) + popup_spacing; // Beyond theme colors private System.Numerics.Vector4 button_fill = new System.Numerics.Vector4(1, 1, 1, 1); private System.Numerics.Vector4 button_hover = new System.Numerics.Vector4(0.9f, 0.9f, 0.9f, 1); private System.Numerics.Vector4 button_click = new System.Numerics.Vector4(0.7f, 0.7f, 0.7f, 1); private System.Numerics.Vector4 button_text = new System.Numerics.Vector4(0, 0, 0, 1); private System.Numerics.Vector4 textbox_fill = new System.Numerics.Vector4(0.401f, 0.217f, 1.68f, 1); private System.Numerics.Vector4 textbox_text = new System.Numerics.Vector4(1, 1, 1, 1); private System.Numerics.Vector4 checkbox_fill = new System.Numerics.Vector4(0.401f, 0.217f, 1.68f, 1); private System.Numerics.Vector4 checkbox_hover = new System.Numerics.Vector4(0.401f, 0.217f, 1.68f, 1); private System.Numerics.Vector4 checkbox_click = new System.Numerics.Vector4(0.201f, 0.017f, 0.868f, 1); private System.Numerics.Vector4 checkbox_icon = new System.Numerics.Vector4(1, 1, 1, 1); private System.Numerics.Vector4 dropdown_fill = new System.Numerics.Vector4(0.401f, 0.217f, 1.68f, 1); private System.Numerics.Vector4 dropdown_hover = new System.Numerics.Vector4(0.401f, 0.217f, 1.68f, 1); private System.Numerics.Vector4 dropdown_click = new System.Numerics.Vector4(0.201f, 0.017f, 0.868f, 1); private System.Numerics.Vector4 dropdown_button = new System.Numerics.Vector4(0.47f, 0.494f, 0.505f, 1); private System.Numerics.Vector4 dropdown_button_hover = new System.Numerics.Vector4(0.37f, 0.394f, 0.405f, 1); private System.Numerics.Vector4 dropdown_button_click = new System.Numerics.Vector4(0.37f, 0.394f, 0.405f, 1); private System.Numerics.Vector4 popup_title_color = new System.Numerics.Vector4(0.401f, 0.217f, 1.68f, 1); private System.Numerics.Vector4 sub_text = new System.Numerics.Vector4(0.437f, 0.494f, 0.505f, 1); private System.Numerics.Vector4 hyperlink_text = new System.Numerics.Vector4(0.2f, 0.6f, 1, 1); private ET_Preferences etSettings { get { return ET_Preferences.Instance; } } private AlignmentHelperProcess alignmentHelper { get { return AlignmentHelperProcess.Instance; } } private TexLoader texLoader { get { return TexLoader.Instance; } } private const int UI_FPS = 60; private const int UI_unfocus_FPS = 10; private const string agreement_link = "https://www.bigscreenvr.com/enrollprivacy"; private const string quadviews_link = "https://github.com/mbucchia/Quad-Views-Foveated/wiki#setup"; private const string mail_link = "mailto:support@bigscreenvr.com"; private xr_launch_type pending_xr_launch; Dictionary errorMessages = new Dictionary { { process_error.UsingCPU, "Beyond Eyetracking is unable to use the GPU. This may impact system performance." }, { process_error.CameraRevoked, "Failed to start eyetracking cameras. Enable camera access in Windows settings and\nclose any app that is using the camera." }, { process_error.NoFrustum, "Unable to load eyetracking data. Please redo an enrollment." } }; List activeErrors = new List(); private Dictionary> announcements = new Dictionary>() { { feature_announcements.Blink, new Dictionary { { "title", "Beyond Eyetracking now supports blink!" }, { "description", "A new eyetracking update has arrived which introduces the first version of blinking!\n\nThis feature is only supported with newer models and currently reserved to binary blink. You will need to perform a new enrollment then select the newest model.\n\nNote: Only a limited number of VRChat avatars support blink through native OSC. VRCFT is recommended for wider offerings. If you use VRCFT, make sure the Beyond module is up-to-date." }, { "delay", "7"} } }, { feature_announcements.DFR, new Dictionary { { "title", "Beyond Eyetracking now supports Dynamic Foveated Rendering!" }, { "description", "The newest eyetracking update includes our hotly anticipated Dynamic Foveated Rendering feature!\n\nTo get started, visit the new \"DFR\" page.\n\nNote: If your eyetracking enrollment of choice is a model older than version 0.3.0, we strongly recommend enrolling again. The latest model includes blinking support, which improves the stability of DFR."}, { "delay", "3"} } }, { feature_announcements.Wink, new Dictionary { { "title", "Beyond Eyetracking now supports winking!" }, { "description", "The latest eyetracking update now supports individual eye winking! Wink is currently a beta feature that can be enabled by the new \"Eye Closure Behavior\" option in \"Advanced Settings\".\n\nNote: The winking feature is only supported through VRCFT and wink-specced VRCFT avatars."}, { "delay", "3"} } } }; private List accuracy_tips = new List() { "There are various ways to wear Beyond, including custom-fit cushion, universal-fit cushion, and halo mount. Get the best results by using a dedicated enrollment for each configuration.", "If the eyetracking camera feed of your eyes is cloudy or too blurry, this can degrade eyetracking accuracy. Use a blunted toothpick wrapped with the microfiber cloth included with your Beyond to gently clean both camera lenses.", "If your enrollment of choice is a model older than version 0.3.0, it is best to enroll again. The latest model includes blinking support, which improves the stability of DFR.", "Modifying your headset's IPD after enrolling can degrade eyetracking accuracy. After settling with a new IPD, redo enrollment and use the resulting model.", "If eyetracking feels off-center during use, correct it by enabling the Alignment Helper tool. This reveals a floating cursor in your SteamVR dashboard. Use it as a point of reference for adjusting the headset on your face to correct eyetracking alignment." }; private List DFR_apps = new List() { new dfr_app { title = "Digital Combat Simulator", desc = "Enable settings: \"VR\" > \"Use Quad View\" | \"Track the eyes position\"", }, new dfr_app { title = "Pavlov VR", desc = "Works out of the box. No setting changes needed.", }, new dfr_app { title = "iRacing*", desc = "Enable settings: \"Display\" > \"VR Mode\" > \"Foveated\" | \"Allow Eye Tracking\"", }, new dfr_app { title = "Microsoft Flight Simulator 2024*", desc = "Foveated rendering support was added starting with Sim Update version 2.\nEnable setting: \"VR\" > \"VR Graphics\" > \"Foveated Rendering\"", }, new dfr_app { title = "Kayak VR: Mirage", desc = "In Steam, add the following launch option to the game's properties: \"-hmd=openxr\"", }, }; private KeyValuePair> active_announcement; private (string Label, string ID, string Tooltip, bool IsButton, Func Get, Action Set)[] subscribers; private (string Label, List Toggles)[] adv_setting_toggles; private (string Label, string ID, int FieldWidth, Func Get, Action Set)[] adv_setting_inputs; private adv_dropdown[] adv_setting_dropdowns; private (string Label, string ID, int FieldWidth, Func Get, Action Set)[] dfr_setting_inputs; private const int toggle_columns = 2; // For debug states private float filter_DCutoff = 1.0f; private float filter_Beta = ET_Preferences.filter_default; private float filter_MinCutoff = 1.0f; // Timer for periodic model check private float modelCheckTimer = 0f; private float checkerLifetime = 0f; private const float MODEL_CHECK_INTERVAL = 15.0f; // Check for new models every 15 seconds private const float MODEL_CHECK_LIFETIME = 7200.0f; // Checker loop only lasts 120 minutes if the flag is enabled // Timer for periodic token check from preferences private float tokenCheckTimer = 0f; private const float TOKEN_CHECK_INTERVAL = 3.0f; // Check the settings file for a changed token every 3 seconds // Enable dismiss button after certain amount of time with feature announcements private float featureDismissTimer = 0f; private ImFontPtr header_font; private int pushed_styles = 0; private bool changedDFRSetting = false; private bool foundModelInProgress = false; private bool tokenInputChanged = false; private string pendingCustomModelName = ""; private bool startedAlignAnimation = false; private System.Numerics.Vector2 alignmentInstSize = new System.Numerics.Vector2(380, 260); private DGifPlayer alignInstructional; private void UpdateWindowScale() { globalScale = 1; Godot.Window w = GetWindow(); w.Size = new Vector2I((int)(windowWidth * globalScale), (int)(windowHeight * globalScale)); w.ContentScaleSize = w.Size; } public override void _Ready() { UpdateWindowScale(); // Connect to the UserIdChanged signal UserDataManager.Instance.Connect(nameof(UserDataManager.UserIdChanged), new Callable(this, nameof(OnUserIdChanged))); // Connect to the UserTrainedModelsChanged signal UserDataManager.Instance.Connect(nameof(UserDataManager.UserTrainedModelsChanged), new Callable(this, nameof(OnUserTrainedModelsChanged))); UserDataManager.Instance.Connect(nameof(UserDataManager.AuthenticationResponse), new Callable(this, nameof(OnAuthenticationResponse))); // Update saved models CallDeferred(nameof(UpdateDownloadedModels)); // Grab the bigger font resource header_font = ImGui.GetIO().Fonts.Fonts[1]; // Load icon resources texLoader.LoadPng("res://images/ico_edit.png"); // Define toggles and input settings subscribers = new (string Label, string ID, string Tooltip, bool IsButton, Func Get, Action Set)[] { ("VRChat OSC", "osc", "Use VRChat's builtin eyetracking with OSC.\nEnable OSC in VRChat's \"Action Menu\".", false, () => etSettings.submit_to_vrchat_osc, v => { etSettings.submit_to_vrchat_osc = v; EyeTrackingPreferences.WriteSettingsFile(); }), ("VRCFT", "cft", "Use the third party VRCFaceTracking app.\nThis app must be running with the Beyond VRCFT module installed.", false, () => etSettings.submit_to_vrcft, v => { etSettings.submit_to_vrcft = v; EyeTrackingPreferences.WriteSettingsFile(); }), ("DFR", "xr", "Manage dynamic foveated rendering settings.", true, null, v => { activePage = window_pages.DFR; }), ("Eye Viewer", "view", "See realtime eyetracking data plotted on a graph.", false, () => etSettings.submit_to_viewer, v => { if (v) { pending_popup = "Eye Viewer Warning"; } else { etSettings.submit_to_viewer = v; } }), }; adv_setting_toggles = new(string Label, List Toggles)[] { ("Toggle eyetracking smoothing filter for jitter reduction.", new List() { new adv_toggle() { ID = "Smoothing Enabled", Get = () => etSettings.smoothing_enabled, Set = v => { etSettings.smoothing_enabled = v; EyeTrackingPreferences.WriteSettingsFile(); } }, /*new adv_toggle() { ID = "Exclude DFR", Get = () => etSettings.openxr_smoothing_excluded, Set = v => { etSettings.openxr_smoothing_excluded = v; EyeTrackingPreferences.WriteSettingsFile(); } }*/ }), ("Toggle which eyes to track. Any disabled eyes will reflect the one that is enabled.", new List() { new adv_toggle() { ID = "Track Right Eye", Get = () => etSettings.right_eye_enabled, Set = v => { if (!v) { etSettings.left_eye_enabled = true; } etSettings.right_eye_enabled = v; EyeTrackingPreferences.WriteSettingsFile(); } }, new adv_toggle() { ID = "Track Left Eye", Get = () => etSettings.left_eye_enabled, Set = v => { if (!v) { etSettings.right_eye_enabled = true; } etSettings.left_eye_enabled = v; EyeTrackingPreferences.WriteSettingsFile(); } }, }) }; adv_setting_inputs = new (string Label, string ID, int FieldWidth, Func Get, Action Set)[] { ("Smoothing Intensity", "Change eyetracking smoothing intensity (0 - 1)." + $" (Default: {ET_Preferences.filter_default.ToString(CultureInfo.InvariantCulture)})", 45, () => etSettings.smoothing_intensity, v => { if (float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed)) { if (etSettings.smoothing_intensity == parsed.ToString(CultureInfo.InvariantCulture)) { return; } parsed = Math.Clamp(parsed, 0, 1); etSettings.smoothing_intensity = parsed.ToString(CultureInfo.InvariantCulture); foreach (EyeTrackingConsumer consumer in consumers) { if (consumer is VRChatConsumer vrc_consumer) { vrc_consumer.Beta = parsed; vrc_consumer.InitializeFilters(); } if (consumer is MemmapConsumer mem_consumer) { // Smoothing is disabled for DFR } if (consumer is VRCFTConsumer vrcrft_consumer) { vrcrft_consumer.Beta = parsed; vrcrft_consumer.InitializeFilters(); } if (consumer is DebugViewer view_consumer) { view_consumer.Beta = parsed; view_consumer.InitializeFilters(); } } EyeTrackingPreferences.WriteSettingsFile(); } }), ("OSC Send IP Address", "Send OSC data to a different IP address. (Default: 127.0.0.1)", 100, () => etSettings.osc_ip, v => { if (IPAddress.TryParse(v, out IPAddress parsed)) { if (etSettings.osc_ip == parsed.ToString()) { return; } etSettings.osc_ip = parsed.ToString(); foreach (EyeTrackingConsumer consumer in consumers) { if (consumer is VRChatConsumer vrc_consumer) { vrc_consumer.InitializeOSC(etSettings.osc_ip, etSettings.osc_port); break; } } EyeTrackingPreferences.WriteSettingsFile(); } }), ("OSC Send Port", "Send OSC data to a different port number. (Default: 9000)", 60, () => etSettings.osc_port.ToString(), v => { if (int.TryParse(v, out int parsed)) { if (etSettings.osc_port == parsed) { return; } etSettings.osc_port = parsed; foreach (EyeTrackingConsumer consumer in consumers) { if (consumer is VRChatConsumer vrc_consumer) { vrc_consumer.InitializeOSC(etSettings.osc_ip, etSettings.osc_port); break; } } EyeTrackingPreferences.WriteSettingsFile(); } }), }; adv_setting_dropdowns = new adv_dropdown[] { new adv_dropdown { Label = "Eye Closure Behavior", ID = "EyeClosureBehavior", Options = new[] { "Blinking", "Winking (VRCFT)", "None" }, GetIndex = () => { int mode = etSettings.blink_mode; if (mode < 0 || mode >= 3) { mode = 0; } return mode; }, SetIndex = index => { int clamped = Math.Clamp(index, 0, 2); if (etSettings.blink_mode == clamped) { return; } etSettings.blink_mode = clamped; EyeTrackingPreferences.WriteSettingsFile(); } } }; dfr_setting_inputs = new (string Label, string ID, int FieldWidth, Func Get, Action Set)[] { ("Peripheral Clarity", "Change the clarity of your peripheral region. A value of 0.25 means 25%% resolution." + $"\n (Recommended: {ET_Preferences.peripheral_default.ToString(CultureInfo.InvariantCulture)}) (0 - 1 max)", 45, () => etSettings.quad_peripheral_multiplier, v => { if (float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed)) { if (etSettings.quad_peripheral_multiplier == parsed.ToString(CultureInfo.InvariantCulture)) { return; } parsed = Math.Clamp(parsed, 0, 1); etSettings.quad_peripheral_multiplier = parsed.ToString(CultureInfo.InvariantCulture); } }), ("Focus Clarity", "Change the clarity of your focus region. A value of 0.5 means 50%% resolution.\nA value above 1 is for supersampling." + $" (Recommended: {ET_Preferences.focus_default.ToString(CultureInfo.InvariantCulture)}) (0 - 5 max)", 45, () => etSettings.quad_focus_multiplier, v => { if (float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed)) { if (etSettings.quad_focus_multiplier == parsed.ToString(CultureInfo.InvariantCulture)) { return; } parsed = Math.Clamp(parsed, 0, 5); etSettings.quad_focus_multiplier = parsed.ToString(CultureInfo.InvariantCulture); } }), ("Horizontal Focus Region Scale", "Change the size of the horizontal foveated region. A value of 0.3\nmeans 30%% of the headset's FOV." + $" (Recommended: {ET_Preferences.horizontal_focus_default.ToString(CultureInfo.InvariantCulture)}) (0 - 1 max)", 45, () => etSettings.quad_horizontal_focus_section, v => { if (float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed)) { if (etSettings.quad_horizontal_focus_section == parsed.ToString(CultureInfo.InvariantCulture)) { return; } parsed = Math.Clamp(parsed, 0, 1); etSettings.quad_horizontal_focus_section = parsed.ToString(CultureInfo.InvariantCulture); } }), ("Vertical Focus Region Scale", "Change the size of the vertical foveated region. A value of 0.3\nmeans 30%% of the headset's FOV." + $" (Recommended: {ET_Preferences.vertical_focus_default.ToString(CultureInfo.InvariantCulture)}) (0 - 1 max)", 45, () => etSettings.quad_vertical_focus_section, v => { if (float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed)) { if (etSettings.quad_vertical_focus_section == parsed.ToString(CultureInfo.InvariantCulture)) { return; } parsed = Math.Clamp(parsed, 0, 1); etSettings.quad_vertical_focus_section = parsed.ToString(CultureInfo.InvariantCulture); } }), ("Edge Smoothing", "Change the \"thickness\" of the transition from the foveated region to the peripheral region.\nA value of 0.5 would be a larger transition." + $" (Recommended: {ET_Preferences.transition_thickness_default.ToString(CultureInfo.InvariantCulture)}) (0 - 0.5 max)", 45, () => etSettings.quad_transition_thickness, v => { if (float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed)) { if (etSettings.quad_transition_thickness == parsed.ToString(CultureInfo.InvariantCulture)) { return; } parsed = Math.Clamp(parsed, 0, 0.5f); etSettings.quad_transition_thickness = parsed.ToString(CultureInfo.InvariantCulture); } }), }; } private void CreateAndPlayVideo(string resource_path) { if (alignInstructional != null) { alignInstructional.GifDispose(); alignInstructional = null; } alignInstructional = new DGifPlayer(); if (alignInstructional.Load(resource_path)) { alignInstructional.Play(); }; } private int PushTextInputStyle() { ImGui.PushStyleColor(ImGuiCol.FrameBg, textbox_fill); return 1; } private int PushCheckboxStyle() { ImGui.PushStyleColor(ImGuiCol.FrameBg, checkbox_fill); ImGui.PushStyleColor(ImGuiCol.FrameBgHovered, checkbox_hover); ImGui.PushStyleColor(ImGuiCol.FrameBgActive, checkbox_click); ImGui.PushStyleColor(ImGuiCol.CheckMark, checkbox_icon); return 4; } private int PushPopupStyle() { ImGui.PushStyleColor(ImGuiCol.TitleBgActive, popup_title_color); return 1; } private int PushDropdownStyle() { ImGui.PushStyleColor(ImGuiCol.FrameBg, dropdown_fill); ImGui.PushStyleColor(ImGuiCol.FrameBgHovered, dropdown_hover); ImGui.PushStyleColor(ImGuiCol.FrameBgActive, dropdown_click); ImGui.PushStyleColor(ImGuiCol.Button, dropdown_button); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, dropdown_button_hover); ImGui.PushStyleColor(ImGuiCol.ButtonActive, dropdown_button_click); ImGui.PushStyleColor(ImGuiCol.PopupBg, dropdown_fill); return 7; } private int PushSubscriberButtonStyle() { ImGui.PushStyleColor(ImGuiCol.Button, checkbox_fill); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, checkbox_hover); ImGui.PushStyleColor(ImGuiCol.ButtonActive, checkbox_click); ImGui.PushStyleColor(ImGuiCol.Text, checkbox_icon); return 4; } private int PushButtonStyle() { ImGui.PushStyleColor(ImGuiCol.Button, button_fill); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, button_hover); ImGui.PushStyleColor(ImGuiCol.ButtonActive, button_click); ImGui.PushStyleColor(ImGuiCol.Text, button_text); return 4; } private int PushButtonCheckboxStyle() { ImGui.PushStyleColor(ImGuiCol.Button, checkbox_fill); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, checkbox_hover); ImGui.PushStyleColor(ImGuiCol.ButtonActive, checkbox_click); ImGui.PushStyleColor(ImGuiCol.Text, checkbox_icon); return 4; } public void ConfigureError(process_error e, bool active) { if (active) { if (!activeErrors.Contains(e)) { activeErrors.Insert(0, e); } } else { activeErrors.Remove(e); } } private void ShowTooltip(string msg) { ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new System.Numerics.Vector2(10, 10)); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 5); ImGui.BeginTooltip(); ImGui.SetWindowFontScale(windowScale); ImGui.Text(msg); ImGui.EndTooltip(); var windowSize = ImGui.GetWindowSize(); var textSize = ImGui.CalcTextSize(msg); var cursorPos = ImGui.GetMousePos(); System.Numerics.Vector2 windowPos = new System.Numerics.Vector2((windowSize.X - textSize.X) / 2, cursorPos.Y); ImGui.SetNextWindowPos(windowPos, ImGuiCond.Always, new System.Numerics.Vector2(0, -1)); // bottom-right ImGui.PopStyleVar(2); } private void UpdateDownloadedModels() { // Always include downloaded models in the list var combinedModels = new Godot.Collections.Array(); // Add downloaded models first if (etSettings.downloaded_models.Count > 0) { foreach (var model in etSettings.downloaded_models) { combinedModels.Add(model); } } // If user is authenticated, network models will be added via OnUserTrainedModelsChanged // For now, just set the initial list with downloaded models if (combinedModels.Count > 0) { model_names = combinedModels; OnUserTrainedModelsChanged(model_names); } } private void OnAuthenticationResponse(int response) { authentication_status = (auth_code)response; // Switch models to downloaded list if the user can't authenticate if (authentication_status != auth_code.Success) { model_names = etSettings.downloaded_models; } } private void OnUserIdChanged(string newUserId) { if (!string.IsNullOrWhiteSpace(newUserId)) { // Save the current valid user token to file etSettings.user_testing_token = UserDataManager.Instance.TestingToken; EyeTrackingPreferences.WriteSettingsFile(); GD.Print($"User ID changed to: {newUserId}"); } } private void OnUserTrainedModelsChanged(Godot.Collections.Array newUserTrainedModels) { // Create a combined list of downloaded models and network models var combinedModels = new Godot.Collections.Array(); // Add downloaded models first (these are local and always available) if (etSettings.downloaded_models.Count > 0) { foreach (var model in etSettings.downloaded_models) { combinedModels.Add(model); } } // Add network models from the server (avoiding duplicates) if (newUserTrainedModels.Count > 0) { bool hasTrainingModel = false; foreach (var model in newUserTrainedModels) { // Only add if not already in the list (avoid duplicates) if (!combinedModels.Contains(model)) { combinedModels.Add(model); } // HACK HACK HACK // Models in training have a percentage indicator in their names. Look for unique characters // Server *could* return status of pending models apart from baking into names. if (model.Contains("(") && model.Contains(")") && model.Contains("%")) { hasTrainingModel = true; foundModelInProgress = true; } } // No models were found in training after discovering one // Assume the model has finished if (!hasTrainingModel && foundModelInProgress) { foundModelInProgress = false; // Turn off loop flag in preferences etSettings.expecting_new_model = false; EyeTrackingPreferences.WriteSettingsFile(); } } model_names = combinedModels; if (!string.IsNullOrEmpty(etSettings.selected_model)) { for (int i = 0; i < model_names.Count; i++) { if (model_names[i] == etSettings.selected_model) { SelectModel(i); break; } } } GD.Print($"Combined models (downloaded + network): {string.Join(", ", combinedModels)}"); } private void RefreshCombinedModels() { // Create a combined list of downloaded models and network models var combinedModels = new Godot.Collections.Array(); // Add downloaded models first (these are local and always available) if (etSettings.downloaded_models.Count > 0) { foreach (var model in etSettings.downloaded_models) { combinedModels.Add(model); } } // Add network models from UserDataManager (avoiding duplicates) if (UserDataManager.Instance.UserTrainedModels.Count > 0) { foreach (var model in UserDataManager.Instance.UserTrainedModels) { // Only add if not already in the list (avoid duplicates) if (!combinedModels.Contains(model)) { combinedModels.Add(model); } } } model_names = combinedModels; // Maintain selection if the currently selected model is still in the list if (selectedModelIndex >= 0 && selectedModelIndex < model_names.Count) { // Check if the selected model is still available if (model_names[selectedModelIndex] != etSettings.selected_model) { // Try to find the previously selected model in the new list for (int i = 0; i < model_names.Count; i++) { if (model_names[i] == etSettings.selected_model) { selectedModelIndex = i; break; } } } } } private void OpenLink(string url) { try { ProcessStartInfo proc = new ProcessStartInfo { FileName = url, UseShellExecute = true }; Process.Start(proc); } catch (System.Exception e) { GD.PrintErr("Failed to open URL: ", e.Message); } } public void ChangeToken(string newToken) { UserDataManager.Instance.TestingToken = newToken; } private void LaunchEnrollment() { if (Engine.IsEditorHint()) { // When running in Godot editor, change to the enroll scene GD.Print("Running in editor - changing to enroll scene"); GetTree().ChangeSceneToFile("scenes/enroll.tscn"); } else { if (etSettings.camera_enabled) { // first disable the camera, so to release the camera for the enrollment EmitSignal(SignalName.DisableCamera); } // Add a small delay to ensure camera is fully released System.Threading.Thread.Sleep(1000); GD.Print("Running as executable - launching with enrollment parameters"); var startInfo = new System.Diagnostics.ProcessStartInfo { FileName = "BeyondET.exe", Arguments = $"--user-id {UserDataManager.Instance.UserId} --token {UserDataManager.Instance.TestingToken} --enroll {1} --xr-mode \"on\"", UseShellExecute = false, // Changed to true to run in a separate window CreateNoWindow = false, // Allow window to be shown WorkingDirectory = System.IO.Path.GetDirectoryName(OS.GetExecutablePath()) }; try { var process = new System.Diagnostics.Process { StartInfo = startInfo, EnableRaisingEvents = true }; process.Exited += (sender, e) => { if (etSettings.camera_enabled) { GD.Print("Process exit: enabling camera"); EmitSignal(SignalName.EnableCamera); } }; process.Start(); GD.Print("Enrollment launched successfully"); // HACK HACK HACK // Start model update loop to periodically fetch from the server // This assumes the user actually completes enrollment checkerLifetime = 0; etSettings.expecting_new_model = true; EyeTrackingPreferences.WriteSettingsFile(); } catch (Exception ex) { GD.PrintErr($"Failed to launch enrollment: {ex.Message}"); } } } private void LaunchAlignment() { if (Engine.IsEditorHint()) { // When running in Godot editor, change to the enroll scene GD.Print("Running in editor - changing to alignment scene"); GetTree().ChangeSceneToFile("scenes/check_alignment.tscn"); } else { if (etSettings.camera_enabled) { // first disable the camera, so to release the camera for alignment EmitSignal(SignalName.DisableCamera); } // Add a small delay to ensure camera is fully released System.Threading.Thread.Sleep(1000); GD.Print("Running as executable - launching alignment parameters"); var startInfo = new System.Diagnostics.ProcessStartInfo { FileName = "BeyondET.exe", Arguments = $"--align {1} --xr-mode \"on\"", UseShellExecute = false, // Changed to true to run in a separate window CreateNoWindow = false, // Allow window to be shown WorkingDirectory = System.IO.Path.GetDirectoryName(OS.GetExecutablePath()) }; try { var process = new System.Diagnostics.Process { StartInfo = startInfo, EnableRaisingEvents = true }; process.Exited += (sender, e) => { if (etSettings.camera_enabled) { GD.Print("Process exit: enabling camera"); EmitSignal(SignalName.EnableCamera); } }; process.Start(); GD.Print("Alignment launched successfully"); } catch (Exception ex) { GD.PrintErr($"Failed to launch alignment: {ex.Message}"); } } } private void SelectModel(int modelIndex) { // Update the selected model index when clicked selectedModelIndex = modelIndex; //Emit the signal that the camera should be enabled if (etSettings.camera_enabled) { EmitSignal(SignalName.EnableCamera); } else { EmitSignal(SignalName.DisableCamera); } //Emit the signal that the model has been selected EmitSignal(SignalName.ModelSelected, model_names[modelIndex]); GD.Print($"Selected model: {model_names[modelIndex]}"); } private void DFRSettings() { var windowSize = ImGui.GetWindowSize(); RenderHeader(); ImGui.PushFont(header_font); ImGui.SetWindowFontScale(sectionFontScale); ImGui.Text("Dynamic Foveated Rendering (Beta)"); ImGui.SetWindowFontScale(windowScale); ImGui.PopFont(); ImGui.Spacing(); ImGui.Spacing(); ImGui.TextWrapped("Enable dynamic foveated rendering (DFR) to improve VR performance. To use this feature, the app in question must support DFR technology."); ImGui.Text("Get started by checking out our"); ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, new System.Numerics.Vector4(0.2f, 0.6f, 1, 1)); ImGui.Text("list of popular DFR-enabled VR titles"); ImGui.PopStyleColor(); if (ImGui.IsItemClicked()) { pending_popup = "DFR Apps"; } ImGui.SameLine(); ImGui.Text(", which includes setup tips."); ImGui.Spacing(); ImGui.Spacing(); ImGui.Text("Enabling DFR will forward the necessary eyetracking data to OpenXR."); ImGui.Spacing(); ImGui.Spacing(); ImGui.Text("Most* titles require installation of a special OpenXR layer called \"Quad-Views-Foveated.\""); ImGui.Spacing(); ImGui.PushStyleColor(ImGuiCol.Text, new System.Numerics.Vector4(0.2f, 0.6f, 1, 1)); ImGui.Text("Click here to download and install."); ImGui.PopStyleColor(); ImGui.Spacing(); if (ImGui.IsItemClicked()) { OpenLink(quadviews_link); } ImGui.TextWrapped("After installing, restart SteamVR to enable. *\"Quad-Views-Foveated\" is not required by some titles, such as iRacing."); ImGui.Spacing(); ImGui.Spacing(); ImGui.TextWrapped("Please note: Dynamic foveated rendering is currently in beta. From its release in December 2025, it will receive many updates and improvements over the next few months!"); ImGui.Spacing(); ImGui.Spacing(); ImGui.Separator(); ImGui.Separator(); ImGui.Spacing(); ImGui.SetWindowFontScale(checkboxScale); pushed_styles = PushCheckboxStyle(); bool next = etSettings.submit_to_xr; ImGui.Checkbox("##DFR", ref next); ImGui.PopStyleColor(pushed_styles); ImGui.SetWindowFontScale(windowScale); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y + 3); ImGui.Text("Enable DFR"); if (next != etSettings.submit_to_xr) { if (next && !etSettings.dfr_settings_prompted) { pending_popup = "DFR Confirm"; } else { etSettings.submit_to_xr = next; EyeTrackingPreferences.WriteSettingsFile(); // Alignment overlay needs DFR, prompt to disable if (!next && etSettings.enabled_alignment_helper) { pending_popup = "DFR Disable"; } } } ImGui.Spacing(); ImGui.Separator(); ImGui.Separator(); ImGui.Spacing(); pushed_styles = PushButtonStyle(); if (ImGui.Button("< Back")) { activePage = window_pages.Home; } ImGui.SameLine(); if (ImGui.Button("Open Quad-View Settings")) { pending_popup = "Quad-View Settings"; } ImGui.PopStyleColor(pushed_styles); } private void AdvancedSettings() { var windowSize = ImGui.GetWindowSize(); RenderHeader(); ImGui.PushFont(header_font); ImGui.SetWindowFontScale(sectionFontScale); ImGui.Text("Advanced Settings"); ImGui.SetWindowFontScale(windowScale); ImGui.PopFont(); ImGui.Spacing(); ImGui.Spacing(); ImGui.Separator(); ImGui.Separator(); ImGui.BeginTable("TogglesTable", 1); ImGui.TableSetupColumn("TogglesCol", ImGuiTableColumnFlags.WidthFixed, windowSize.X); for (int i = 0; i < adv_setting_toggles.Length; i++) { var (label, toggles) = adv_setting_toggles[i]; if (i % 1 == 0) { ImGui.TableNextRow(); } ImGui.TableNextColumn(); if (i != 0) { ImGui.Separator(); ImGui.Spacing(); } for (int t = 0; t < toggles.Count; t++) { adv_toggle toggle = toggles[t]; bool val = toggle.Get(); ImGui.PushID(toggle.ID); ImGui.SetWindowFontScale(checkboxScale); pushed_styles = PushCheckboxStyle(); ImGui.Checkbox("##" + toggle.ID, ref val); ImGui.PopStyleColor(pushed_styles); ImGui.SetWindowFontScale(windowScale); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y + 3); ImGui.Text(toggle.ID); ImGui.PopID(); if (toggle.Get() != val) { toggle.Set(val); } ImGui.SameLine(); } ImGui.Spacing(); ImGui.PushStyleColor(ImGuiCol.Text, sub_text); ImGui.Text(label); ImGui.PopStyleColor(); } ImGui.EndTable(); // Dropdown-style advanced settings if (adv_setting_dropdowns != null && adv_setting_dropdowns.Length > 0) { ImGui.Separator(); ImGui.Spacing(); ImGui.BeginTable("DropdownsTable", 1); ImGui.TableSetupColumn("DropdownCol", ImGuiTableColumnFlags.WidthFixed, windowSize.X); for (int i = 0; i < adv_setting_dropdowns.Length; i++) { ImGui.TableNextRow(); ImGui.TableNextColumn(); var dropdown = adv_setting_dropdowns[i]; int index = dropdown.GetIndex(); if (index < 0 || index >= dropdown.Options.Length) { index = 0; } string currentLabel = dropdown.Options[index]; ImGui.Text(dropdown.Label); ImGui.Spacing(); ImGui.PushID(dropdown.ID); ImGui.SetNextItemWidth(150); pushed_styles = PushDropdownStyle(); if (ImGui.BeginCombo("##" + dropdown.ID, currentLabel)) { for (int opt = 0; opt < dropdown.Options.Length; opt++) { bool isSelected = (opt == index); if (ImGui.Selectable(dropdown.Options[opt], isSelected)) { dropdown.SetIndex(opt); index = opt; } if (isSelected) { ImGui.SetItemDefaultFocus(); } } ImGui.EndCombo(); } ImGui.PopStyleColor(pushed_styles); ImGui.PopID(); if (dropdown.ID == "EyeClosureBehavior") { ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y - 9); ImGui.PushStyleColor(ImGuiCol.Text, sub_text); ImGui.TextWrapped("Choose how eye closure data is interpreted: classic blinking, per-eye winking, or ignore blink flags entirely."); ImGui.PopStyleColor(); } } ImGui.EndTable(); } ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y - 6); ImGui.BeginTable("InputsTable", 1); ImGui.TableSetupColumn("InputCol", ImGuiTableColumnFlags.WidthFixed, windowSize.X); for (int i = 0; i < adv_setting_inputs.Length; i++) { if (i % 1 == 0) { ImGui.TableNextRow(); } ImGui.TableNextColumn(); var (label, id, width, get, set) = adv_setting_inputs[i]; string val = get(); ImGui.Separator(); ImGui.Spacing(); ImGui.Text(label); ImGui.Spacing(); ImGui.PushID(id); ImGui.SetNextItemWidth(width); pushed_styles = PushTextInputStyle(); if (ImGui.InputText("", ref val, 256)) { set(val); } ImGui.PopStyleColor(pushed_styles); ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, sub_text); ImGui.Text(id); ImGui.PopStyleColor(); ImGui.PopID(); if (get() != val) { set(val); } } ImGui.EndTable(); ImGui.Separator(); ImGui.Separator(); pushed_styles = PushButtonStyle(); if (ImGui.Button("< Back")) { activePage = window_pages.Home; } ImGui.PopStyleColor(pushed_styles); ImGui.SameLine(); string disclaimer = "Any changes will save and immediately take effect."; var textSize = ImGui.CalcTextSize(disclaimer); ImGui.SetCursorPosX((windowSize.X - textSize.X) * 0.5f); ImGui.TextWrapped(disclaimer); } private void DebugSettings() { RenderHeader(); ImGui.Text("Filter:"); ImGui.Spacing(); ImGui.Spacing(); string v = filter_MinCutoff.ToString(CultureInfo.InvariantCulture); ImGui.Text("MinCutoff:"); ImGui.SameLine(); pushed_styles = PushTextInputStyle(); if (ImGui.InputText("##FilterMinCutoff", ref v, 10)) { if (float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out float f)) { filter_MinCutoff = f; foreach (EyeTrackingConsumer consumer in consumers) { if (consumer is VRChatConsumer vrc_consumer) { vrc_consumer.MinCutoff = f; vrc_consumer.InitializeFilters(); } if (consumer is MemmapConsumer mem_consumer) { // Smoothing is disabled for DFR } if (consumer is VRCFTConsumer vrcrft_consumer) { vrcrft_consumer.MinCutoff = f; vrcrft_consumer.InitializeFilters(); } if (consumer is DebugViewer view_consumer) { view_consumer.MinCutoff = f; view_consumer.InitializeFilters(); } } } } ImGui.PopStyleColor(pushed_styles); v = filter_Beta.ToString(CultureInfo.InvariantCulture); ImGui.Text("Beta:"); ImGui.SameLine(); pushed_styles = PushTextInputStyle(); if (ImGui.InputText("##Beta", ref v, 10)) { if (float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out float f)) { filter_Beta = f; foreach (EyeTrackingConsumer consumer in consumers) { if (consumer is VRChatConsumer vrc_consumer) { vrc_consumer.Beta = f; vrc_consumer.InitializeFilters(); } if (consumer is MemmapConsumer mem_consumer) { // Smoothing is disabled for DFR } if (consumer is VRCFTConsumer vrcrft_consumer) { vrcrft_consumer.Beta = f; vrcrft_consumer.InitializeFilters(); } if (consumer is DebugViewer view_consumer) { view_consumer.Beta = f; view_consumer.InitializeFilters(); } } } } ImGui.PopStyleColor(pushed_styles); v = filter_DCutoff.ToString(CultureInfo.InvariantCulture); ImGui.Text("DCutoff:"); ImGui.SameLine(); pushed_styles = PushTextInputStyle(); if (ImGui.InputText("##DCutoff", ref v, 10)) { if (float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out float f)) { filter_DCutoff = f; foreach (EyeTrackingConsumer consumer in consumers) { if (consumer is VRChatConsumer vrc_consumer) { vrc_consumer.DCutoff = f; vrc_consumer.InitializeFilters(); } if (consumer is MemmapConsumer mem_consumer) { // Smoothing is disabled for DFR } if (consumer is VRCFTConsumer vrcrft_consumer) { vrcrft_consumer.DCutoff = f; vrcrft_consumer.InitializeFilters(); } if (consumer is DebugViewer view_consumer) { view_consumer.DCutoff = f; view_consumer.InitializeFilters(); } } } } ImGui.PopStyleColor(pushed_styles); ImGui.Spacing(); ImGui.Spacing(); ImGui.Separator(); ImGui.Separator(); ImGui.Spacing(); ImGui.Spacing(); pushed_styles = PushButtonStyle(); if (ImGui.Button("Exit Debug")) { activePage = window_pages.Home; } ImGui.PopStyleColor(pushed_styles); } private void RenderHeader() { var windowSize = ImGui.GetWindowSize(); string title = "Beyond Eyetracking (Beta)"; bool showDirectMLWarning = gazeEstimation != null && gazeEstimation.IsRunning && gazeEstimation._neuralNetwork != null && !gazeEstimation._neuralNetwork.UsingDirectML; bool cameraAccessRevoked = gazeEstimation != null && gazeEstimation.cameraManager != null && gazeEstimation.cameraManager.CameraAccessRevoked; ConfigureError(process_error.UsingCPU, showDirectMLWarning); ConfigureError(process_error.CameraRevoked, cameraAccessRevoked); if (activeErrors.Count > 0) { var boxSize = new System.Numerics.Vector2(windowSize.X * 0.98f, 32f); var boxPos = new System.Numerics.Vector2((windowSize.X - boxSize.X) * 0.5f, ImGui.GetCursorPosY()); var boxColor = new System.Numerics.Vector4(0.4f, 0, 0, 1); // Red color ImGui.GetWindowDrawList().AddRectFilled( boxPos, new System.Numerics.Vector2(boxPos.X + boxSize.X, boxPos.Y + boxSize.Y), ImGui.GetColorU32(boxColor) ); var text = "! " + errorMessages[activeErrors[0]]; var textSize = ImGui.CalcTextSize(text); // Center text horizontally inside the box var textPos = new System.Numerics.Vector2( boxPos.X + (boxSize.X - textSize.X) * 0.5f, boxPos.Y + (boxSize.Y - textSize.Y) * 0.5f // vertical centering (optional) ); ImGui.SetCursorScreenPos(textPos); ImGui.PushStyleColor(ImGuiCol.Text, new System.Numerics.Vector4(10, 0, 0, 1)); ImGui.TextWrapped(text); ImGui.PopStyleColor(); ImGui.SetCursorScreenPos(boxPos + new System.Numerics.Vector2(0, boxSize.Y)); } else { ImGui.Spacing(); // Get text bounds for later alignments var titleSize = ImGui.CalcTextSize(title) * 1.8f; ImGui.SetCursorPosX((windowSize.X - titleSize.X) * 0.5f); ImGui.PushFont(header_font); ImGui.Text(title); ImGui.PopFont(); ImGui.Spacing(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y - 5); } ImGui.Spacing(); ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); ImGui.Spacing(); } private void RenderSubscribers() { ImGui.PushFont(header_font); ImGui.SetWindowFontScale(sectionFontScale); ImGui.Text("Recipients"); ImGui.SetWindowFontScale(windowScale); ImGui.PopFont(); ImGui.Spacing(); ImGui.Spacing(); ImGui.BeginTable("TogglesTable", toggle_columns); for (int i = 0; i < subscribers.Length / toggle_columns; i++) { ImGui.TableSetupColumn("Col1", ImGuiTableColumnFlags.WidthFixed, 165f); } for (int i = 0; i < subscribers.Length; i++) { if (i % toggle_columns == 0) { ImGui.TableNextRow(); } ImGui.TableNextColumn(); var (label, id, tooltip, isButton, get, set) = subscribers[i]; if (isButton) { ImGui.PushID(id); ImGui.PushFont(header_font); pushed_styles = PushSubscriberButtonStyle(); if (ImGui.Button("##btn", new System.Numerics.Vector2(27, 27))) { set(false); }; var min = ImGui.GetItemRectMin(); var max = ImGui.GetItemRectMax(); var dl = ImGui.GetWindowDrawList(); // Shift up text by a few pixels var text = ">"; var size = ImGui.CalcTextSize(text); float x = min.X + (max.X - min.X - size.X) * 0.5f; float y = min.Y + (max.Y - min.Y - size.Y) * 0.5f; dl.AddText(new System.Numerics.Vector2(x, y), ImGui.GetColorU32(ImGuiCol.Text), text); ImGui.PopStyleColor(pushed_styles); ImGui.PopFont(); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y + 3); ImGui.Text(label); ImGui.PopID(); } else { ImGui.PushID(id); ImGui.SetWindowFontScale(checkboxScale); bool val = get(); pushed_styles = PushCheckboxStyle(); ImGui.Checkbox("##" + id, ref val); ImGui.PopStyleColor(pushed_styles); ImGui.SetWindowFontScale(windowScale); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y + 3); ImGui.Text(label); ImGui.PopID(); if (get() != val) { set(val); } } // Tooltip label ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, sub_text); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y - 2); ImGui.SetCursorPosX(ImGui.GetCursorScreenPos().X - 4); ImGui.Text("(?)"); ImGui.PopStyleColor(); if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ShowTooltip(tooltip); } } ImGui.EndTable(); } private void RenderPopups(double delta) { var viewport = ImGui.GetMainViewport(); var center = viewport.GetCenter(); var pivot = new System.Numerics.Vector2(0.5f, 0.5f); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 5); PushPopupStyle(); // Any unseen feature announcements foreach (var kvp in announcements) { feature_announcements feature = kvp.Key; if ((etSettings.seen_announcements & (int)feature) == 0 && active_announcement.Key == 0) { active_announcement = kvp; pending_popup = "Feature Announcement"; featureDismissTimer = 0; // mark as seen etSettings.seen_announcements |= (int)feature; EyeTrackingPreferences.WriteSettingsFile(); } } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("Feature Announcement", ImGuiWindowFlags.NoResize)) { // Resize to match the main viewport width (inside) ImGui.SetWindowSize(new System.Numerics.Vector2(viewport.Size.X - 4, 0)); // full width, auto height ImGui.PushFont(header_font); ImGui.SetWindowFontScale(sectionFontScale); var textSize = ImGui.CalcTextSize(active_announcement.Value["title"]); ImGui.SetCursorPosX((viewport.Size.X - (textSize.X)) * 0.5f); ImGui.Text(active_announcement.Value["title"]); ImGui.SetWindowFontScale(windowScale); ImGui.PopFont(); ImGui.Spacing(); ImGui.Separator(); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped(active_announcement.Value["description"]); ImGui.Spacing(); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; ImGui.SetCursorPosX(offsetX); featureDismissTimer += (float)delta; float dismiss_timeout = 0; if (!(active_announcement.Value.ContainsKey("delay") && float.TryParse(active_announcement.Value["delay"], NumberStyles.Float, CultureInfo.InvariantCulture, out dismiss_timeout))) { dismiss_timeout = 7; } bool canDismiss = featureDismissTimer >= dismiss_timeout; // Can't dismiss until timer expires if (!canDismiss) { ImGui.BeginDisabled(); } pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0))) { active_announcement = new KeyValuePair>(); ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); if (!canDismiss) { ImGui.EndDisabled(); } ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("Alignment Helper Guide", ImGuiWindowFlags.NoResize)) { string alignTitle = "Alignment Helper Guide"; if (!startedAlignAnimation) { startedAlignAnimation = true; CreateAndPlayVideo(ProjectSettings.GlobalizePath("res://anim/align.gif")); } // Resize to match the main viewport width (inside) ImGui.SetWindowSize(new System.Numerics.Vector2(viewport.Size.X - 4, 0)); // full width, auto height ImGui.PushFont(header_font); ImGui.SetWindowFontScale(sectionFontScale); var textSize = ImGui.CalcTextSize(alignTitle); ImGui.SetCursorPosX((viewport.Size.X - (textSize.X)) * 0.5f); ImGui.Text("Alignment Helper Guide"); ImGui.SetWindowFontScale(windowScale); ImGui.PopFont(); ImGui.Spacing(); ImGui.Separator(); ImGui.Separator(); ImGui.Spacing(); ImGui.TextWrapped("The Alignment Helper tool is an overlay that appears when looking downward in the SteamVR dashboard. The white cursor marks your eyetracking gaze.\n\nAdjust the headset on your face while looking straight inside the ring until the cursor stays roughly inside it. Often you may need to tighten or loosen your headset if the cursor is way off."); ImGui.Spacing(); ImGui.Spacing(); if (alignInstructional != null && alignInstructional.Texture != null) { ImGui.SetCursorPosX((viewport.Size.X - (alignmentInstSize.X)) * 0.5f); ImGui.Image((nint)alignInstructional.Texture.GetRid().Id, alignmentInstSize); } ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0))) { startedAlignAnimation = false; if (alignInstructional != null) { alignInstructional.GifDispose(); alignInstructional = null; } ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("Enroll Confirm", ImGuiWindowFlags.AlwaysAutoResize)) { if (model_names.Count > 0) { ImGui.Text("Redo an enrollment if the IPD was changed or a model has inaccuracies.\nPerforming an enrollment will disrupt any active VR session."); } else { ImGui.Text("Performing an enrollment will disrupt any active VR session."); } ImGui.Spacing(); ImGui.Text("Click start when you are ready."); ImGui.Spacing(); ImGui.Separator(); if (ENABLE_POLICY_NOTICE) { ImGui.Text("By performing an enrollment, you consent to the"); ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, new System.Numerics.Vector4(0.2f, 0.6f, 1, 1)); ImGui.Text("Enrollment Data Privacy Agreement"); ImGui.PopStyleColor(); if (ImGui.IsItemClicked()) { OpenLink(agreement_link); } ImGui.Separator(); ImGui.Spacing(); } ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - popup_totalWidth) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("Start", new System.Numerics.Vector2(popup_buttonWidth, 0))) { pending_xr_launch = xr_launch_type.Enrollment; if (main.GetXRLayerCount() > 0) { pending_popup = "OpenXR Layers"; } else { LaunchEnrollment(); } ImGui.CloseCurrentPopup(); } ImGui.SameLine(); if (ImGui.Button("Cancel", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("Enable Alignment Helper", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.Text("Enable the alignment helper tool?"); ImGui.Spacing(); ImGui.Text("This will show an overlay in the SteamVR dashboard while eyetracking is active."); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - popup_totalWidth) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("Yes", new System.Numerics.Vector2(popup_buttonWidth, 0))) { etSettings.enabled_alignment_helper = true; EyeTrackingPreferences.WriteSettingsFile(); alignmentHelper.StartAlignmentWatch(); // Only show the guide one time after enabling if (!etSettings.seen_alignment_guide) { etSettings.seen_alignment_guide = true; pending_popup = "Alignment Helper Guide"; } ImGui.CloseCurrentPopup(); } ImGui.SameLine(); if (ImGui.Button("No", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("Alignment Helper Requirements", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.Text("To use the Alignment Helper, DFR must first be enabled."); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - popup_totalWidth) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0))) { activePage = window_pages.DFR; ImGui.CloseCurrentPopup(); } ImGui.SameLine(); if (ImGui.Button("Cancel", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("DFR Disable", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.Text("The Alignment Helper requires DFR to be enabled. Continuing will disable it."); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - popup_totalWidth) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("Continue", new System.Numerics.Vector2(popup_buttonWidth, 0))) { etSettings.enabled_alignment_helper = false; EyeTrackingPreferences.WriteSettingsFile(); ImGui.CloseCurrentPopup(); alignmentHelper.EndAlignmentWatch(); } ImGui.SameLine(); if (ImGui.Button("Cancel", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("OpenXR Layers", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.Text("There are OpenXR Layer(s) installed. If you have problems\nstarting, disable your OpenXR Layers in SteamVR settings."); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0))) { if (pending_xr_launch == xr_launch_type.Enrollment) { LaunchEnrollment(); } if (pending_xr_launch == xr_launch_type.Aligner) { LaunchAlignment(); } ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("Eye Viewer Warning", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.Text("Keeping the eye viewer active while in a VR session can add latency to eyetracking."); ImGui.Spacing(); ImGui.Text("It is recommended to keep the eye viewer closed while playing in VR."); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0))) { etSettings.submit_to_viewer = true; ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); ImGui.SetNextWindowSize(new System.Numerics.Vector2(500, 0), ImGuiCond.Always); if (ImGui.BeginPopupModal("Accuracy Tips", ImGuiWindowFlags.AlwaysAutoResize)) { foreach (string t in accuracy_tips) { ImGui.Spacing(); ImGui.Bullet(); ImGui.SameLine(); ImGui.TextWrapped(t); ImGui.Spacing(); ImGui.Separator(); } ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("Label Model", ImGuiWindowFlags.AlwaysAutoResize)) { int headerMax = (int)ImGui.GetMainViewport().Size.X / 2; string header = "Rename model: " + FormatModelDisplayName(model_names[selectedModelIndex]); string displayText = header; System.Numerics.Vector2 textSize = ImGui.CalcTextSize(header); if (textSize.X > headerMax) { int len = header.Length; while (len > 0) { string final = header.Substring(0, len) + "..."; if (ImGui.CalcTextSize(final).X <= headerMax) { displayText = final; break; } len--; } } ImGui.Text(displayText); ImGui.Spacing(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - popup_totalWidth) * 0.5f; pushed_styles = PushTextInputStyle(); ImGui.SetCursorPosX(offsetX - 38); ImGui.InputText("##ModelName", ref pendingCustomModelName, 50); ImGui.PopStyleColor(pushed_styles); ImGui.Separator(); ImGui.SetItemDefaultFocus(); ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0)) && pendingCustomModelName.Length > 0) { string modelName = FormatModelDisplayName(model_names[selectedModelIndex], true); etSettings.named_models[modelName] = pendingCustomModelName; EyeTrackingPreferences.WriteSettingsFile(); pendingCustomModelName = ""; ImGui.CloseCurrentPopup(); } ImGui.SameLine(); if (ImGui.Button("Cancel", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("Model Training", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.Text("Your model is still being trained. Try again in a few minutes."); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("DFR Settings Success", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.Text("DFR settings have been applied. Please restart SteamVR for the change to take effect."); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("DFR Settings Failure", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.Text("Could not write \"Quad-Views-Foveated\" settings file. Please close\nSteamVR or any process that is using it."); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("OK", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("DFR Confirm", ImGuiWindowFlags.AlwaysAutoResize)) { if (!etSettings.dfr_settings_prompted) { ImGui.Text("Before enabling DFR, apply the recommended \"Quad-Views-Foveated\" settings for Beyond?"); ImGui.Spacing(); ImGui.Text("Any existing user settings you have will be overwritten."); } else { ImGui.Text("Apply the change? This only effects the \"Quad-Views-Foveated\" layer."); ImGui.Spacing(); ImGui.Text("Any existing user settings you have will be overwritten."); } ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - popup_totalWidth) * 0.5f; ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("Yes", new System.Numerics.Vector2(popup_buttonWidth, 0))) { if (main.ApplyQuadViewSettings()) { pending_popup = "DFR Settings Success"; // Store flag so the popup won't show again etSettings.dfr_settings_prompted = true; EyeTrackingPreferences.WriteSettingsFile(); } else { pending_popup = "DFR Settings Failure"; } ImGui.CloseCurrentPopup(); } ImGui.SameLine(); if (ImGui.Button("No", new System.Numerics.Vector2(popup_buttonWidth, 0))) { // Store flag so the popup won't show again etSettings.dfr_settings_prompted = true; EyeTrackingPreferences.WriteSettingsFile(); ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("DFR Apps", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.Text("List of most popular apps utilizing DFR features. The list is not exhaustive.\nMany other apps support DFR via quad views but your mileage may vary."); ImGui.Text("(*): The \"Quad-Views-Foveated\" OpenXR layer is not required."); ImGui.Spacing(); ImGui.Separator(); ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; foreach (dfr_app d_app in DFR_apps) { ImGui.Separator(); ImGui.Spacing(); ImGui.Text(d_app.title); ImGui.PushStyleColor(ImGuiCol.Text, sub_text); ImGui.Text(d_app.desc); ImGui.PopStyleColor(); ImGui.Spacing(); } ImGui.Separator(); ImGui.Spacing(); ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("Close", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.SetNextWindowPos(center, ImGuiCond.Always, pivot); if (ImGui.BeginPopupModal("Quad-View Settings", ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.SetItemDefaultFocus(); float windowWidth = ImGui.GetWindowSize().X; float offsetX = (windowWidth - (popup_totalWidth / 2)) * 0.5f; // User settings for (int i = 0; i < dfr_setting_inputs.Length; i++) { var (label, id, width, get, set) = dfr_setting_inputs[i]; string val = get(); ImGui.Separator(); ImGui.Spacing(); ImGui.Text(label); ImGui.Spacing(); ImGui.PushID(id); ImGui.SetNextItemWidth(width); pushed_styles = PushTextInputStyle(); if (ImGui.InputText("", ref val, 256)) { changedDFRSetting = true; set(val); } ImGui.PopStyleColor(pushed_styles); System.Numerics.Vector2 size = ImGui.CalcTextSize(id); ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, sub_text); ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (size.Y / 4)); ImGui.Text(id); ImGui.PopStyleColor(); ImGui.PopID(); } ImGui.Separator(); ImGui.Spacing(); // Gaze cursor checkbox bool usingCursor = etSettings.quad_debug_gaze > 0 ? true : false; ImGui.SetWindowFontScale(checkboxScale); pushed_styles = PushCheckboxStyle(); if (ImGui.Checkbox("##cursor", ref usingCursor)) { changedDFRSetting = true; etSettings.quad_debug_gaze = usingCursor ? 1 : 0; } ImGui.PopStyleColor(pushed_styles); ImGui.SetWindowFontScale(windowScale); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); ImGui.Text("DFR Gaze Cursor"); string cursorDesc = "Enable a visual cursor to follow your gaze."; ImGui.PushStyleColor(ImGuiCol.Text, sub_text); ImGui.Text(cursorDesc); ImGui.PopStyleColor(); if (!ImGui.IsAnyItemActive() && changedDFRSetting) { ImGui.CloseCurrentPopup(); changedDFRSetting = false; pending_popup = "DFR Confirm"; } ImGui.Separator(); ImGui.Spacing(); ImGui.SetCursorPosX(offsetX); pushed_styles = PushButtonStyle(); if (ImGui.Button("Close", new System.Numerics.Vector2(popup_buttonWidth, 0))) { ImGui.CloseCurrentPopup(); } ImGui.PopStyleColor(pushed_styles); ImGui.EndPopup(); } ImGui.PopStyleColor(1); ImGui.PopStyleVar(1); } private void RenderHomeSettings() { RenderHeader(); // Create a section for subscriber toggles RenderSubscribers(); if (DEBUG_SETTINGS) { pushed_styles = PushButtonStyle(); if (ImGui.Button("Enter Debug")) { activePage = window_pages.Debug; } ImGui.PopStyleColor(pushed_styles); } ImGui.Spacing(); ImGui.Spacing(); ImGui.Separator(); ImGui.Separator(); ImGui.Spacing(); ImGui.Spacing(); ImGui.PushFont(header_font); ImGui.SetWindowFontScale(sectionFontScale); ImGui.Text("Enrollment"); ImGui.SetWindowFontScale(windowScale); ImGui.PopFont(); ImGui.Spacing(); ImGui.Spacing(); // Disable the enroll button if no valid user ID exists (which requires a valid token) if (authentication_status != auth_code.Success) { ImGui.BeginDisabled(); } // Add button to change to enroll scene pushed_styles = PushButtonStyle(); if (ImGui.Button(model_names.Count > 0 ? "Redo Enrollment" : "Perform Enrollment")) { pending_popup = "Enroll Confirm"; } ImGui.PopStyleColor(pushed_styles); // End the disabled state if it was enabled if (authentication_status != auth_code.Success) { ImGui.EndDisabled(); // Show tooltip explaining why button is disabled if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ShowTooltip("To enroll, you need a valid user token."); } } ImGui.Spacing(); // Create a horizontal layout for the token field and paste button ImGui.BeginGroup(); // Token input field string tempToken = UserDataManager.Instance.TestingToken; ImGui.Text("User Token:"); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y - 3); pushed_styles = PushTextInputStyle(); if (ImGui.InputText("##TestingToken", ref tempToken, 256)) { tokenInputChanged = true; } ImGui.PopStyleColor(pushed_styles); // Only make request if the field is no longer in focus if (!ImGui.IsItemActive() && tokenInputChanged) { tokenInputChanged = false; UserDataManager.Instance.TestingToken = tempToken; GD.Print("User token updated!"); } // Paste button ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y - 3); pushed_styles = PushButtonStyle(); if (ImGui.Button("Paste")) { // Get clipboard content string clipboardText = DisplayServer.ClipboardGet(); if (!string.IsNullOrEmpty(clipboardText)) { UserDataManager.Instance.TestingToken = clipboardText; GD.Print("User token pasted from clipboard."); } } ImGui.PopStyleColor(pushed_styles); ImGui.EndGroup(); if (authentication_status != auth_code.Success) { ImGui.Spacing(); if (authentication_status == auth_code.InvalidToken) { ImGui.TextWrapped("To get started with eyetracking, perform a calibration enrollment to receive a personal model. During this beta rollout phase, a user token is required."); } if (authentication_status == auth_code.ExpiredToken) { ImGui.TextWrapped("Your current token has expired. User tokens expire after a few weeks. Any downloaded models are still available for use."); } ImGui.Text("If you did not receive an email with your token, contact"); ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, hyperlink_text); ImGui.Text("support@bigscreenvr.com"); ImGui.PopStyleColor(); if (ImGui.IsItemClicked()) { OpenLink(mail_link); } ImGui.SameLine(); ImGui.Text("to get one."); } else { // Display user ID field ImGui.Text("User ID:"); ImGui.SameLine(); ImGui.Text(!string.IsNullOrWhiteSpace(UserDataManager.Instance.UserId) ? UserDataManager.Instance.UserId : "N/A"); } ImGui.Spacing(); ImGui.Spacing(); ImGui.Separator(); ImGui.Separator(); ImGui.Spacing(); ImGui.Spacing(); // Display the dropdown of models DisplayModels(); // Status if (gazeEstimation != null) { bool has_critical_error = activeErrors.Count > 0 && activeErrors[0] != process_error.UsingCPU; bool searching = gazeEstimation != null && gazeEstimation.cameraManager != null && gazeEstimation.cameraManager.Searching && !gazeEstimation.cameraManager.CameraAccessRevoked; ImGui.Text("Eyetracking Status:"); ImGui.SameLine(); System.Numerics.Vector4 c = searching ? new System.Numerics.Vector4(1, 0.1f, 1, 1) : has_critical_error ? new System.Numerics.Vector4(1, 0, 0, 1) : gazeEstimation.IsRunning ? new System.Numerics.Vector4(0, 1, 0, 1) : new System.Numerics.Vector4(1, 0, 0, 1); ImGui.PushStyleColor(ImGuiCol.Text, c); if (searching) { int dotCount = (int)(ImGui.GetTime() * 2) % 4; string dots = new string('.', dotCount); ImGui.Text($"Searching for device{dots}"); } else { ImGui.Text(has_critical_error ? "Error" : gazeEstimation.IsRunning ? "Active" : "Not running"); } ImGui.PopStyleColor(); } ImGui.SameLine(); string adv_btn_text = "Advanced Settings >"; ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y - 3); ImGui.SetCursorPosX(ImGui.GetWindowSize().X - (ImGui.CalcTextSize(adv_btn_text).X + 15)); pushed_styles = PushButtonStyle(); if (ImGui.Button(adv_btn_text)) { activePage = window_pages.Advanced; } ImGui.PopStyleColor(pushed_styles); } public override void _Process(double delta) { Engine.MaxFps = DisplayServer.WindowIsFocused() ? UI_FPS : UI_unfocus_FPS; // Window is minimized — skip rendering if (hault_render || DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Minimized) { return; } if (etSettings.expecting_new_model) { checkerLifetime += (float)delta; // Max time has lapsed and no model was received if (checkerLifetime >= MODEL_CHECK_LIFETIME) { checkerLifetime = 0; etSettings.expecting_new_model = false; EyeTrackingPreferences.WriteSettingsFile(); } } if (etSettings.expecting_new_model || !UserDataManager.Instance.ReceivedModelsResponse) { // Update check timers modelCheckTimer += (float)delta; // Periodically check for new models if (modelCheckTimer >= MODEL_CHECK_INTERVAL) { modelCheckTimer = 0f; UserDataManager.Instance.FetchUserTrainedModels(); // Refresh the combined models list after fetching new models CallDeferred(nameof(RefreshCombinedModels)); } } tokenCheckTimer += (float)delta; // Periodically check for a changed token from URI if (tokenCheckTimer >= TOKEN_CHECK_INTERVAL) { tokenCheckTimer = 0f; if (EyeTrackingPreferences.CheckTokenChanged()) { ChangeToken(etSettings.user_testing_token); } } // Set window to be full size and expand to edges ImGui.SetNextWindowPos(new System.Numerics.Vector2(0, 0)); ImGui.SetNextWindowSize(new System.Numerics.Vector2(windowWidth * globalScale, windowHeight * globalScale)); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 3f); ImGui.PushStyleVar(ImGuiStyleVar.GrabRounding, 3f); ImGui.PushStyleVar(ImGuiStyleVar.PopupRounding, 3f); // Total black background ImGui.PushStyleColor(ImGuiCol.WindowBg, 0xFF000000); ImGuiWindowFlags windowFlags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoNavFocus; ImGui.Begin("ETHome", windowFlags); if (!string.IsNullOrEmpty(pending_popup)) { ImGui.OpenPopup(pending_popup); pending_popup = ""; } if (activePage == window_pages.Home) { RenderHomeSettings(); } else if (activePage == window_pages.Debug) { DebugSettings(); } else if (activePage == window_pages.Advanced) { AdvancedSettings(); } else if (activePage == window_pages.DFR) { DFRSettings(); } RenderPopups(delta); ImGui.PopStyleColor(); ImGui.PopStyleVar(3); ImGui.End(); } private string FormatModelDisplayName(string modelPath, bool skipCustom = false) { string displayName = ""; string[] pathParts = modelPath.Split('/'); // Look for the date format and model name in the path parts for (int j = 0; j < pathParts.Length; j++) { // Check if this part matches date format (YYYY-MM-DD_HH-mm-ss) if (pathParts[j].Length == 19 && pathParts[j].Contains('-') && pathParts[j].Contains('_')) { // Found the date, now get the model name from the next part if it exists displayName = pathParts[j]; if (j + 1 < pathParts.Length) { string modelName = pathParts[j + 1]; if (modelName.EndsWith(".model", StringComparison.OrdinalIgnoreCase)) { modelName = modelName.Substring(0, modelName.Length - 6); // Remove ".model" } displayName += "/" + modelName; } break; } } // If we couldn't find the date format, fall back to just the model name if (string.IsNullOrEmpty(displayName)) { string modelName = pathParts[pathParts.Length - 1]; if (modelName.EndsWith(".model", StringComparison.OrdinalIgnoreCase)) { modelName = modelName.Substring(0, modelName.Length - 6); // Remove ".model" } displayName = modelName; } // If a custom name exists, use it for display if (!skipCustom && etSettings.named_models.TryGetValue(displayName, out string customName)) { return customName; } return displayName; } private void DisplayModels() { int dotCount = (int)(ImGui.GetTime() * 2) % 4; string dots = new string('.', dotCount); ImGui.PushFont(header_font); ImGui.SetWindowFontScale(sectionFontScale); ImGui.Text($"Available Models ({model_names.Count}): {(etSettings.expecting_new_model ? " " + dots : "")}"); ImGui.SetWindowFontScale(windowScale); ImGui.PopFont(); ImGui.Spacing(); ImGui.Spacing(); ImGui.SetWindowFontScale(modelUIScale); // Store the enabled state bool isEnabled = model_names.Count > 0 && !UserDataManager.Instance.IsLoadingModel; if (!isEnabled) { ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * 0.5f); } if (selectedModelIndex >= model_names.Count) { selectedModelIndex = -1; } string comboLabel = isEnabled ? (selectedModelIndex >= 0 ? FormatModelDisplayName(model_names[selectedModelIndex]) : "None Selected") : "None Available"; if (UserDataManager.Instance.IsLoadingModel) { comboLabel = $"Downloading model{dots}"; } pushed_styles = PushDropdownStyle(); ImGui.PushFont(header_font); ImGui.SetWindowFontScale(sectionFontScale); if (ImGui.BeginCombo("##ModelSelectCombo", comboLabel)) { if (isEnabled) { for (int i = 0; i < model_names.Count; i++) { if (i >= model_names.Count) { break; } string modelPath = model_names[i]; // Skip if not a .model file if (!modelPath.EndsWith(".model", StringComparison.OrdinalIgnoreCase)) { continue; } string displayName = FormatModelDisplayName(modelPath); // Determine if this model is selected bool isModelSelected = (i == selectedModelIndex); if (ImGui.Selectable($"{displayName}##model{i}", isModelSelected)) { // Skip selection if the model path contains a percentage if (!(modelPath.Contains("(") && modelPath.Contains(")") && modelPath.Contains("%"))) { SelectModel(i); etSettings.selected_model = modelPath; EyeTrackingPreferences.WriteSettingsFile(); } else { pending_popup = "Model Training"; } } if (isModelSelected) { ImGui.SetItemDefaultFocus(); } } } ImGui.EndCombo(); } ImGui.PopStyleColor(pushed_styles); ImGui.SetWindowFontScale(windowScale); ImGui.PopFont(); if (!isEnabled) { ImGui.PopStyleVar(); // Show tooltip explaining option if (ImGui.IsItemHovered() && !UserDataManager.Instance.IsLoadingModel) { ShowTooltip(!string.IsNullOrEmpty(UserDataManager.Instance.UserId) ? "No models available to run eyetracking. Perform enrollment to receive any." : "Set a valid user token to receive your models."); } } ImGui.SetWindowFontScale(windowScale); ImGui.SameLine(); // Model edit button if (string.IsNullOrEmpty(etSettings.selected_model)) { ImGui.BeginDisabled(); } ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 3); pushed_styles = PushButtonCheckboxStyle(); var (texId, _, _) = texLoader.GetPng("res://images/ico_edit.png"); if (ImGui.ImageButton("##modeledit", texId, new System.Numerics.Vector2(19, 21))) { pendingCustomModelName = FormatModelDisplayName(model_names[selectedModelIndex]); pending_popup = "Label Model"; } ImGui.PopStyleColor(pushed_styles); ImGui.SameLine(); if (string.IsNullOrEmpty(etSettings.selected_model)) { ImGui.EndDisabled(); } // Add camera toggle checkbox bool current = etSettings.camera_enabled; ImGui.SetWindowFontScale(checkboxScale); pushed_styles = PushCheckboxStyle(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 3); if (ImGui.Checkbox("##0", ref current)) { etSettings.camera_enabled = current; EyeTrackingPreferences.WriteSettingsFile(); //Emit the signal that the camera should be enabled or disabled if (etSettings.camera_enabled) { EmitSignal(SignalName.EnableCamera); } else { EmitSignal(SignalName.DisableCamera); } GD.Print($"Camera enabled: {etSettings.camera_enabled}"); } ImGui.PopStyleColor(pushed_styles); ImGui.SetWindowFontScale(windowScale); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y + 3); ImGui.Text("Eyetracking Enabled"); if (!string.IsNullOrEmpty(UserDataManager.Instance.ModelLoadingError)) { ImGui.PushStyleColor(ImGuiCol.Text, new System.Numerics.Vector4(1, 0, 0, 1)); ImGui.Text("! " + UserDataManager.Instance.ModelLoadingError); ImGui.Text("Please try again."); ImGui.PopStyleColor(); } else if (gazeEstimation != null && gazeEstimation.IsRunning) { ImGui.Spacing(); ImGui.Spacing(); ImGui.Text("Eyetracking slightly off?"); ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, new System.Numerics.Vector4(0.2f, 0.6f, 1, 1)); ImGui.Text("See tips."); ImGui.PopStyleColor(); if (ImGui.IsItemClicked()) { pending_popup = "Accuracy Tips"; } if (etSettings.selected_model.Contains("v0.1") || etSettings.selected_model.Contains("v0.2")) { ImGui.PushStyleColor(ImGuiCol.Text, new System.Numerics.Vector4(1, 0, 0, 1)); ImGui.Text("! This model version does not support blinking. Redo enrollment to receive a new model version."); ImGui.PopStyleColor(); } else { bool alignmentEnabled = etSettings.enabled_alignment_helper; ImGui.Spacing(); ImGui.Text("Show Alignment Helper"); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y - 3); pushed_styles = PushCheckboxStyle(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 3); if (ImGui.Checkbox("##a", ref alignmentEnabled)) { if (alignmentEnabled) { if (etSettings.submit_to_xr) { pending_popup = "Enable Alignment Helper"; } else { pending_popup = "Alignment Helper Requirements"; } } else { etSettings.enabled_alignment_helper = false; EyeTrackingPreferences.WriteSettingsFile(); alignmentHelper.EndAlignmentWatch(); } GD.Print($"Alignment helper toggled: {etSettings.camera_enabled}"); } ImGui.PopStyleColor(pushed_styles); if (etSettings.enabled_alignment_helper) { ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorScreenPos().Y - 3); ImGui.PushStyleColor(ImGuiCol.Text, new System.Numerics.Vector4(0.2f, 0.6f, 1, 1)); ImGui.Text("See guide."); ImGui.PopStyleColor(); if (ImGui.IsItemClicked()) { pending_popup = "Alignment Helper Guide"; } } } } ImGui.Spacing(); ImGui.Spacing(); ImGui.Separator(); ImGui.Separator(); ImGui.Spacing(); } }