From 16102f34f86716bf8603870c692286a51c9593cc Mon Sep 17 00:00:00 2001 From: Extrys Date: Wed, 26 Nov 2025 22:20:17 +0100 Subject: [PATCH 1/4] Fixed XR input recording Enhance InputEventTrace to store and retrieve device usages facilitating accurate device recreation during input replay. This fixes XR devices not replaying due not writing its Left/Right usages Also included original device resetting to avoid incorrect rest state passthrough input to override replay input --- .../InputSystem/Events/InputEventTrace.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs index c8b3a40413..afea1acc21 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs @@ -269,6 +269,7 @@ public void WriteTo(Stream stream) writer.Write(device.layout); writer.Write(device.stateFormat); writer.Write(device.stateSizeInBytes); + writer.Write(device.m_UsagesJson ?? string.Empty); writer.Write(device.m_FullLayoutJson ?? string.Empty); } @@ -392,6 +393,7 @@ public void ReadFrom(Stream stream) layout = reader.ReadString(), stateFormat = reader.ReadInt32(), stateSizeInBytes = reader.ReadInt32(), + m_UsagesJson = reader.ReadString(), m_FullLayoutJson = reader.ReadString() }; } @@ -924,6 +926,12 @@ private void OnInputEvent(InputEventPtr inputEvent, InputDevice device) m_StateFormat = device.stateBlock.format, m_StateSizeInBytes = (int)device.stateBlock.alignedSizeInBytes, + // if the device has usages, store them as JSON in the device info + // This way, when replaying the trace, we can recreate the device with the correct usages. For example XR devices + m_UsagesJson = device.usages.Count > 0 + ? JsonUtility.ToJson(new DeviceInfo.UsagesJsonWrapper(device.usages)) + : null, + // If it's a generated layout, store the full layout JSON in the device info. We do this so that // when saving traces for this kind of input, we can recreate the device. m_FullLayoutJson = InputControlLayout.s_Layouts.IsGeneratedLayout(device.m_Layout) @@ -1498,8 +1506,38 @@ private int ApplyDeviceMapping(int originalDeviceId) InputSystem.RegisterLayout(deviceInfo.m_FullLayoutJson); } + // Make sure original device is in a clean state. + // Its useful to avoid some inactive devices to overwrite recorded state, for example what happens with XRHMD. + var originalDevice = InputSystem.GetDeviceById(deviceInfo.m_DeviceId); + if (originalDevice != null) + InputSystem.ResetDevice(originalDevice); + + // Retrieve original usages. For example, LeftHand, RightHand, etc. + ReadOnlyArray originalUsages = null; + if (!string.IsNullOrEmpty(deviceInfo.m_UsagesJson)) + originalUsages = DeviceInfo.UsagesJsonWrapper.GetUsagesFromJson(deviceInfo.m_UsagesJson); + // Create device. var device = InputSystem.AddDevice(layoutName); + + // Ensure usages from original device are present on the new device. + bool usagesUpdated = false; + for (int i = originalUsages.Count - 1; i >= 0; i--) + { + InternedString usage = originalUsages[i]; + if (!device.usages.Contains(usage)) + { + // Adds missing usages from original device. + device.AddDeviceUsage(usage); + usagesUpdated = true; + } + } + if (usagesUpdated) + { + // Notify about usage change. Needed for XR devices to work with input replay. + InputActionState.OnDeviceChange(device, InputDeviceChange.UsageChanged); + } + WithDeviceMappedFromTo(originalDeviceId, device.deviceId); m_CreatedDevices.AppendWithCapacity(device); return device.deviceId; @@ -1567,6 +1605,37 @@ public int stateSizeInBytes [SerializeField] internal FourCC m_StateFormat; [SerializeField] internal int m_StateSizeInBytes; [SerializeField] internal string m_FullLayoutJson; + [SerializeField] internal string m_UsagesJson; + + [Serializable] + public struct UsagesJsonWrapper + { + [SerializeField] internal string[] m_usages; + + public static ReadOnlyArray GetUsagesFromJson(string json) + { + return JsonUtility.FromJson(json).GetUsagesInternedStringArray(); + } + + public UsagesJsonWrapper(ReadOnlyArray usages) + { + m_usages = new string[usages.Count]; + for (int i = 0; i < usages.Count; i++) + { + m_usages[i] = usages[i].ToString(); + } + } + + internal readonly ReadOnlyArray GetUsagesInternedStringArray() + { + InternedString[] internedUsages = new InternedString[m_usages.Length]; + for (int i = 0; i < m_usages.Length; i++) + { + internedUsages[i] = new InternedString(m_usages[i]); + } + return new ReadOnlyArray(internedUsages); + } + } } } } From 663ab36aae619348ef078742ebc1c8ca59cbb2cd Mon Sep 17 00:00:00 2001 From: Extrys Date: Wed, 26 Nov 2025 23:53:14 +0100 Subject: [PATCH 2/4] Updated InputEventTrace to allow Older file loading --- .../InputSystem/Events/InputEventTrace.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs index afea1acc21..f4adcd3906 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs @@ -269,8 +269,8 @@ public void WriteTo(Stream stream) writer.Write(device.layout); writer.Write(device.stateFormat); writer.Write(device.stateSizeInBytes); - writer.Write(device.m_UsagesJson ?? string.Empty); writer.Write(device.m_FullLayoutJson ?? string.Empty); + writer.Write(device.m_UsagesJson ?? string.Empty); } // Write offset of device list. @@ -393,8 +393,8 @@ public void ReadFrom(Stream stream) layout = reader.ReadString(), stateFormat = reader.ReadInt32(), stateSizeInBytes = reader.ReadInt32(), - m_UsagesJson = reader.ReadString(), - m_FullLayoutJson = reader.ReadString() + m_FullLayoutJson = reader.ReadString(), + m_UsagesJson = kFileVersion >= 2 ? reader.ReadString() : null }; } @@ -926,16 +926,16 @@ private void OnInputEvent(InputEventPtr inputEvent, InputDevice device) m_StateFormat = device.stateBlock.format, m_StateSizeInBytes = (int)device.stateBlock.alignedSizeInBytes, - // if the device has usages, store them as JSON in the device info - // This way, when replaying the trace, we can recreate the device with the correct usages. For example XR devices - m_UsagesJson = device.usages.Count > 0 - ? JsonUtility.ToJson(new DeviceInfo.UsagesJsonWrapper(device.usages)) - : null, - // If it's a generated layout, store the full layout JSON in the device info. We do this so that // when saving traces for this kind of input, we can recreate the device. m_FullLayoutJson = InputControlLayout.s_Layouts.IsGeneratedLayout(device.m_Layout) ? InputSystem.LoadLayout(device.layout).ToJson() + : null, + + // if the device has usages, store them as JSON in the device info + // This way, when replaying the trace, we can recreate the device with the correct usages. For example XR devices + m_UsagesJson = device.usages.Count > 0 + ? JsonUtility.ToJson(new DeviceInfo.UsagesJsonWrapper(device.usages)) : null }); } @@ -987,7 +987,7 @@ public void Reset() } private static FourCC kFileFormat => new FourCC('I', 'E', 'V', 'T'); - private static int kFileVersion = 1; + private static int kFileVersion = 2; [Flags] private enum FileFlags From ec423957cace1614deab1bc395eb11c66bb89e2f Mon Sep 17 00:00:00 2001 From: Extrys Date: Thu, 27 Nov 2025 00:03:03 +0100 Subject: [PATCH 3/4] Fixed an error when checking for file version --- .../InputSystem/Events/InputEventTrace.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs index f4adcd3906..00413b7866 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs @@ -325,7 +325,8 @@ public void ReadFrom(Stream stream) // Read header. if (reader.ReadInt32() != kFileFormat) throw new IOException($"Stream does not appear to be an InputEventTrace (no '{kFileFormat}' code)"); - if (reader.ReadInt32() > kFileVersion) + int fileVersion = reader.ReadInt32(); + if (fileVersion > kFileVersion) throw new IOException($"Stream is an InputEventTrace but a newer version (expected version {kFileVersion} or below)"); reader.ReadInt32(); // Flags; ignored for now. reader.ReadInt32(); // Platform; for now we're not doing anything with it. @@ -394,7 +395,7 @@ public void ReadFrom(Stream stream) stateFormat = reader.ReadInt32(), stateSizeInBytes = reader.ReadInt32(), m_FullLayoutJson = reader.ReadString(), - m_UsagesJson = kFileVersion >= 2 ? reader.ReadString() : null + m_UsagesJson = fileVersion >= 2 ? reader.ReadString() : null // Usages were added in version 2 }; } From fdcabae8e3be3081c734ee31f36f972ab1043d43 Mon Sep 17 00:00:00 2001 From: Extrys Date: Thu, 27 Nov 2025 00:55:17 +0100 Subject: [PATCH 4/4] Removed devide reset logic due unseen id collision, fixed potential null --- .../InputSystem/Events/InputEventTrace.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs index 00413b7866..3c8c1ac477 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs @@ -1507,14 +1507,8 @@ private int ApplyDeviceMapping(int originalDeviceId) InputSystem.RegisterLayout(deviceInfo.m_FullLayoutJson); } - // Make sure original device is in a clean state. - // Its useful to avoid some inactive devices to overwrite recorded state, for example what happens with XRHMD. - var originalDevice = InputSystem.GetDeviceById(deviceInfo.m_DeviceId); - if (originalDevice != null) - InputSystem.ResetDevice(originalDevice); - // Retrieve original usages. For example, LeftHand, RightHand, etc. - ReadOnlyArray originalUsages = null; + ReadOnlyArray originalUsages = default; if (!string.IsNullOrEmpty(deviceInfo.m_UsagesJson)) originalUsages = DeviceInfo.UsagesJsonWrapper.GetUsagesFromJson(deviceInfo.m_UsagesJson); @@ -1629,6 +1623,8 @@ public UsagesJsonWrapper(ReadOnlyArray usages) internal readonly ReadOnlyArray GetUsagesInternedStringArray() { + if(m_usages == null) + return default; InternedString[] internedUsages = new InternedString[m_usages.Length]; for (int i = 0; i < m_usages.Length; i++) {