Skip to content

Commit f61a89b

Browse files
committed
NEW: Pen.isSupported, Mouse.isSupported and Touchscreen.isPressureSupported [ISX-2046]
Legacy UnityEngine.Input conflates "is X supported on this platform" with "is an X present right now", and the Input System had no equivalent for the first question at all. Input.stylusTouchSupported reports true on any iPad new enough to pair an Apple Pencil, whether or not one is paired. Input.mousePresent is genuine detection on Windows and the consoles but a hardcoded true on macOS and Linux. These three properties answer the capability question. Presence keeps its existing answer, Device.current != null. The queries go to the engine's system endpoint, which is addressed by a reserved device id that is never registered, so it never appears in the device list and no device can answer a question about the platform. That endpoint lands separately in the engine repository under the same ticket. Touch pressure is answered at platform scope rather than per touchscreen because that is the scope at which the answer exists: every platform sources it from a device model or an OS API property rather than by enumerating digitizers. A per-instance property can be added later, preferring a per-device answer over this one, without changing what is here. The commands and the InputCapabilitySupport tristate are internal rather than public, unlike the rest of the Commands folder. They target the system endpoint, so there is no custom device for a user to answer them for, and keeping them internal avoids freezing a capability model that the low level input API is expected to revisit. The engine answers with a tristate whose Unknown is zero, so an unimplemented query reads as "we do not know" rather than a confident false; the public properties collapse anything other than Supported to false. Both the properties and the plumbing are gated on UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES, so the API is absent rather than present-and-always-false on engine versions that cannot answer. The version expression is currently the local development engine and must be updated to whichever version the engine side actually ships in. ExecuteGlobalCommand is replaced by ExecuteSystemCommand. The former had no callers, having been orphaned when UseWindowsGamingInputCommand was removed by ISXB-927, and its premise was wrong: it addressed device id 0 on the assumption that the engine routes such commands by FourCC alone, but InputDeviceIOCTL resolves the id against the device registry and 0 is the invalid-device sentinel, so nothing sent there could ever be answered. Capabilities cannot change while the process runs, so each is queried at most once. The cache lives on InputManager rather than in a static, so a domain reload or a test installing a different runtime discards it without needing an explicit reset hook. Tests cover each state including Unknown, that the properties answer with no device added, that a query is never delivered to a real device, that repeated reads issue one command, that nothing answering reports false rather than throwing, and that the enum values, the FourCC codes and the payload size all agree with the engine's own constants.
1 parent f2c8a0b commit f61a89b

17 files changed

Lines changed: 699 additions & 7 deletions

