UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// In retrospect, allowing Touchscreen to do what it does the way it does it was a mistake. It came out of thinking that
|
||||
// we need Touchscreen to have a large pool of TouchStates from which to dynamically allocate -- as this was what the old
|
||||
// input system does. This made it unfeasible/unwise to put the burden of touch allocation on platform backends and thus
|
||||
// led to the current setup where backends are sending TouchState events which Touchscreen dynamically incorporates.
|
||||
//
|
||||
// This shouldn't have happened.
|
||||
//
|
||||
// Ultimately, this led to IInputStateCallbackReceiver in its current form. While quite flexible in what it allows you to
|
||||
// do, it introduces a lot of additional complication and deviation from an otherwise very simple model based on trivially
|
||||
// understood chunks of input state.
|
||||
|
||||
namespace UnityEngine.InputSystem.LowLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for devices that implement their own state update handling.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The input system has built-in logic to automatically handle the state buffers that store input values for devices. This
|
||||
/// means that if an input event containing input state is processed, its data will be copied automatically into the state
|
||||
/// memory for the device.
|
||||
///
|
||||
/// However, some devices need to apply custom logic whenever new input is received. An example of this is <see cref="Pointer.delta"/>
|
||||
/// which needs to accumulate deltas as they are received within a frame and then reset the delta at the beginning of a new frame.
|
||||
///
|
||||
/// Also, devices like <see cref="Touchscreen"/> extensively customize event handling in order to implement features such as
|
||||
/// tap detection and primary touch handling. This is what allows the device to receive state events in <see cref="TouchState"/>
|
||||
/// format even though that is not the format of the device itself (which is mainly a composite of several TouchStates).
|
||||
///
|
||||
/// This interface allows to bypass the built-in logic and instead intercept and manually handle state updates.
|
||||
/// </remarks>
|
||||
/// <seealso cref="InputDevice"/>
|
||||
/// <seealso cref="Pointer"/>
|
||||
/// <seealso cref="Touchscreen"/>
|
||||
public interface IInputStateCallbackReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// A new input update begins. This means that the current state of the device is being carried over into the next
|
||||
/// frame.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is called without the front and back buffer for the device having been flipped. You can use <see cref="InputState.Change"/>
|
||||
/// to write values into the device's state (e.g. to reset a given control to its default state) which will implicitly perform
|
||||
/// the buffer flip.
|
||||
/// </remarks>
|
||||
void OnNextUpdate();
|
||||
|
||||
/// <summary>
|
||||
/// A new state event has been received and is being processed.
|
||||
/// </summary>
|
||||
/// <param name="eventPtr">The state event. This will be either a <see cref="StateEvent"/> or a <see cref="DeltaStateEvent"/>.</param>
|
||||
/// <remarks>
|
||||
/// Use <see cref="InputState.Change"/> to write state updates into the device state buffers. While nothing will prevent a device
|
||||
/// from writing directly into the memory buffers retrieved with <see cref="InputControl.currentStatePtr"/>, doing so will bypass
|
||||
/// the buffer flipping logic as well as change detection from change monitors (<see cref="IInputStateChangeMonitor"/>; this will
|
||||
/// cause <see cref="InputAction"/> to not work with the device) and thus lead to incorrect behavior.
|
||||
/// </remarks>
|
||||
/// <seealso cref="StateEvent"/>
|
||||
/// <seealso cref="DeltaStateEvent"/>
|
||||
void OnStateEvent(InputEventPtr eventPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Compute an offset that correlates <paramref name="control"/> with the state in <paramref name="eventPtr"/>.
|
||||
/// </summary>
|
||||
/// <param name="control">Control the state of which we want to access within <paramref name="eventPtr"/>.</param>
|
||||
/// <param name="eventPtr">An input event. Must be a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/></param>
|
||||
/// <param name="offset"></param>
|
||||
/// <returns>False if the correlation failed or true if <paramref name="offset"/> has been set and should be used
|
||||
/// as the offset for the state of <paramref name="control"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will only be called if the given state event has a state format different than that of the device. In that case,
|
||||
/// the memory of the input state captured in the given state event cannot be trivially correlated with the control.
|
||||
///
|
||||
/// The input system calls the method to know which offset (if any) in the device's state block to consider the state
|
||||
/// in <paramref name="eventPtr"/> relative to when accessing the state for <paramref name="control"/> as found in
|
||||
/// the event.
|
||||
///
|
||||
/// An example of when this is called is for touch events. These are normally sent in <see cref="TouchState"/> format
|
||||
/// which, however, is not the state format of <see cref="Touchscreen"/> (which uses a composite of several TouchStates).
|
||||
/// When trying to access the state in <paramref name="eventPtr"/> to, for example, read out the touch position,
|
||||
/// </remarks>
|
||||
/// <seealso cref="InputControlExtensions.GetStatePtrFromStateEvent"/>
|
||||
bool GetStateOffsetForEvent(InputControl control, InputEventPtr eventPtr, ref uint offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35b7608ac9ab8464280c16fa4adc02f1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
////REVIEW: could have a monitor path where if there's multiple state monitors on the same control with
|
||||
//// the same listener, the monitor is notified only once but made aware of the multiple triggers
|
||||
|
||||
namespace UnityEngine.InputSystem.LowLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface used to monitor input state changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use <see cref="InputState.AddChangeMonitor(InputControl,IInputStateChangeMonitor,long,uint)"/> to install a state change monitor receiving state change
|
||||
/// callbacks for a specific control.
|
||||
/// </remarks>
|
||||
/// <seealso cref="InputState.AddChangeMonitor(InputControl,IInputStateChangeMonitor,long,uint)"/>
|
||||
public interface IInputStateChangeMonitor
|
||||
{
|
||||
////REVIEW: For v2, consider changing the signature of this to put the "was consumed" signal *outside* the eventPtr
|
||||
/// <summary>
|
||||
/// Called when the state monitored by a state change monitor has been modified.
|
||||
/// </summary>
|
||||
/// <param name="control">Control that is being monitored by the state change monitor and that had its state
|
||||
/// memory changed.</param>
|
||||
/// <param name="time">Time on the <see cref="InputEvent.time"/> timeline at which the control state change was received.</param>
|
||||
/// <param name="eventPtr">If the state change was initiated by a state event (either a <see cref="StateEvent"/>
|
||||
/// or <see cref="DeltaStateEvent"/>), this is the pointer to that event. Otherwise it is pointer that is still
|
||||
/// <see cref="InputEventPtr.valid"/>, but refers a "dummy" event that is not a <see cref="StateEvent"/> or <see cref="DeltaStateEvent"/>.</param>
|
||||
/// <param name="monitorIndex">Index of the monitor as passed to <see cref="InputState.AddChangeMonitor(InputControl,IInputStateChangeMonitor,long,uint)"/>.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// To signal that the state change has been processed by the monitor and that no other pending notifications on the
|
||||
/// same monitor instance should be sent, set the <see cref="InputEventPtr.handled"/> flag to <c>true</c> on <paramref name="eventPtr"/>.
|
||||
/// Note, however, that aside from only silencing change monitors on the same <see cref="IInputStateChangeMonitor"/> instance,
|
||||
/// it also only silences change monitors with the same <c>groupIndex</c> value as supplied to
|
||||
/// <see cref="InputState.AddChangeMonitor(InputControl,IInputStateChangeMonitor,long,uint)"/>.
|
||||
/// </remarks>
|
||||
void NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a timeout set on a state change monitor has expired.
|
||||
/// </summary>
|
||||
/// <param name="control">Control on which the timeout expired.</param>
|
||||
/// <param name="time">Input time at which the timer expired. This is the time at which an <see cref="InputSystem.Update"/> is being
|
||||
/// run whose <see cref="InputState.currentTime"/> is past the time of expiration.</param>
|
||||
/// <param name="monitorIndex">Index of the monitor as given to <see cref="InputState.AddChangeMonitor(InputControl,IInputStateChangeMonitor,long,uint)"/>.</param>
|
||||
/// <param name="timerIndex">Index of the timer as given to <see cref="InputState.AddChangeMonitorTimeout"/>.</param>
|
||||
/// <seealso cref="InputState.AddChangeMonitorTimeout"/>
|
||||
void NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab2c7c4773181604f9b207518e96ca1b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,12 @@
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
namespace UnityEngine.InputSystem.LowLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface implemented by all input device state structs which reports the data format identifier of the state.
|
||||
/// </summary>
|
||||
public interface IInputStateTypeInfo
|
||||
{
|
||||
FourCC format { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdf4b325f88d407f9013b427a6b33c58
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
283
Packages/com.unity.inputsystem/InputSystem/State/InputState.cs
Normal file
283
Packages/com.unity.inputsystem/InputSystem/State/InputState.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
using System;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
////TODO: method to get raw state pointer for device/control
|
||||
|
||||
////REVIEW: allow to restrict state change monitors to specific updates?
|
||||
|
||||
namespace UnityEngine.InputSystem.LowLevel
|
||||
{
|
||||
using NotifyControlValueChangeAction = Action<InputControl, double, InputEventPtr, long>;
|
||||
using NotifyTimerExpiredAction = Action<InputControl, double, long, int>;
|
||||
|
||||
/// <summary>
|
||||
/// Low-level APIs for working with input state memory.
|
||||
/// </summary>
|
||||
public static class InputState
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of update that was last run or is currently being run on the input state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This determines which set of buffers are currently active and thus determines which view code
|
||||
/// that queries input state will receive. For example, during editor updates, this will be
|
||||
/// <see cref="InputUpdateType.Editor"/> and the state buffers for the editor will be active.
|
||||
/// </remarks>
|
||||
public static InputUpdateType currentUpdateType => InputUpdate.s_LatestUpdateType;
|
||||
|
||||
////FIXME: ATM this does not work for editor updates
|
||||
/// <summary>
|
||||
/// The number of times the current input state has been updated.
|
||||
/// </summary>
|
||||
public static uint updateCount => InputUpdate.s_UpdateStepCount;
|
||||
|
||||
public static double currentTime => InputRuntime.s_Instance.currentTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the state of an input device changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The first parameter is the device whose state was changed the second parameter is the event
|
||||
/// that triggered the change in state. Note that the latter may be <c>null</c> in case the
|
||||
/// change was performed directly through <see cref="Change"/> rather than through an event.
|
||||
/// </remarks>
|
||||
public static event Action<InputDevice, InputEventPtr> onChange
|
||||
{
|
||||
add => InputSystem.s_Manager.onDeviceStateChange += value;
|
||||
remove => InputSystem.s_Manager.onDeviceStateChange -= value;
|
||||
}
|
||||
|
||||
public static unsafe void Change(InputDevice device, InputEventPtr eventPtr, InputUpdateType updateType = default)
|
||||
{
|
||||
if (device == null)
|
||||
throw new ArgumentNullException(nameof(device));
|
||||
if (!eventPtr.valid)
|
||||
throw new ArgumentNullException(nameof(eventPtr));
|
||||
|
||||
// Make sure event is a StateEvent or DeltaStateEvent and has a format matching the device.
|
||||
FourCC stateFormat;
|
||||
var eventType = eventPtr.type;
|
||||
if (eventType == StateEvent.Type)
|
||||
stateFormat = StateEvent.FromUnchecked(eventPtr)->stateFormat;
|
||||
else if (eventType == DeltaStateEvent.Type)
|
||||
stateFormat = DeltaStateEvent.FromUnchecked(eventPtr)->stateFormat;
|
||||
else
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
InputSystem.s_Manager.m_Diagnostics?.OnEventFormatMismatch(eventPtr, device);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (stateFormat != device.stateBlock.format)
|
||||
throw new ArgumentException(
|
||||
$"State format {stateFormat} from event does not match state format {device.stateBlock.format} of device {device}",
|
||||
nameof(eventPtr));
|
||||
|
||||
InputSystem.s_Manager.UpdateState(device, eventPtr,
|
||||
updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform one update of input state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Incorporates the given state and triggers all state change monitors as needed.
|
||||
///
|
||||
/// Note that input state changes performed with this method will not be visible on remotes as they will bypass
|
||||
/// event processing. It is effectively equivalent to directly writing into input state memory except that it
|
||||
/// also performs related tasks such as checking state change monitors, flipping buffers, or making the respective
|
||||
/// device current.
|
||||
/// </remarks>
|
||||
public static void Change<TState>(InputControl control, TState state, InputUpdateType updateType = default,
|
||||
InputEventPtr eventPtr = default)
|
||||
where TState : struct
|
||||
{
|
||||
Change(control, ref state, updateType, eventPtr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform one update of input state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Incorporates the given state and triggers all state change monitors as needed.
|
||||
///
|
||||
/// Note that input state changes performed with this method will not be visible on remotes as they will bypass
|
||||
/// event processing. It is effectively equivalent to directly writing into input state memory except that it
|
||||
/// also performs related tasks such as checking state change monitors, flipping buffers, or making the respective
|
||||
/// device current.
|
||||
/// </remarks>
|
||||
public static unsafe void Change<TState>(InputControl control, ref TState state, InputUpdateType updateType = default,
|
||||
InputEventPtr eventPtr = default)
|
||||
where TState : struct
|
||||
{
|
||||
if (control == null)
|
||||
throw new ArgumentNullException(nameof(control));
|
||||
if (control.stateBlock.bitOffset != 0 || control.stateBlock.sizeInBits % 8 != 0)
|
||||
throw new ArgumentException($"Cannot change state of bitfield control '{control}' using this method", nameof(control));
|
||||
|
||||
var device = control.device;
|
||||
var stateSize = Math.Min(UnsafeUtility.SizeOf<TState>(), control.m_StateBlock.alignedSizeInBytes);
|
||||
var statePtr = UnsafeUtility.AddressOf(ref state);
|
||||
var stateOffset = control.stateBlock.byteOffset - device.stateBlock.byteOffset;
|
||||
|
||||
InputSystem.s_Manager.UpdateState(device,
|
||||
updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType, statePtr, stateOffset,
|
||||
(uint)stateSize,
|
||||
eventPtr.valid
|
||||
? eventPtr.internalTime
|
||||
: InputRuntime.s_Instance.currentTime,
|
||||
eventPtr: eventPtr);
|
||||
}
|
||||
|
||||
public static bool IsIntegerFormat(this FourCC format)
|
||||
{
|
||||
return format == InputStateBlock.FormatBit ||
|
||||
format == InputStateBlock.FormatInt ||
|
||||
format == InputStateBlock.FormatByte ||
|
||||
format == InputStateBlock.FormatShort ||
|
||||
format == InputStateBlock.FormatSBit ||
|
||||
format == InputStateBlock.FormatUInt ||
|
||||
format == InputStateBlock.FormatUShort ||
|
||||
format == InputStateBlock.FormatLong ||
|
||||
format == InputStateBlock.FormatULong;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a monitor that gets triggered every time the state of <paramref name="control"/> changes.
|
||||
/// </summary>
|
||||
/// <param name="control">A control sitting on an <see cref="InputDevice"/> that has been <see cref="InputDevice.added"/>.</param>
|
||||
/// <param name="monitor">Instance of the monitor that should be notified when state changes occur.</param>
|
||||
/// <param name="monitorIndex">Numeric index of the monitors. Monitors on a device are ordered by <em>decreasing</em> monitor index
|
||||
/// and invoked in that order.</param>
|
||||
/// <param name="groupIndex">Numeric group of the monitor. See remarks.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c> -or- <paramref name="monitor"/> is <c>null</c>.</exception>
|
||||
/// <exception cref="ArgumentException">The <see cref="InputDevice"/> of <paramref name="control"/> has not been <see cref="InputDevice.added"/>.</exception>
|
||||
/// <remarks>
|
||||
/// All monitors on an <see cref="InputDevice"/> are sorted by the complexity specified in their <paramref name="monitorIndex"/> (in decreasing order) and invoked
|
||||
/// in that order.
|
||||
///
|
||||
/// Every handler gets an opportunity to set <see cref="InputEventPtr.handled"/> to <c>true</c>. When doing so, all remaining pending monitors
|
||||
/// from the same <paramref name="monitor"/> instance that have the same <paramref name="groupIndex"/> will be silenced and skipped over.
|
||||
/// This can be used to establish an order of event "consumption" where one change monitor may prevent another change monitor from triggering.
|
||||
///
|
||||
/// Monitors are invoked <em>after</em> a state change has been written to the device. If, for example, a <see cref="StateEvent"/> is
|
||||
/// received that sets <see cref="Gamepad.leftTrigger"/> to <c>0.5</c>, the value is first applied to the control and then any state
|
||||
/// monitors that may be listening to the change are invoked (thus getting <c>0.5</c> if calling <see cref="InputControl{TValue}.ReadValue()"/>).
|
||||
///
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// class InputMonitor : IInputStateChangeMonitor
|
||||
/// {
|
||||
/// public InputMonitor()
|
||||
/// {
|
||||
/// // Watch the left and right mouse button.
|
||||
/// // By supplying monitor indices here, we not only receive the indices in NotifyControlStateChanged,
|
||||
/// // we also create an ordering between the two monitors. The one on RMB will fire *before* the one
|
||||
/// // on LMB in case there is a single event that changes both buttons.
|
||||
/// InputState.AddChangeMonitor(Mouse.current.leftButton, this, monitorIndex: 1);
|
||||
/// InputState.AddChangeMonitor(Mouse.current.rightButton, this, monitorIndex: 2);
|
||||
/// }
|
||||
///
|
||||
/// public void NotifyControlStateChanged(InputControl control, double currentTime, InputEventPtr eventPtr, long monitorIndex)
|
||||
/// {
|
||||
/// Debug.Log($"{control} changed");
|
||||
///
|
||||
/// // We can add a monitor timeout that will trigger in case the state of the
|
||||
/// // given control is not changed within the given time. Let's watch the control
|
||||
/// // for 2 seconds. If nothing happens, we will get a call to NotifyTimerExpired.
|
||||
/// InputState.AddChangeMonitorTimeout(control, this, currentTime + 2);
|
||||
/// }
|
||||
///
|
||||
/// public void NotifyTimerExpired(InputControl control, double currentTime, long monitorIndex, int timerIndex)
|
||||
/// {
|
||||
/// Debug.Log($"{control} was not changed within 2 seconds");
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </remarks>
|
||||
public static void AddChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex = -1, uint groupIndex = default)
|
||||
{
|
||||
if (control == null)
|
||||
throw new ArgumentNullException(nameof(control));
|
||||
if (monitor == null)
|
||||
throw new ArgumentNullException(nameof(monitor));
|
||||
if (!control.device.added)
|
||||
throw new ArgumentException($"Device for control '{control}' has not been added to system");
|
||||
|
||||
InputSystem.s_Manager.AddStateChangeMonitor(control, monitor, monitorIndex, groupIndex);
|
||||
}
|
||||
|
||||
public static IInputStateChangeMonitor AddChangeMonitor(InputControl control,
|
||||
NotifyControlValueChangeAction valueChangeCallback, int monitorIndex = -1,
|
||||
NotifyTimerExpiredAction timerExpiredCallback = null)
|
||||
{
|
||||
if (valueChangeCallback == null)
|
||||
throw new ArgumentNullException(nameof(valueChangeCallback));
|
||||
var monitor = new StateChangeMonitorDelegate
|
||||
{
|
||||
valueChangeCallback = valueChangeCallback,
|
||||
timerExpiredCallback = timerExpiredCallback
|
||||
};
|
||||
AddChangeMonitor(control, monitor, monitorIndex);
|
||||
return monitor;
|
||||
}
|
||||
|
||||
public static void RemoveChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex = -1)
|
||||
{
|
||||
if (control == null)
|
||||
throw new ArgumentNullException(nameof(control));
|
||||
if (monitor == null)
|
||||
throw new ArgumentNullException(nameof(monitor));
|
||||
|
||||
InputSystem.s_Manager.RemoveStateChangeMonitor(control, monitor, monitorIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Put a timeout on a previously registered state change monitor.
|
||||
/// </summary>
|
||||
/// <param name="control"></param>
|
||||
/// <param name="monitor"></param>
|
||||
/// <param name="time"></param>
|
||||
/// <param name="monitorIndex"></param>
|
||||
/// <param name="timerIndex"></param>
|
||||
/// <remarks>
|
||||
/// If by the given <paramref name="time"/>, no state change has been registered on the control monitored
|
||||
/// by the given <paramref name="monitor">state change monitor</paramref>, <see cref="IInputStateChangeMonitor.NotifyTimerExpired"/>
|
||||
/// will be called on <paramref name="monitor"/>.
|
||||
/// </remarks>
|
||||
public static void AddChangeMonitorTimeout(InputControl control, IInputStateChangeMonitor monitor, double time, long monitorIndex = -1, int timerIndex = -1)
|
||||
{
|
||||
if (monitor == null)
|
||||
throw new ArgumentNullException(nameof(monitor));
|
||||
|
||||
InputSystem.s_Manager.AddStateChangeMonitorTimeout(control, monitor, time, monitorIndex, timerIndex);
|
||||
}
|
||||
|
||||
public static void RemoveChangeMonitorTimeout(IInputStateChangeMonitor monitor, long monitorIndex = -1, int timerIndex = -1)
|
||||
{
|
||||
if (monitor == null)
|
||||
throw new ArgumentNullException(nameof(monitor));
|
||||
|
||||
InputSystem.s_Manager.RemoveStateChangeMonitorTimeout(monitor, monitorIndex, timerIndex);
|
||||
}
|
||||
|
||||
private class StateChangeMonitorDelegate : IInputStateChangeMonitor
|
||||
{
|
||||
public NotifyControlValueChangeAction valueChangeCallback;
|
||||
public NotifyTimerExpiredAction timerExpiredCallback;
|
||||
|
||||
public void NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex)
|
||||
{
|
||||
valueChangeCallback(control, time, eventPtr, monitorIndex);
|
||||
}
|
||||
|
||||
public void NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex)
|
||||
{
|
||||
timerExpiredCallback?.Invoke(control, time, monitorIndex, timerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8d605b1e92964b5b94668b1511799ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,766 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
////TODO: the Debug.Asserts here should be also be made as checks ahead of time (on the layout)
|
||||
|
||||
////TODO: the read/write methods need a proper pass for consistency
|
||||
|
||||
////FIXME: some architectures have strict memory alignment requirements; we should honor them when
|
||||
//// we read/write primitive values or support stitching values together from bytes manually
|
||||
//// where needed
|
||||
|
||||
////TODO: allow bitOffset to be non-zero for byte-aligned control as long as result is byte-aligned
|
||||
|
||||
////REVIEW: The combination of byte and bit offset instead of just a single bit offset has turned out
|
||||
//// to be plenty awkward to use in practice; should be replace it?
|
||||
|
||||
////REVIEW: AutomaticOffset is a very awkward mechanism; it's primary use really is for "parking" unused
|
||||
//// controls for which a more elegant and robust mechanism can surely be devised
|
||||
|
||||
namespace UnityEngine.InputSystem.LowLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about a memory region storing input state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Input state is kept in raw memory blocks. All state is centrally managed by the input system;
|
||||
/// controls cannot keep their own independent state.
|
||||
///
|
||||
/// Each state block is tagged with a format code indicating the storage format used for the
|
||||
/// memory block. This can either be one out of a set of primitive formats (such as "INT") or a custom
|
||||
/// format code indicating a more complex format.
|
||||
///
|
||||
/// Memory using primitive formats can be converted to and from primitive values directly by this struct.
|
||||
///
|
||||
/// State memory is bit-addressable, meaning that it can be offset from a byte address in bits (<see cref="bitOffset"/>)
|
||||
/// and is sized in bits instead of bytes (<see cref="sizeInBits"/>). However, in practice, bit-addressing
|
||||
/// memory reads and writes are only supported on the <see cref="FormatBit">bitfield primitive format</see>.
|
||||
///
|
||||
/// Input state memory is restricted to a maximum of 4GB in size. Offsets are recorded in 32 bits.
|
||||
/// </remarks>
|
||||
/// <seealso cref="InputControl.stateBlock"/>
|
||||
public unsafe struct InputStateBlock
|
||||
{
|
||||
public const uint InvalidOffset = 0xffffffff;
|
||||
public const uint AutomaticOffset = 0xfffffffe;
|
||||
|
||||
/// <summary>
|
||||
/// Format code for invalid value type
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatInvalid = new FourCC(0);
|
||||
internal const int kFormatInvalid = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a variable-width bitfield representing an unsigned value,
|
||||
/// i.e. all bits including the highest one represent the magnitude of the value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatBit = new FourCC('B', 'I', 'T');
|
||||
internal const int kFormatBit = 'B' << 24 | 'I' << 16 | 'T' << 8 | ' ';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a variable-width bitfield representing a signed value, i.e. the
|
||||
/// highest bit is used as a sign bit (0=unsigned, 1=signed) and the remaining bits represent
|
||||
/// the magnitude of the value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatSBit = new FourCC('S', 'B', 'I', 'T');
|
||||
internal const int kFormatSBit = 'S' << 24 | 'B' << 16 | 'I' << 8 | 'T';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a 32-bit signed integer value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatInt = new FourCC('I', 'N', 'T');
|
||||
internal const int kFormatInt = 'I' << 24 | 'N' << 16 | 'T' << 8 | ' ';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a 32-bit unsigned integer value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatUInt = new FourCC('U', 'I', 'N', 'T');
|
||||
internal const int kFormatUInt = 'U' << 24 | 'I' << 16 | 'N' << 8 | 'T';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a 16-bit signed integer value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatShort = new FourCC('S', 'H', 'R', 'T');
|
||||
internal const int kFormatShort = 'S' << 24 | 'H' << 16 | 'R' << 8 | 'T';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a 16-bit unsigned integer value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatUShort = new FourCC('U', 'S', 'H', 'T');
|
||||
internal const int kFormatUShort = 'U' << 24 | 'S' << 16 | 'H' << 8 | 'T';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for an 8-bit unsigned integer value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatByte = new FourCC('B', 'Y', 'T', 'E');
|
||||
internal const int kFormatByte = 'B' << 24 | 'Y' << 16 | 'T' << 8 | 'E';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for an 8-bit signed integer value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatSByte = new FourCC('S', 'B', 'Y', 'T');
|
||||
internal const int kFormatSByte = 'S' << 24 | 'B' << 16 | 'Y' << 8 | 'T';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a 64-bit signed integer value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatLong = new FourCC('L', 'N', 'G');
|
||||
internal const int kFormatLong = 'L' << 24 | 'N' << 16 | 'G' << 8 | ' ';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a 64-bit unsigned integer value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatULong = new FourCC('U', 'L', 'N', 'G');
|
||||
internal const int kFormatULong = 'U' << 24 | 'L' << 16 | 'N' << 8 | 'G';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a 32-bit floating-point value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatFloat = new FourCC('F', 'L', 'T');
|
||||
internal const int kFormatFloat = 'F' << 24 | 'L' << 16 | 'T' << 8 | ' ';
|
||||
|
||||
/// <summary>
|
||||
/// Format code for a 64-bit floating-point value.
|
||||
/// </summary>
|
||||
/// <seealso cref="format"/>
|
||||
public static readonly FourCC FormatDouble = new FourCC('D', 'B', 'L');
|
||||
internal const int kFormatDouble = 'D' << 24 | 'B' << 16 | 'L' << 8 | ' ';
|
||||
|
||||
////REVIEW: are these really useful?
|
||||
public static readonly FourCC FormatVector2 = new FourCC('V', 'E', 'C', '2');
|
||||
internal const int kFormatVector2 = 'V' << 24 | 'E' << 16 | 'C' << 8 | '2';
|
||||
public static readonly FourCC FormatVector3 = new FourCC('V', 'E', 'C', '3');
|
||||
internal const int kFormatVector3 = 'V' << 24 | 'E' << 16 | 'C' << 8 | '3';
|
||||
public static readonly FourCC FormatQuaternion = new FourCC('Q', 'U', 'A', 'T');
|
||||
internal const int kFormatQuaternion = 'Q' << 24 | 'U' << 16 | 'A' << 8 | 'T';
|
||||
public static readonly FourCC FormatVector2Short = new FourCC('V', 'C', '2', 'S');
|
||||
public static readonly FourCC FormatVector3Short = new FourCC('V', 'C', '3', 'S');
|
||||
public static readonly FourCC FormatVector2Byte = new FourCC('V', 'C', '2', 'B');
|
||||
public static readonly FourCC FormatVector3Byte = new FourCC('V', 'C', '3', 'B');
|
||||
public static readonly FourCC FormatPose = new FourCC('P', 'o', 's', 'e');
|
||||
internal const int kFormatPose = 'P' << 24 | 'o' << 16 | 's' << 8 | 'e';
|
||||
|
||||
public static int GetSizeOfPrimitiveFormatInBits(FourCC type)
|
||||
{
|
||||
if (type == FormatBit || type == FormatSBit)
|
||||
return 1;
|
||||
if (type == FormatInt || type == FormatUInt)
|
||||
return 4 * 8;
|
||||
if (type == FormatShort || type == FormatUShort)
|
||||
return 2 * 8;
|
||||
if (type == FormatByte || type == FormatSByte)
|
||||
return 1 * 8;
|
||||
if (type == FormatLong || type == FormatULong)
|
||||
return 8 * 8;
|
||||
if (type == FormatFloat)
|
||||
return 4 * 8;
|
||||
if (type == FormatDouble)
|
||||
return 8 * 8;
|
||||
if (type == FormatVector2)
|
||||
return 2 * 4 * 8;
|
||||
if (type == FormatVector3)
|
||||
return 3 * 4 * 8;
|
||||
if (type == FormatQuaternion)
|
||||
return 4 * 4 * 8;
|
||||
if (type == FormatVector2Short)
|
||||
return 2 * 2 * 8;
|
||||
if (type == FormatVector3Short)
|
||||
return 3 * 2 * 8;
|
||||
if (type == FormatVector2Byte)
|
||||
return 2 * 1 * 8;
|
||||
if (type == FormatVector3Byte)
|
||||
return 3 * 1 * 8;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static FourCC GetPrimitiveFormatFromType(Type type)
|
||||
{
|
||||
if (ReferenceEquals(type, typeof(int)))
|
||||
return FormatInt;
|
||||
if (ReferenceEquals(type, typeof(uint)))
|
||||
return FormatUInt;
|
||||
if (ReferenceEquals(type, typeof(short)))
|
||||
return FormatShort;
|
||||
if (ReferenceEquals(type, typeof(ushort)))
|
||||
return FormatUShort;
|
||||
if (ReferenceEquals(type, typeof(byte)))
|
||||
return FormatByte;
|
||||
if (ReferenceEquals(type, typeof(sbyte)))
|
||||
return FormatSByte;
|
||||
if (ReferenceEquals(type, typeof(long)))
|
||||
return FormatLong;
|
||||
if (ReferenceEquals(type, typeof(ulong)))
|
||||
return FormatULong;
|
||||
if (ReferenceEquals(type, typeof(float)))
|
||||
return FormatFloat;
|
||||
if (ReferenceEquals(type, typeof(double)))
|
||||
return FormatDouble;
|
||||
if (ReferenceEquals(type, typeof(Vector2)))
|
||||
return FormatVector2;
|
||||
if (ReferenceEquals(type, typeof(Vector3)))
|
||||
return FormatVector3;
|
||||
if (ReferenceEquals(type, typeof(Quaternion)))
|
||||
return FormatQuaternion;
|
||||
return new FourCC();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type identifier for the memory layout used by the state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used for safety checks to make sure that when the system copies state memory, it
|
||||
/// copies between compatible layouts. If set to a primitive state format, also used to
|
||||
/// determine the size of the state block.
|
||||
/// </remarks>
|
||||
public FourCC format { get; set; }
|
||||
|
||||
////TODO: collapse byteOffset and bitOffset into a single 'offset' field
|
||||
// Offset into state buffer. After a device is added to the system, this is relative
|
||||
// to the global buffers; otherwise it is relative to the device root.
|
||||
// During setup, this can be InvalidOffset to indicate a control that should be placed
|
||||
// at an offset automatically; otherwise it denotes a fixed offset relative to the
|
||||
// parent control.
|
||||
public uint byteOffset
|
||||
{
|
||||
get => m_ByteOffset;
|
||||
set
|
||||
{
|
||||
m_ByteOffset = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Needed for fast access to avoid a call to getter in some places
|
||||
internal uint m_ByteOffset;
|
||||
|
||||
// Bit offset from the given byte offset. Also zero-based (i.e. first bit is at bit
|
||||
// offset #0).
|
||||
public uint bitOffset { get; set; }
|
||||
|
||||
// Size of the state in bits. If this % 8 is not 0, the control is considered a
|
||||
// bitfield control.
|
||||
// During setup, if this field is 0 it means the size of the control should be automatically
|
||||
// computed from either its children (if it has any) or its set format. If it has neither,
|
||||
// setup will throw.
|
||||
public uint sizeInBits { get; set; }
|
||||
|
||||
internal uint alignedSizeInBytes => (sizeInBits + 7) >> 3;
|
||||
internal uint effectiveByteOffset => byteOffset + (bitOffset >> 3);
|
||||
internal uint effectiveBitOffset => byteOffset * 8 + bitOffset;
|
||||
|
||||
public int ReadInt(void* statePtr)
|
||||
{
|
||||
Debug.Assert(sizeInBits != 0);
|
||||
|
||||
var valuePtr = (byte*)statePtr + (int)byteOffset;
|
||||
|
||||
var fmt = (int)format;
|
||||
switch (fmt)
|
||||
{
|
||||
case kFormatBit:
|
||||
if (sizeInBits == 1)
|
||||
return MemoryHelpers.ReadSingleBit(valuePtr, bitOffset) ? 1 : 0;
|
||||
return (int)MemoryHelpers.ReadMultipleBitsAsUInt(valuePtr, bitOffset, sizeInBits);
|
||||
case kFormatSBit:
|
||||
if (sizeInBits == 1)
|
||||
return MemoryHelpers.ReadSingleBit(valuePtr, bitOffset) ? 1 : -1;
|
||||
return MemoryHelpers.ReadExcessKMultipleBitsAsInt(valuePtr, bitOffset, sizeInBits);
|
||||
case kFormatInt:
|
||||
case kFormatUInt:
|
||||
Debug.Assert(sizeInBits == 32, "INT and UINT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "INT and UINT state must be byte-aligned");
|
||||
if (fmt == kFormatUInt)
|
||||
Debug.Assert(*(uint*)valuePtr <= int.MaxValue, "UINT must fit in the int");
|
||||
return *(int*)valuePtr;
|
||||
case kFormatShort:
|
||||
Debug.Assert(sizeInBits == 16, "SHRT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "SHRT state must be byte-aligned");
|
||||
return *(short*)valuePtr;
|
||||
case kFormatUShort:
|
||||
Debug.Assert(sizeInBits == 16, "USHT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "USHT state must be byte-aligned");
|
||||
return *(ushort*)valuePtr;
|
||||
case kFormatByte:
|
||||
Debug.Assert(sizeInBits == 8, "BYTE state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "BYTE state must be byte-aligned");
|
||||
return *valuePtr;
|
||||
case kFormatSByte:
|
||||
Debug.Assert(sizeInBits == 8, "SBYT state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "SBYT state must be byte-aligned");
|
||||
return *(sbyte*)valuePtr;
|
||||
// Not supported:
|
||||
// - kFormatLong
|
||||
// - kFormatULong
|
||||
// - kFormatFloat
|
||||
// - kFormatDouble
|
||||
default:
|
||||
throw new InvalidOperationException($"State format '{format}' is not supported as integer format");
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteInt(void* statePtr, int value)
|
||||
{
|
||||
Debug.Assert(sizeInBits != 0);
|
||||
|
||||
var valuePtr = (byte*)statePtr + (int)byteOffset;
|
||||
|
||||
var fmt = (int)format;
|
||||
switch (fmt)
|
||||
{
|
||||
case kFormatBit:
|
||||
if (sizeInBits == 1)
|
||||
MemoryHelpers.WriteSingleBit(valuePtr, bitOffset, value != 0);
|
||||
else
|
||||
MemoryHelpers.WriteUIntAsMultipleBits(valuePtr, bitOffset, sizeInBits, (uint)value);
|
||||
break;
|
||||
case kFormatSBit:
|
||||
if (sizeInBits == 1)
|
||||
MemoryHelpers.WriteSingleBit(valuePtr, bitOffset, value > 0);
|
||||
else
|
||||
MemoryHelpers.WriteIntAsExcessKMultipleBits(valuePtr, bitOffset, sizeInBits, value);
|
||||
break;
|
||||
case kFormatInt:
|
||||
case kFormatUInt:
|
||||
Debug.Assert(sizeInBits == 32, "INT and UINT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "INT and UINT state must be byte-aligned");
|
||||
*(int*)valuePtr = value;
|
||||
break;
|
||||
case kFormatShort:
|
||||
Debug.Assert(sizeInBits == 16, "SHRT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "SHRT state must be byte-aligned");
|
||||
*(short*)valuePtr = (short)value;
|
||||
break;
|
||||
case kFormatUShort:
|
||||
Debug.Assert(sizeInBits == 16, "USHT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "USHT state must be byte-aligned");
|
||||
*(ushort*)valuePtr = (ushort)value;
|
||||
break;
|
||||
case kFormatByte:
|
||||
Debug.Assert(sizeInBits == 8, "BYTE state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "BYTE state must be byte-aligned");
|
||||
*valuePtr = (byte)value;
|
||||
break;
|
||||
case kFormatSByte:
|
||||
Debug.Assert(sizeInBits == 8, "SBYT state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "SBYT state must be byte-aligned");
|
||||
*(sbyte*)valuePtr = (sbyte)value;
|
||||
break;
|
||||
// Not supported:
|
||||
// - kFormatLong
|
||||
// - kFormatULong
|
||||
// - kFormatFloat
|
||||
// - kFormatDouble
|
||||
default:
|
||||
throw new Exception($"State format '{format}' is not supported as integer format");
|
||||
}
|
||||
}
|
||||
|
||||
public float ReadFloat(void* statePtr)
|
||||
{
|
||||
Debug.Assert(sizeInBits != 0);
|
||||
|
||||
var valuePtr = (byte*)statePtr + (int)byteOffset;
|
||||
|
||||
var fmt = (int)format;
|
||||
switch (fmt)
|
||||
{
|
||||
// If a control with an integer-based representation does not use the full range
|
||||
// of its integer size (e.g. only goes from [0..128]), processors or the parameters
|
||||
// above have to be used to re-process the resulting float values.
|
||||
case kFormatBit:
|
||||
if (sizeInBits == 1)
|
||||
return MemoryHelpers.ReadSingleBit(valuePtr, bitOffset) ? 1.0f : 0.0f;
|
||||
return MemoryHelpers.ReadMultipleBitsAsNormalizedUInt(valuePtr, bitOffset, sizeInBits);
|
||||
case kFormatSBit:
|
||||
if (sizeInBits == 1)
|
||||
return MemoryHelpers.ReadSingleBit(valuePtr, bitOffset) ? 1.0f : -1.0f;
|
||||
return MemoryHelpers.ReadMultipleBitsAsNormalizedUInt(valuePtr, bitOffset, sizeInBits) * 2.0f - 1.0f;
|
||||
case kFormatInt:
|
||||
Debug.Assert(sizeInBits == 32, "INT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "INT state must be byte-aligned");
|
||||
return NumberHelpers.IntToNormalizedFloat(*(int*)valuePtr, int.MinValue, int.MaxValue) * 2.0f - 1.0f;
|
||||
case kFormatUInt:
|
||||
Debug.Assert(sizeInBits == 32, "UINT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "UINT state must be byte-aligned");
|
||||
return NumberHelpers.UIntToNormalizedFloat(*(uint*)valuePtr, uint.MinValue, uint.MaxValue);
|
||||
case kFormatShort:
|
||||
Debug.Assert(sizeInBits == 16, "SHRT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "SHRT state must be byte-aligned");
|
||||
return NumberHelpers.IntToNormalizedFloat(*(short*)valuePtr, short.MinValue, short.MaxValue) * 2.0f - 1.0f;
|
||||
case kFormatUShort:
|
||||
Debug.Assert(sizeInBits == 16, "USHT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "USHT state must be byte-aligned");
|
||||
return NumberHelpers.UIntToNormalizedFloat(*(ushort*)valuePtr, ushort.MinValue, ushort.MaxValue);
|
||||
case kFormatByte:
|
||||
Debug.Assert(sizeInBits == 8, "BYTE state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "BYTE state must be byte-aligned");
|
||||
return NumberHelpers.UIntToNormalizedFloat(*valuePtr, byte.MinValue, byte.MaxValue);
|
||||
case kFormatSByte:
|
||||
Debug.Assert(sizeInBits == 8, "SBYT state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "SBYT state must be byte-aligned");
|
||||
return NumberHelpers.IntToNormalizedFloat(*(sbyte*)valuePtr, sbyte.MinValue, sbyte.MaxValue) * 2.0f - 1.0f;
|
||||
case kFormatFloat:
|
||||
Debug.Assert(sizeInBits == 32, "FLT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "FLT state must be byte-aligned");
|
||||
return *(float*)valuePtr;
|
||||
case kFormatDouble:
|
||||
Debug.Assert(sizeInBits == 64, "DBL state must have sizeInBits=64");
|
||||
Debug.Assert(bitOffset == 0, "DBL state must be byte-aligned");
|
||||
return (float)*(double*)valuePtr;
|
||||
// Not supported:
|
||||
// - kFormatLong
|
||||
// - kFormatULong
|
||||
default:
|
||||
throw new InvalidOperationException($"State format '{format}' is not supported as floating-point format");
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteFloat(void* statePtr, float value)
|
||||
{
|
||||
var valuePtr = (byte*)statePtr + (int)byteOffset;
|
||||
|
||||
var fmt = (int)format;
|
||||
switch (fmt)
|
||||
{
|
||||
case kFormatBit:
|
||||
if (sizeInBits == 1)
|
||||
MemoryHelpers.WriteSingleBit(valuePtr, bitOffset, value >= 0.5f);////REVIEW: Shouldn't this be the global button press point?
|
||||
else
|
||||
MemoryHelpers.WriteNormalizedUIntAsMultipleBits(valuePtr, bitOffset, sizeInBits, value);
|
||||
break;
|
||||
case kFormatSBit:
|
||||
if (sizeInBits == 1)
|
||||
MemoryHelpers.WriteSingleBit(valuePtr, bitOffset, value >= 0.0f);
|
||||
else
|
||||
MemoryHelpers.WriteNormalizedUIntAsMultipleBits(valuePtr, bitOffset, sizeInBits, value * 0.5f + 0.5f);
|
||||
break;
|
||||
case kFormatInt:
|
||||
Debug.Assert(sizeInBits == 32, "INT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "INT state must be byte-aligned");
|
||||
*(int*)valuePtr = (int)NumberHelpers.NormalizedFloatToInt(value * 0.5f + 0.5f, int.MinValue, int.MaxValue);
|
||||
break;
|
||||
case kFormatUInt:
|
||||
Debug.Assert(sizeInBits == 32, "UINT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "UINT state must be byte-aligned");
|
||||
*(uint*)valuePtr = NumberHelpers.NormalizedFloatToUInt(value, uint.MinValue, uint.MaxValue);
|
||||
break;
|
||||
case kFormatShort:
|
||||
Debug.Assert(sizeInBits == 16, "SHRT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "SHRT state must be byte-aligned");
|
||||
*(short*)valuePtr = (short)NumberHelpers.NormalizedFloatToInt(value * 0.5f + 0.5f, short.MinValue, short.MaxValue);
|
||||
break;
|
||||
case kFormatUShort:
|
||||
Debug.Assert(sizeInBits == 16, "USHT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "USHT state must be byte-aligned");
|
||||
*(ushort*)valuePtr = (ushort)NumberHelpers.NormalizedFloatToUInt(value, ushort.MinValue, ushort.MaxValue);
|
||||
break;
|
||||
case kFormatByte:
|
||||
Debug.Assert(sizeInBits == 8, "BYTE state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "BYTE state must be byte-aligned");
|
||||
*valuePtr = (byte)NumberHelpers.NormalizedFloatToUInt(value, byte.MinValue, byte.MaxValue);
|
||||
break;
|
||||
case kFormatSByte:
|
||||
Debug.Assert(sizeInBits == 8, "SBYT state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "SBYT state must be byte-aligned");
|
||||
*(sbyte*)valuePtr = (sbyte)NumberHelpers.NormalizedFloatToInt(value * 0.5f + 0.5f, sbyte.MinValue, sbyte.MaxValue);
|
||||
break;
|
||||
case kFormatFloat:
|
||||
Debug.Assert(sizeInBits == 32, "FLT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "FLT state must be byte-aligned");
|
||||
*(float*)valuePtr = value;
|
||||
break;
|
||||
case kFormatDouble:
|
||||
Debug.Assert(sizeInBits == 64, "DBL state must have sizeInBits=64");
|
||||
Debug.Assert(bitOffset == 0, "DBL state must be byte-aligned");
|
||||
*(double*)valuePtr = value;
|
||||
break;
|
||||
// Not supported:
|
||||
// - kFormatLong
|
||||
// - kFormatULong
|
||||
default:
|
||||
throw new Exception($"State format '{format}' is not supported as floating-point format");
|
||||
}
|
||||
}
|
||||
|
||||
internal PrimitiveValue FloatToPrimitiveValue(float value)
|
||||
{
|
||||
var fmt = (int)format;
|
||||
switch (fmt)
|
||||
{
|
||||
case kFormatBit:
|
||||
if (sizeInBits == 1)
|
||||
return value >= 0.5f;
|
||||
////FIXME: is this supposed to be int or uint?
|
||||
return (int)NumberHelpers.NormalizedFloatToUInt(value, 0, (uint)((1UL << (int)sizeInBits) - 1));
|
||||
case kFormatSBit:
|
||||
{
|
||||
if (sizeInBits == 1)
|
||||
return value >= 0.0f;
|
||||
var minValue = (int)-(long)(1UL << ((int)sizeInBits - 1));
|
||||
var maxValue = (int)((1UL << ((int)sizeInBits - 1)) - 1);
|
||||
return NumberHelpers.NormalizedFloatToInt(value, minValue, maxValue);
|
||||
}
|
||||
case kFormatInt:
|
||||
Debug.Assert(sizeInBits == 32, "INT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "INT state must be byte-aligned");
|
||||
return NumberHelpers.NormalizedFloatToInt(value * 0.5f + 0.5f, int.MinValue, int.MaxValue);
|
||||
case kFormatUInt:
|
||||
Debug.Assert(sizeInBits == 32, "UINT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "UINT state must be byte-aligned");
|
||||
return NumberHelpers.NormalizedFloatToUInt(value, uint.MinValue, uint.MaxValue);
|
||||
case kFormatShort:
|
||||
Debug.Assert(sizeInBits == 16, "SHRT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "SHRT state must be byte-aligned");
|
||||
return (short)NumberHelpers.NormalizedFloatToInt(value * 0.5f + 0.5f, short.MinValue, short.MaxValue);
|
||||
case kFormatUShort:
|
||||
Debug.Assert(sizeInBits == 16, "USHT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "USHT state must be byte-aligned");
|
||||
return (ushort)NumberHelpers.NormalizedFloatToUInt(value, ushort.MinValue, ushort.MaxValue);
|
||||
case kFormatByte:
|
||||
Debug.Assert(sizeInBits == 8, "BYTE state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "BYTE state must be byte-aligned");
|
||||
return (byte)NumberHelpers.NormalizedFloatToUInt(value, byte.MinValue, byte.MaxValue);
|
||||
case kFormatSByte:
|
||||
Debug.Assert(sizeInBits == 8, "SBYT state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "SBYT state must be byte-aligned");
|
||||
return (sbyte)NumberHelpers.NormalizedFloatToInt(value * 0.5f + 0.5f, sbyte.MinValue, sbyte.MaxValue);
|
||||
case kFormatFloat:
|
||||
Debug.Assert(sizeInBits == 32, "FLT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "FLT state must be byte-aligned");
|
||||
return value;
|
||||
case kFormatDouble:
|
||||
Debug.Assert(sizeInBits == 64, "DBL state must have sizeInBits=64");
|
||||
Debug.Assert(bitOffset == 0, "DBL state must be byte-aligned");
|
||||
return value;
|
||||
// Not supported:
|
||||
// - kFormatLong
|
||||
// - kFormatULong
|
||||
default:
|
||||
throw new Exception($"State format '{format}' is not supported as floating-point format");
|
||||
}
|
||||
}
|
||||
|
||||
////REVIEW: This is some bad code duplication here between Read/WriteFloat&Double but given that there's no
|
||||
//// way to use a type argument here, not sure how to get rid of it.
|
||||
|
||||
public double ReadDouble(void* statePtr)
|
||||
{
|
||||
Debug.Assert(sizeInBits != 0);
|
||||
|
||||
var valuePtr = (byte*)statePtr + (int)byteOffset;
|
||||
|
||||
var fmt = (int)format;
|
||||
switch (fmt)
|
||||
{
|
||||
// If a control with an integer-based representation does not use the full range
|
||||
// of its integer size (e.g. only goes from [0..128]), processors or the parameters
|
||||
// above have to be used to re-process the resulting float values.
|
||||
case kFormatBit:
|
||||
if (sizeInBits == 1)
|
||||
return MemoryHelpers.ReadSingleBit(valuePtr, bitOffset) ? 1.0f : 0.0f;
|
||||
return MemoryHelpers.ReadMultipleBitsAsNormalizedUInt(valuePtr, bitOffset, sizeInBits);
|
||||
case kFormatSBit:
|
||||
if (sizeInBits == 1)
|
||||
return MemoryHelpers.ReadSingleBit(valuePtr, bitOffset) ? 1.0f : -1.0f;
|
||||
return MemoryHelpers.ReadMultipleBitsAsNormalizedUInt(valuePtr, bitOffset, sizeInBits) * 2.0f - 1.0f;
|
||||
case kFormatInt:
|
||||
Debug.Assert(sizeInBits == 32, "INT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "INT state must be byte-aligned");
|
||||
return NumberHelpers.IntToNormalizedFloat(*(int*)valuePtr, int.MinValue, int.MaxValue) * 2.0f - 1.0f;
|
||||
case kFormatUInt:
|
||||
Debug.Assert(sizeInBits == 32, "UINT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "UINT state must be byte-aligned");
|
||||
return NumberHelpers.UIntToNormalizedFloat(*(uint*)valuePtr, uint.MinValue, uint.MaxValue);
|
||||
case kFormatShort:
|
||||
Debug.Assert(sizeInBits == 16, "SHRT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "SHRT state must be byte-aligned");
|
||||
return NumberHelpers.IntToNormalizedFloat(*(short*)valuePtr, short.MinValue, short.MaxValue) * 2.0f - 1.0f;
|
||||
case kFormatUShort:
|
||||
Debug.Assert(sizeInBits == 16, "USHT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "USHT state must be byte-aligned");
|
||||
return NumberHelpers.UIntToNormalizedFloat(*(ushort*)valuePtr, ushort.MinValue, ushort.MaxValue);
|
||||
case kFormatByte:
|
||||
Debug.Assert(sizeInBits == 8, "BYTE state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "BYTE state must be byte-aligned");
|
||||
return NumberHelpers.UIntToNormalizedFloat(*valuePtr, byte.MinValue, byte.MaxValue);
|
||||
case kFormatSByte:
|
||||
Debug.Assert(sizeInBits == 8, "SBYT state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "SBYT state must be byte-aligned");
|
||||
return NumberHelpers.IntToNormalizedFloat(*(sbyte*)valuePtr, sbyte.MinValue, sbyte.MaxValue) * 2.0f - 1.0f;
|
||||
case kFormatFloat:
|
||||
Debug.Assert(sizeInBits == 32, "FLT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "FLT state must be byte-aligned");
|
||||
return *(float*)valuePtr;
|
||||
case kFormatDouble:
|
||||
Debug.Assert(sizeInBits == 64, "DBL state must have sizeInBits=64");
|
||||
Debug.Assert(bitOffset == 0, "DBL state must be byte-aligned");
|
||||
return *(double*)valuePtr;
|
||||
// Not supported:
|
||||
// - kFormatLong
|
||||
// - kFormatULong
|
||||
// - kFormatFloat
|
||||
// - kFormatDouble
|
||||
default:
|
||||
throw new Exception($"State format '{format}' is not supported as floating-point format");
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteDouble(void* statePtr, double value)
|
||||
{
|
||||
var valuePtr = (byte*)statePtr + (int)byteOffset;
|
||||
|
||||
var fmt = (int)format;
|
||||
switch (fmt)
|
||||
{
|
||||
case kFormatBit:
|
||||
if (sizeInBits == 1)
|
||||
MemoryHelpers.WriteSingleBit(valuePtr, bitOffset, value >= 0.5f);
|
||||
else
|
||||
MemoryHelpers.WriteNormalizedUIntAsMultipleBits(valuePtr, bitOffset, sizeInBits, (float)value);
|
||||
break;
|
||||
case kFormatSBit:
|
||||
if (sizeInBits == 1)
|
||||
MemoryHelpers.WriteSingleBit(valuePtr, bitOffset, value >= 0.0f);
|
||||
else
|
||||
MemoryHelpers.WriteNormalizedUIntAsMultipleBits(valuePtr, bitOffset, sizeInBits, (float)value * 0.5f + 0.5f);
|
||||
break;
|
||||
case kFormatInt:
|
||||
Debug.Assert(sizeInBits == 32, "INT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "INT state must be byte-aligned");
|
||||
*(int*)valuePtr = NumberHelpers.NormalizedFloatToInt((float)value * 0.5f + 0.5f, int.MinValue, int.MaxValue);
|
||||
break;
|
||||
case kFormatUInt:
|
||||
Debug.Assert(sizeInBits == 32, "UINT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "UINT state must be byte-aligned");
|
||||
*(uint*)valuePtr = NumberHelpers.NormalizedFloatToUInt((float)value, uint.MinValue, uint.MaxValue);
|
||||
break;
|
||||
case kFormatShort:
|
||||
Debug.Assert(sizeInBits == 16, "SHRT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "SHRT state must be byte-aligned");
|
||||
*(short*)valuePtr = (short)NumberHelpers.NormalizedFloatToInt((float)value * 0.5f + 0.5f, short.MinValue, short.MaxValue);
|
||||
break;
|
||||
case kFormatUShort:
|
||||
Debug.Assert(sizeInBits == 16, "USHT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "USHT state must be byte-aligned");
|
||||
*(ushort*)valuePtr = (ushort)NumberHelpers.NormalizedFloatToUInt((float)value, ushort.MinValue, ushort.MaxValue);
|
||||
break;
|
||||
case kFormatByte:
|
||||
Debug.Assert(sizeInBits == 8, "BYTE state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "BYTE state must be byte-aligned");
|
||||
*valuePtr = (byte)NumberHelpers.NormalizedFloatToUInt((float)value, byte.MinValue, byte.MaxValue);
|
||||
break;
|
||||
case kFormatSByte:
|
||||
Debug.Assert(sizeInBits == 8, "SBYT state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "SBYT state must be byte-aligned");
|
||||
*(sbyte*)valuePtr = (sbyte)NumberHelpers.NormalizedFloatToInt((float)value * 0.5f + 0.5f, sbyte.MinValue, sbyte.MaxValue);
|
||||
break;
|
||||
case kFormatFloat:
|
||||
Debug.Assert(sizeInBits == 32, "FLT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "FLT state must be byte-aligned");
|
||||
*(float*)valuePtr = (float)value;
|
||||
break;
|
||||
case kFormatDouble:
|
||||
Debug.Assert(sizeInBits == 64, "DBL state must have sizeInBits=64");
|
||||
Debug.Assert(bitOffset == 0, "DBL state must be byte-aligned");
|
||||
*(double*)valuePtr = value;
|
||||
break;
|
||||
// Not supported:
|
||||
// - kFormatLong
|
||||
// - kFormatULong
|
||||
// - kFormatFloat
|
||||
// - kFormatDouble
|
||||
default:
|
||||
throw new InvalidOperationException($"State format '{format}' is not supported as floating-point format");
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(void* statePtr, PrimitiveValue value)
|
||||
{
|
||||
var valuePtr = (byte*)statePtr + (int)byteOffset;
|
||||
|
||||
var fmt = (int)format;
|
||||
switch (fmt)
|
||||
{
|
||||
case kFormatBit:
|
||||
if (sizeInBits == 1)
|
||||
MemoryHelpers.WriteSingleBit(valuePtr, bitOffset, value.ToBoolean());
|
||||
else
|
||||
MemoryHelpers.WriteUIntAsMultipleBits(valuePtr, bitOffset, sizeInBits, value.ToUInt32());
|
||||
break;
|
||||
case kFormatSBit:
|
||||
if (sizeInBits == 1)
|
||||
MemoryHelpers.WriteSingleBit(valuePtr, bitOffset, value.ToBoolean());
|
||||
else
|
||||
////REVIEW: previous implementation was writing int32 as two's complement here
|
||||
MemoryHelpers.WriteIntAsExcessKMultipleBits(valuePtr, bitOffset, sizeInBits, value.ToInt32());
|
||||
break;
|
||||
case kFormatInt:
|
||||
Debug.Assert(sizeInBits == 32, "INT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "INT state must be byte-aligned");
|
||||
*(int*)valuePtr = value.ToInt32();
|
||||
break;
|
||||
case kFormatUInt:
|
||||
Debug.Assert(sizeInBits == 32, "UINT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "UINT state must be byte-aligned");
|
||||
*(uint*)valuePtr = value.ToUInt32();
|
||||
break;
|
||||
case kFormatShort:
|
||||
Debug.Assert(sizeInBits == 16, "SHRT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "SHRT state must be byte-aligned");
|
||||
*(short*)valuePtr = value.ToInt16();
|
||||
break;
|
||||
case kFormatUShort:
|
||||
Debug.Assert(sizeInBits == 16, "USHT state must have sizeInBits=16");
|
||||
Debug.Assert(bitOffset == 0, "USHT state must be byte-aligned");
|
||||
*(ushort*)valuePtr = value.ToUInt16();
|
||||
break;
|
||||
case kFormatByte:
|
||||
Debug.Assert(sizeInBits == 8, "BYTE state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "BYTE state must be byte-aligned");
|
||||
*valuePtr = value.ToByte();
|
||||
break;
|
||||
case kFormatSByte:
|
||||
Debug.Assert(sizeInBits == 8, "SBYT state must have sizeInBits=8");
|
||||
Debug.Assert(bitOffset == 0, "SBYT state must be byte-aligned");
|
||||
*(sbyte*)valuePtr = value.ToSByte();
|
||||
break;
|
||||
case kFormatFloat:
|
||||
Debug.Assert(sizeInBits == 32, "FLT state must have sizeInBits=32");
|
||||
Debug.Assert(bitOffset == 0, "FLT state must be byte-aligned");
|
||||
*(float*)valuePtr = value.ToSingle();
|
||||
break;
|
||||
// Not supported:
|
||||
// - kFormatLong
|
||||
// - kFormatULong
|
||||
// - kFormatDouble
|
||||
default:
|
||||
throw new NotImplementedException(
|
||||
$"Writing primitive value of type '{value.type}' into state block with format '{format}'");
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyToFrom(void* toStatePtr, void* fromStatePtr)
|
||||
{
|
||||
if (bitOffset != 0 || sizeInBits % 8 != 0)
|
||||
throw new NotImplementedException("Copying bitfields");
|
||||
|
||||
var from = (byte*)fromStatePtr + byteOffset;
|
||||
var to = (byte*)toStatePtr + byteOffset;
|
||||
|
||||
UnsafeUtility.MemCpy(to, from, alignedSizeInBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef4e8c908ae24e438f4baea55d866f5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,397 @@
|
||||
using System;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
|
||||
////REVIEW: Can we change this into a setup where the buffering depth isn't fixed to 2 but rather
|
||||
//// can be set on a per device basis?
|
||||
|
||||
namespace UnityEngine.InputSystem.LowLevel
|
||||
{
|
||||
// The raw memory blocks which are indexed by InputStateBlocks.
|
||||
//
|
||||
// Internally, we perform only a single combined unmanaged allocation for all state
|
||||
// buffers needed by the system. Externally, we expose them as if they are each separate
|
||||
// buffers.
|
||||
internal unsafe struct InputStateBuffers
|
||||
{
|
||||
// State buffers are set up in a double buffering scheme where the "back buffer"
|
||||
// represents the previous state of devices and the "front buffer" represents
|
||||
// the current state.
|
||||
//
|
||||
// Edit mode and play mode each get their own double buffering. Updates to them
|
||||
// are tied to focus and only one mode will actually receive state events while the
|
||||
// other mode is dormant. In the player, we only get play mode buffers, of course.
|
||||
|
||||
////TODO: need to clear the current buffers when switching between edit and play mode
|
||||
//// (i.e. if you click an editor window while in play mode, the play mode
|
||||
//// device states will all go back to default)
|
||||
//// actually, if we really reset on mode change, can't we just keep a single set buffers?
|
||||
|
||||
public uint sizePerBuffer;
|
||||
public uint totalSize;
|
||||
|
||||
/// <summary>
|
||||
/// Buffer that has state for each device initialized with default values.
|
||||
/// </summary>
|
||||
public void* defaultStateBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Buffer that contains a bit mask that masks out all noisy controls.
|
||||
/// </summary>
|
||||
public void* noiseMaskBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Buffer that contains a bit mask that masks out all dontReset controls.
|
||||
/// </summary>
|
||||
public void* resetMaskBuffer;
|
||||
|
||||
// Secretly we perform only a single allocation.
|
||||
// This allocation also contains the device-to-state mappings.
|
||||
private void* m_AllBuffers;
|
||||
|
||||
// Contains information about a double buffer setup.
|
||||
[Serializable]
|
||||
internal struct DoubleBuffers
|
||||
{
|
||||
////REVIEW: store timestamps along with each device-to-buffer mapping?
|
||||
// An array of pointers that maps devices to their respective
|
||||
// front and back buffer. Mapping is [deviceIndex*2] is front
|
||||
// buffer and [deviceIndex*2+1] is back buffer. Each device
|
||||
// has its buffers swapped individually with SwapDeviceBuffers().
|
||||
public void** deviceToBufferMapping;
|
||||
public int deviceCount;
|
||||
|
||||
public bool valid => deviceToBufferMapping != null;
|
||||
|
||||
public void SetFrontBuffer(int deviceIndex, void* ptr)
|
||||
{
|
||||
if (deviceIndex < deviceCount)
|
||||
deviceToBufferMapping[deviceIndex * 2] = ptr;
|
||||
}
|
||||
|
||||
public void SetBackBuffer(int deviceIndex, void* ptr)
|
||||
{
|
||||
if (deviceIndex < deviceCount)
|
||||
deviceToBufferMapping[deviceIndex * 2 + 1] = ptr;
|
||||
}
|
||||
|
||||
public void* GetFrontBuffer(int deviceIndex)
|
||||
{
|
||||
if (deviceIndex < deviceCount)
|
||||
return deviceToBufferMapping[deviceIndex * 2];
|
||||
return null;
|
||||
}
|
||||
|
||||
public void* GetBackBuffer(int deviceIndex)
|
||||
{
|
||||
if (deviceIndex < deviceCount)
|
||||
return deviceToBufferMapping[deviceIndex * 2 + 1];
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SwapBuffers(int deviceIndex)
|
||||
{
|
||||
// Ignore if the double buffer set has not been initialized.
|
||||
// Means the respective update type is disabled.
|
||||
if (!valid)
|
||||
return;
|
||||
|
||||
var front = GetFrontBuffer(deviceIndex);
|
||||
var back = GetBackBuffer(deviceIndex);
|
||||
|
||||
SetFrontBuffer(deviceIndex, back);
|
||||
SetBackBuffer(deviceIndex, front);
|
||||
}
|
||||
}
|
||||
|
||||
internal DoubleBuffers m_PlayerStateBuffers;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
internal DoubleBuffers m_EditorStateBuffers;
|
||||
#endif
|
||||
|
||||
public DoubleBuffers GetDoubleBuffersFor(InputUpdateType updateType)
|
||||
{
|
||||
switch (updateType)
|
||||
{
|
||||
case InputUpdateType.BeforeRender:
|
||||
case InputUpdateType.Fixed:
|
||||
case InputUpdateType.Dynamic:
|
||||
case InputUpdateType.Manual:
|
||||
return m_PlayerStateBuffers;
|
||||
#if UNITY_EDITOR
|
||||
case InputUpdateType.Editor:
|
||||
return m_EditorStateBuffers;
|
||||
#endif
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unrecognized InputUpdateType: " + updateType, nameof(updateType));
|
||||
}
|
||||
|
||||
internal static void* s_DefaultStateBuffer;
|
||||
internal static void* s_NoiseMaskBuffer;
|
||||
internal static void* s_ResetMaskBuffer;
|
||||
internal static DoubleBuffers s_CurrentBuffers;
|
||||
|
||||
public static void* GetFrontBufferForDevice(int deviceIndex)
|
||||
{
|
||||
return s_CurrentBuffers.GetFrontBuffer(deviceIndex);
|
||||
}
|
||||
|
||||
public static void* GetBackBufferForDevice(int deviceIndex)
|
||||
{
|
||||
return s_CurrentBuffers.GetBackBuffer(deviceIndex);
|
||||
}
|
||||
|
||||
// Switch the current set of buffers used by the system.
|
||||
public static void SwitchTo(InputStateBuffers buffers, InputUpdateType update)
|
||||
{
|
||||
s_CurrentBuffers = buffers.GetDoubleBuffersFor(update);
|
||||
}
|
||||
|
||||
// Allocates all buffers to serve the given updates and comes up with a spot
|
||||
// for the state block of each device. Returns the new state blocks for the
|
||||
// devices (it will *NOT* install them on the devices).
|
||||
public void AllocateAll(InputDevice[] devices, int deviceCount)
|
||||
{
|
||||
sizePerBuffer = ComputeSizeOfSingleStateBuffer(devices, deviceCount);
|
||||
if (sizePerBuffer == 0)
|
||||
return;
|
||||
sizePerBuffer = sizePerBuffer.AlignToMultipleOf(4);
|
||||
|
||||
// Determine how much memory we need.
|
||||
var mappingTableSizePerBuffer = (uint)(deviceCount * sizeof(void*) * 2);
|
||||
|
||||
totalSize = 0;
|
||||
|
||||
totalSize += sizePerBuffer * 2;
|
||||
totalSize += mappingTableSizePerBuffer;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
totalSize += sizePerBuffer * 2;
|
||||
totalSize += mappingTableSizePerBuffer;
|
||||
#endif
|
||||
|
||||
// Plus 3 more buffers (one for default states, one for noise masks, and one for dontReset masks).
|
||||
totalSize += sizePerBuffer * 3;
|
||||
|
||||
// Allocate.
|
||||
m_AllBuffers = UnsafeUtility.Malloc(totalSize, 4, Allocator.Persistent);
|
||||
UnsafeUtility.MemClear(m_AllBuffers, totalSize);
|
||||
|
||||
// Set up device to buffer mappings.
|
||||
var ptr = (byte*)m_AllBuffers;
|
||||
m_PlayerStateBuffers =
|
||||
SetUpDeviceToBufferMappings(deviceCount, ref ptr, sizePerBuffer,
|
||||
mappingTableSizePerBuffer);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
m_EditorStateBuffers =
|
||||
SetUpDeviceToBufferMappings(deviceCount, ref ptr, sizePerBuffer, mappingTableSizePerBuffer);
|
||||
#endif
|
||||
|
||||
// Default state and noise filter buffers go last.
|
||||
defaultStateBuffer = ptr;
|
||||
noiseMaskBuffer = ptr + sizePerBuffer;
|
||||
resetMaskBuffer = ptr + sizePerBuffer * 2;
|
||||
}
|
||||
|
||||
private static DoubleBuffers SetUpDeviceToBufferMappings(int deviceCount, ref byte* bufferPtr, uint sizePerBuffer, uint mappingTableSizePerBuffer)
|
||||
{
|
||||
var front = bufferPtr;
|
||||
var back = bufferPtr + sizePerBuffer;
|
||||
var mappings = (void**)(bufferPtr + sizePerBuffer * 2); // Put mapping table at end.
|
||||
bufferPtr += sizePerBuffer * 2 + mappingTableSizePerBuffer;
|
||||
|
||||
var buffers = new DoubleBuffers
|
||||
{
|
||||
deviceToBufferMapping = mappings,
|
||||
deviceCount = deviceCount
|
||||
};
|
||||
|
||||
for (var i = 0; i < deviceCount; ++i)
|
||||
{
|
||||
var deviceIndex = i;
|
||||
buffers.SetFrontBuffer(deviceIndex, front);
|
||||
buffers.SetBackBuffer(deviceIndex, back);
|
||||
}
|
||||
|
||||
return buffers;
|
||||
}
|
||||
|
||||
public void FreeAll()
|
||||
{
|
||||
if (m_AllBuffers != null)
|
||||
{
|
||||
UnsafeUtility.Free(m_AllBuffers, Allocator.Persistent);
|
||||
m_AllBuffers = null;
|
||||
}
|
||||
|
||||
m_PlayerStateBuffers = new DoubleBuffers();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
m_EditorStateBuffers = new DoubleBuffers();
|
||||
#endif
|
||||
|
||||
s_CurrentBuffers = new DoubleBuffers();
|
||||
|
||||
if (s_DefaultStateBuffer == defaultStateBuffer)
|
||||
s_DefaultStateBuffer = null;
|
||||
|
||||
defaultStateBuffer = null;
|
||||
|
||||
if (s_NoiseMaskBuffer == noiseMaskBuffer)
|
||||
s_NoiseMaskBuffer = null;
|
||||
|
||||
if (s_ResetMaskBuffer == resetMaskBuffer)
|
||||
s_ResetMaskBuffer = null;
|
||||
|
||||
noiseMaskBuffer = null;
|
||||
resetMaskBuffer = null;
|
||||
|
||||
totalSize = 0;
|
||||
sizePerBuffer = 0;
|
||||
}
|
||||
|
||||
// Migrate state data for all devices from a previous set of buffers to the current set of buffers.
|
||||
// Copies all state from their old locations to their new locations and bakes the new offsets into
|
||||
// the control hierarchies of the given devices.
|
||||
// NOTE: When having oldBuffers, this method only works properly if the only alteration compared to the
|
||||
// new buffers is that either devices have been removed or devices have been added. Cannot be
|
||||
// a mix of the two. Also, new devices MUST be added to the end and cannot be inserted in the middle.
|
||||
// NOTE: Also, state formats MUST not change from before. A device that has changed its format must
|
||||
// be treated as a newly device that didn't exist before.
|
||||
public void MigrateAll(InputDevice[] devices, int deviceCount, InputStateBuffers oldBuffers)
|
||||
{
|
||||
// If we have old data, perform migration.
|
||||
if (oldBuffers.totalSize > 0)
|
||||
{
|
||||
MigrateDoubleBuffer(m_PlayerStateBuffers, devices, deviceCount, oldBuffers.m_PlayerStateBuffers);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
MigrateDoubleBuffer(m_EditorStateBuffers, devices, deviceCount, oldBuffers.m_EditorStateBuffers);
|
||||
#endif
|
||||
|
||||
MigrateSingleBuffer(defaultStateBuffer, devices, deviceCount, oldBuffers.defaultStateBuffer);
|
||||
MigrateSingleBuffer(noiseMaskBuffer, devices, deviceCount, oldBuffers.noiseMaskBuffer);
|
||||
MigrateSingleBuffer(resetMaskBuffer, devices, deviceCount, oldBuffers.resetMaskBuffer);
|
||||
}
|
||||
|
||||
// Assign state blocks. This is where devices will receive their updates state offsets. Up
|
||||
// until now we've left any previous m_StateBlocks alone.
|
||||
var newOffset = 0u;
|
||||
for (var i = 0; i < deviceCount; ++i)
|
||||
{
|
||||
var device = devices[i];
|
||||
var oldOffset = device.m_StateBlock.byteOffset;
|
||||
|
||||
if (oldOffset == InputStateBlock.InvalidOffset)
|
||||
{
|
||||
// Device is new and has no offset yet baked into it.
|
||||
device.m_StateBlock.byteOffset = 0;
|
||||
if (newOffset != 0)
|
||||
device.BakeOffsetIntoStateBlockRecursive(newOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Device is not new and still has its old offset baked into it. We could first unbake the old offset
|
||||
// and then bake the new one but instead just bake a relative offset.
|
||||
var delta = newOffset - oldOffset;
|
||||
if (delta != 0)
|
||||
device.BakeOffsetIntoStateBlockRecursive(delta);
|
||||
}
|
||||
|
||||
Debug.Assert(device.m_StateBlock.byteOffset == newOffset, "Device state offset not set correctly");
|
||||
|
||||
newOffset = NextDeviceOffset(newOffset, device);
|
||||
}
|
||||
}
|
||||
|
||||
private static void MigrateDoubleBuffer(DoubleBuffers newBuffer, InputDevice[] devices, int deviceCount, DoubleBuffers oldBuffer)
|
||||
{
|
||||
// Nothing to migrate if we no longer keep a buffer of the corresponding type.
|
||||
if (!newBuffer.valid)
|
||||
return;
|
||||
|
||||
// We do the same if we don't had a corresponding buffer before.
|
||||
if (!oldBuffer.valid)
|
||||
return;
|
||||
|
||||
// Migrate every device that has allocated state blocks.
|
||||
var newStateBlockOffset = 0u;
|
||||
for (var i = 0; i < deviceCount; ++i)
|
||||
{
|
||||
var device = devices[i];
|
||||
|
||||
// Stop as soon as we're hitting a new device. Newly added devices *must* be *appended* to the
|
||||
// array as otherwise our computing of offsets into the old buffer may be wrong.
|
||||
// NOTE: This also means that device indices of
|
||||
if (device.m_StateBlock.byteOffset == InputStateBlock.InvalidOffset)
|
||||
{
|
||||
#if DEVELOPMENT_BUILD || UNITY_EDITOR
|
||||
for (var n = i + 1; n < deviceCount; ++n)
|
||||
Debug.Assert(devices[n].m_StateBlock.byteOffset == InputStateBlock.InvalidOffset,
|
||||
"New devices must be appended to the array; found an old device coming in the array after a newly added device");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
var oldDeviceIndex = device.m_DeviceIndex;
|
||||
var newDeviceIndex = i;
|
||||
var numBytes = device.m_StateBlock.alignedSizeInBytes;
|
||||
|
||||
var oldFrontPtr = (byte*)oldBuffer.GetFrontBuffer(oldDeviceIndex) + (int)device.m_StateBlock.byteOffset; // m_StateBlock still refers to oldBuffer.
|
||||
var oldBackPtr = (byte*)oldBuffer.GetBackBuffer(oldDeviceIndex) + (int)device.m_StateBlock.byteOffset;
|
||||
|
||||
var newFrontPtr = (byte*)newBuffer.GetFrontBuffer(newDeviceIndex) + (int)newStateBlockOffset;
|
||||
var newBackPtr = (byte*)newBuffer.GetBackBuffer(newDeviceIndex) + (int)newStateBlockOffset;
|
||||
|
||||
// Copy state.
|
||||
UnsafeUtility.MemCpy(newFrontPtr, oldFrontPtr, numBytes);
|
||||
UnsafeUtility.MemCpy(newBackPtr, oldBackPtr, numBytes);
|
||||
|
||||
newStateBlockOffset = NextDeviceOffset(newStateBlockOffset, device);
|
||||
}
|
||||
}
|
||||
|
||||
private static void MigrateSingleBuffer(void* newBuffer, InputDevice[] devices, int deviceCount, void* oldBuffer)
|
||||
{
|
||||
// Migrate every device that has allocated state blocks.
|
||||
var newDeviceCount = deviceCount;
|
||||
var newStateBlockOffset = 0u;
|
||||
for (var i = 0; i < newDeviceCount; ++i)
|
||||
{
|
||||
var device = devices[i];
|
||||
|
||||
// Stop if we've reached newly added devices.
|
||||
if (device.m_StateBlock.byteOffset == InputStateBlock.InvalidOffset)
|
||||
break;
|
||||
|
||||
var numBytes = device.m_StateBlock.alignedSizeInBytes;
|
||||
var oldStatePtr = (byte*)oldBuffer + (int)device.m_StateBlock.byteOffset;
|
||||
var newStatePtr = (byte*)newBuffer + (int)newStateBlockOffset;
|
||||
|
||||
UnsafeUtility.MemCpy(newStatePtr, oldStatePtr, numBytes);
|
||||
|
||||
newStateBlockOffset = NextDeviceOffset(newStateBlockOffset, device);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint ComputeSizeOfSingleStateBuffer(InputDevice[] devices, int deviceCount)
|
||||
{
|
||||
var sizeInBytes = 0u;
|
||||
for (var i = 0; i < deviceCount; ++i)
|
||||
sizeInBytes = NextDeviceOffset(sizeInBytes, devices[i]);
|
||||
return sizeInBytes;
|
||||
}
|
||||
|
||||
private static uint NextDeviceOffset(uint currentOffset, InputDevice device)
|
||||
{
|
||||
var sizeOfDevice = device.m_StateBlock.alignedSizeInBytes;
|
||||
if (sizeOfDevice == 0) // Shouldn't happen as we don't allow empty layouts but make sure we catch this if something slips through.
|
||||
throw new ArgumentException($"Device '{device}' has a zero-size state buffer", nameof(device));
|
||||
return currentOffset + sizeOfDevice.AlignToMultipleOf(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6633b33d60f14b81927d89a4b06c8a1f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ccdc5c2ae66744782ab7e326c053ad0a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user