11#if UNITY_EDITOR
22using System ;
33using System . Collections . Generic ;
4+ using System . Globalization ;
45using System . Linq ;
6+ using System . Text ;
57using UnityEditor ;
68using UnityEditor . IMGUI . Controls ;
79using UnityEngine . InputSystem . LowLevel ;
@@ -90,6 +92,7 @@ internal void OnDestroy()
9092 InputSystem . onSettingsChange -= NeedControlValueRefresh ;
9193 Application . focusChanged -= OnApplicationFocusChange ;
9294 EditorApplication . playModeStateChanged += OnPlayModeChange ;
95+ EditorApplication . update -= OnEditorUpdate ;
9396 }
9497
9598 m_EventTrace ? . Dispose ( ) ;
@@ -143,6 +146,14 @@ internal void OnGUI()
143146 EditorGUILayout . LabelField ( "Flags" , m_DeviceFlagsString ) ;
144147 if ( m_Device is Keyboard )
145148 EditorGUILayout . LabelField ( "Keyboard Layout" , ( ( Keyboard ) m_Device ) . keyboardLayout ) ;
149+ const string sampleFrequencyTooltip = "Displays the current average event or sample frequency of this device in Hertz (Hz). " +
150+ "The target frequency is device and backend dependent and may not be supported by all devices nor backends. " +
151+ "The Polling Frequency indicates system polling target frequency." ;
152+ if ( ! string . IsNullOrEmpty ( m_DeviceFrequencyString ) )
153+ EditorGUILayout . LabelField ( new GUIContent ( "Sample Frequency" , sampleFrequencyTooltip ) , new GUIContent ( m_DeviceFrequencyString ) , EditorStyles . label ) ;
154+ const string inputSystemLatencyTooltip = "Displays the average input system latency (Excluding OS, driver, firmware or transport latency) for data reported for this device." ;
155+ if ( ! string . IsNullOrEmpty ( m_DeviceLatencyString ) )
156+ EditorGUILayout . LabelField ( new GUIContent ( "Input Latency" , inputSystemLatencyTooltip ) , new GUIContent ( m_DeviceLatencyString ) , EditorStyles . label ) ;
146157 EditorGUILayout . EndVertical ( ) ;
147158
148159 DrawControlTree ( ) ;
@@ -287,9 +298,20 @@ private void InitializeWith(InputDevice device)
287298 m_DeviceId = device . deviceId ;
288299 m_DeviceIdString = device . deviceId . ToString ( ) ;
289300 m_DeviceUsagesString = string . Join ( ", " , device . usages . Select ( x => x . ToString ( ) ) . ToArray ( ) ) ;
290-
301+
291302 UpdateDeviceFlags ( ) ;
292303
304+ // Query the sampling frequency of the device.
305+ // We do this synchronously here for simplicity.
306+ var queryFrequency = QuerySamplingFrequencyCommand . Create ( ) ;
307+ var result = device . ExecuteCommand ( ref queryFrequency ) ;
308+ var targetFrequency = float . NaN ;
309+ if ( result >= 0 )
310+ targetFrequency = queryFrequency . frequency ;
311+ var realtimeSinceStartup = Time . realtimeSinceStartupAsDouble ;
312+ m_FrequencyCalculator = new FrequencyCalculator ( targetFrequency , realtimeSinceStartup ) ;
313+ m_LatencyCalculator = new LatencyCalculator ( realtimeSinceStartup ) ;
314+
293315 // Set up event trace. The default trace size of 512kb fits a ton of events and will
294316 // likely bog down the UI if we try to display that many events. Instead, come up
295317 // with a more reasonable sized based on the state size of the device.
@@ -326,6 +348,84 @@ private void InitializeWith(InputDevice device)
326348 InputState . onChange += OnDeviceStateChange ;
327349 Application . focusChanged += OnApplicationFocusChange ;
328350 EditorApplication . playModeStateChanged += OnPlayModeChange ;
351+ EditorApplication . update += OnEditorUpdate ;
352+ }
353+
354+ private void OnEditorUpdate ( )
355+ {
356+ StringBuilder sb = null ;
357+ bool needControlValueRefresh = false ;
358+ var realtimeSinceStartup = Time . realtimeSinceStartupAsDouble ;
359+ if ( m_FrequencyCalculator . Update ( realtimeSinceStartup ) )
360+ {
361+ m_DeviceFrequencyString = CreateDeviceFrequencyString ( ref sb ) ;
362+ needControlValueRefresh = true ;
363+ }
364+ if ( m_LatencyCalculator . Update ( realtimeSinceStartup ) )
365+ {
366+ m_DeviceLatencyString = CreateDeviceLatencyString ( ref sb ) ;
367+ needControlValueRefresh = true ;
368+ }
369+ if ( needControlValueRefresh )
370+ NeedControlValueRefresh ( ) ;
371+ }
372+
373+ private string CreateDeviceFrequencyString ( ref StringBuilder sb )
374+ {
375+ if ( sb == null )
376+ sb = new StringBuilder ( ) ;
377+ else
378+ sb . Clear ( ) ;
379+
380+ // Display achievable frequency for device
381+ const string frequencyFormat = "Average: 0.000 Hz" ;
382+ sb . Append ( m_FrequencyCalculator . frequency . ToString ( frequencyFormat , CultureInfo . InvariantCulture ) ) ;
383+
384+ // Display target frequency reported for device
385+ sb . Append ( " (Target @ " ) ;
386+ sb . Append ( float . IsNaN ( m_FrequencyCalculator . targetFrequency )
387+ ? "n/a"
388+ : m_FrequencyCalculator . targetFrequency . ToString ( frequencyFormat ) ) ;
389+
390+ // Display system-wide polling frequency
391+ sb . Append ( ", Polling-Frequency @ " ) ;
392+ sb . Append ( InputSystem . pollingFrequency . ToString ( frequencyFormat ) ) ;
393+ sb . Append ( ')' ) ;
394+
395+ return sb . ToString ( ) ;
396+ }
397+
398+ private static void FormatLatency ( StringBuilder sb , float value )
399+ {
400+ const string latencyFormat = "0.000 ms" ;
401+ if ( float . IsNaN ( value ) )
402+ {
403+ sb . Append ( "n/a" ) ;
404+ return ;
405+ }
406+
407+ var millis = 1000.0f * value ;
408+ sb . Append ( millis <= 1000.0f
409+ ? ( millis ) . ToString ( latencyFormat , CultureInfo . InvariantCulture )
410+ : ">1000.0 ms" ) ;
411+ }
412+
413+ private string CreateDeviceLatencyString ( ref StringBuilder sb )
414+ {
415+ if ( sb == null )
416+ sb = new StringBuilder ( ) ;
417+ else
418+ sb . Clear ( ) ;
419+
420+ // Display latency in seconds for device
421+ sb . Append ( "Average: " ) ;
422+ FormatLatency ( sb , m_LatencyCalculator . averageLatencySeconds ) ;
423+ sb . Append ( ", Min: " ) ;
424+ FormatLatency ( sb , m_LatencyCalculator . minLatencySeconds ) ;
425+ sb . Append ( ", Max: " ) ;
426+ FormatLatency ( sb , m_LatencyCalculator . maxLatencySeconds ) ;
427+
428+ return sb . ToString ( ) ;
329429 }
330430
331431 private void UpdateDeviceFlags ( )
@@ -396,6 +496,8 @@ internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device)
396496 private string m_DeviceIdString ;
397497 private string m_DeviceUsagesString ;
398498 private string m_DeviceFlagsString ;
499+ private string m_DeviceFrequencyString ;
500+ private string m_DeviceLatencyString ;
399501 private InputDevice . DeviceFlags m_DeviceFlags ;
400502 private InputControlTreeView m_ControlTree ;
401503 private InputEventTreeView m_EventTree ;
@@ -404,6 +506,8 @@ internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device)
404506 private InputEventTrace . ReplayController m_ReplayController ;
405507 private InputEventTrace m_EventTrace ;
406508 private InputUpdateType m_InputUpdateTypeShownInControlTree ;
509+ private LatencyCalculator m_LatencyCalculator ;
510+ private FrequencyCalculator m_FrequencyCalculator ;
407511
408512 [ SerializeField ] private int m_DeviceId = InputDevice . InvalidDeviceId ;
409513 [ SerializeField ] private TreeViewState m_ControlTreeState ;
@@ -461,10 +565,130 @@ private void OnDeviceChange(InputDevice device, InputDeviceChange change)
461565 }
462566 }
463567
568+ private struct LatencyCalculator
569+ {
570+ private double m_LastUpdateTime ;
571+ private double m_AccumulatedLatencySeconds ;
572+ private double m_AccumulatedMinLatencySeconds ;
573+ private double m_AccumulatedMaxLatencySeconds ;
574+ private int m_SampleCount ;
575+
576+ public LatencyCalculator ( double realtimeSinceStartup )
577+ {
578+ m_LastUpdateTime = realtimeSinceStartup ;
579+ m_AccumulatedLatencySeconds = 0.0 ;
580+ m_AccumulatedMinLatencySeconds = 0.0 ;
581+ m_AccumulatedMaxLatencySeconds = 0.0 ;
582+ m_SampleCount = 0 ;
583+ averageLatencySeconds = float . NaN ;
584+ minLatencySeconds = float . NaN ;
585+ maxLatencySeconds = float . NaN ;
586+ }
587+
588+ public void ProcessSample ( InputEventPtr eventPtr ) => ProcessSample ( eventPtr , Time . realtimeSinceStartupAsDouble ) ;
589+
590+ public void ProcessSample ( InputEventPtr eventPtr , double realtimeSinceStartup )
591+ {
592+ if ( ! eventPtr . valid )
593+ return ;
594+
595+ var ageInSeconds = realtimeSinceStartup - eventPtr . time ;
596+ m_AccumulatedLatencySeconds += ageInSeconds ;
597+ if ( ++ m_SampleCount == 1 )
598+ {
599+ m_AccumulatedMinLatencySeconds = ageInSeconds ;
600+ m_AccumulatedMaxLatencySeconds = ageInSeconds ;
601+ }
602+ else if ( ageInSeconds < m_AccumulatedMaxLatencySeconds )
603+ m_AccumulatedMinLatencySeconds = ageInSeconds ;
604+ else if ( ageInSeconds > m_AccumulatedMaxLatencySeconds )
605+ m_AccumulatedMaxLatencySeconds = ageInSeconds ;
606+ }
607+
608+ public float averageLatencySeconds { get ; private set ; }
609+ public float minLatencySeconds { get ; private set ; }
610+ public float maxLatencySeconds { get ; private set ; }
611+
612+ public bool Update ( ) => Update ( Time . realtimeSinceStartupAsDouble ) ;
613+
614+ public bool Update ( double realtimeSinceStartup )
615+ {
616+ var timeSinceLastUpdate = realtimeSinceStartup - m_LastUpdateTime ;
617+ if ( timeSinceLastUpdate < 1.0 )
618+ return false ; // Only update once per second (and avoid division by zero)
619+
620+ if ( m_SampleCount == 0 )
621+ {
622+ averageLatencySeconds = float . NaN ;
623+ minLatencySeconds = float . NaN ;
624+ maxLatencySeconds = float . NaN ;
625+ }
626+ else
627+ {
628+ averageLatencySeconds = ( float ) ( m_AccumulatedLatencySeconds / m_SampleCount ) ;
629+ minLatencySeconds = ( float ) m_AccumulatedMinLatencySeconds ;
630+ maxLatencySeconds = ( float ) m_AccumulatedMaxLatencySeconds ;
631+ }
632+
633+ m_LastUpdateTime = realtimeSinceStartup ;
634+ m_SampleCount = 0 ;
635+
636+ m_AccumulatedLatencySeconds = 0.0 ;
637+
638+ return true ;
639+ }
640+ }
641+
642+ private struct FrequencyCalculator
643+ {
644+ private double m_LastUpdateTime ;
645+ private int m_SampleCount ;
646+
647+ public FrequencyCalculator ( float targetFrequency , double realtimeSinceStartup )
648+ {
649+ this . targetFrequency = targetFrequency ;
650+ this . m_SampleCount = 0 ;
651+ this . frequency = 0.0f ;
652+ this . m_LastUpdateTime = realtimeSinceStartup ;
653+ }
654+
655+ public float targetFrequency { get ; private set ; }
656+ public float frequency { get ; private set ; }
657+
658+ public void ProcessSample ( InputEventPtr eventPtr )
659+ {
660+ // Only count actual samples instead of device-state changes which may be reported anyway it seems.
661+ // For determining frequency we at least absolute do not want to count state changes not driven
662+ // by an associated event/sample.
663+ if ( eventPtr != null )
664+ ++ m_SampleCount ;
665+ }
666+
667+ public bool Update ( ) => Update ( Time . realtimeSinceStartupAsDouble ) ;
668+
669+ public bool Update ( double realtimeSinceStartup )
670+ {
671+ var timeSinceLastUpdate = realtimeSinceStartup - m_LastUpdateTime ;
672+ if ( timeSinceLastUpdate < 1.0 )
673+ return false ; // Only update once per second (and avoid division by zero)
674+
675+ m_LastUpdateTime = realtimeSinceStartup ;
676+ frequency = ( float ) ( m_SampleCount / timeSinceLastUpdate ) ;
677+ m_SampleCount = 0 ;
678+
679+ return true ;
680+ }
681+ }
682+
464683 private void OnDeviceStateChange ( InputDevice device , InputEventPtr eventPtr )
465684 {
466685 if ( device == m_Device )
467- NeedControlValueRefresh ( ) ;
686+ {
687+ m_LatencyCalculator . ProcessSample ( eventPtr ) ;
688+ m_FrequencyCalculator . ProcessSample ( eventPtr ) ;
689+
690+ NeedControlValueRefresh ( ) ;
691+ }
468692 }
469693
470694 private static class Styles
0 commit comments