Skip to content

Commit a022e21

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 a022e21

16 files changed

Lines changed: 536 additions & 3 deletions

Assets/Tests/InputSystem/CoreTests_Devices.cs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5895,4 +5895,167 @@ 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.False));
6035+
Assert.That((byte)InputCapabilitySupport.Supported, Is.EqualTo((byte)CapabilityState.True));
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+
#endif // UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES
58986061
}

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ 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`, which report what the current platform is capable of rather than what is connected right now. Use them to decide whether to offer device-specific functionality, and keep using `Device.current != null` to ask whether a device is available to read from. This replaces the legacy `UnityEngine.Input.stylusTouchSupported` and `Input.mousePresent`, which conflated the two questions. The properties require an Editor version that can answer the query and are not compiled in on older versions [ISX-2046](https://jira.unity3d.com/browse/ISX-2046)
13+
1014
### Fixed
1115

1216
- 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)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#if UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES
2+
namespace UnityEngine.InputSystem.LowLevel
3+
{
4+
/// <summary>
5+
/// Answer to a platform capability query, meaning what the platform can deliver rather than
6+
/// what is currently connected.
7+
/// </summary>
8+
/// <remarks>
9+
/// Mirrors <c>CapabilityState</c> in the engine's <c>Modules/Input/InputDeviceIOCTL.h</c>, whose
10+
/// wire values are pinned by tests on both sides. <see cref="Unknown"/> is zero so that an
11+
/// unwritten payload, or a platform that has not implemented a query, reads as "we do not know"
12+
/// rather than as a confident <see cref="NotSupported"/>.
13+
///
14+
/// The value space is open. Treat anything other than <see cref="Supported"/> as not supported
15+
/// rather than rejecting it, because a newer engine may answer with a value this version of the
16+
/// package does not know about.
17+
/// </remarks>
18+
internal enum InputCapabilitySupport : byte
19+
{
20+
/// <summary>
21+
/// The platform has no answer, typically because it has not implemented the query yet.
22+
/// </summary>
23+
Unknown = 0,
24+
25+
/// <summary>
26+
/// The platform definitively cannot deliver it.
27+
/// </summary>
28+
NotSupported = 1,
29+
30+
/// <summary>
31+
/// The platform definitively can deliver it.
32+
/// </summary>
33+
Supported = 2
34+
}
35+
}
36+
#endif // UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES

Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Commands/InputCapabilitySupport.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#if UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES
2+
using System.Runtime.InteropServices;
3+
using UnityEngine.InputSystem.Utilities;
4+
5+
namespace UnityEngine.InputSystem.LowLevel
6+
{
7+
/// <summary>
8+
/// Queries whether the platform can deliver mouse input at all, as opposed to whether a mouse is
9+
/// connected right now.
10+
/// </summary>
11+
/// <remarks>
12+
/// Addressed to the engine's system endpoint rather than to a device, so it is sent through
13+
/// <see cref="InputManager.ExecuteSystemCommand{TCommand}"/> rather than
14+
/// <see cref="InputDevice.ExecuteCommand{TCommand}"/>. Presence is answered by the device list.
15+
///
16+
/// The FourCC must match <c>kInputFourCCIOCTLQueryMouseSupported</c> in the engine's
17+
/// <c>Modules/Input/InputFourCC.h</c>.
18+
/// </remarks>
19+
/// <seealso cref="Mouse.isSupported"/>
20+
[StructLayout(LayoutKind.Explicit, Size = kSize)]
21+
internal struct QueryMouseSupportedCommand : IInputDeviceCommandInfo
22+
{
23+
public static FourCC Type => new FourCC('Q', 'M', 'O', 'U');
24+
25+
internal const int kSize = InputDeviceCommand.kBaseCommandSize + sizeof(byte);
26+
27+
[FieldOffset(0)]
28+
public InputDeviceCommand baseCommand;
29+
30+
[FieldOffset(InputDeviceCommand.kBaseCommandSize)]
31+
public InputCapabilitySupport isSupported;
32+
33+
public FourCC typeStatic => Type;
34+
35+
public static QueryMouseSupportedCommand Create()
36+
{
37+
return new QueryMouseSupportedCommand
38+
{
39+
baseCommand = new InputDeviceCommand(Type, kSize),
40+
isSupported = InputCapabilitySupport.Unknown
41+
};
42+
}
43+
}
44+
}
45+
#endif // UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES

Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Commands/QueryMouseSupportedCommand.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#if UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES
2+
using System.Runtime.InteropServices;
3+
using UnityEngine.InputSystem.Utilities;
4+
5+
namespace UnityEngine.InputSystem.LowLevel
6+
{
7+
/// <summary>
8+
/// Queries whether the platform can deliver pen input at all, as opposed to whether a pen is
9+
/// connected right now.
10+
/// </summary>
11+
/// <remarks>
12+
/// Addressed to the engine's system endpoint rather than to a device, so it is sent through
13+
/// <see cref="InputManager.ExecuteSystemCommand{TCommand}"/> rather than
14+
/// <see cref="InputDevice.ExecuteCommand{TCommand}"/>. Presence is answered by the device list.
15+
///
16+
/// The FourCC must match <c>kInputFourCCIOCTLQueryPenSupported</c> in the engine's
17+
/// <c>Modules/Input/InputFourCC.h</c>.
18+
/// </remarks>
19+
/// <seealso cref="Pen.isSupported"/>
20+
[StructLayout(LayoutKind.Explicit, Size = kSize)]
21+
internal struct QueryPenSupportedCommand : IInputDeviceCommandInfo
22+
{
23+
public static FourCC Type => new FourCC('Q', 'P', 'E', 'N');
24+
25+
internal const int kSize = InputDeviceCommand.kBaseCommandSize + sizeof(byte);
26+
27+
[FieldOffset(0)]
28+
public InputDeviceCommand baseCommand;
29+
30+
[FieldOffset(InputDeviceCommand.kBaseCommandSize)]
31+
public InputCapabilitySupport isSupported;
32+
33+
public FourCC typeStatic => Type;
34+
35+
public static QueryPenSupportedCommand Create()
36+
{
37+
return new QueryPenSupportedCommand
38+
{
39+
baseCommand = new InputDeviceCommand(Type, kSize),
40+
isSupported = InputCapabilitySupport.Unknown
41+
};
42+
}
43+
}
44+
}
45+
#endif // UNITY_INPUTSYSTEM_SUPPORTS_CAPABILITY_QUERIES

Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Commands/QueryPenSupportedCommand.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)