Assets/Tests/InputSystem/CoreTests_Devices.cs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5895,4 +5895,168 @@ public unsafe void Devices_DoesntErrorOutOnMaxTouchCount()
58955895
BeginTouch(i, new Vector2(i * 1.0f, i * 2.0f), time: 0);
58965896
}, Throws.Nothing);
58975897
}
5898+
5899+
#if UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES
5900+
// Platform capability queries. These are addressed to the engine's system endpoint rather than
5901+
// to a device, because they answer "can this platform do X" rather than "is an X connected".
5902+
// What a given platform actually answers is asserted natively in the engine repository, from
5903+
// PlatformDependent, where the file location gates the test to that platform.
5904+
5905+
private unsafe void AnswerCapabilityQuery(FourCC type, InputCapabilitySupport answer)
5906+
{
5907+
runtime.SetDeviceCommandCallback(NativeInputCapabilities.systemDeviceId,
5908+
(id, command) =>
5909+
{
5910+
if (command->type != type)
5911+
return InputDeviceCommand.GenericFailure;
5912+
5913+
*(InputCapabilitySupport*)((byte*)command + InputDeviceCommand.kBaseCommandSize) = answer;
5914+
return InputDeviceCommand.GenericSuccess;
5915+
});
5916+
}
5917+
5918+
[Test]
5919+
[Category("Devices")]
5920+
[TestCase(InputCapabilitySupport.Supported, true)]
5921+
[TestCase(InputCapabilitySupport.NotSupported, false)]
5922+
// Unknown collapses to false: the platform has not answered, and a maybe is not something a
5923+
// bool property can express.
5924+
[TestCase(InputCapabilitySupport.Unknown, false)]
5925+
public void Devices_PenIsSupported_ReflectsWhatThePlatformAnswers(InputCapabilitySupport answer, bool expected)
5926+
{
5927+
AnswerCapabilityQuery(QueryPenSupportedCommand.Type, answer);
5928+
5929+
Assert.That(Pen.isSupported, Is.EqualTo(expected));
5930+
}
5931+
5932+
[Test]
5933+
[Category("Devices")]
5934+
[TestCase(InputCapabilitySupport.Supported, true)]
5935+
[TestCase(InputCapabilitySupport.NotSupported, false)]
5936+
[TestCase(InputCapabilitySupport.Unknown, false)]
5937+
public void Devices_MouseIsSupported_ReflectsWhatThePlatformAnswers(InputCapabilitySupport answer, bool expected)
5938+
{
5939+
AnswerCapabilityQuery(QueryMouseSupportedCommand.Type, answer);
5940+
5941+
Assert.That(Mouse.isSupported, Is.EqualTo(expected));
5942+
}
5943+
5944+
[Test]
5945+
[Category("Devices")]
5946+
[TestCase(InputCapabilitySupport.Supported, true)]
5947+
[TestCase(InputCapabilitySupport.NotSupported, false)]
5948+
[TestCase(InputCapabilitySupport.Unknown, false)]
5949+
public void Devices_TouchscreenIsPressureSupported_ReflectsWhatThePlatformAnswers(InputCapabilitySupport answer, bool expected)
5950+
{
5951+
AnswerCapabilityQuery(QueryTouchPressureSupportedCommand.Type, answer);
5952+
5953+
Assert.That(Touchscreen.isPressureSupported, Is.EqualTo(expected));
5954+
}
5955+
5956+
// The properties describe the platform, not a device, so they must answer without one. This is
5957+
// the case that separates them from Device.current != null.
5958+
[Test]
5959+
[Category("Devices")]
5960+
public void Devices_CapabilityQueries_AreAnsweredWithNoDeviceAdded()
5961+
{
5962+
AnswerCapabilityQuery(QueryPenSupportedCommand.Type, InputCapabilitySupport.Supported);
5963+
5964+
Assert.That(InputSystem.devices, Is.Empty);
5965+
Assert.That(Pen.isSupported, Is.True);
5966+
Assert.That(Pen.current, Is.Null);
5967+
}
5968+
5969+
// The endpoint is addressed by a reserved id. A capability query must not be delivered to a
5970+
// real device, which would let a device answer a question about the platform.
5971+
[Test]
5972+
[Category("Devices")]
5973+
public unsafe void Devices_CapabilityQueries_AreNotDeliveredToDevices()
5974+
{
5975+
var pen = InputSystem.AddDevice<Pen>();
5976+
var receivedByDevice = 0;
5977+
runtime.SetDeviceCommandCallback(pen,
5978+
(id, command) =>
5979+
{
5980+
if (command->type == QueryPenSupportedCommand.Type)
5981+
++receivedByDevice;
5982+
return InputDeviceCommand.GenericFailure;
5983+
});
5984+
AnswerCapabilityQuery(QueryPenSupportedCommand.Type, InputCapabilitySupport.Supported);
5985+
5986+
Assert.That(Pen.isSupported, Is.True);
5987+
Assert.That(receivedByDevice, Is.Zero);
5988+
}
5989+
5990+
// A platform capability cannot change while the application runs, so reading the property
5991+
// repeatedly must not keep issuing commands.
5992+
[Test]
5993+
[Category("Devices")]
5994+
public unsafe void Devices_CapabilityQueries_AreOnlyIssuedOnce()
5995+
{
5996+
var queryCount = 0;
5997+
runtime.SetDeviceCommandCallback(NativeInputCapabilities.systemDeviceId,
5998+
(id, command) =>
5999+
{
6000+
if (command->type != QueryPenSupportedCommand.Type)
6001+
return InputDeviceCommand.GenericFailure;
6002+
6003+
++queryCount;
6004+
*(InputCapabilitySupport*)((byte*)command + InputDeviceCommand.kBaseCommandSize) =
6005+
InputCapabilitySupport.Supported;
6006+
return InputDeviceCommand.GenericSuccess;
6007+
});
6008+
6009+
Assert.That(Pen.isSupported, Is.True);
6010+
Assert.That(Pen.isSupported, Is.True);
6011+
Assert.That(Pen.isSupported, Is.True);
6012+
6013+
Assert.That(queryCount, Is.EqualTo(1));
6014+
}
6015+
6016+
// Nothing answers, which is what an engine without the endpoint looks like. The property must
6017+
// report false rather than throwing, and must not retry on every read.
6018+
[Test]
6019+
[Category("Devices")]
6020+
public void Devices_CapabilityQueries_ReportFalseWhenNothingAnswers()
6021+
{
6022+
Assert.That(Pen.isSupported, Is.False);
6023+
Assert.That(Mouse.isSupported, Is.False);
6024+
Assert.That(Touchscreen.isPressureSupported, Is.False);
6025+
}
6026+
6027+
// Nothing generates the mirror of the engine's enum, and a reordering would silently invert
6028+
// Supported and NotSupported across the boundary. The engine pins the same values from its side.
6029+
[Test]
6030+
[Category("Devices")]
6031+
public void Devices_CapabilitySupport_MatchesTheEngineWireValues()
6032+
{
6033+
Assert.That((byte)InputCapabilitySupport.Unknown, Is.EqualTo((byte)CapabilityState.Unknown));
6034+
Assert.That((byte)InputCapabilitySupport.NotSupported, Is.EqualTo((byte)CapabilityState.NotSupported));
6035+
Assert.That((byte)InputCapabilitySupport.Supported, Is.EqualTo((byte)CapabilityState.Supported));
6036+
}
6037+
6038+
// Same reasoning for the codes: the package spells them as FourCC characters, matching every
6039+
// other command in the Commands folder, while the engine declares them as integer constants.
6040+
[Test]
6041+
[Category("Devices")]
6042+
public void Devices_CapabilityQueryCodes_MatchTheEngineCodes()
6043+
{
6044+
Assert.That((int)QueryPenSupportedCommand.Type, Is.EqualTo(NativeInputCapabilities.queryPenSupported));
6045+
Assert.That((int)QueryMouseSupportedCommand.Type, Is.EqualTo(NativeInputCapabilities.queryMouseSupported));
6046+
Assert.That((int)QueryTouchPressureSupportedCommand.Type,
6047+
Is.EqualTo(NativeInputCapabilities.queryTouchPressureSupported));
6048+
}
6049+
6050+
// The payload the package sends must be exactly the one byte the engine's payload validation
6051+
// accepts. The base command header is stripped before it reaches native.
6052+
[Test]
6053+
[Category("Devices")]
6054+
public void Devices_CapabilityQueryPayload_IsOneByteAfterTheCommandHeader()
6055+
{
6056+
Assert.That(QueryPenSupportedCommand.kSize - InputDeviceCommand.kBaseCommandSize, Is.EqualTo(1));
6057+
Assert.That(QueryMouseSupportedCommand.kSize - InputDeviceCommand.kBaseCommandSize, Is.EqualTo(1));
6058+
Assert.That(QueryTouchPressureSupportedCommand.kSize - InputDeviceCommand.kBaseCommandSize, Is.EqualTo(1));
6059+
}
6060+
6061+
#endif // UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES
58986062
}

Assets/Tests/InputSystem/Unity.InputSystem.Tests.asmdef

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@
8181
"name": "Unity",
8282
"expression": "6000.5.0a8",
8383
"define": "UNITY_INPUTSYSTEM_SUPPORTS_FOCUS_EVENTS"
84+
},
85+
{
86+
"name": "Unity",
87+
"expression": "6000.7.0a6",
88+
"define": "UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES"
8489
}
8590
],
8691
"noEngineReferences": false

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased] - yyyy-mm-dd
99

10+
### Added
11+
12+
- Added `Pen.isSupported`, `Mouse.isSupported` and `Touchscreen.isPressureSupported`, reporting what the current platform is capable of rather than what is connected right now. Use them to decide whether to offer device-specific functionality at all. These are not drop-in replacements for the legacy `UnityEngine.Input.stylusTouchSupported` and `Input.mousePresent`: those conflated capability with presence, and on platforms where legacy did real hardware detection the new properties report capability instead, so they can be `true` where legacy was `false`. To act on input, check `Device.current != null && Device.current.enabled` rather than `Device.current != null` alone; a non-null `current` means a device object is registered, which several platforms do unconditionally, and `enabled` is what tells you it is active. The properties require an Editor version that can answer the query and are not compiled in on older versions. `Pen.isSupported` and `Touchscreen.isPressureSupported` come from [ISX-2046](https://jira.unity3d.com/browse/ISX-2046); `Mouse.isSupported` is the short-term scope of [ISX-2079](https://jira.unity3d.com/browse/ISX-2079), whose remaining half is a real presence primitive rather than a capability one
13+
14+
```csharp
15+
// Before, legacy input. Reported true on any iPad new enough to pair a Pencil, whether or
16+
// not one was paired, so the two questions could not be told apart.
17+
if (Input.stylusTouchSupported) { }
18+
if (Input.mousePresent) { }
19+
20+
// After. Capability is its own question with its own answer.
21+
if (Pen.isSupported) { } // could a pen ever work on this platform
22+
if (Mouse.isSupported) { } // could a mouse ever work on this platform
23+
24+
// Acting on input is a different question, and needs both parts. A non-null current means a
25+
// device object is registered, not that hardware is attached; enabled is what says it is active.
26+
if (Pen.current != null && Pen.current.enabled) { }
27+
```
28+
1029
### Fixed
1130

1231
- Fixed the Inspector help button for a selected `.inputactions` asset ("Open Reference for Input Action Importer") opening a missing documentation page; it now links to the Action Assets manual page [UUM-149518](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-149518)

Packages/com.unity.inputsystem/Documentation~/corresponding-old-new-api.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,29 @@ Directly reading hardware controls bypasses the new Input System's action-based
6767
[`Input.imeIsSelected`](https://docs.unity3d.com/ScriptReference/Input-imeIsSelected.html)|Use: [`Keyboard.current.imeSelected`](xref:UnityEngine.InputSystem.Keyboard)
6868
[`Input.inputString`](https://docs.unity3d.com/ScriptReference/Input-inputString.html)|Subscribe to the [`Keyboard.onTextInput`](xref:UnityEngine.InputSystem.Keyboard) event:<br/>`Keyboard.current.onTextInput += character => /* ... */;`
6969

70+
## Device capability and device availability
71+
72+
Several old Input Manager properties, such as `Input.mousePresent` and `Input.stylusTouchSupported`, answered
73+
two questions at once, and answered them differently depending on the platform. On some platforms they were a
74+
hardcoded constant meaning roughly "this platform has this kind of device", and on others they performed real
75+
hardware detection.
76+
77+
The new Input System separates the two:
78+
79+
- **Can this platform do it at all?** Use the capability properties: [`Mouse.isSupported`](xref:UnityEngine.InputSystem.Mouse),
80+
[`Pen.isSupported`](xref:UnityEngine.InputSystem.Pen) and [`Touchscreen.isPressureSupported`](xref:UnityEngine.InputSystem.Touchscreen).
81+
These never change while the application runs, so read them once and decide whether to offer device-specific
82+
functionality.
83+
- **Is a device available to read from right now?** Use `Device.current != null && Device.current.enabled`.
84+
Both parts matter. A non-null `current` only means a device object is registered, which some platforms do
85+
unconditionally regardless of whether hardware is attached, and `enabled` is what tells you the device is
86+
active. The Device Simulator is a good example of the difference: while simulating a touch device it disables
87+
the native mouse and pen without removing them, so `current` stays non-null while `enabled` becomes false.
88+
89+
Because the old properties mixed these two meanings, the capability properties are **not drop-in replacements**.
90+
On platforms where the old property performed real detection, the new property answers the capability question
91+
instead, so it can be `true` where the old one was `false`. The tables below note this per API.
92+
7093
## Mouse
7194

7295
`MonoBehaviour.OnMouse` events, such as [MonoBehaviour.OnMouseDown](https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnMouseDown.html), are supported in Unity 6.4 and later.
@@ -77,19 +100,19 @@ Directly reading hardware controls bypasses the new Input System's action-based
77100
[`Input.GetMouseButtonDown`](https://docs.unity3d.com/ScriptReference/Input.GetMouseButtonDown.html)<br/>Example: `Input.GetMouseButtonDown(0)`|Use [`wasPressedThisFrame`](xref:UnityEngine.InputSystem.Controls.ButtonControl) on the corresponding mouse button.<br/>Example: `InputSystem.Mouse.current.leftButton.wasPressedThisFrame`
78101
[`Input.GetMouseButtonUp`](https://docs.unity3d.com/ScriptReference/Input.GetMouseButtonUp.html)<br/>Example: `Input.GetMouseButtonUp(0)`|Use [`wasReleasedThisFrame`](xref:UnityEngine.InputSystem.Controls.ButtonControl) on the corresponding mouse button.<br/>Example: `InputSystem.Mouse.current.leftButton.wasReleasedThisFrame`
79102
[`Input.mousePosition`](https://docs.unity3d.com/ScriptReference/Input-mousePosition.html)|Use [`Mouse.current.position.ReadValue()`](xref:UnityEngine.InputSystem.Mouse)<br/>Example: `Vector2 position = Mouse.current.position.ReadValue();`<br/> **Note:** Mouse simulation from touch isn't implemented yet.
80-
[`Input.mousePresent`](https://docs.unity3d.com/ScriptReference/Input-mousePresent.html)|No corresponding API yet.
103+
[`Input.mousePresent`](https://docs.unity3d.com/ScriptReference/Input-mousePresent.html)|Use [`isSupported`](xref:UnityEngine.InputSystem.Mouse) to check whether the platform supports mouse input at all.<br/>Example: `if (Mouse.isSupported) ShowMouseSettings();`<br/>**Note:** Not a drop-in replacement; see [Device capability and device availability](#device-capability-and-device-availability) above. Input System does not currently deliver mouse input on iOS, iPadOS or visionOS, so `Mouse.isSupported` is `false` there even though the platform itself supports indirect mice. Requires a recent Editor version.
81104

82105
## Touch and Pen
83106

84107
|Input Manager (Old)|Input System (New)|
85108
|--|--|
86109
[`Input.GetTouch`](https://docs.unity3d.com/ScriptReference/Input.GetTouch.html)<br/>For example:<br/>`Touch touch = Input.GetTouch(0);`<br/>`Vector2 touchPos = touch.position;`|Use [`EnhancedTouch.Touch.activeTouches[i]`](xref:UnityEngine.InputSystem.EnhancedTouch.Touch)<br/>Example: `Vector2 touchPos = EnhancedTouch.Touch.activeTouches[0].position;`<br/> **Note:** Enable enhanced touch support first by calling [`EnhancedTouch.Enable()`](xref:UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport).
87-
[`Input.multiTouchEnabled`](https://docs.unity3d.com/ScriptReference/Input-multiTouchEnabled.html)|No corresponding API yet.
110+
[`Input.multiTouchEnabled`](https://docs.unity3d.com/ScriptReference/Input-multiTouchEnabled.html)|There is no direct equivalent, because this is a setting rather than a hardware capability. To get the same first-touch-wins behaviour, read [`primaryTouch`](xref:UnityEngine.InputSystem.Touchscreen) instead of iterating all touches, or bind to `<Touchscreen>/primaryTouch`.<br/>Example: `if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.isPressed)`<br/>**Note:** Two differences from setting `Input.multiTouchEnabled = false`. First, `primaryTouch` filters only itself: [`touches`](xref:UnityEngine.InputSystem.Touchscreen), the `<Touchscreen>/touch*` bindings and [`EnhancedTouch`](xref:UnityEngine.InputSystem.EnhancedTouch.Touch) still report every finger, whereas the legacy setting suppressed additional touches globally. Second, when the finger that started the primary touch lifts while other fingers are still down, the primary touch is retained rather than ended until the last finger is released, so a control bound to it stays actuated in the meantime.
88111
[`Input.simulateMouseWithTouches`](https://docs.unity3d.com/ScriptReference/Input-multiTouchEnabled.html)|No corresponding API yet.
89-
[`Input.stylusTouchSupported`](https://docs.unity3d.com/ScriptReference/Input-stylusTouchSupported.html)|No corresponding API yet.
112+
[`Input.stylusTouchSupported`](https://docs.unity3d.com/ScriptReference/Input-stylusTouchSupported.html)|Use [`isSupported`](xref:UnityEngine.InputSystem.Pen) to check whether the platform supports pen input at all.<br/>Example: `if (Pen.isSupported) ShowPenSettings();`<br/>**Note:** Not a drop-in replacement; see [Device capability and device availability](#device-capability-and-device-availability) above. Requires a recent Editor version.
90113
[`Input.touchCount`](https://docs.unity3d.com/ScriptReference/Input-touchCount.html)|[`EnhancedTouch.Touch.activeTouches.Count`](xref:UnityEngine.InputSystem.EnhancedTouch.Touch)<br/> **Note:** Enable enhanced touch support first by calling [`EnhancedTouchSupport.Enable()`](xref:UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport)
91114
[`Input.touches`](https://docs.unity3d.com/scriptreference/input-touches.html)|[`EnhancedTouch.Touch.activeTouches`](xref:UnityEngine.InputSystem.EnhancedTouch.Touch)<br/> **Note:** Enable enhanced touch support first by calling [`EnhancedTouch.Enable()`](xref:UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport)
92-
[`Input.touchPressureSupported`](https://docs.unity3d.com/ScriptReference/Input-touchPressureSupported.html)|No corresponding API yet.
115+
[`Input.touchPressureSupported`](https://docs.unity3d.com/ScriptReference/Input-touchPressureSupported.html)|Use [`isPressureSupported`](xref:UnityEngine.InputSystem.Touchscreen) to check whether the platform delivers a real pressure value with touch input.<br/>Example: `if (Touchscreen.isPressureSupported) UsePressureForBrushWidth();`<br/>**Note:** When this is `false`, [`pressure`](xref:UnityEngine.InputSystem.Controls.TouchControl) reports a constant `1` while a finger is down rather than a measured value. This is a platform-wide answer rather than a per-device one. Requires a recent Editor version.
93116
[`Input.touchSupported`](https://docs.unity3d.com/ScriptReference/Input-touchSupported.html)|[`Touchscreen.current != null`](xref:UnityEngine.InputSystem.Touchscreen)
94117
[`Input.backButtonLeavesApp`](https://docs.unity3d.com/ScriptReference/Input-backButtonLeavesApp.html)|No corresponding API yet.
95118
[`GetPenEvent`](https://docs.unity3d.com/ScriptReference/Input.GetPenEvent.html)<br/>[`GetLastPenContactEvent`](https://docs.unity3d.com/ScriptReference/Input.GetLastPenContactEvent.html)<br/>[`ResetPenEvents`](https://docs.unity3d.com/ScriptReference/Input.ResetPenEvents.html)<br/>[`ClearLastPenContactEvent`](https://docs.unity3d.com/ScriptReference/Input.ClearLastPenContactEvent.html)|Use: [`Pen.current`](xref:UnityEngine.InputSystem.Pen)<br/>See the [Pen, tablet and stylus support](devices-pen.md) docs for more information.

0 commit comments

Comments
 (0)