123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020 |
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
- using Unity.Collections;
- using UnityEngine.InputSystem.Composites;
- using UnityEngine.InputSystem.Controls;
- using Unity.Collections.LowLevel.Unsafe;
- using UnityEngine.Profiling;
- using UnityEngine.InputSystem.LowLevel;
- using UnityEngine.InputSystem.Processors;
- using UnityEngine.InputSystem.Interactions;
- using UnityEngine.InputSystem.Utilities;
- using UnityEngine.InputSystem.Layouts;
-
- #if UNITY_EDITOR
- using UnityEngine.InputSystem.Editor;
- #endif
-
- #if UNITY_EDITOR
- using CustomBindingPathValidator = System.Func<string, System.Action>;
- #endif
-
- ////TODO: make diagnostics available in dev players and give it a public API to enable them
-
- ////TODO: work towards InputManager having no direct knowledge of actions
-
- ////TODO: allow pushing events into the system any which way; decouple from the buffer in NativeInputSystem being the only source
-
- ////REVIEW: change the event properties over to using IObservable?
-
- ////REVIEW: instead of RegisterInteraction and RegisterProcessor, have a generic RegisterInterface (or something)?
-
- ////REVIEW: can we do away with the 'previous == previous frame' and simply buffer flip on every value write?
-
- ////REVIEW: should we force keeping mouse/pen/keyboard/touch around in editor even if not in list of supported devices?
-
- ////REVIEW: do we want to filter out state events that result in no state change?
-
- #pragma warning disable CS0649
- namespace UnityEngine.InputSystem
- {
- using DeviceChangeListener = Action<InputDevice, InputDeviceChange>;
- using DeviceStateChangeListener = Action<InputDevice, InputEventPtr>;
- using LayoutChangeListener = Action<string, InputControlLayoutChange>;
- using EventListener = Action<InputEventPtr, InputDevice>;
- using UpdateListener = Action;
-
- /// <summary>
- /// Hub of the input system.
- /// </summary>
- /// <remarks>
- /// Not exposed. Use <see cref="InputSystem"/> as the public entry point to the system.
- ///
- /// Manages devices, layouts, and event processing.
- /// </remarks>
- internal partial class InputManager
- {
- public ReadOnlyArray<InputDevice> devices => new ReadOnlyArray<InputDevice>(m_Devices, 0, m_DevicesCount);
-
- public TypeTable processors => m_Processors;
- public TypeTable interactions => m_Interactions;
- public TypeTable composites => m_Composites;
-
- public InputMetrics metrics
- {
- get
- {
- var result = m_Metrics;
-
- result.currentNumDevices = m_DevicesCount;
- result.currentStateSizeInBytes = (int)m_StateBuffers.totalSize;
-
- // Count controls.
- result.currentControlCount = m_DevicesCount;
- for (var i = 0; i < m_DevicesCount; ++i)
- result.currentControlCount += m_Devices[i].allControls.Count;
-
- // Count layouts.
- result.currentLayoutCount = m_Layouts.layoutTypes.Count;
- result.currentLayoutCount += m_Layouts.layoutStrings.Count;
- result.currentLayoutCount += m_Layouts.layoutBuilders.Count;
- result.currentLayoutCount += m_Layouts.layoutOverrides.Count;
-
- return result;
- }
- }
-
- public InputSettings settings
- {
- get
- {
- Debug.Assert(m_Settings != null);
- return m_Settings;
- }
- set
- {
- if (value == null)
- throw new ArgumentNullException(nameof(value));
-
- if (m_Settings == value)
- return;
-
- m_Settings = value;
- ApplySettings();
- }
- }
-
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- public InputActionAsset actions
- {
- get
- {
- return m_Actions;
- }
-
- set
- {
- m_Actions = value;
- ApplyActions();
- }
- }
- #endif
-
- public InputUpdateType updateMask
- {
- get => m_UpdateMask;
- set
- {
- // In editor, we don't allow disabling editor updates.
- #if UNITY_EDITOR
- value |= InputUpdateType.Editor;
- #endif
-
- if (m_UpdateMask == value)
- return;
-
- m_UpdateMask = value;
-
- // Recreate state buffers.
- if (m_DevicesCount > 0)
- ReallocateStateBuffers();
- }
- }
-
- public InputUpdateType defaultUpdateType
- {
- get
- {
- if (m_CurrentUpdate != default)
- return m_CurrentUpdate;
-
- #if UNITY_EDITOR
- if (!m_RunPlayerUpdatesInEditMode && (!gameIsPlaying || !gameHasFocus))
- return InputUpdateType.Editor;
- #endif
-
- return m_UpdateMask.GetUpdateTypeForPlayer();
- }
- }
-
- public float pollingFrequency
- {
- get => m_PollingFrequency;
- set
- {
- ////REVIEW: allow setting to zero to turn off polling altogether?
- if (value <= 0)
- throw new ArgumentException("Polling frequency must be greater than zero", "value");
-
- m_PollingFrequency = value;
- if (m_Runtime != null)
- m_Runtime.pollingFrequency = value;
- }
- }
-
- public event DeviceChangeListener onDeviceChange
- {
- add => m_DeviceChangeListeners.AddCallback(value);
- remove => m_DeviceChangeListeners.RemoveCallback(value);
- }
-
- public event DeviceStateChangeListener onDeviceStateChange
- {
- add => m_DeviceStateChangeListeners.AddCallback(value);
- remove => m_DeviceStateChangeListeners.RemoveCallback(value);
- }
-
- public event InputDeviceCommandDelegate onDeviceCommand
- {
- add => m_DeviceCommandCallbacks.AddCallback(value);
- remove => m_DeviceCommandCallbacks.RemoveCallback(value);
- }
-
- ////REVIEW: would be great to have a way to sort out precedence between two callbacks
- public event InputDeviceFindControlLayoutDelegate onFindControlLayoutForDevice
- {
- add
- {
- m_DeviceFindLayoutCallbacks.AddCallback(value);
-
- // Having a new callback on this event can change the set of devices we recognize.
- // See if there's anything in the list of available devices that we can now turn
- // into an InputDevice whereas we couldn't before.
- //
- // NOTE: A callback could also impact already existing devices and theoretically alter
- // what layout we would have used for those. We do *NOT* retroactively apply
- // those changes.
- AddAvailableDevicesThatAreNowRecognized();
- }
- remove => m_DeviceFindLayoutCallbacks.RemoveCallback(value);
- }
-
- public event LayoutChangeListener onLayoutChange
- {
- add => m_LayoutChangeListeners.AddCallback(value);
- remove => m_LayoutChangeListeners.RemoveCallback(value);
- }
-
- ////TODO: add InputEventBuffer struct that uses NativeArray underneath
- ////TODO: make InputEventTrace use NativeArray
- ////TODO: introduce an alternative that consumes events in bulk
- public event EventListener onEvent
- {
- add => m_EventListeners.AddCallback(value);
- remove => m_EventListeners.RemoveCallback(value);
- }
-
- public event UpdateListener onBeforeUpdate
- {
- add
- {
- InstallBeforeUpdateHookIfNecessary();
- m_BeforeUpdateListeners.AddCallback(value);
- }
- remove => m_BeforeUpdateListeners.RemoveCallback(value);
- }
-
- public event UpdateListener onAfterUpdate
- {
- add => m_AfterUpdateListeners.AddCallback(value);
- remove => m_AfterUpdateListeners.RemoveCallback(value);
- }
-
- public event Action onSettingsChange
- {
- add => m_SettingsChangedListeners.AddCallback(value);
- remove => m_SettingsChangedListeners.RemoveCallback(value);
- }
-
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- public event Action onActionsChange
- {
- add => m_ActionsChangedListeners.AddCallback(value);
- remove => m_ActionsChangedListeners.RemoveCallback(value);
- }
- #endif
-
- public bool isProcessingEvents => m_InputEventStream.isOpen;
-
- #if UNITY_EDITOR
- /// <summary>
- /// Callback that can be used to display a warning and draw additional custom Editor UI for bindings.
- /// </summary>
- /// <seealso cref="InputSystem.customBindingPathValidators"/>
- /// <remarks>
- /// This is not intended to be called directly.
- /// Please use <see cref="InputSystem.customBindingPathValidators"/> instead.
- /// </remarks>
- internal event CustomBindingPathValidator customBindingPathValidators
- {
- add => m_customBindingPathValidators.AddCallback(value);
- remove => m_customBindingPathValidators.RemoveCallback(value);
- }
-
- /// <summary>
- /// Invokes any custom UI rendering code for this Binding Path in the editor.
- /// </summary>
- /// <seealso cref="InputSystem.customBindingPathValidators"/>
- /// <remarks>
- /// This is not intended to be called directly.
- /// Please use <see cref="InputSystem.OnDrawCustomWarningForBindingPath"/> instead.
- /// </remarks>
- internal void OnDrawCustomWarningForBindingPath(string bindingPath)
- {
- DelegateHelpers.InvokeCallbacksSafe_AndInvokeReturnedActions(
- ref m_customBindingPathValidators,
- bindingPath,
- "InputSystem.OnDrawCustomWarningForBindingPath");
- }
-
- /// <summary>
- /// Determines if any warning icon is to be displayed for this Binding Path in the editor.
- /// </summary>
- /// <seealso cref="InputSystem.customBindingPathValidators"/>
- /// <remarks>
- /// This is not intended to be called directly.
- /// Please use <see cref="InputSystem.OnDrawCustomWarningForBindingPath"/> instead.
- /// </remarks>
- internal bool ShouldDrawWarningIconForBinding(string bindingPath)
- {
- return DelegateHelpers.InvokeCallbacksSafe_AnyCallbackReturnsObject(
- ref m_customBindingPathValidators,
- bindingPath,
- "InputSystem.ShouldDrawWarningIconForBinding");
- }
-
- #endif // UNITY_EDITOR
-
- #if UNITY_EDITOR
- private bool m_RunPlayerUpdatesInEditMode;
-
- /// <summary>
- /// If true, consider the editor to be in "perpetual play mode". Meaning, we ignore editor
- /// updates and just go and continuously process Dynamic/Fixed/BeforeRender regardless of
- /// whether we're in play mode or not.
- ///
- /// In this mode, we also ignore game view focus.
- /// </summary>
- public bool runPlayerUpdatesInEditMode
- {
- get => m_RunPlayerUpdatesInEditMode;
- set => m_RunPlayerUpdatesInEditMode = value;
- }
- #endif
-
- private bool gameIsPlaying =>
- #if UNITY_EDITOR
- (m_Runtime.isInPlayMode && !m_Runtime.isPaused) || m_RunPlayerUpdatesInEditMode;
- #else
- true;
- #endif
-
- private bool gameHasFocus =>
- #if UNITY_EDITOR
- m_RunPlayerUpdatesInEditMode || m_HasFocus || gameShouldGetInputRegardlessOfFocus;
- #else
- m_HasFocus || gameShouldGetInputRegardlessOfFocus;
- #endif
-
- private bool gameShouldGetInputRegardlessOfFocus =>
- m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus
- #if UNITY_EDITOR
- && m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView
- #endif
- ;
-
- ////TODO: when registering a layout that exists as a layout of a different type (type vs string vs constructor),
- //// remove the existing registration
-
- // Add a layout constructed from a type.
- // If a layout with the same name already exists, the new layout
- // takes its place.
- public void RegisterControlLayout(string name, Type type)
- {
- if (string.IsNullOrEmpty(name))
- throw new ArgumentNullException(nameof(name));
- if (type == null)
- throw new ArgumentNullException(nameof(type));
-
- // Note that since InputDevice derives from InputControl, isDeviceLayout implies
- // isControlLayout to be true as well.
- var isDeviceLayout = typeof(InputDevice).IsAssignableFrom(type);
- var isControlLayout = typeof(InputControl).IsAssignableFrom(type);
-
- if (!isDeviceLayout && !isControlLayout)
- throw new ArgumentException($"Types used as layouts have to be InputControls or InputDevices; '{type.Name}' is a '{type.BaseType.Name}'",
- nameof(type));
-
- var internedName = new InternedString(name);
- var isReplacement = m_Layouts.HasLayout(internedName);
-
- // All we do is enter the type into a map. We don't construct an InputControlLayout
- // from it until we actually need it in an InputDeviceBuilder to create a device.
- // This not only avoids us creating a bunch of objects on the managed heap but
- // also avoids us laboriously constructing a XRController layout, for example,
- // in a game that never uses XR.
- m_Layouts.layoutTypes[internedName] = type;
-
- ////TODO: make this independent of initialization order
- ////TODO: re-scan base type information after domain reloads
-
- // Walk class hierarchy all the way up to InputControl to see
- // if there's another type that's been registered as a layout.
- // If so, make it a base layout for this one.
- string baseLayout = null;
- for (var baseType = type.BaseType; baseLayout == null && baseType != typeof(InputControl);
- baseType = baseType.BaseType)
- {
- foreach (var entry in m_Layouts.layoutTypes)
- if (entry.Value == baseType)
- {
- baseLayout = entry.Key;
- break;
- }
- }
-
- PerformLayoutPostRegistration(internedName, new InlinedArray<InternedString>(new InternedString(baseLayout)),
- isReplacement, isKnownToBeDeviceLayout: isDeviceLayout);
- }
-
- public void RegisterControlLayout(string json, string name = null, bool isOverride = false)
- {
- if (string.IsNullOrEmpty(json))
- throw new ArgumentNullException(nameof(json));
-
- ////REVIEW: as long as no one has instantiated the layout, the base layout information is kinda pointless
-
- // Parse out name, device description, and base layout.
- InputControlLayout.ParseHeaderFieldsFromJson(json, out var nameFromJson, out var baseLayouts,
- out var deviceMatcher);
-
- // Decide whether to take name from JSON or from code.
- var internedLayoutName = new InternedString(name);
- if (internedLayoutName.IsEmpty())
- {
- internedLayoutName = nameFromJson;
-
- // Make sure we have a name.
- if (internedLayoutName.IsEmpty())
- throw new ArgumentException("Layout name has not been given and is not set in JSON layout",
- nameof(name));
- }
-
- // If it's an override, it must have a layout the overrides apply to.
- if (isOverride && baseLayouts.length == 0)
- {
- throw new ArgumentException(
- $"Layout override '{internedLayoutName}' must have 'extend' property mentioning layout to which to apply the overrides",
- nameof(json));
- }
-
- // Add it to our records.
- var isReplacement = m_Layouts.HasLayout(internedLayoutName);
- if (isReplacement && isOverride)
- { // Do not allow a layout override to replace a "base layout" by name, but allow layout overrides
- // to replace an existing layout override.
- // This is required to guarantee that its a hierarchy (directed graph) rather
- // than a cyclic graph.
-
- var isReplacingOverride = m_Layouts.layoutOverrideNames.Contains(internedLayoutName);
- if (!isReplacingOverride)
- {
- throw new ArgumentException($"Failed to register layout override '{internedLayoutName}'" +
- $"since a layout named '{internedLayoutName}' already exist. Layout overrides must " +
- $"have unique names with respect to existing layouts.");
- }
- }
-
- m_Layouts.layoutStrings[internedLayoutName] = json;
- if (isOverride)
- {
- m_Layouts.layoutOverrideNames.Add(internedLayoutName);
- for (var i = 0; i < baseLayouts.length; ++i)
- {
- var baseLayoutName = baseLayouts[i];
- m_Layouts.layoutOverrides.TryGetValue(baseLayoutName, out var overrideList);
- if (!isReplacement)
- ArrayHelpers.Append(ref overrideList, internedLayoutName);
-
-
- m_Layouts.layoutOverrides[baseLayoutName] = overrideList;
- }
- }
-
- PerformLayoutPostRegistration(internedLayoutName, baseLayouts,
- isReplacement: isReplacement, isOverride: isOverride);
-
- // If the layout contained a device matcher, register it.
- if (!deviceMatcher.empty)
- RegisterControlLayoutMatcher(internedLayoutName, deviceMatcher);
- }
-
- public void RegisterControlLayoutBuilder(Func<InputControlLayout> method, string name,
- string baseLayout = null)
- {
- if (method == null)
- throw new ArgumentNullException(nameof(method));
- if (string.IsNullOrEmpty(name))
- throw new ArgumentNullException(nameof(name));
-
- var internedLayoutName = new InternedString(name);
- var internedBaseLayoutName = new InternedString(baseLayout);
- var isReplacement = m_Layouts.HasLayout(internedLayoutName);
-
- m_Layouts.layoutBuilders[internedLayoutName] = method;
-
- PerformLayoutPostRegistration(internedLayoutName, new InlinedArray<InternedString>(internedBaseLayoutName),
- isReplacement);
- }
-
- private void PerformLayoutPostRegistration(InternedString layoutName, InlinedArray<InternedString> baseLayouts,
- bool isReplacement, bool isKnownToBeDeviceLayout = false, bool isOverride = false)
- {
- ++m_LayoutRegistrationVersion;
-
- // Force-clear layout cache. Don't clear reference count so that
- // the cache gets cleared out properly when released in case someone
- // is using it ATM.
- InputControlLayout.s_CacheInstance.Clear();
-
- // For layouts that aren't overrides, add the name of the base
- // layout to the lookup table.
- if (!isOverride && baseLayouts.length > 0)
- {
- if (baseLayouts.length > 1)
- throw new NotSupportedException(
- $"Layout '{layoutName}' has multiple base layouts; this is only supported on layout overrides");
-
- var baseLayoutName = baseLayouts[0];
- if (!baseLayoutName.IsEmpty())
- m_Layouts.baseLayoutTable[layoutName] = baseLayoutName;
- }
-
- // Nuke any precompiled layouts that are invalidated by the layout registration.
- m_Layouts.precompiledLayouts.Remove(layoutName);
- if (m_Layouts.precompiledLayouts.Count > 0)
- {
- foreach (var layout in m_Layouts.precompiledLayouts.Keys.ToArray())
- {
- var metadata = m_Layouts.precompiledLayouts[layout].metadata;
-
- // If it's an override, we remove any precompiled layouts to which overrides are applied.
- if (isOverride)
- {
- for (var i = 0; i < baseLayouts.length; ++i)
- if (layout == baseLayouts[i] ||
- StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(metadata,
- baseLayouts[i], ';'))
- m_Layouts.precompiledLayouts.Remove(layout);
- }
- else
- {
- // Otherwise, we remove any precompile layouts that use the layout we just changed.
- if (StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(metadata,
- layoutName, ';'))
- m_Layouts.precompiledLayouts.Remove(layout);
- }
- }
- }
-
- // Recreate any devices using the layout. If it's an override, recreate devices using any of the base layouts.
- if (isOverride)
- {
- for (var i = 0; i < baseLayouts.length; ++i)
- RecreateDevicesUsingLayout(baseLayouts[i], isKnownToBeDeviceLayout: isKnownToBeDeviceLayout);
- }
- else
- {
- RecreateDevicesUsingLayout(layoutName, isKnownToBeDeviceLayout: isKnownToBeDeviceLayout);
- }
-
- // In the editor, layouts may become available successively after a domain reload so
- // we may end up retaining device information all the way until we run the first full
- // player update. For every layout we register, we check here whether we have a saved
- // device state using a layout with the same name but not having a device description
- // (the latter is important as in that case, we should go through the normal matching
- // process and not just rely on the name of the layout). If so, we try here to recreate
- // the device with the just registered layout.
- #if UNITY_EDITOR
- for (var i = 0; i < m_SavedDeviceStates.LengthSafe(); ++i)
- {
- ref var deviceState = ref m_SavedDeviceStates[i];
- if (layoutName != deviceState.layout || !deviceState.description.empty)
- continue;
-
- if (RestoreDeviceFromSavedState(ref deviceState, layoutName))
- {
- ArrayHelpers.EraseAt(ref m_SavedDeviceStates, i);
- --i;
- }
- }
- #endif
-
- // Let listeners know.
- var change = isReplacement ? InputControlLayoutChange.Replaced : InputControlLayoutChange.Added;
- DelegateHelpers.InvokeCallbacksSafe(ref m_LayoutChangeListeners, layoutName.ToString(), change, "InputSystem.onLayoutChange");
- }
-
- public void RegisterPrecompiledLayout<TDevice>(string metadata)
- where TDevice : InputDevice, new()
- {
- if (metadata == null)
- throw new ArgumentNullException(nameof(metadata));
-
- var deviceType = typeof(TDevice).BaseType;
- var layoutName = FindOrRegisterDeviceLayoutForType(deviceType);
-
- m_Layouts.precompiledLayouts[layoutName] = new InputControlLayout.Collection.PrecompiledLayout
- {
- factoryMethod = () => new TDevice(),
- metadata = metadata
- };
- }
-
- private void RecreateDevicesUsingLayout(InternedString layout, bool isKnownToBeDeviceLayout = false)
- {
- if (m_DevicesCount == 0)
- return;
-
- List<InputDevice> devicesUsingLayout = null;
-
- // Find all devices using the layout.
- for (var i = 0; i < m_DevicesCount; ++i)
- {
- var device = m_Devices[i];
-
- bool usesLayout;
- if (isKnownToBeDeviceLayout)
- usesLayout = IsControlUsingLayout(device, layout);
- else
- usesLayout = IsControlOrChildUsingLayoutRecursive(device, layout);
-
- if (usesLayout)
- {
- if (devicesUsingLayout == null)
- devicesUsingLayout = new List<InputDevice>();
- devicesUsingLayout.Add(device);
- }
- }
-
- // If there's none, we're good.
- if (devicesUsingLayout == null)
- return;
-
- // Remove and re-add the matching devices.
- using (InputDeviceBuilder.Ref())
- {
- for (var i = 0; i < devicesUsingLayout.Count; ++i)
- {
- var device = devicesUsingLayout[i];
- RecreateDevice(device, device.m_Layout);
- }
- }
- }
-
- private bool IsControlOrChildUsingLayoutRecursive(InputControl control, InternedString layout)
- {
- // Check control itself.
- if (IsControlUsingLayout(control, layout))
- return true;
-
- // Check children.
- var children = control.children;
- for (var i = 0; i < children.Count; ++i)
- if (IsControlOrChildUsingLayoutRecursive(children[i], layout))
- return true;
-
- return false;
- }
-
- private bool IsControlUsingLayout(InputControl control, InternedString layout)
- {
- // Check direct match.
- if (control.layout == layout)
- return true;
-
- // Check base layout chain.
- var baseLayout = control.m_Layout;
- while (m_Layouts.baseLayoutTable.TryGetValue(baseLayout, out baseLayout))
- if (baseLayout == layout)
- return true;
-
- return false;
- }
-
- public void RegisterControlLayoutMatcher(string layoutName, InputDeviceMatcher matcher)
- {
- if (string.IsNullOrEmpty(layoutName))
- throw new ArgumentNullException(nameof(layoutName));
- if (matcher.empty)
- throw new ArgumentException("Matcher cannot be empty", nameof(matcher));
-
- // Add to table.
- var internedLayoutName = new InternedString(layoutName);
- m_Layouts.AddMatcher(internedLayoutName, matcher);
-
- // Recreate any device that we match better than its current layout.
- RecreateDevicesUsingLayoutWithInferiorMatch(matcher);
-
- // See if we can make sense of any device we couldn't make sense of before.
- AddAvailableDevicesMatchingDescription(matcher, internedLayoutName);
- }
-
- public void RegisterControlLayoutMatcher(Type type, InputDeviceMatcher matcher)
- {
- if (type == null)
- throw new ArgumentNullException(nameof(type));
- if (matcher.empty)
- throw new ArgumentException("Matcher cannot be empty", nameof(matcher));
-
- var layoutName = m_Layouts.TryFindLayoutForType(type);
- if (layoutName.IsEmpty())
- throw new ArgumentException(
- $"Type '{type.Name}' has not been registered as a control layout", nameof(type));
-
- RegisterControlLayoutMatcher(layoutName, matcher);
- }
-
- private void RecreateDevicesUsingLayoutWithInferiorMatch(InputDeviceMatcher deviceMatcher)
- {
- if (m_DevicesCount == 0)
- return;
-
- using (InputDeviceBuilder.Ref())
- {
- var deviceCount = m_DevicesCount;
- for (var i = 0; i < deviceCount; ++i)
- {
- var device = m_Devices[i];
- var deviceDescription = device.description;
-
- if (deviceDescription.empty || !(deviceMatcher.MatchPercentage(deviceDescription) > 0))
- continue;
-
- var layoutName = TryFindMatchingControlLayout(ref deviceDescription, device.deviceId);
- if (layoutName != device.m_Layout)
- {
- device.m_Description = deviceDescription;
-
- RecreateDevice(device, layoutName);
-
- // We're removing devices in the middle of the array and appending
- // them at the end. Adjust our index and device count to make sure
- // we're not iterating all the way into already processed devices.
-
- --i;
- --deviceCount;
- }
- }
- }
- }
-
- private void RecreateDevice(InputDevice oldDevice, InternedString newLayout)
- {
- // Remove.
- RemoveDevice(oldDevice, keepOnListOfAvailableDevices: true);
-
- // Re-setup device.
- var newDevice = InputDevice.Build<InputDevice>(newLayout, oldDevice.m_Variants,
- deviceDescription: oldDevice.m_Description);
-
- // Preserve device properties that should not be changed by the re-creation
- // of a device.
- newDevice.m_DeviceId = oldDevice.m_DeviceId;
- newDevice.m_Description = oldDevice.m_Description;
- if (oldDevice.native)
- newDevice.m_DeviceFlags |= InputDevice.DeviceFlags.Native;
- if (oldDevice.remote)
- newDevice.m_DeviceFlags |= InputDevice.DeviceFlags.Remote;
- if (!oldDevice.enabled)
- {
- newDevice.m_DeviceFlags |= InputDevice.DeviceFlags.DisabledStateHasBeenQueriedFromRuntime;
- newDevice.m_DeviceFlags |= InputDevice.DeviceFlags.DisabledInFrontend;
- }
-
- // Re-add.
- AddDevice(newDevice);
- }
-
- private void AddAvailableDevicesMatchingDescription(InputDeviceMatcher matcher, InternedString layout)
- {
- #if UNITY_EDITOR
- // If we still have some devices saved from the last domain reload, see
- // if they are matched by the given matcher. If so, turn them into devices.
- for (var i = 0; i < m_SavedDeviceStates.LengthSafe(); ++i)
- {
- ref var deviceState = ref m_SavedDeviceStates[i];
- if (matcher.MatchPercentage(deviceState.description) > 0)
- {
- RestoreDeviceFromSavedState(ref deviceState, layout);
- ArrayHelpers.EraseAt(ref m_SavedDeviceStates, i);
- --i;
- }
- }
- #endif
-
- // See if the new description to layout mapping allows us to make
- // sense of a device we couldn't make sense of so far.
- for (var i = 0; i < m_AvailableDeviceCount; ++i)
- {
- // Ignore if it's a device that has been explicitly removed.
- if (m_AvailableDevices[i].isRemoved)
- continue;
-
- var deviceId = m_AvailableDevices[i].deviceId;
- if (TryGetDeviceById(deviceId) != null)
- continue;
-
- if (matcher.MatchPercentage(m_AvailableDevices[i].description) > 0f)
- {
- // Try to create InputDevice instance.
- try
- {
- AddDevice(layout, deviceId, deviceDescription: m_AvailableDevices[i].description,
- deviceFlags: m_AvailableDevices[i].isNative ? InputDevice.DeviceFlags.Native : 0);
- }
- catch (Exception exception)
- {
- Debug.LogError(
- $"Layout '{layout}' matches existing device '{m_AvailableDevices[i].description}' but failed to instantiate: {exception}");
- Debug.LogException(exception);
- continue;
- }
-
- // Re-enable device.
- var command = EnableDeviceCommand.Create();
- m_Runtime.DeviceCommand(deviceId, ref command);
- }
- }
- }
-
- public void RemoveControlLayout(string name)
- {
- if (string.IsNullOrEmpty(name))
- throw new ArgumentNullException(nameof(name));
-
- var internedName = new InternedString(name);
-
- // Remove all devices using the layout.
- for (var i = 0; i < m_DevicesCount;)
- {
- var device = m_Devices[i];
- if (IsControlOrChildUsingLayoutRecursive(device, internedName))
- {
- RemoveDevice(device, keepOnListOfAvailableDevices: true);
- }
- else
- {
- ++i;
- }
- }
-
- // Remove layout record.
- m_Layouts.layoutTypes.Remove(internedName);
- m_Layouts.layoutStrings.Remove(internedName);
- m_Layouts.layoutBuilders.Remove(internedName);
- m_Layouts.baseLayoutTable.Remove(internedName);
- ++m_LayoutRegistrationVersion;
-
- ////TODO: check all layout inheritance chain for whether they are based on the layout and if so
- //// remove those layouts, too
-
- // Let listeners know.
- DelegateHelpers.InvokeCallbacksSafe(ref m_LayoutChangeListeners, name, InputControlLayoutChange.Removed, "InputSystem.onLayoutChange");
- }
-
- public InputControlLayout TryLoadControlLayout(Type type)
- {
- if (type == null)
- throw new ArgumentNullException(nameof(type));
- if (!typeof(InputControl).IsAssignableFrom(type))
- throw new ArgumentException($"Type '{type.Name}' is not an InputControl", nameof(type));
-
- // Find the layout name that the given type was registered with.
- var layoutName = m_Layouts.TryFindLayoutForType(type);
- if (layoutName.IsEmpty())
- throw new ArgumentException(
- $"Type '{type.Name}' has not been registered as a control layout", nameof(type));
-
- return m_Layouts.TryLoadLayout(layoutName);
- }
-
- public InputControlLayout TryLoadControlLayout(InternedString name)
- {
- return m_Layouts.TryLoadLayout(name);
- }
-
- ////FIXME: allowing the description to be modified as part of this is surprising; find a better way
- public InternedString TryFindMatchingControlLayout(ref InputDeviceDescription deviceDescription, int deviceId = InputDevice.InvalidDeviceId)
- {
- InternedString layoutName = new InternedString(string.Empty);
- try
- {
- Profiler.BeginSample("InputSystem.TryFindMatchingControlLayout");
- ////TODO: this will want to take overrides into account
-
- // See if we can match by description.
- layoutName = m_Layouts.TryFindMatchingLayout(deviceDescription);
- if (layoutName.IsEmpty())
- {
- // No, so try to match by device class. If we have a "Gamepad" layout,
- // for example, a device that classifies itself as a "Gamepad" will match
- // that layout.
- //
- // NOTE: Have to make sure here that we get a device layout and not a
- // control layout.
- if (!string.IsNullOrEmpty(deviceDescription.deviceClass))
- {
- var deviceClassLowerCase = new InternedString(deviceDescription.deviceClass);
- var type = m_Layouts.GetControlTypeForLayout(deviceClassLowerCase);
- if (type != null && typeof(InputDevice).IsAssignableFrom(type))
- layoutName = new InternedString(deviceDescription.deviceClass);
- }
- }
-
- ////REVIEW: listeners registering new layouts from in here may potentially lead to the creation of devices; should we disallow that?
- ////REVIEW: if a callback picks a layout, should we re-run through the list of callbacks? or should we just remove haveOverridenLayoutName?
- // Give listeners a shot to select/create a layout.
- if (m_DeviceFindLayoutCallbacks.length > 0)
- {
- // First time we get here, put our delegate for executing device commands
- // in place. We wrap the call to IInputRuntime.DeviceCommand so that we don't
- // need to expose the runtime to the onFindLayoutForDevice callbacks.
- if (m_DeviceFindExecuteCommandDelegate == null)
- m_DeviceFindExecuteCommandDelegate =
- (ref InputDeviceCommand commandRef) =>
- {
- if (m_DeviceFindExecuteCommandDeviceId == InputDevice.InvalidDeviceId)
- return InputDeviceCommand.GenericFailure;
- return m_Runtime.DeviceCommand(m_DeviceFindExecuteCommandDeviceId, ref commandRef);
- };
- m_DeviceFindExecuteCommandDeviceId = deviceId;
-
- var haveOverriddenLayoutName = false;
- m_DeviceFindLayoutCallbacks.LockForChanges();
- for (var i = 0; i < m_DeviceFindLayoutCallbacks.length; ++i)
- {
- try
- {
- var newLayout = m_DeviceFindLayoutCallbacks[i](ref deviceDescription, layoutName, m_DeviceFindExecuteCommandDelegate);
- if (!string.IsNullOrEmpty(newLayout) && !haveOverriddenLayoutName)
- {
- layoutName = new InternedString(newLayout);
- haveOverriddenLayoutName = true;
- }
- }
- catch (Exception exception)
- {
- Debug.LogError($"{exception.GetType().Name} while executing 'InputSystem.onFindLayoutForDevice' callbacks");
- Debug.LogException(exception);
- }
- }
- m_DeviceFindLayoutCallbacks.UnlockForChanges();
- }
- }
- finally
- {
- Profiler.EndSample();
- }
- return layoutName;
- }
-
- private InternedString FindOrRegisterDeviceLayoutForType(Type type)
- {
- var layoutName = m_Layouts.TryFindLayoutForType(type);
- if (layoutName.IsEmpty())
- {
- // Automatically register the given type as a layout.
- if (layoutName.IsEmpty())
- {
- layoutName = new InternedString(type.Name);
- RegisterControlLayout(type.Name, type);
- }
- }
-
- return layoutName;
- }
-
- /// <summary>
- /// Return true if the given device layout is supported by the game according to <see cref="InputSettings.supportedDevices"/>.
- /// </summary>
- /// <param name="layoutName">Name of the device layout.</param>
- /// <returns>True if a device with the given layout should be created for the game, false otherwise.</returns>
- private bool IsDeviceLayoutMarkedAsSupportedInSettings(InternedString layoutName)
- {
- // In the editor, "Supported Devices" can be overridden by a user setting. This causes
- // all available devices to be added regardless of what "Supported Devices" says. This
- // is useful to ensure that things like keyboard, mouse, and pen keep working in the editor
- // even if not supported as devices in the game.
- #if UNITY_EDITOR
- if (InputEditorUserSettings.addDevicesNotSupportedByProject)
- return true;
- #endif
-
- var supportedDevices = m_Settings.supportedDevices;
- if (supportedDevices.Count == 0)
- {
- // If supportedDevices is empty, all device layouts are considered supported.
- return true;
- }
-
- for (var n = 0; n < supportedDevices.Count; ++n)
- {
- var supportedLayout = new InternedString(supportedDevices[n]);
- if (layoutName == supportedLayout || m_Layouts.IsBasedOn(supportedLayout, layoutName))
- return true;
- }
-
- return false;
- }
-
- public IEnumerable<string> ListControlLayouts(string basedOn = null)
- {
- ////FIXME: this may add a name twice
-
- if (!string.IsNullOrEmpty(basedOn))
- {
- var internedBasedOn = new InternedString(basedOn);
- foreach (var entry in m_Layouts.layoutTypes)
- if (m_Layouts.IsBasedOn(internedBasedOn, entry.Key))
- yield return entry.Key;
- foreach (var entry in m_Layouts.layoutStrings)
- if (m_Layouts.IsBasedOn(internedBasedOn, entry.Key))
- yield return entry.Key;
- foreach (var entry in m_Layouts.layoutBuilders)
- if (m_Layouts.IsBasedOn(internedBasedOn, entry.Key))
- yield return entry.Key;
- }
- else
- {
- foreach (var entry in m_Layouts.layoutTypes)
- yield return entry.Key;
- foreach (var entry in m_Layouts.layoutStrings)
- yield return entry.Key;
- foreach (var entry in m_Layouts.layoutBuilders)
- yield return entry.Key;
- }
- }
-
- // Adds all controls that match the given path spec to the given list.
- // Returns number of controls added to the list.
- // NOTE: Does not create garbage.
-
- /// <summary>
- /// Adds to the given list all controls that match the given <see cref="InputControlPath">path spec</see>
- /// and are assignable to the given type.
- /// </summary>
- /// <param name="path"></param>
- /// <param name="controls"></param>
- /// <typeparam name="TControl"></typeparam>
- /// <returns></returns>
- public int GetControls<TControl>(string path, ref InputControlList<TControl> controls)
- where TControl : InputControl
- {
- if (string.IsNullOrEmpty(path))
- return 0;
- if (m_DevicesCount == 0)
- return 0;
-
- var deviceCount = m_DevicesCount;
- var numMatches = 0;
- for (var i = 0; i < deviceCount; ++i)
- {
- var device = m_Devices[i];
- numMatches += InputControlPath.TryFindControls(device, path, 0, ref controls);
- }
-
- return numMatches;
- }
-
- public void SetDeviceUsage(InputDevice device, InternedString usage)
- {
- if (device == null)
- throw new ArgumentNullException(nameof(device));
- if (device.usages.Count == 1 && device.usages[0] == usage)
- return;
- if (device.usages.Count == 0 && usage.IsEmpty())
- return;
-
- device.ClearDeviceUsages();
- if (!usage.IsEmpty())
- device.AddDeviceUsage(usage);
- NotifyUsageChanged(device);
- }
-
- public void AddDeviceUsage(InputDevice device, InternedString usage)
- {
- if (device == null)
- throw new ArgumentNullException(nameof(device));
- if (usage.IsEmpty())
- throw new ArgumentException("Usage string cannot be empty", nameof(usage));
- if (device.usages.Contains(usage))
- return;
-
- device.AddDeviceUsage(usage);
- NotifyUsageChanged(device);
- }
-
- public void RemoveDeviceUsage(InputDevice device, InternedString usage)
- {
- if (device == null)
- throw new ArgumentNullException(nameof(device));
- if (usage.IsEmpty())
- throw new ArgumentException("Usage string cannot be empty", nameof(usage));
- if (!device.usages.Contains(usage))
- return;
-
- device.RemoveDeviceUsage(usage);
- NotifyUsageChanged(device);
- }
-
- private void NotifyUsageChanged(InputDevice device)
- {
- InputActionState.OnDeviceChange(device, InputDeviceChange.UsageChanged);
-
- // Notify listeners.
- DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, InputDeviceChange.UsageChanged, "InputSystem.onDeviceChange");
-
- ////REVIEW: This was for the XRController leftHand and rightHand getters but these do lookups dynamically now; remove?
- // Usage may affect current device so update.
- device.MakeCurrent();
- }
-
- ////TODO: make sure that no device or control with a '/' in the name can creep into the system
-
- public InputDevice AddDevice(Type type, string name = null)
- {
- if (type == null)
- throw new ArgumentNullException(nameof(type));
-
- // Find the layout name that the given type was registered with.
- var layoutName = FindOrRegisterDeviceLayoutForType(type);
- Debug.Assert(!layoutName.IsEmpty(), name);
-
- // Note that since we go through the normal by-name lookup here, this will
- // still work if the layout from the type was override with a string layout.
- return AddDevice(layoutName, name);
- }
-
- // Creates a device from the given layout and adds it to the system.
- // NOTE: Creates garbage.
- public InputDevice AddDevice(string layout, string name = null, InternedString variants = new InternedString())
- {
- if (string.IsNullOrEmpty(layout))
- throw new ArgumentNullException(nameof(layout));
-
- var device = InputDevice.Build<InputDevice>(layout, variants);
-
- if (!string.IsNullOrEmpty(name))
- device.m_Name = new InternedString(name);
-
- AddDevice(device);
-
- return device;
- }
-
- // Add device with a forced ID. Used when creating devices reported to us by native.
- private InputDevice AddDevice(InternedString layout, int deviceId,
- string deviceName = null,
- InputDeviceDescription deviceDescription = new InputDeviceDescription(),
- InputDevice.DeviceFlags deviceFlags = 0,
- InternedString variants = default)
- {
- var device = InputDevice.Build<InputDevice>(new InternedString(layout),
- deviceDescription: deviceDescription,
- layoutVariants: variants);
-
- device.m_DeviceId = deviceId;
- device.m_Description = deviceDescription;
- device.m_DeviceFlags |= deviceFlags;
- if (!string.IsNullOrEmpty(deviceName))
- device.m_Name = new InternedString(deviceName);
-
- // Default display name to product name.
- if (!string.IsNullOrEmpty(deviceDescription.product))
- device.m_DisplayName = deviceDescription.product;
-
- AddDevice(device);
-
- return device;
- }
-
- public void AddDevice(InputDevice device)
- {
- if (device == null)
- throw new ArgumentNullException(nameof(device));
- if (string.IsNullOrEmpty(device.layout))
- throw new InvalidOperationException("Device has no associated layout");
-
- // Ignore if the same device gets added multiple times.
- if (ArrayHelpers.Contains(m_Devices, device))
- return;
-
- MakeDeviceNameUnique(device);
- AssignUniqueDeviceId(device);
-
- // Add to list.
- device.m_DeviceIndex = ArrayHelpers.AppendWithCapacity(ref m_Devices, ref m_DevicesCount, device);
-
- ////REVIEW: Not sure a full-blown dictionary is the right way here. Alternatives are to keep
- //// a sparse array that directly indexes using the linearly increasing IDs (though that
- //// may get large over time). Or to just do a linear search through m_Devices (but
- //// that may end up tapping a bunch of memory locations in the heap to find the right
- //// device; could be improved by sorting m_Devices by ID and picking a good starting
- //// point based on the ID we have instead of searching from [0] always).
- m_DevicesById[device.deviceId] = device;
-
- // Let InputStateBuffers know this device doesn't have any associated state yet.
- device.m_StateBlock.byteOffset = InputStateBlock.InvalidOffset;
-
- // Update state buffers.
- ReallocateStateBuffers();
- InitializeDeviceState(device);
-
- // Update metrics.
- m_Metrics.maxNumDevices = Mathf.Max(m_DevicesCount, m_Metrics.maxNumDevices);
- m_Metrics.maxStateSizeInBytes = Mathf.Max((int)m_StateBuffers.totalSize, m_Metrics.maxStateSizeInBytes);
-
- // Make sure that if the device ID is listed in m_AvailableDevices, the device
- // is no longer marked as removed.
- for (var i = 0; i < m_AvailableDeviceCount; ++i)
- {
- if (m_AvailableDevices[i].deviceId == device.deviceId)
- m_AvailableDevices[i].isRemoved = false;
- }
-
- // If we're running in the background, find out whether the device can run in
- // the background. If not, disable it.
- var isPlaying = true;
- #if UNITY_EDITOR
- isPlaying = m_Runtime.isInPlayMode;
- #endif
- if (isPlaying && !gameHasFocus
- && m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.IgnoreFocus
- && m_Runtime.runInBackground
- && device.QueryEnabledStateFromRuntime()
- && !ShouldRunDeviceInBackground(device))
- {
- EnableOrDisableDevice(device, false, DeviceDisableScope.TemporaryWhilePlayerIsInBackground);
- }
-
- ////REVIEW: we may want to suppress this during the initial device discovery phase
- // Let actions re-resolve their paths.
- InputActionState.OnDeviceChange(device, InputDeviceChange.Added);
-
- // If the device wants automatic callbacks before input updates,
- // put it on the list.
- if (device is IInputUpdateCallbackReceiver beforeUpdateCallbackReceiver)
- onBeforeUpdate += beforeUpdateCallbackReceiver.OnUpdate;
-
- // If the device has state callbacks, make a note of it.
- if (device is IInputStateCallbackReceiver)
- {
- InstallBeforeUpdateHookIfNecessary();
- device.m_DeviceFlags |= InputDevice.DeviceFlags.HasStateCallbacks;
- m_HaveDevicesWithStateCallbackReceivers = true;
- }
-
- // If the device has event merger, make a note of it.
- if (device is IEventMerger)
- device.hasEventMerger = true;
-
- // If the device has event preprocessor, make a note of it.
- if (device is IEventPreProcessor)
- device.hasEventPreProcessor = true;
-
- // If the device wants before-render updates, enable them if they
- // aren't already.
- if (device.updateBeforeRender)
- updateMask |= InputUpdateType.BeforeRender;
-
- // Notify device.
- device.NotifyAdded();
-
- ////REVIEW: is this really a good thing to do? just plugging in a device shouldn't make
- //// it current, no?
- // Make the device current.
- // BEWARE: if this will not happen for whatever reason, you will break Android sensors,
- // as they rely on .current for enabling native backend, see https://fogbugz.unity3d.com/f/cases/1371204/
- device.MakeCurrent();
-
- // Notify listeners.
- DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, InputDeviceChange.Added, "InputSystem.onDeviceChange");
-
- // Request device to send us an initial state update.
- if (device.enabled)
- device.RequestSync();
-
- device.SetOptimizedControlDataTypeRecursively();
- }
-
- ////TODO: this path should really put the device on the list of available devices
- ////TODO: this path should discover disconnected devices
- public InputDevice AddDevice(InputDeviceDescription description)
- {
- ////REVIEW: is throwing here really such a useful thing?
- return AddDevice(description, throwIfNoLayoutFound: true);
- }
-
- public InputDevice AddDevice(InputDeviceDescription description, bool throwIfNoLayoutFound,
- string deviceName = null, int deviceId = InputDevice.InvalidDeviceId, InputDevice.DeviceFlags deviceFlags = 0)
- {
- Profiler.BeginSample("InputSystem.AddDevice");
- // Look for matching layout.
- var layout = TryFindMatchingControlLayout(ref description, deviceId);
-
- // If no layout was found, bail out.
- if (layout.IsEmpty())
- {
- if (throwIfNoLayoutFound)
- {
- Profiler.EndSample();
- throw new ArgumentException($"Cannot find layout matching device description '{description}'", nameof(description));
- }
-
- // If it's a device coming from the runtime, disable it.
- if (deviceId != InputDevice.InvalidDeviceId)
- {
- var command = DisableDeviceCommand.Create();
- m_Runtime.DeviceCommand(deviceId, ref command);
- }
-
- Profiler.EndSample();
- return null;
- }
-
- var device = AddDevice(layout, deviceId, deviceName, description, deviceFlags);
- device.m_Description = description;
- Profiler.EndSample();
- return device;
- }
-
- public InputDevice AddDevice(InputDeviceDescription description, InternedString layout, string deviceName = null,
- int deviceId = InputDevice.InvalidDeviceId, InputDevice.DeviceFlags deviceFlags = 0)
- {
- try
- {
- Profiler.BeginSample("InputSystem.AddDevice");
-
- var device = AddDevice(layout, deviceId, deviceName, description, deviceFlags);
- device.m_Description = description;
- return device;
- }
- finally
- {
- Profiler.EndSample();
- }
- }
-
- public void RemoveDevice(InputDevice device, bool keepOnListOfAvailableDevices = false)
- {
- if (device == null)
- throw new ArgumentNullException(nameof(device));
-
- // If device has not been added, ignore.
- if (device.m_DeviceIndex == InputDevice.kInvalidDeviceIndex)
- return;
-
- // Remove state monitors while device index is still valid.
- RemoveStateChangeMonitors(device);
-
- // Remove from device array.
- var deviceIndex = device.m_DeviceIndex;
- var deviceId = device.deviceId;
- if (deviceIndex < m_StateChangeMonitors.LengthSafe())
- {
- // m_StateChangeMonitors mirrors layout of m_Devices *but* may be shorter.
- var count = m_StateChangeMonitors.Length;
- ArrayHelpers.EraseAtWithCapacity(m_StateChangeMonitors, ref count, deviceIndex);
- }
- ArrayHelpers.EraseAtWithCapacity(m_Devices, ref m_DevicesCount, deviceIndex);
-
- m_DevicesById.Remove(deviceId);
-
- if (m_Devices != null)
- {
- // Remove from state buffers.
- ReallocateStateBuffers();
- }
- else
- {
- // No more devices. Kill state buffers.
- m_StateBuffers.FreeAll();
- }
-
- ////TODO: When we remove a native device like this, make sure we tell the backend to disable it (and re-enable it when re-add it)
-
- // Update device indices. Do this after reallocating state buffers as that call requires
- // the old indices to still be in place.
- for (var i = deviceIndex; i < m_DevicesCount; ++i)
- --m_Devices[i].m_DeviceIndex; // Indices have shifted down by one.
- device.m_DeviceIndex = InputDevice.kInvalidDeviceIndex;
-
- // Update list of available devices.
- for (var i = 0; i < m_AvailableDeviceCount; ++i)
- {
- if (m_AvailableDevices[i].deviceId == deviceId)
- {
- if (keepOnListOfAvailableDevices)
- m_AvailableDevices[i].isRemoved = true;
- else
- ArrayHelpers.EraseAtWithCapacity(m_AvailableDevices, ref m_AvailableDeviceCount, i);
- break;
- }
- }
-
- // Unbake offset into global state buffers.
- device.BakeOffsetIntoStateBlockRecursive((uint)-device.m_StateBlock.byteOffset);
-
- // Force enabled actions to remove controls from the device.
- // We've already set the device index to be invalid so we any attempts
- // by actions to uninstall state monitors will get ignored.
- InputActionState.OnDeviceChange(device, InputDeviceChange.Removed);
-
- // Kill before update callback, if applicable.
- if (device is IInputUpdateCallbackReceiver beforeUpdateCallbackReceiver)
- onBeforeUpdate -= beforeUpdateCallbackReceiver.OnUpdate;
-
- // Disable before-render updates if this was the last device
- // that requires them.
- if (device.updateBeforeRender)
- {
- var haveDeviceRequiringBeforeRender = false;
- for (var i = 0; i < m_DevicesCount; ++i)
- if (m_Devices[i].updateBeforeRender)
- {
- haveDeviceRequiringBeforeRender = true;
- break;
- }
-
- if (!haveDeviceRequiringBeforeRender)
- updateMask &= ~InputUpdateType.BeforeRender;
- }
-
- // Let device know.
- device.NotifyRemoved();
-
- // Let listeners know.
- DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, InputDeviceChange.Removed, "InputSystem.onDeviceChange");
-
- // Try setting next device of same type as current
- InputSystem.GetDevice(device.GetType())?.MakeCurrent();
- }
-
- public void FlushDisconnectedDevices()
- {
- m_DisconnectedDevices.Clear(m_DisconnectedDevicesCount);
- m_DisconnectedDevicesCount = 0;
- }
-
- public unsafe void ResetDevice(InputDevice device, bool alsoResetDontResetControls = false, bool? issueResetCommand = null)
- {
- if (device == null)
- throw new ArgumentNullException(nameof(device));
- if (!device.added)
- throw new InvalidOperationException($"Device '{device}' has not been added to the system");
-
- var isHardReset = alsoResetDontResetControls || !device.hasDontResetControls;
-
- // Trigger reset notification.
- var change = isHardReset ? InputDeviceChange.HardReset : InputDeviceChange.SoftReset;
- InputActionState.OnDeviceChange(device, change);
- DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, change, "onDeviceChange");
-
- // If the device implements its own reset, let it handle it.
- if (!alsoResetDontResetControls && device is ICustomDeviceReset customReset)
- {
- customReset.Reset();
- }
- else
- {
- var defaultStatePtr = device.defaultStatePtr;
- var deviceStateBlockSize = device.stateBlock.alignedSizeInBytes;
-
- // Allocate temp memory to hold one state event.
- ////REVIEW: the need for an event here is sufficiently obscure to warrant scrutiny; likely, there's a better way
- //// to tell synthetic input (or input sources in general) apart
- // NOTE: We wrap the reset in an artificial state event so that it appears to the rest of the system
- // like any other input. If we don't do that but rather just call UpdateState() with a null event
- // pointer, the change will be considered an internal state change and will get ignored by some
- // pieces of code (such as EnhancedTouch which filters out internal state changes of Touchscreen
- // by ignoring any change that is not coming from an input event).
- using (var tempBuffer =
- new NativeArray<byte>(InputEvent.kBaseEventSize + sizeof(int) + (int)deviceStateBlockSize, Allocator.Temp))
- {
- var stateEventPtr = (StateEvent*)tempBuffer.GetUnsafePtr();
- var statePtr = stateEventPtr->state;
- var currentTime = m_Runtime.currentTime;
-
- // Set up the state event.
- ref var stateBlock = ref device.m_StateBlock;
- stateEventPtr->baseEvent.type = StateEvent.Type;
- stateEventPtr->baseEvent.sizeInBytes = InputEvent.kBaseEventSize + sizeof(int) + deviceStateBlockSize;
- stateEventPtr->baseEvent.time = currentTime;
- stateEventPtr->baseEvent.deviceId = device.deviceId;
- stateEventPtr->baseEvent.eventId = -1;
- stateEventPtr->stateFormat = device.m_StateBlock.format;
-
- // Decide whether we perform a soft reset or a hard reset.
- if (isHardReset)
- {
- // Perform a hard reset where we wipe the entire device and set a full
- // reset request to the backend.
- UnsafeUtility.MemCpy(statePtr,
- (byte*)defaultStatePtr + stateBlock.byteOffset,
- deviceStateBlockSize);
- }
- else
- {
- // Perform a soft reset where we exclude any dontReset control (which is automatically
- // toggled on for noisy controls) and do *NOT* send a reset request to the backend.
-
- var currentStatePtr = device.currentStatePtr;
- var resetMaskPtr = m_StateBuffers.resetMaskBuffer;
-
- // To preserve values from dontReset controls, we need to first copy their current values.
- UnsafeUtility.MemCpy(statePtr,
- (byte*)currentStatePtr + stateBlock.byteOffset,
- deviceStateBlockSize);
-
- // And then we copy over default values masked by dontReset bits.
- MemoryHelpers.MemCpyMasked(statePtr,
- (byte*)defaultStatePtr + stateBlock.byteOffset,
- (int)deviceStateBlockSize,
- (byte*)resetMaskPtr + stateBlock.byteOffset);
- }
-
- UpdateState(device, defaultUpdateType, statePtr, 0, deviceStateBlockSize, currentTime,
- new InputEventPtr((InputEvent*)stateEventPtr));
- }
- }
-
- // In the editor, we don't want to issue RequestResetCommand to devices based on focus of the game view
- // as this would also reset device state for the editor. And we don't need the reset commands in this case
- // as -- unlike in the player --, Unity keeps running and we will keep seeing OS messages for these devices.
- // So, in the editor, we generally suppress reset commands.
- //
- // The only exception is when the editor itself loses focus. We issue sync requests to all devices when
- // coming back into focus. But for any device that doesn't support syncs, we actually do want to have a
- // reset command reach the background.
- //
- // Finally, in the player, we also avoid reset commands when disabling a device as these are pointless.
- // We sync/reset when enabling a device in the backend.
- var doIssueResetCommand = isHardReset;
- if (issueResetCommand != null)
- doIssueResetCommand = issueResetCommand.Value;
- #if UNITY_EDITOR
- else if (m_Settings.editorInputBehaviorInPlayMode != InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView)
- doIssueResetCommand = false;
- #endif
-
- if (doIssueResetCommand)
- device.RequestReset();
- }
-
- public InputDevice TryGetDevice(string nameOrLayout)
- {
- if (string.IsNullOrEmpty(nameOrLayout))
- throw new ArgumentException("Name is null or empty.", nameof(nameOrLayout));
-
- if (m_DevicesCount == 0)
- return null;
-
- var nameOrLayoutLowerCase = nameOrLayout.ToLower();
-
- for (var i = 0; i < m_DevicesCount; ++i)
- {
- var device = m_Devices[i];
- if (device.m_Name.ToLower() == nameOrLayoutLowerCase ||
- device.m_Layout.ToLower() == nameOrLayoutLowerCase)
- return device;
- }
-
- return null;
- }
-
- public InputDevice GetDevice(string nameOrLayout)
- {
- var device = TryGetDevice(nameOrLayout);
- if (device == null)
- throw new ArgumentException($"Cannot find device with name or layout '{nameOrLayout}'", nameof(nameOrLayout));
-
- return device;
- }
-
- public InputDevice TryGetDevice(Type layoutType)
- {
- var layoutName = m_Layouts.TryFindLayoutForType(layoutType);
- if (layoutName.IsEmpty())
- return null;
-
- return TryGetDevice(layoutName);
- }
-
- public InputDevice TryGetDeviceById(int id)
- {
- if (m_DevicesById.TryGetValue(id, out var result))
- return result;
- return null;
- }
-
- // Adds any device that's been reported to the system but could not be matched to
- // a layout to the given list.
- public int GetUnsupportedDevices(List<InputDeviceDescription> descriptions)
- {
- if (descriptions == null)
- throw new ArgumentNullException(nameof(descriptions));
-
- var numFound = 0;
- for (var i = 0; i < m_AvailableDeviceCount; ++i)
- {
- if (TryGetDeviceById(m_AvailableDevices[i].deviceId) != null)
- continue;
-
- descriptions.Add(m_AvailableDevices[i].description);
- ++numFound;
- }
-
- return numFound;
- }
-
- // We have three different levels of disabling a device.
- internal enum DeviceDisableScope
- {
- Everywhere, // Device is disabled globally and explicitly. Should neither send nor receive events.
- InFrontendOnly, // Device is only disabled on managed side but not in backend. Should keep sending events but should not receive them (useful for redirecting their data).
- TemporaryWhilePlayerIsInBackground, // Device has been disabled automatically and temporarily by system while application is running in the background.
- }
-
- public void EnableOrDisableDevice(InputDevice device, bool enable, DeviceDisableScope scope = default)
- {
- if (device == null)
- throw new ArgumentNullException(nameof(device));
-
- // Synchronize the enable/disabled state of the device.
- if (enable)
- {
- ////REVIEW: Do we really want to allow overriding disabledWhileInBackground like it currently does?
-
- // Enable device.
- switch (scope)
- {
- case DeviceDisableScope.Everywhere:
- device.disabledWhileInBackground = false;
- if (!device.disabledInFrontend && !device.disabledInRuntime)
- return;
- if (device.disabledInRuntime)
- {
- device.ExecuteEnableCommand();
- device.disabledInRuntime = false;
- }
- if (device.disabledInFrontend)
- {
- if (!device.RequestSync())
- ResetDevice(device);
- device.disabledInFrontend = false;
- }
- break;
-
- case DeviceDisableScope.InFrontendOnly:
- device.disabledWhileInBackground = false;
- if (!device.disabledInFrontend && device.disabledInRuntime)
- return;
- if (!device.disabledInRuntime)
- {
- device.ExecuteDisableCommand();
- device.disabledInRuntime = true;
- }
- if (device.disabledInFrontend)
- {
- if (!device.RequestSync())
- ResetDevice(device);
- device.disabledInFrontend = false;
- }
- break;
-
- case DeviceDisableScope.TemporaryWhilePlayerIsInBackground:
- if (device.disabledWhileInBackground)
- {
- if (device.disabledInRuntime)
- {
- device.ExecuteEnableCommand();
- device.disabledInRuntime = false;
- }
- if (!device.RequestSync())
- ResetDevice(device);
- device.disabledWhileInBackground = false;
- }
- break;
- }
- }
- else
- {
- // Disable device.
- switch (scope)
- {
- case DeviceDisableScope.Everywhere:
- device.disabledWhileInBackground = false;
- if (device.disabledInFrontend && device.disabledInRuntime)
- return;
- if (!device.disabledInRuntime)
- {
- device.ExecuteDisableCommand();
- device.disabledInRuntime = true;
- }
- if (!device.disabledInFrontend)
- {
- // When disabling a device, also issuing a reset in the backend is pointless.
- ResetDevice(device, issueResetCommand: false);
- device.disabledInFrontend = true;
- }
- break;
-
- case DeviceDisableScope.InFrontendOnly:
- device.disabledWhileInBackground = false;
- if (!device.disabledInRuntime && device.disabledInFrontend)
- return;
- if (device.disabledInRuntime)
- {
- device.ExecuteEnableCommand();
- device.disabledInRuntime = false;
- }
- if (!device.disabledInFrontend)
- {
- // When disabling a device, also issuing a reset in the backend is pointless.
- ResetDevice(device, issueResetCommand: false);
- device.disabledInFrontend = true;
- }
- break;
-
- case DeviceDisableScope.TemporaryWhilePlayerIsInBackground:
- // Won't flag a device as DisabledWhileInBackground if it is explicitly disabled in
- // the frontend.
- if (device.disabledInFrontend || device.disabledWhileInBackground)
- return;
- device.disabledWhileInBackground = true;
- ResetDevice(device, issueResetCommand: false);
- #if UNITY_EDITOR
- if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView)
- #endif
- {
- device.ExecuteDisableCommand();
- device.disabledInRuntime = true;
- }
- break;
- }
- }
-
- // Let listeners know.
- var deviceChange = enable ? InputDeviceChange.Enabled : InputDeviceChange.Disabled;
- DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, deviceChange, "InputSystem.onDeviceChange");
- }
-
- private unsafe void QueueEvent(InputEvent* eventPtr)
- {
- // If we're currently in OnUpdate(), the m_InputEventStream will be open. In that case,
- // append events directly to that buffer and do *NOT* go into native.
- if (m_InputEventStream.isOpen)
- {
- m_InputEventStream.Write(eventPtr);
- return;
- }
-
- // Don't bother keeping the data on the managed side. Just stuff the raw data directly
- // into the native buffers. This also means this method is thread-safe.
- m_Runtime.QueueEvent(eventPtr);
- }
-
- public unsafe void QueueEvent(InputEventPtr ptr)
- {
- QueueEvent(ptr.data);
- }
-
- public unsafe void QueueEvent<TEvent>(ref TEvent inputEvent)
- where TEvent : struct, IInputEventTypeInfo
- {
- QueueEvent((InputEvent*)UnsafeUtility.AddressOf(ref inputEvent));
- }
-
- public void Update()
- {
- Update(defaultUpdateType);
- }
-
- public void Update(InputUpdateType updateType)
- {
- m_Runtime.Update(updateType);
- }
-
- internal void Initialize(IInputRuntime runtime, InputSettings settings)
- {
- Debug.Assert(settings != null);
-
- m_Settings = settings;
-
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- InitializeActions();
- #endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- InitializeData();
- InstallRuntime(runtime);
- InstallGlobals();
-
- ApplySettings();
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- ApplyActions();
- #endif
- }
-
- internal void Destroy()
- {
- // There isn't really much of a point in removing devices but we still
- // want to clear out any global state they may be keeping. So just tell
- // the devices that they got removed without actually removing them.
- for (var i = 0; i < m_DevicesCount; ++i)
- m_Devices[i].NotifyRemoved();
-
- // Free all state memory.
- m_StateBuffers.FreeAll();
-
- // Uninstall globals.
- UninstallGlobals();
-
- // Destroy settings if they are temporary.
- if (m_Settings != null && m_Settings.hideFlags == HideFlags.HideAndDontSave)
- Object.DestroyImmediate(m_Settings);
-
- // Project-wide Actions are never temporary so we do not destroy them.
- }
-
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- // Initialize project-wide actions:
- // - In editor (edit mode or play-mode) we always use the editor build preferences persisted setting.
- // - In player build we always attempt to find a preloaded asset.
- private void InitializeActions()
- {
- #if UNITY_EDITOR
- m_Actions = ProjectWideActionsBuildProvider.actionsToIncludeInPlayerBuild;
- #else
- m_Actions = null;
- var candidates = Resources.FindObjectsOfTypeAll<InputActionAsset>();
- foreach (var candidate in candidates)
- {
- if (candidate.m_IsProjectWide)
- {
- m_Actions = candidate;
- break;
- }
- }
- #endif // UNITY_EDITOR
- }
-
- #endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
-
- internal void InitializeData()
- {
- m_Layouts.Allocate();
- m_Processors.Initialize();
- m_Interactions.Initialize();
- m_Composites.Initialize();
- m_DevicesById = new Dictionary<int, InputDevice>();
-
- // Determine our default set of enabled update types. By
- // default we enable both fixed and dynamic update because
- // we don't know which one the user is going to use. The user
- // can manually turn off one of them to optimize operation.
- m_UpdateMask = InputUpdateType.Dynamic | InputUpdateType.Fixed;
- m_HasFocus = Application.isFocused;
- #if UNITY_EDITOR
- m_EditorIsActive = true;
- m_UpdateMask |= InputUpdateType.Editor;
- #endif
-
- // Default polling frequency is 60 Hz.
- m_PollingFrequency = 60;
-
- // Register layouts.
- // NOTE: Base layouts must be registered before their derived layouts
- // for the detection of base layouts to work.
- RegisterControlLayout("Axis", typeof(AxisControl)); // Controls.
- RegisterControlLayout("Button", typeof(ButtonControl));
- RegisterControlLayout("DiscreteButton", typeof(DiscreteButtonControl));
- RegisterControlLayout("Key", typeof(KeyControl));
- RegisterControlLayout("Analog", typeof(AxisControl));
- RegisterControlLayout("Integer", typeof(IntegerControl));
- RegisterControlLayout("Digital", typeof(IntegerControl));
- RegisterControlLayout("Double", typeof(DoubleControl));
- RegisterControlLayout("Vector2", typeof(Vector2Control));
- RegisterControlLayout("Vector3", typeof(Vector3Control));
- RegisterControlLayout("Delta", typeof(DeltaControl));
- RegisterControlLayout("Quaternion", typeof(QuaternionControl));
- RegisterControlLayout("Stick", typeof(StickControl));
- RegisterControlLayout("Dpad", typeof(DpadControl));
- RegisterControlLayout("DpadAxis", typeof(DpadControl.DpadAxisControl));
- RegisterControlLayout("AnyKey", typeof(AnyKeyControl));
- RegisterControlLayout("Touch", typeof(TouchControl));
- RegisterControlLayout("TouchPhase", typeof(TouchPhaseControl));
- RegisterControlLayout("TouchPress", typeof(TouchPressControl));
-
- RegisterControlLayout("Gamepad", typeof(Gamepad)); // Devices.
- RegisterControlLayout("Joystick", typeof(Joystick));
- RegisterControlLayout("Keyboard", typeof(Keyboard));
- RegisterControlLayout("Pointer", typeof(Pointer));
- RegisterControlLayout("Mouse", typeof(Mouse));
- RegisterControlLayout("Pen", typeof(Pen));
- RegisterControlLayout("Touchscreen", typeof(Touchscreen));
- RegisterControlLayout("Sensor", typeof(Sensor));
- RegisterControlLayout("Accelerometer", typeof(Accelerometer));
- RegisterControlLayout("Gyroscope", typeof(Gyroscope));
- RegisterControlLayout("GravitySensor", typeof(GravitySensor));
- RegisterControlLayout("AttitudeSensor", typeof(AttitudeSensor));
- RegisterControlLayout("LinearAccelerationSensor", typeof(LinearAccelerationSensor));
- RegisterControlLayout("MagneticFieldSensor", typeof(MagneticFieldSensor));
- RegisterControlLayout("LightSensor", typeof(LightSensor));
- RegisterControlLayout("PressureSensor", typeof(PressureSensor));
- RegisterControlLayout("HumiditySensor", typeof(HumiditySensor));
- RegisterControlLayout("AmbientTemperatureSensor", typeof(AmbientTemperatureSensor));
- RegisterControlLayout("StepCounter", typeof(StepCounter));
- RegisterControlLayout("TrackedDevice", typeof(TrackedDevice));
-
- // Precompiled layouts.
- RegisterPrecompiledLayout<FastKeyboard>(FastKeyboard.metadata);
- RegisterPrecompiledLayout<FastTouchscreen>(FastTouchscreen.metadata);
- RegisterPrecompiledLayout<FastMouse>(FastMouse.metadata);
-
- // Register processors.
- processors.AddTypeRegistration("Invert", typeof(InvertProcessor));
- processors.AddTypeRegistration("InvertVector2", typeof(InvertVector2Processor));
- processors.AddTypeRegistration("InvertVector3", typeof(InvertVector3Processor));
- processors.AddTypeRegistration("Clamp", typeof(ClampProcessor));
- processors.AddTypeRegistration("Normalize", typeof(NormalizeProcessor));
- processors.AddTypeRegistration("NormalizeVector2", typeof(NormalizeVector2Processor));
- processors.AddTypeRegistration("NormalizeVector3", typeof(NormalizeVector3Processor));
- processors.AddTypeRegistration("Scale", typeof(ScaleProcessor));
- processors.AddTypeRegistration("ScaleVector2", typeof(ScaleVector2Processor));
- processors.AddTypeRegistration("ScaleVector3", typeof(ScaleVector3Processor));
- processors.AddTypeRegistration("StickDeadzone", typeof(StickDeadzoneProcessor));
- processors.AddTypeRegistration("AxisDeadzone", typeof(AxisDeadzoneProcessor));
- processors.AddTypeRegistration("CompensateDirection", typeof(CompensateDirectionProcessor));
- processors.AddTypeRegistration("CompensateRotation", typeof(CompensateRotationProcessor));
-
- #if UNITY_EDITOR
- processors.AddTypeRegistration("AutoWindowSpace", typeof(EditorWindowSpaceProcessor));
- #endif
-
- // Register interactions.
- interactions.AddTypeRegistration("Hold", typeof(HoldInteraction));
- interactions.AddTypeRegistration("Tap", typeof(TapInteraction));
- interactions.AddTypeRegistration("SlowTap", typeof(SlowTapInteraction));
- interactions.AddTypeRegistration("MultiTap", typeof(MultiTapInteraction));
- interactions.AddTypeRegistration("Press", typeof(PressInteraction));
-
- // Register composites.
- composites.AddTypeRegistration("1DAxis", typeof(AxisComposite));
- composites.AddTypeRegistration("2DVector", typeof(Vector2Composite));
- composites.AddTypeRegistration("3DVector", typeof(Vector3Composite));
- composites.AddTypeRegistration("Axis", typeof(AxisComposite));// Alias for pre-0.2 name.
- composites.AddTypeRegistration("Dpad", typeof(Vector2Composite));// Alias for pre-0.2 name.
- composites.AddTypeRegistration("ButtonWithOneModifier", typeof(ButtonWithOneModifier));
- composites.AddTypeRegistration("ButtonWithTwoModifiers", typeof(ButtonWithTwoModifiers));
- composites.AddTypeRegistration("OneModifier", typeof(OneModifierComposite));
- composites.AddTypeRegistration("TwoModifiers", typeof(TwoModifiersComposite));
- }
-
- internal void InstallRuntime(IInputRuntime runtime)
- {
- if (m_Runtime != null)
- {
- m_Runtime.onUpdate = null;
- m_Runtime.onBeforeUpdate = null;
- m_Runtime.onDeviceDiscovered = null;
- m_Runtime.onPlayerFocusChanged = null;
- m_Runtime.onShouldRunUpdate = null;
- #if UNITY_EDITOR
- m_Runtime.onPlayerLoopInitialization = null;
- #endif
- }
-
- m_Runtime = runtime;
- m_Runtime.onUpdate = OnUpdate;
- m_Runtime.onDeviceDiscovered = OnNativeDeviceDiscovered;
- m_Runtime.onPlayerFocusChanged = OnFocusChanged;
- m_Runtime.onShouldRunUpdate = ShouldRunUpdate;
- #if UNITY_EDITOR
- m_Runtime.onPlayerLoopInitialization = OnPlayerLoopInitialization;
- #endif
- m_Runtime.pollingFrequency = pollingFrequency;
- m_HasFocus = m_Runtime.isPlayerFocused;
-
- // We only hook NativeInputSystem.onBeforeUpdate if necessary.
- if (m_BeforeUpdateListeners.length > 0 || m_HaveDevicesWithStateCallbackReceivers)
- {
- m_Runtime.onBeforeUpdate = OnBeforeUpdate;
- m_NativeBeforeUpdateHooked = true;
- }
-
- #if UNITY_ANALYTICS || UNITY_EDITOR
- InputAnalytics.Initialize(this);
- m_Runtime.onShutdown = () => InputAnalytics.OnShutdown(this);
- #endif
- }
-
- internal void InstallGlobals()
- {
- Debug.Assert(m_Runtime != null);
-
- InputControlLayout.s_Layouts = m_Layouts;
- InputProcessor.s_Processors = m_Processors;
- InputInteraction.s_Interactions = m_Interactions;
- InputBindingComposite.s_Composites = m_Composites;
-
- InputRuntime.s_Instance = m_Runtime;
- InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup =
- m_Runtime.currentTimeOffsetToRealtimeSinceStartup;
-
- // Reset update state.
- InputUpdate.Restore(new InputUpdate.SerializedState());
-
- unsafe
- {
- InputStateBuffers.SwitchTo(m_StateBuffers, InputUpdateType.Dynamic);
- InputStateBuffers.s_DefaultStateBuffer = m_StateBuffers.defaultStateBuffer;
- InputStateBuffers.s_NoiseMaskBuffer = m_StateBuffers.noiseMaskBuffer;
- InputStateBuffers.s_ResetMaskBuffer = m_StateBuffers.resetMaskBuffer;
- }
- }
-
- internal void UninstallGlobals()
- {
- if (ReferenceEquals(InputControlLayout.s_Layouts.baseLayoutTable, m_Layouts.baseLayoutTable))
- InputControlLayout.s_Layouts = new InputControlLayout.Collection();
- if (ReferenceEquals(InputProcessor.s_Processors.table, m_Processors.table))
- InputProcessor.s_Processors = new TypeTable();
- if (ReferenceEquals(InputInteraction.s_Interactions.table, m_Interactions.table))
- InputInteraction.s_Interactions = new TypeTable();
- if (ReferenceEquals(InputBindingComposite.s_Composites.table, m_Composites.table))
- InputBindingComposite.s_Composites = new TypeTable();
-
- // Clear layout cache.
- InputControlLayout.s_CacheInstance = default;
- InputControlLayout.s_CacheInstanceRef = 0;
-
- // Detach from runtime.
- if (m_Runtime != null)
- {
- m_Runtime.onUpdate = null;
- m_Runtime.onDeviceDiscovered = null;
- m_Runtime.onBeforeUpdate = null;
- m_Runtime.onPlayerFocusChanged = null;
- m_Runtime.onShouldRunUpdate = null;
-
- if (ReferenceEquals(InputRuntime.s_Instance, m_Runtime))
- InputRuntime.s_Instance = null;
- }
- }
-
- [Serializable]
- internal struct AvailableDevice
- {
- public InputDeviceDescription description;
- public int deviceId;
- public bool isNative;
- public bool isRemoved;
- }
-
- // Used by EditorInputControlLayoutCache to determine whether its state is outdated.
- internal int m_LayoutRegistrationVersion;
- private float m_PollingFrequency;
-
- internal InputControlLayout.Collection m_Layouts;
- private TypeTable m_Processors;
- private TypeTable m_Interactions;
- private TypeTable m_Composites;
-
- private int m_DevicesCount;
- private InputDevice[] m_Devices;
-
- private Dictionary<int, InputDevice> m_DevicesById;
- internal int m_AvailableDeviceCount;
- internal AvailableDevice[] m_AvailableDevices; // A record of all devices reported to the system (from native or user code).
-
- ////REVIEW: should these be weak-referenced?
- internal int m_DisconnectedDevicesCount;
- internal InputDevice[] m_DisconnectedDevices;
-
- internal InputUpdateType m_UpdateMask; // Which of our update types are enabled.
- private InputUpdateType m_CurrentUpdate;
- internal InputStateBuffers m_StateBuffers;
-
- #if UNITY_EDITOR
- // remember time offset to correctly restore it after editor mode is done
- private double latestNonEditorTimeOffsetToRealtimeSinceStartup;
- #endif
-
- // We don't use UnityEvents and thus don't persist the callbacks during domain reloads.
- // Restoration of UnityActions is unreliable and it's too easy to end up with double
- // registrations what will lead to all kinds of misbehavior.
- private CallbackArray<DeviceChangeListener> m_DeviceChangeListeners;
- private CallbackArray<DeviceStateChangeListener> m_DeviceStateChangeListeners;
- private CallbackArray<InputDeviceFindControlLayoutDelegate> m_DeviceFindLayoutCallbacks;
- internal CallbackArray<InputDeviceCommandDelegate> m_DeviceCommandCallbacks;
- private CallbackArray<LayoutChangeListener> m_LayoutChangeListeners;
- private CallbackArray<EventListener> m_EventListeners;
- private CallbackArray<UpdateListener> m_BeforeUpdateListeners;
- private CallbackArray<UpdateListener> m_AfterUpdateListeners;
- private CallbackArray<Action> m_SettingsChangedListeners;
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- private CallbackArray<Action> m_ActionsChangedListeners;
- #endif
- private bool m_NativeBeforeUpdateHooked;
- private bool m_HaveDevicesWithStateCallbackReceivers;
- private bool m_HasFocus;
- private InputEventStream m_InputEventStream;
-
- // We want to sync devices when the editor comes back into focus. Unfortunately, there's no
- // callback for this so we have to poll this state.
- #if UNITY_EDITOR
- private bool m_EditorIsActive;
- #endif
-
- // Allow external users to hook in validators and draw custom UI in the binding path editor
- #if UNITY_EDITOR
- private Utilities.CallbackArray<CustomBindingPathValidator> m_customBindingPathValidators;
- #endif
-
- // We allocate the 'executeDeviceCommand' closure passed to 'onFindLayoutForDevice'
- // only once to avoid creating garbage.
- private InputDeviceExecuteCommandDelegate m_DeviceFindExecuteCommandDelegate;
- private int m_DeviceFindExecuteCommandDeviceId;
-
- #if UNITY_ANALYTICS || UNITY_EDITOR
- private bool m_HaveSentStartupAnalytics;
- #endif
-
- internal IInputRuntime m_Runtime;
- internal InputMetrics m_Metrics;
- internal InputSettings m_Settings;
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- private InputActionAsset m_Actions;
- #endif
-
- #if UNITY_EDITOR
- internal IInputDiagnostics m_Diagnostics;
- #endif
-
- ////REVIEW: Make it so that device names *always* have a number appended? (i.e. Gamepad1, Gamepad2, etc. instead of Gamepad, Gamepad1, etc)
-
- private void MakeDeviceNameUnique(InputDevice device)
- {
- if (m_DevicesCount == 0)
- return;
-
- var deviceName = StringHelpers.MakeUniqueName(device.name, m_Devices, x => x != null ? x.name : string.Empty);
- if (deviceName != device.name)
- {
- // If we have changed the name of the device, nuke all path strings in the control
- // hierarchy so that they will get re-recreated when queried.
- ResetControlPathsRecursive(device);
-
- // Assign name.
- device.m_Name = new InternedString(deviceName);
- }
- }
-
- private static void ResetControlPathsRecursive(InputControl control)
- {
- control.m_Path = null;
-
- var children = control.children;
- var childCount = children.Count;
-
- for (var i = 0; i < childCount; ++i)
- ResetControlPathsRecursive(children[i]);
- }
-
- private void AssignUniqueDeviceId(InputDevice device)
- {
- // If the device already has an ID, make sure it's unique.
- if (device.deviceId != InputDevice.InvalidDeviceId)
- {
- // Safety check to make sure out IDs are really unique.
- // Given they are assigned by the native system they should be fine
- // but let's make sure.
- var existingDeviceWithId = TryGetDeviceById(device.deviceId);
- if (existingDeviceWithId != null)
- throw new InvalidOperationException(
- $"Duplicate device ID {device.deviceId} detected for devices '{device.name}' and '{existingDeviceWithId.name}'");
- }
- else
- {
- device.m_DeviceId = m_Runtime.AllocateDeviceId();
- }
- }
-
- // (Re)allocates state buffers and assigns each device that's been added
- // a segment of the buffer. Preserves the current state of devices.
- // NOTE: Installs the buffers globally.
- private unsafe void ReallocateStateBuffers()
- {
- var oldBuffers = m_StateBuffers;
-
- // Allocate new buffers.
- var newBuffers = new InputStateBuffers();
- newBuffers.AllocateAll(m_Devices, m_DevicesCount);
-
- // Migrate state.
- newBuffers.MigrateAll(m_Devices, m_DevicesCount, oldBuffers);
-
- // Install the new buffers.
- oldBuffers.FreeAll();
- m_StateBuffers = newBuffers;
- InputStateBuffers.s_DefaultStateBuffer = newBuffers.defaultStateBuffer;
- InputStateBuffers.s_NoiseMaskBuffer = newBuffers.noiseMaskBuffer;
- InputStateBuffers.s_ResetMaskBuffer = newBuffers.resetMaskBuffer;
-
- // Switch to buffers.
- InputStateBuffers.SwitchTo(m_StateBuffers,
- InputUpdate.s_LatestUpdateType != InputUpdateType.None ? InputUpdate.s_LatestUpdateType : defaultUpdateType);
-
- ////TODO: need to update state change monitors
- }
-
- /// <summary>
- /// Initialize default state for given device.
- /// </summary>
- /// <param name="device">A newly added input device.</param>
- /// <remarks>
- /// For every device, one copy of its state is kept around which is initialized with the default
- /// values for the device. If the device has no control that has an explicitly specified control
- /// value, the buffer simply contains all zeroes.
- ///
- /// The default state buffer is initialized once when a device is added to the system and then
- /// migrated by <see cref="InputStateBuffers"/> like other device state and removed when the device
- /// is removed from the system.
- /// </remarks>
- private unsafe void InitializeDefaultState(InputDevice device)
- {
- // Nothing to do if device has a default state of all zeroes.
- if (!device.hasControlsWithDefaultState)
- return;
-
- // Otherwise go through each control and write its default value.
- var controls = device.allControls;
- var controlCount = controls.Count;
- var defaultStateBuffer = m_StateBuffers.defaultStateBuffer;
- for (var n = 0; n < controlCount; ++n)
- {
- var control = controls[n];
- if (!control.hasDefaultState)
- continue;
-
- control.m_StateBlock.Write(defaultStateBuffer, control.m_DefaultState);
- }
-
- // Copy default state to all front and back buffers.
- var stateBlock = device.m_StateBlock;
- var deviceIndex = device.m_DeviceIndex;
- if (m_StateBuffers.m_PlayerStateBuffers.valid)
- {
- stateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer);
- stateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer);
- }
-
- #if UNITY_EDITOR
- if (m_StateBuffers.m_EditorStateBuffers.valid)
- {
- stateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer);
- stateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer);
- }
- #endif
- }
-
- private unsafe void InitializeDeviceState(InputDevice device)
- {
- Debug.Assert(device != null, "Device must not be null");
- Debug.Assert(device.added, "Device must have been added");
- Debug.Assert(device.stateBlock.byteOffset != InputStateBlock.InvalidOffset, "Device state block offset is invalid");
- Debug.Assert(device.stateBlock.byteOffset + device.stateBlock.alignedSizeInBytes <= m_StateBuffers.sizePerBuffer,
- "Device state block is not contained in state buffer");
-
- var controls = device.allControls;
- var controlCount = controls.Count;
- var resetMaskBuffer = m_StateBuffers.resetMaskBuffer;
-
- var haveControlsWithDefaultState = device.hasControlsWithDefaultState;
-
- // Assume that everything in the device is noise. This way we also catch memory regions
- // that are not actually covered by a control and implicitly mark them as noise (e.g. the
- // report ID in HID input reports).
- //
- // NOTE: Noise is indicated by *unset* bits so we don't have to do anything here to start
- // with all-noise as we expect noise mask memory to be cleared on allocation.
- var noiseMaskBuffer = m_StateBuffers.noiseMaskBuffer;
-
- // We first toggle all bits *on* and then toggle bits for noisy and dontReset controls *off* individually.
- // We do this instead of just leaving all bits *off* and then going through controls that aren't noisy/dontReset *on*.
- // If we did the latter, we'd have the problem that a parent control such as TouchControl would toggle on bits for
- // the entirety of its state block and thus cover the state of all its child controls.
- MemoryHelpers.SetBitsInBuffer(noiseMaskBuffer, (int)device.stateBlock.byteOffset, 0, (int)device.stateBlock.sizeInBits, false);
- MemoryHelpers.SetBitsInBuffer(resetMaskBuffer, (int)device.stateBlock.byteOffset, 0, (int)device.stateBlock.sizeInBits, true);
-
- // Go through controls.
- var defaultStateBuffer = m_StateBuffers.defaultStateBuffer;
- for (var n = 0; n < controlCount; ++n)
- {
- var control = controls[n];
-
- // Don't allow controls that hijack state from other controls to set independent noise or dontReset flags.
- if (control.usesStateFromOtherControl)
- continue;
-
- if (!control.noisy || control.dontReset)
- {
- ref var stateBlock = ref control.m_StateBlock;
-
- Debug.Assert(stateBlock.byteOffset != InputStateBlock.InvalidOffset, "Byte offset is invalid on control's state block");
- Debug.Assert(stateBlock.bitOffset != InputStateBlock.InvalidOffset, "Bit offset is invalid on control's state block");
- Debug.Assert(stateBlock.sizeInBits != InputStateBlock.InvalidOffset, "Size is invalid on control's state block");
- Debug.Assert(stateBlock.byteOffset >= device.stateBlock.byteOffset, "Control's offset is located below device's offset");
- Debug.Assert(stateBlock.byteOffset + stateBlock.alignedSizeInBytes <=
- device.stateBlock.byteOffset + device.stateBlock.alignedSizeInBytes, "Control state block lies outside of state buffer");
-
- // If control isn't noisy, toggle its bits *on* in the noise mask.
- if (!control.noisy)
- MemoryHelpers.SetBitsInBuffer(noiseMaskBuffer, (int)stateBlock.byteOffset, (int)stateBlock.bitOffset,
- (int)stateBlock.sizeInBits, true);
-
- // If control shouldn't be reset, toggle its bits *off* in the reset mask.
- if (control.dontReset)
- MemoryHelpers.SetBitsInBuffer(resetMaskBuffer, (int)stateBlock.byteOffset, (int)stateBlock.bitOffset,
- (int)stateBlock.sizeInBits, false);
- }
-
- // If control has default state, write it into to the device's default state.
- if (haveControlsWithDefaultState && control.hasDefaultState)
- control.m_StateBlock.Write(defaultStateBuffer, control.m_DefaultState);
- }
-
- // Copy default state to all front and back buffers.
- if (haveControlsWithDefaultState)
- {
- ref var deviceStateBlock = ref device.m_StateBlock;
- var deviceIndex = device.m_DeviceIndex;
- if (m_StateBuffers.m_PlayerStateBuffers.valid)
- {
- deviceStateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer);
- deviceStateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer);
- }
-
- #if UNITY_EDITOR
- if (m_StateBuffers.m_EditorStateBuffers.valid)
- {
- deviceStateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer);
- deviceStateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer);
- }
- #endif
- }
- }
-
- private void OnNativeDeviceDiscovered(int deviceId, string deviceDescriptor)
- {
- // Make sure we're not adding to m_AvailableDevices before we restored what we
- // had before a domain reload.
- RestoreDevicesAfterDomainReloadIfNecessary();
-
- // See if we have a disconnected device we can revive.
- // NOTE: We do this all the way up here as the first thing before we even parse the JSON descriptor so
- // if we do have a device we can revive, we can do so without allocating any GC memory.
- var device = TryMatchDisconnectedDevice(deviceDescriptor);
-
- // Parse description, if need be.
- var description = device?.description ?? InputDeviceDescription.FromJson(deviceDescriptor);
-
- // Add it.
- var markAsRemoved = false;
- try
- {
- // If we have a restricted set of supported devices, first check if it's a device
- // we should support.
- if (m_Settings.supportedDevices.Count > 0)
- {
- var layout = device != null ? device.m_Layout : TryFindMatchingControlLayout(ref description, deviceId);
- if (!IsDeviceLayoutMarkedAsSupportedInSettings(layout))
- {
- // Not supported. Ignore device. Still will get added to m_AvailableDevices
- // list in finally clause below. If later the set of supported devices changes
- // so that the device is now supported, ApplySettings() will pull it back out
- // and create the device.
- markAsRemoved = true;
- return;
- }
- }
-
- if (device != null)
- {
- // It's a device we pulled from the disconnected list. Update the device with the
- // new ID, re-add it and notify that we've reconnected.
-
- device.m_DeviceId = deviceId;
- device.m_DeviceFlags |= InputDevice.DeviceFlags.Native;
- device.m_DeviceFlags &= ~InputDevice.DeviceFlags.DisabledInFrontend;
- device.m_DeviceFlags &= ~InputDevice.DeviceFlags.DisabledWhileInBackground;
- device.m_DeviceFlags &= ~InputDevice.DeviceFlags.DisabledStateHasBeenQueriedFromRuntime;
-
- AddDevice(device);
-
- DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, InputDeviceChange.Reconnected,
- "InputSystem.onDeviceChange");
- }
- else
- {
- // Go through normal machinery to try to create a new device.
- AddDevice(description, throwIfNoLayoutFound: false, deviceId: deviceId,
- deviceFlags: InputDevice.DeviceFlags.Native);
- }
- }
- // We're catching exceptions very aggressively here. The reason is that we don't want
- // exceptions thrown as a result of trying to create devices from device discoveries reported
- // by native to break the system as a whole. Instead, we want to make the error visible but then
- // go and work with whatever devices we *did* manage to create successfully.
- catch (Exception exception)
- {
- Debug.LogError($"Could not create a device for '{description}' (exception: {exception})");
- }
- finally
- {
- // Remember it. Do this *after* the AddDevice() call above so that if there's
- // a listener creating layouts on the fly we won't end up matching this device and
- // create an InputDevice right away (which would then conflict with the one we
- // create in AddDevice).
- ArrayHelpers.AppendWithCapacity(ref m_AvailableDevices, ref m_AvailableDeviceCount,
- new AvailableDevice
- {
- description = description,
- deviceId = deviceId,
- isNative = true,
- isRemoved = markAsRemoved,
- });
- }
- }
-
- private InputDevice TryMatchDisconnectedDevice(string deviceDescriptor)
- {
- for (var i = 0; i < m_DisconnectedDevicesCount; ++i)
- {
- var device = m_DisconnectedDevices[i];
- var description = device.description;
-
- // We don't parse the full description but rather go property by property in order to not
- // allocate GC memory if we can avoid it.
-
- if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("interface", description.interfaceName, deviceDescriptor))
- continue;
- if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("product", description.product, deviceDescriptor))
- continue;
- if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("manufacturer", description.manufacturer, deviceDescriptor))
- continue;
- if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("type", description.deviceClass, deviceDescriptor))
- continue;
- if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("capabilities", description.capabilities, deviceDescriptor))
- continue;
- if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("serial", description.serial, deviceDescriptor))
- continue;
-
- ArrayHelpers.EraseAtWithCapacity(m_DisconnectedDevices, ref m_DisconnectedDevicesCount, i);
- return device;
- }
-
- return null;
- }
-
- private void InstallBeforeUpdateHookIfNecessary()
- {
- if (m_NativeBeforeUpdateHooked || m_Runtime == null)
- return;
-
- m_Runtime.onBeforeUpdate = OnBeforeUpdate;
- m_NativeBeforeUpdateHooked = true;
- }
-
- private void RestoreDevicesAfterDomainReloadIfNecessary()
- {
- #if UNITY_EDITOR
- if (m_SavedDeviceStates != null)
- RestoreDevicesAfterDomainReload();
- #endif
- }
-
- #if UNITY_EDITOR
- private void SyncAllDevicesWhenEditorIsActivated()
- {
- var isActive = m_Runtime.isEditorActive;
- if (isActive == m_EditorIsActive)
- return;
-
- m_EditorIsActive = isActive;
- if (m_EditorIsActive)
- SyncAllDevices();
- }
-
- private void SyncAllDevices()
- {
- for (var i = 0; i < m_DevicesCount; ++i)
- {
- // When the editor comes back into focus, we actually do want resets to happen
- // for devices that don't support syncs as they will likely have missed input while
- // we were in the background.
- if (!m_Devices[i].RequestSync())
- ResetDevice(m_Devices[i], issueResetCommand: true);
- }
- }
-
- internal void SyncAllDevicesAfterEnteringPlayMode()
- {
- // Because we ignore all events between exiting edit mode and entering play mode,
- // that includes any potential device resets/syncs/etc,
- // we need to resync all devices after we're in play mode proper.
- ////TODO: this is a hacky workaround, implement a proper solution where events from sync/resets are not ignored.
- SyncAllDevices();
- }
-
- #endif
-
- private void WarnAboutDevicesFailingToRecreateAfterDomainReload()
- {
- // If we still have any saved device states, we have devices that we couldn't figure
- // out how to recreate after a domain reload. Log a warning for each of them and
- // let go of them.
- #if UNITY_EDITOR
- if (m_SavedDeviceStates == null)
- return;
-
- for (var i = 0; i < m_SavedDeviceStates.Length; ++i)
- {
- ref var state = ref m_SavedDeviceStates[i];
- Debug.LogWarning($"Could not recreate device '{state.name}' with layout '{state.layout}' after domain reload");
- }
-
- // At this point, we throw the device states away and forget about
- // what we had before the domain reload.
- m_SavedDeviceStates = null;
- #endif
- }
-
- private void OnBeforeUpdate(InputUpdateType updateType)
- {
- // Restore devices before checking update mask. See InputSystem.RunInitialUpdate().
- RestoreDevicesAfterDomainReloadIfNecessary();
-
- if ((updateType & m_UpdateMask) == 0)
- return;
-
- InputStateBuffers.SwitchTo(m_StateBuffers, updateType);
-
- InputUpdate.OnBeforeUpdate(updateType);
-
- // For devices that have state callbacks, tell them we're carrying state over
- // into the next frame.
- if (m_HaveDevicesWithStateCallbackReceivers && updateType != InputUpdateType.BeforeRender) ////REVIEW: before-render handling is probably wrong
- {
- for (var i = 0; i < m_DevicesCount; ++i)
- {
- var device = m_Devices[i];
- if (!device.hasStateCallbacks)
- continue;
-
- // NOTE: We do *not* perform a buffer flip here as we do not want to change what is the
- // current and what is the previous state when we carry state forward. Rather,
- // OnBeforeUpdate, if it modifies state, it modifies the current state directly.
- // Also, for the same reasons, we do not modify the dynamic/fixed update counts
- // on the device. If an event comes in in the upcoming update, it should lead to
- // a buffer flip.
-
- ((IInputStateCallbackReceiver)device).OnNextUpdate();
- }
- }
-
- DelegateHelpers.InvokeCallbacksSafe(ref m_BeforeUpdateListeners, "onBeforeUpdate");
- }
-
- /// <summary>
- /// Apply the settings in <see cref="m_Settings"/>.
- /// </summary>
- internal void ApplySettings()
- {
- // Sync update mask.
- var newUpdateMask = InputUpdateType.Editor;
- if ((m_UpdateMask & InputUpdateType.BeforeRender) != 0)
- {
- // BeforeRender updates are enabled in response to devices needing BeforeRender updates
- // so we always preserve this if set.
- newUpdateMask |= InputUpdateType.BeforeRender;
- }
- if (m_Settings.updateMode == InputSettings.s_OldUnsupportedFixedAndDynamicUpdateSetting)
- m_Settings.updateMode = InputSettings.UpdateMode.ProcessEventsInDynamicUpdate;
- switch (m_Settings.updateMode)
- {
- case InputSettings.UpdateMode.ProcessEventsInDynamicUpdate:
- newUpdateMask |= InputUpdateType.Dynamic;
- break;
- case InputSettings.UpdateMode.ProcessEventsInFixedUpdate:
- newUpdateMask |= InputUpdateType.Fixed;
- break;
- case InputSettings.UpdateMode.ProcessEventsManually:
- newUpdateMask |= InputUpdateType.Manual;
- break;
- default:
- throw new NotSupportedException("Invalid input update mode: " + m_Settings.updateMode);
- }
-
- #if UNITY_EDITOR
- // In the editor, we force editor updates to be on even if InputEditorUserSettings.lockInputToGameView is
- // on as otherwise we'll end up accumulating events in edit mode without anyone flushing the
- // queue out regularly.
- newUpdateMask |= InputUpdateType.Editor;
- #endif
- updateMask = newUpdateMask;
-
- ////TODO: optimize this so that we don't repeatedly recreate state if we add/remove multiple devices
- //// (same goes for not resolving actions repeatedly)
-
- // Check if there's any native device we aren't using ATM which now fits
- // the set of supported devices.
- AddAvailableDevicesThatAreNowRecognized();
-
- // If the settings restrict the set of supported devices, demote any native
- // device we currently have that doesn't fit the requirements.
- if (settings.supportedDevices.Count > 0)
- {
- for (var i = 0; i < m_DevicesCount; ++i)
- {
- var device = m_Devices[i];
- var layout = device.m_Layout;
-
- // If it's not in m_AvailableDevices, we don't automatically remove it.
- // Whatever has been added directly through AddDevice(), we keep and don't
- // restrict by `supportDevices`.
- var isInAvailableDevices = false;
- for (var n = 0; n < m_AvailableDeviceCount; ++n)
- {
- if (m_AvailableDevices[n].deviceId == device.deviceId)
- {
- isInAvailableDevices = true;
- break;
- }
- }
- if (!isInAvailableDevices)
- continue;
-
- // If the device layout isn't supported according to the current settings,
- // remove the device.
- if (!IsDeviceLayoutMarkedAsSupportedInSettings(layout))
- {
- RemoveDevice(device, keepOnListOfAvailableDevices: true);
- --i;
- }
- }
- }
-
- // Apply feature flags.
- if (m_Settings.m_FeatureFlags != null)
- {
- #if UNITY_EDITOR
- runPlayerUpdatesInEditMode = m_Settings.IsFeatureEnabled(InputFeatureNames.kRunPlayerUpdatesInEditMode);
- #endif
-
- if (m_Settings.IsFeatureEnabled(InputFeatureNames.kUseWindowsGamingInputBackend))
- {
- var command = UseWindowsGamingInputCommand.Create(true);
- if (ExecuteGlobalCommand(ref command) < 0)
- Debug.LogError($"Could not enable Windows.Gaming.Input");
- }
- }
-
- // Cache some values.
- Touchscreen.s_TapTime = settings.defaultTapTime;
- Touchscreen.s_TapDelayTime = settings.multiTapDelayTime;
- Touchscreen.s_TapRadiusSquared = settings.tapRadius * settings.tapRadius;
- // Extra clamp here as we can't tell what we're getting from serialized data.
- ButtonControl.s_GlobalDefaultButtonPressPoint = Mathf.Clamp(settings.defaultButtonPressPoint, ButtonControl.kMinButtonPressPoint, float.MaxValue);
- ButtonControl.s_GlobalDefaultButtonReleaseThreshold = settings.buttonReleaseThreshold;
-
- // Update devices control optimization
- foreach (var device in devices)
- device.SetOptimizedControlDataTypeRecursively();
-
- // Invalidate control caches due to potential changes to processors or value readers
- foreach (var device in devices)
- device.MarkAsStaleRecursively();
-
- // Let listeners know.
- DelegateHelpers.InvokeCallbacksSafe(ref m_SettingsChangedListeners,
- "InputSystem.onSettingsChange");
- }
-
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- internal void ApplyActions()
- {
- // Let listeners know.
- DelegateHelpers.InvokeCallbacksSafe(ref m_ActionsChangedListeners, "InputSystem.onActionsChange");
- }
-
- #endif
-
- internal unsafe long ExecuteGlobalCommand<TCommand>(ref TCommand command)
- where TCommand : struct, IInputDeviceCommandInfo
- {
- var ptr = (InputDeviceCommand*)UnsafeUtility.AddressOf(ref command);
- // device id is irrelevant as we route it based on fourcc internally
- return InputRuntime.s_Instance.DeviceCommand(0, ptr);
- }
-
- internal void AddAvailableDevicesThatAreNowRecognized()
- {
- for (var i = 0; i < m_AvailableDeviceCount; ++i)
- {
- var id = m_AvailableDevices[i].deviceId;
- if (TryGetDeviceById(id) != null)
- continue;
-
- var layout = TryFindMatchingControlLayout(ref m_AvailableDevices[i].description, id);
- if (!IsDeviceLayoutMarkedAsSupportedInSettings(layout)) continue;
-
- if (layout.IsEmpty())
- {
- // If it's a device coming from the runtime, disable it.
- if (id != InputDevice.InvalidDeviceId)
- {
- var command = DisableDeviceCommand.Create();
- m_Runtime.DeviceCommand(id, ref command);
- }
-
- continue;
- }
-
- try
- {
- AddDevice(m_AvailableDevices[i].description, layout, deviceId: id,
- deviceFlags: m_AvailableDevices[i].isNative ? InputDevice.DeviceFlags.Native : 0);
- }
- catch (Exception)
- {
- // the user might have changed the layout of one device, but others in the system might still have
- // layouts we can't make sense of. Just quietly swallow exceptions from those so as not to spam
- // the user with information about devices unrelated to what was actually changed.
- }
- }
- }
-
- private bool ShouldRunDeviceInBackground(InputDevice device)
- {
- var runDeviceInBackground =
- m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.ResetAndDisableAllDevices &&
- device.canRunInBackground;
-
- // In editor, we may override canRunInBackground depending on the gameViewFocus setting.
- #if UNITY_EDITOR
- if (runDeviceInBackground)
- {
- if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDevicesRespectGameViewFocus)
- runDeviceInBackground = false;
- else if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.PointersAndKeyboardsRespectGameViewFocus)
- runDeviceInBackground = !(device is Pointer || device is Keyboard);
- }
- #endif
-
- return runDeviceInBackground;
- }
-
- internal void OnFocusChanged(bool focus)
- {
- #if UNITY_EDITOR
- SyncAllDevicesWhenEditorIsActivated();
-
- if (!m_Runtime.isInPlayMode)
- {
- m_HasFocus = focus;
- return;
- }
-
- var gameViewFocus = m_Settings.editorInputBehaviorInPlayMode;
- #endif
-
- var runInBackground =
- #if UNITY_EDITOR
- // In the editor, the player loop will always be run even if the Game View does not have focus. This
- // amounts to runInBackground being always true in the editor, regardless of what the setting in
- // the Player Settings window is.
- //
- // If, however, "Game View Focus" is set to "Exactly As In Player", we force code here down the same
- // path as in the player.
- gameViewFocus != InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView || m_Runtime.runInBackground;
- #else
- m_Runtime.runInBackground;
- #endif
-
- var backgroundBehavior = m_Settings.backgroundBehavior;
- if (backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus && runInBackground)
- {
- // If runInBackground is true, no device changes should happen, even when focus is gained. So early out.
- // If runInBackground is false, we still want to sync devices when focus is gained. So we need to continue further.
- m_HasFocus = focus;
- return;
- }
-
- #if UNITY_EDITOR
- // Set the current update type while we process the focus changes to make sure we
- // feed into the right buffer. No need to do this in the player as it doesn't have
- // the editor/player confusion.
- m_CurrentUpdate = m_UpdateMask.GetUpdateTypeForPlayer();
- #endif
-
- if (!focus)
- {
- // We only react to loss of focus when we will keep running in the background. If not,
- // we'll do nothing and just wait for focus to come back (where we then try to sync all devices).
- if (runInBackground)
- {
- for (var i = 0; i < m_DevicesCount; ++i)
- {
- // Determine whether to run this device in the background.
- var device = m_Devices[i];
- if (!device.enabled || ShouldRunDeviceInBackground(device))
- continue;
-
- // Disable the device. This will also soft-reset it.
- EnableOrDisableDevice(device, false, DeviceDisableScope.TemporaryWhilePlayerIsInBackground);
-
- // In case we invoked a callback that messed with our device array, adjust our index.
- var index = m_Devices.IndexOfReference(device, m_DevicesCount);
- if (index == -1)
- --i;
- else
- i = index;
- }
- }
- }
- else
- {
- // On focus gain, reenable and sync devices.
- for (var i = 0; i < m_DevicesCount; ++i)
- {
- var device = m_Devices[i];
-
- // Re-enable the device if we disabled it on focus loss. This will also issue a sync.
- if (device.disabledWhileInBackground)
- EnableOrDisableDevice(device, true, DeviceDisableScope.TemporaryWhilePlayerIsInBackground);
- // Try to sync. If it fails and we didn't run in the background, perform
- // a reset instead. This is to cope with backends that are unable to sync but
- // may still retain state which now may be outdated because the input device may
- // have changed state while we weren't running. So at least make the backend flush
- // its state (if any).
- else if (device.enabled && !runInBackground && !device.RequestSync())
- ResetDevice(device);
- }
- }
-
- #if UNITY_EDITOR
- m_CurrentUpdate = InputUpdateType.None;
- #endif
-
- // We set this *after* the block above as defaultUpdateType is influenced by the setting.
- m_HasFocus = focus;
- }
-
- #if UNITY_EDITOR
- internal void LeavePlayMode()
- {
- // Reenable all devices and reset their play mode state.
- m_CurrentUpdate = InputUpdate.GetUpdateTypeForPlayer(m_UpdateMask);
- InputStateBuffers.SwitchTo(m_StateBuffers, m_CurrentUpdate);
- for (var i = 0; i < m_DevicesCount; ++i)
- {
- var device = m_Devices[i];
- if (device.disabledWhileInBackground)
- EnableOrDisableDevice(device, true, scope: DeviceDisableScope.TemporaryWhilePlayerIsInBackground);
- ResetDevice(device, alsoResetDontResetControls: true);
- }
- m_CurrentUpdate = default;
- }
-
- private void OnPlayerLoopInitialization()
- {
- if (!gameIsPlaying || // if game is not playing
- !InputUpdate.s_LatestUpdateType.IsEditorUpdate() || // or last update was not editor update
- !InputUpdate.s_LatestNonEditorUpdateType.IsPlayerUpdate()) // or update before that was not player update
- return; // then no need to restore anything
-
- InputUpdate.RestoreStateAfterEditorUpdate();
- InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup = latestNonEditorTimeOffsetToRealtimeSinceStartup;
- InputStateBuffers.SwitchTo(m_StateBuffers, InputUpdate.s_LatestUpdateType);
- }
-
- #endif
-
- internal bool ShouldRunUpdate(InputUpdateType updateType)
- {
- // We perform a "null" update after domain reloads and on startup to get our devices
- // in place before the runtime calls MonoBehaviour callbacks. See InputSystem.RunInitialUpdate().
- if (updateType == InputUpdateType.None)
- return true;
-
- var mask = m_UpdateMask;
-
- #if UNITY_EDITOR
- // If the player isn't running, the only thing we run is editor updates, except if
- // explicitly overriden via `runUpdatesInEditMode`.
- // NOTE: This means that in edit mode (outside of play mode) we *never* switch to player
- // input state. So, any script anywhere will see input state from the editor. If you
- // have an [ExecuteInEditMode] MonoBehaviour and it polls the gamepad, for example,
- // it will see gamepad inputs going to the editor and respond to them.
- if (!gameIsPlaying && updateType != InputUpdateType.Editor && !runPlayerUpdatesInEditMode)
- return false;
- #endif
-
- return (updateType & mask) != 0;
- }
-
- /// <summary>
- /// Process input events.
- /// </summary>
- /// <param name="updateType"></param>
- /// <param name="eventBuffer"></param>
- /// <remarks>
- /// This method is the core workhorse of the input system. It is called from <see cref="UnityEngineInternal.Input.NativeInputSystem"/>.
- /// Usually this happens in response to the player loop running and triggering updates at set points. However,
- /// updates can also be manually triggered through <see cref="InputSystem.Update"/>.
- ///
- /// The method receives the event buffer used internally by the runtime to collect events.
- ///
- /// Note that update types do *NOT* say what the events we receive are for. The update type only indicates
- /// where in the Unity's application loop we got called from. Where the event data goes depends wholly on
- /// which buffers we activate in the update and write the event data into.
- /// </remarks>
- /// <exception cref="InvalidOperationException">Thrown if OnUpdate is called recursively.</exception>
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1809:AvoidExcessiveLocals", Justification = "TODO: Refactor later.")]
- private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer eventBuffer)
- {
- ////TODO: switch from Profiler to CustomSampler API
- // NOTE: This is *not* using try/finally as we've seen unreliability in the EndSample()
- // execution (and we're not sure where it's coming from).
- Profiler.BeginSample("InputUpdate");
-
- if (m_InputEventStream.isOpen)
- {
- Profiler.EndSample();
- throw new InvalidOperationException("Already have an event buffer set! Was OnUpdate() called recursively?");
- }
-
- // Restore devices before checking update mask. See InputSystem.RunInitialUpdate().
- RestoreDevicesAfterDomainReloadIfNecessary();
-
- // In the editor, we issue a sync on all devices when the editor comes back to the foreground.
- #if UNITY_EDITOR
- SyncAllDevicesWhenEditorIsActivated();
- #endif
-
- if ((updateType & m_UpdateMask) == 0)
- {
- Profiler.EndSample();
- return;
- }
-
- WarnAboutDevicesFailingToRecreateAfterDomainReload();
-
- // First update sends out startup analytics.
- #if UNITY_ANALYTICS || UNITY_EDITOR
- if (!m_HaveSentStartupAnalytics)
- {
- InputAnalytics.OnStartup(this);
- m_HaveSentStartupAnalytics = true;
- }
- #endif
-
- // Update metrics.
- ++m_Metrics.totalUpdateCount;
-
- #if UNITY_EDITOR
- // If current update is editor update and previous update was non-editor,
- // store the time offset so we can restore it right after editor update is complete
- if (((updateType & InputUpdateType.Editor) == InputUpdateType.Editor) && (m_CurrentUpdate & InputUpdateType.Editor) == 0)
- latestNonEditorTimeOffsetToRealtimeSinceStartup =
- InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;
- #endif
-
- // Store current time offset.
- InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup = m_Runtime.currentTimeOffsetToRealtimeSinceStartup;
-
- InputStateBuffers.SwitchTo(m_StateBuffers, updateType);
-
- m_CurrentUpdate = updateType;
- InputUpdate.OnUpdate(updateType);
-
- // Ensure optimized controls are in valid state
- foreach (var device in devices)
- device.EnsureOptimizationTypeHasNotChanged();
-
- var shouldProcessActionTimeouts = updateType.IsPlayerUpdate() && gameIsPlaying;
-
- // See if we're supposed to only take events up to a certain time.
- // NOTE: We do not require the events in the queue to be sorted. Instead, we will walk over
- // all events in the buffer each time. Note that if there are multiple events for the same
- // device, it depends on the producer of these events to queue them in correct order.
- // Otherwise, once an event with a newer timestamp has been processed, events coming later
- // in the buffer and having older timestamps will get rejected.
-
- var currentTime = updateType == InputUpdateType.Fixed ? m_Runtime.currentTimeForFixedUpdate : m_Runtime.currentTime;
- var timesliceEvents = (updateType == InputUpdateType.Fixed || updateType == InputUpdateType.BeforeRender) &&
- InputSystem.settings.updateMode == InputSettings.UpdateMode.ProcessEventsInFixedUpdate;
-
- // Figure out if we can just flush the buffer and early out.
- var canFlushBuffer =
- false
- #if UNITY_EDITOR
- // If out of focus and runInBackground is off and ExactlyAsInPlayer is on, discard input.
- || (!gameHasFocus && m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView &&
- (!m_Runtime.runInBackground ||
- m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices))
- #else
- || (!gameHasFocus && !m_Runtime.runInBackground)
- #endif
- ;
- var canEarlyOut =
- // Early out if there's no events to process.
- eventBuffer.eventCount == 0
- || canFlushBuffer
-
- #if UNITY_EDITOR
- // If we're in the background and not supposed to process events in this update (but somehow
- // still ended up here), we're done.
- || ((!gameHasFocus || gameShouldGetInputRegardlessOfFocus) &&
- ((m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices && updateType != InputUpdateType.Editor)
- || (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDevicesRespectGameViewFocus && updateType != InputUpdateType.Editor)
- || (m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus && m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView && updateType == InputUpdateType.Editor)
- )
- // When the game is playing and has focus, we never process input in editor updates. All we
- // do is just switch to editor state buffers and then exit.
- || (gameIsPlaying && gameHasFocus && updateType == InputUpdateType.Editor))
- #endif
- ;
-
-
- #if UNITY_EDITOR
- var dropStatusEvents = false;
- if (!gameIsPlaying && gameShouldGetInputRegardlessOfFocus && (eventBuffer.sizeInBytes > (100 * 1024)))
- {
- // If the game is not playing but we're sending all input events to the game, the buffer can just grow unbounded.
- // So, in that case, set a flag to say we'd like to drop status events, and do not early out.
- canEarlyOut = false;
- dropStatusEvents = true;
- }
- #endif
-
- if (canEarlyOut)
- {
- // Normally, we process action timeouts after first processing all events. If we have no
- // events, we still need to check timeouts.
- if (shouldProcessActionTimeouts)
- ProcessStateChangeMonitorTimeouts();
-
- Profiler.EndSample();
- InvokeAfterUpdateCallback(updateType);
- if (canFlushBuffer)
- eventBuffer.Reset();
- m_CurrentUpdate = default;
- return;
- }
-
- var processingStartTime = Stopwatch.GetTimestamp();
- var totalEventLag = 0.0;
-
- #if UNITY_EDITOR
- var isPlaying = gameIsPlaying;
- #endif
-
- try
- {
- m_InputEventStream = new InputEventStream(ref eventBuffer, m_Settings.maxQueuedEventsPerUpdate);
- var totalEventBytesProcessed = 0U;
-
- InputEvent* skipEventMergingFor = null;
-
- // Handle events.
- while (m_InputEventStream.remainingEventCount > 0)
- {
- if (m_Settings.maxEventBytesPerUpdate > 0 &&
- totalEventBytesProcessed >= m_Settings.maxEventBytesPerUpdate)
- {
- Debug.LogError(
- "Exceeded budget for maximum input event throughput per InputSystem.Update(). Discarding remaining events. "
- + "Increase InputSystem.settings.maxEventBytesPerUpdate or set it to 0 to remove the limit.");
- break;
- }
-
- InputDevice device = null;
- var currentEventReadPtr = m_InputEventStream.currentEventPtr;
-
- Debug.Assert(!currentEventReadPtr->handled, "Event in buffer is already marked as handled");
-
- // In before render updates, we only take state events and only those for devices
- // that have before render updates enabled.
- if (updateType == InputUpdateType.BeforeRender)
- {
- while (m_InputEventStream.remainingEventCount > 0)
- {
- Debug.Assert(!currentEventReadPtr->handled,
- "Iterated to event in buffer that is already marked as handled");
-
- device = TryGetDeviceById(currentEventReadPtr->deviceId);
- if (device != null && device.updateBeforeRender &&
- (currentEventReadPtr->type == StateEvent.Type ||
- currentEventReadPtr->type == DeltaStateEvent.Type))
- break;
-
- currentEventReadPtr = m_InputEventStream.Advance(leaveEventInBuffer: true);
- }
- }
-
- if (m_InputEventStream.remainingEventCount == 0)
- break;
-
- var currentEventTimeInternal = currentEventReadPtr->internalTime;
- var currentEventType = currentEventReadPtr->type;
-
- #if UNITY_EDITOR
- if (dropStatusEvents)
- {
- // If the type here is a status event, ask advance not to leave the event in the buffer. Otherwise, leave it there.
- if (currentEventType == StateEvent.Type || currentEventType == DeltaStateEvent.Type || currentEventType == IMECompositionEvent.Type)
- m_InputEventStream.Advance(false);
- else
- m_InputEventStream.Advance(true);
-
- continue;
- }
- #endif
-
- // In the editor, we discard all input events that occur in-between exiting edit mode and having
- // entered play mode as otherwise we'll spill a bunch of UI events that have occurred while the
- // UI was sort of neither in this mode nor in that mode. This would usually lead to the game receiving
- // an accumulation of spurious inputs right in one of its first updates.
- //
- // NOTE: There's a chance the solution here will prove inadequate on the long run. We may do things
- // here such as throwing partial touches away and then letting the rest of a touch go through.
- // Could be that ultimately we need to issue a full reset of all devices at the beginning of
- // play mode in the editor.
- #if UNITY_EDITOR
- if ((currentEventType == StateEvent.Type ||
- currentEventType == DeltaStateEvent.Type) &&
- (updateType & InputUpdateType.Editor) == 0 &&
- InputSystem.s_SystemObject.exitEditModeTime > 0 &&
- currentEventTimeInternal >= InputSystem.s_SystemObject.exitEditModeTime &&
- (currentEventTimeInternal < InputSystem.s_SystemObject.enterPlayModeTime ||
- InputSystem.s_SystemObject.enterPlayModeTime == 0))
- {
- m_InputEventStream.Advance(false);
- continue;
- }
- #endif
-
- // If we're timeslicing, check if the event time is within limits.
- if (timesliceEvents && currentEventTimeInternal >= currentTime)
- {
- m_InputEventStream.Advance(true);
- continue;
- }
-
- // If we can't find the device, ignore the event.
- if (device == null)
- device = TryGetDeviceById(currentEventReadPtr->deviceId);
- if (device == null)
- {
- #if UNITY_EDITOR
- ////TODO: see if this is a device we haven't created and if so, just ignore
- m_Diagnostics?.OnCannotFindDeviceForEvent(new InputEventPtr(currentEventReadPtr));
- #endif
-
- m_InputEventStream.Advance(false);
- continue;
- }
-
- // In the editor, we may need to bump events from editor updates into player updates
- // and vice versa.
- #if UNITY_EDITOR
- if (isPlaying && !gameHasFocus)
- {
- if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode
- .PointersAndKeyboardsRespectGameViewFocus &&
- m_Settings.backgroundBehavior !=
- InputSettings.BackgroundBehavior.ResetAndDisableAllDevices)
- {
- var isPointerOrKeyboard = device is Pointer || device is Keyboard;
- if (updateType != InputUpdateType.Editor)
- {
- // Let everything but pointer and keyboard input through.
- // If the event is from a pointer or keyboard, leave it in the buffer so it can be dealt with
- // in a subsequent editor update. Otherwise, take it out.
- if (isPointerOrKeyboard)
- {
- m_InputEventStream.Advance(true);
- continue;
- }
- }
- else
- {
- // Let only pointer and keyboard input through.
- if (!isPointerOrKeyboard)
- {
- m_InputEventStream.Advance(true);
- continue;
- }
- }
- }
- }
- #endif
-
- // If device is disabled, we let the event through only in certain cases.
- // Removal and configuration change events should always be processed.
- if (!device.enabled &&
- currentEventType != DeviceRemoveEvent.Type &&
- currentEventType != DeviceConfigurationEvent.Type &&
- (device.m_DeviceFlags & (InputDevice.DeviceFlags.DisabledInRuntime |
- InputDevice.DeviceFlags.DisabledWhileInBackground)) != 0)
- {
- #if UNITY_EDITOR
- // If the device is disabled in the backend, getting events for them
- // is something that indicates a problem in the backend so diagnose.
- if ((device.m_DeviceFlags & InputDevice.DeviceFlags.DisabledInRuntime) != 0)
- m_Diagnostics?.OnEventForDisabledDevice(currentEventReadPtr, device);
- #endif
-
- m_InputEventStream.Advance(false);
- continue;
- }
-
- // Check if the device wants to merge successive events.
- if (!settings.disableRedundantEventsMerging && device.hasEventMerger && currentEventReadPtr != skipEventMergingFor)
- {
- // NOTE: This relies on events in the buffer being consecutive for the same device. This is not
- // necessarily the case for events coming in from the background event queue where parallel
- // producers may create interleaved input sequences. This will be fixed once we have the
- // new buffering scheme for input events working in the native runtime.
-
- var nextEvent = m_InputEventStream.Peek();
- // If there is next event after current one.
- if ((nextEvent != null)
- // And if next event is for the same device.
- && (currentEventReadPtr->deviceId == nextEvent->deviceId)
- // And if next event is in the same timeslicing slot.
- && (timesliceEvents ? (nextEvent->internalTime < currentTime) : true)
- )
- {
- // Then try to merge current event into next event.
- if (((IEventMerger)device).MergeForward(currentEventReadPtr, nextEvent))
- {
- // And if succeeded, skip current event, as it was merged into next event.
- m_InputEventStream.Advance(false);
- continue;
- }
-
- // If we can't merge current event with next one for any reason, we assume the next event
- // carries crucial entropy (button changed state, phase changed, counter changed, etc).
- // Hence semantic meaning for current event is "can't merge current with next because next is different".
- // But semantic meaning for next event is "next event carries important information and should be preserved",
- // from that point of view next event should not be merged with current nor with _next after next_ event.
- //
- // For example, given such stream of events:
- // Mouse Mouse Mouse Mouse Mouse Mouse Mouse
- // Event no1 Event no2 Event no3 Event no4 Event no5 Event no6 Event no7
- // Time 1 Time 2 Time 3 Time 4 Time 5 Time 6 Time 7
- // Pos(10,20) Pos(12,21) Pos(13,23) Pos(14,24) Pos(16,25) Pos(17,27) Pos(18,28)
- // Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1)
- // BtnLeft(0) BtnLeft(0) BtnLeft(0) BtnLeft(1) BtnLeft(1) BtnLeft(1) BtnLeft(1)
- //
- // if we then merge without skipping next event here:
- // Mouse Mouse
- // Event no3 Event no7
- // Time 3 Time 7
- // Pos(13,23) Pos(18,28)
- // Delta(4,4) Delta(5,5)
- // BtnLeft(0) BtnLeft(1)
- //
- // As you can see, the event no4 containing mouse button press was lost,
- // and with it we lose the important information of timestamp of mouse button press.
- //
- // With skipping merging next event we will get:
- // Mouse Mouse Mouse
- // Time 3 Time 4 Time 7
- // Event no3 Event no4 Event no7
- // Pos(13,23) Pos(14,24) Pos(18,28)
- // Delta(3,3) Delta(1,1) Delta(4,4)
- // BtnLeft(0) BtnLeft(1) BtnLeft(1)
- //
- // And no4 is preserved, with the exact timestamp of button press.
- skipEventMergingFor = nextEvent;
- }
- }
-
- // Give the device a chance to do something with data before we propagate it to event listeners.
- if (device.hasEventPreProcessor)
- {
- #if UNITY_EDITOR
- var eventSizeBeforePreProcessor = currentEventReadPtr->sizeInBytes;
- #endif
- var shouldProcess = ((IEventPreProcessor)device).PreProcessEvent(currentEventReadPtr);
- #if UNITY_EDITOR
- if (currentEventReadPtr->sizeInBytes > eventSizeBeforePreProcessor)
- {
- Profiler.EndSample();
- throw new AccessViolationException($"'{device}'.PreProcessEvent tries to grow an event from {eventSizeBeforePreProcessor} bytes to {currentEventReadPtr->sizeInBytes} bytes, this will potentially corrupt events after the current event and/or cause out-of-bounds memory access.");
- }
- #endif
- if (!shouldProcess)
- {
- // Skip event if PreProcessEvent considers it to be irrelevant.
- m_InputEventStream.Advance(false);
- continue;
- }
- }
-
- // Give listeners a shot at the event.
- // NOTE: We call listeners also for events where the device is disabled. This is crucial for code
- // such as TouchSimulation that disables the originating devices and then uses its events to
- // create simulated events from.
- if (m_EventListeners.length > 0)
- {
- DelegateHelpers.InvokeCallbacksSafe(ref m_EventListeners,
- new InputEventPtr(currentEventReadPtr), device, "InputSystem.onEvent");
-
- // If a listener marks the event as handled, we don't process it further.
- if (currentEventReadPtr->handled)
- {
- m_InputEventStream.Advance(false);
- continue;
- }
- }
-
- // Update metrics.
- if (currentEventTimeInternal <= currentTime)
- totalEventLag += currentTime - currentEventTimeInternal;
- ++m_Metrics.totalEventCount;
- m_Metrics.totalEventBytes += (int)currentEventReadPtr->sizeInBytes;
-
- // Process.
- switch (currentEventType)
- {
- case StateEvent.Type:
- case DeltaStateEvent.Type:
-
- var eventPtr = new InputEventPtr(currentEventReadPtr);
-
- // Ignore the event if the last state update we received for the device was
- // newer than this state event is. We don't allow devices to go back in time.
- //
- // NOTE: We make an exception here for devices that implement IInputStateCallbackReceiver (such
- // as Touchscreen). For devices that dynamically incorporate state it can be hard ensuring
- // a global ordering of events as there may be multiple substreams (e.g. each individual touch)
- // that are generated in the backend and would require considerable work to ensure monotonically
- // increasing timestamps across all such streams.
- var deviceIsStateCallbackReceiver = device.hasStateCallbacks;
- if (currentEventTimeInternal < device.m_LastUpdateTimeInternal &&
- !(deviceIsStateCallbackReceiver && device.stateBlock.format != eventPtr.stateFormat))
- {
- #if UNITY_EDITOR
- m_Diagnostics?.OnEventTimestampOutdated(new InputEventPtr(currentEventReadPtr), device);
- #elif UNITY_ANDROID
- // Android keyboards can send events out of order: Holding down a key will send multiple
- // presses after a short time, like on most platforms. Unfortunately, on Android, the
- // last of these "presses" can be timestamped to be after the event of the key release.
- // If that happens, we'd skip the keyUp here, and the device state will have the key
- // "stuck" pressed. So, special case here to not skip keyboard events on Android. ISXB-475
- // N.B. Android seems to have similar issues with touch input (OnStateEvent, Touchscreen.cs)
- if (!(device is Keyboard))
- #endif
- break;
- }
-
- // Update the state of the device from the event. If the device is an IInputStateCallbackReceiver,
- // let the device handle the event. If not, we do it ourselves.
- var haveChangedStateOtherThanNoise = true;
- if (deviceIsStateCallbackReceiver)
- {
- m_ShouldMakeCurrentlyUpdatingDeviceCurrent = true;
- // NOTE: We leave it to the device to make sure the event has the right format. This allows the
- // device to handle multiple different incoming formats.
- ((IInputStateCallbackReceiver)device).OnStateEvent(eventPtr);
-
- haveChangedStateOtherThanNoise = m_ShouldMakeCurrentlyUpdatingDeviceCurrent;
- }
- else
- {
- // If the state format doesn't match, ignore the event.
- if (device.stateBlock.format != eventPtr.stateFormat)
- {
- #if UNITY_EDITOR
- m_Diagnostics?.OnEventFormatMismatch(currentEventReadPtr, device);
- #endif
- break;
- }
-
- haveChangedStateOtherThanNoise = UpdateState(device, eventPtr, updateType);
- }
-
- totalEventBytesProcessed += eventPtr.sizeInBytes;
-
- // Update timestamp on device.
- // NOTE: We do this here and not in UpdateState() so that InputState.Change() will *NOT* change timestamps.
- // Only events should. If running play mode updates in editor, we want to defer to the play mode
- // callbacks to set the last update time to avoid dropping events only processed by the editor state.
- if (device.m_LastUpdateTimeInternal <= eventPtr.internalTime
- #if UNITY_EDITOR
- && !(updateType == InputUpdateType.Editor && runPlayerUpdatesInEditMode)
- #endif
- )
- device.m_LastUpdateTimeInternal = eventPtr.internalTime;
-
- // Make device current. Again, only do this when receiving events.
- if (haveChangedStateOtherThanNoise)
- device.MakeCurrent();
-
- break;
-
- case TextEvent.Type:
- {
- var textEventPtr = (TextEvent*)currentEventReadPtr;
- if (device is ITextInputReceiver textInputReceiver)
- {
- var utf32Char = textEventPtr->character;
- if (utf32Char >= 0x10000)
- {
- // Send surrogate pair.
- utf32Char -= 0x10000;
- var highSurrogate = 0xD800 + ((utf32Char >> 10) & 0x3FF);
- var lowSurrogate = 0xDC00 + (utf32Char & 0x3FF);
-
- textInputReceiver.OnTextInput((char)highSurrogate);
- textInputReceiver.OnTextInput((char)lowSurrogate);
- }
- else
- {
- // Send single, plain character.
- textInputReceiver.OnTextInput((char)utf32Char);
- }
- }
-
- break;
- }
-
- case IMECompositionEvent.Type:
- {
- var imeEventPtr = (IMECompositionEvent*)currentEventReadPtr;
- var textInputReceiver = device as ITextInputReceiver;
- textInputReceiver?.OnIMECompositionChanged(imeEventPtr->compositionString);
- break;
- }
-
- case DeviceRemoveEvent.Type:
- {
- RemoveDevice(device, keepOnListOfAvailableDevices: false);
-
- // If it's a native device with a description, put it on the list of disconnected
- // devices.
- if (device.native && !device.description.empty)
- {
- ArrayHelpers.AppendWithCapacity(ref m_DisconnectedDevices,
- ref m_DisconnectedDevicesCount, device);
- DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners,
- device, InputDeviceChange.Disconnected, "InputSystem.onDeviceChange");
- }
-
- break;
- }
-
- case DeviceConfigurationEvent.Type:
- device.NotifyConfigurationChanged();
- InputActionState.OnDeviceChange(device, InputDeviceChange.ConfigurationChanged);
- DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners,
- device, InputDeviceChange.ConfigurationChanged, "InputSystem.onDeviceChange");
- break;
-
- case DeviceResetEvent.Type:
- ResetDevice(device,
- alsoResetDontResetControls: ((DeviceResetEvent*)currentEventReadPtr)->hardReset);
- break;
- }
-
- m_InputEventStream.Advance(leaveEventInBuffer: false);
- }
-
- m_Metrics.totalEventProcessingTime +=
- ((double)(Stopwatch.GetTimestamp() - processingStartTime)) / Stopwatch.Frequency;
- m_Metrics.totalEventLagTime += totalEventLag;
-
- m_InputEventStream.Close(ref eventBuffer);
- }
- catch (Exception)
- {
- // We need to restore m_InputEventStream to a sound state
- // to avoid failing recursive OnUpdate check next frame.
- Profiler.EndSample();
- m_InputEventStream.CleanUpAfterException();
- throw;
- }
-
- if (shouldProcessActionTimeouts)
- ProcessStateChangeMonitorTimeouts();
-
- Profiler.EndSample();
-
- ////FIXME: need to ensure that if someone calls QueueEvent() from an onAfterUpdate callback, we don't end up with a
- //// mess in the event buffer
- //// same goes for events that someone may queue from a change monitor callback
- InvokeAfterUpdateCallback(updateType);
- m_CurrentUpdate = default;
- }
-
- private void InvokeAfterUpdateCallback(InputUpdateType updateType)
- {
- // don't invoke the after update callback if this is an editor update and the game is playing. We
- // skip event processing when playing in the editor and the game has focus, which means that any
- // handlers for this delegate that query input state during this update will get no values.
- if (updateType == InputUpdateType.Editor && gameIsPlaying)
- return;
-
- DelegateHelpers.InvokeCallbacksSafe(ref m_AfterUpdateListeners,
- "InputSystem.onAfterUpdate");
- }
-
- private bool m_ShouldMakeCurrentlyUpdatingDeviceCurrent;
-
- // This is a dirty hot fix to expose entropy from device back to input manager to make a choice if we want to make device current or not.
- // A proper fix would be to change IInputStateCallbackReceiver.OnStateEvent to return bool to make device current or not.
- internal void DontMakeCurrentlyUpdatingDeviceCurrent()
- {
- m_ShouldMakeCurrentlyUpdatingDeviceCurrent = false;
- }
-
- internal unsafe bool UpdateState(InputDevice device, InputEvent* eventPtr, InputUpdateType updateType)
- {
- Debug.Assert(eventPtr != null, "Received NULL event ptr");
-
- var stateBlockOfDevice = device.m_StateBlock;
- var stateBlockSizeOfDevice = stateBlockOfDevice.sizeInBits / 8; // Always byte-aligned; avoid calling alignedSizeInBytes.
- var offsetInDeviceStateToCopyTo = 0u;
- uint sizeOfStateToCopy;
- uint receivedStateSize;
- byte* ptrToReceivedState;
- FourCC receivedStateFormat;
-
- // Grab state data from event and decide where to copy to and how much to copy.
- if (eventPtr->type == StateEvent.Type)
- {
- var stateEventPtr = (StateEvent*)eventPtr;
- receivedStateFormat = stateEventPtr->stateFormat;
- receivedStateSize = stateEventPtr->stateSizeInBytes;
- ptrToReceivedState = (byte*)stateEventPtr->state;
-
- // Ignore extra state at end of event.
- sizeOfStateToCopy = receivedStateSize;
- if (sizeOfStateToCopy > stateBlockSizeOfDevice)
- sizeOfStateToCopy = stateBlockSizeOfDevice;
- }
- else
- {
- Debug.Assert(eventPtr->type == DeltaStateEvent.Type, "Given event must either be a StateEvent or a DeltaStateEvent");
-
- var deltaEventPtr = (DeltaStateEvent*)eventPtr;
- receivedStateFormat = deltaEventPtr->stateFormat;
- receivedStateSize = deltaEventPtr->deltaStateSizeInBytes;
- ptrToReceivedState = (byte*)deltaEventPtr->deltaState;
- offsetInDeviceStateToCopyTo = deltaEventPtr->stateOffset;
-
- // Ignore extra state at end of event.
- sizeOfStateToCopy = receivedStateSize;
- if (offsetInDeviceStateToCopyTo + sizeOfStateToCopy > stateBlockSizeOfDevice)
- {
- if (offsetInDeviceStateToCopyTo >= stateBlockSizeOfDevice)
- return false; // Entire delta state is out of range.
-
- sizeOfStateToCopy = stateBlockSizeOfDevice - offsetInDeviceStateToCopyTo;
- }
- }
-
- Debug.Assert(device.m_StateBlock.format == receivedStateFormat, "Received state format does not match format of device");
-
- // Write state.
- return UpdateState(device, updateType, ptrToReceivedState, offsetInDeviceStateToCopyTo,
- sizeOfStateToCopy, eventPtr->internalTime, eventPtr);
- }
-
- /// <summary>
- /// This method is the workhorse for updating input state in the system. It runs all the logic of incorporating
- /// new state into devices and triggering whatever change monitors are attached to the state memory that gets
- /// touched.
- /// </summary>
- /// <remarks>
- /// This method can be invoked from outside the event processing loop and the given data does not have to come
- /// from an event.
- ///
- /// This method does NOT respect <see cref="IInputStateCallbackReceiver"/>. This means that the device will
- /// NOT get a shot at intervening in the state write.
- /// </remarks>
- /// <param name="device">Device to update state on. <paramref name="stateOffsetInDevice"/> is relative to device's
- /// starting offset in memory.</param>
- /// <param name="eventPtr">Pointer to state event from which the state change was initiated. Null if the state
- /// change is not coming from an event.</param>
- internal unsafe bool UpdateState(InputDevice device, InputUpdateType updateType,
- void* statePtr, uint stateOffsetInDevice, uint stateSize, double internalTime, InputEventPtr eventPtr = default)
- {
- var deviceIndex = device.m_DeviceIndex;
- ref var stateBlockOfDevice = ref device.m_StateBlock;
-
- ////TODO: limit stateSize and StateOffset by the device's state memory
-
- var deviceBuffer = (byte*)InputStateBuffers.GetFrontBufferForDevice(deviceIndex);
-
- // If state monitors need to be re-sorted, do it now.
- // NOTE: This must happen with the monitors in non-signalled state!
- SortStateChangeMonitorsIfNecessary(deviceIndex);
-
- // Before we update state, let change monitors compare the old and the new state.
- // We do this instead of first updating the front buffer and then comparing to the
- // back buffer as that would require a buffer flip for each state change in order
- // for the monitors to work reliably. By comparing the *event* data to the current
- // state, we can have multiple state events in the same frame yet still get reliable
- // change notifications.
- var haveSignalledMonitors =
- ProcessStateChangeMonitors(deviceIndex, statePtr,
- deviceBuffer + stateBlockOfDevice.byteOffset,
- stateSize, stateOffsetInDevice);
-
- var deviceStateOffset = device.m_StateBlock.byteOffset + stateOffsetInDevice;
- var deviceStatePtr = deviceBuffer + deviceStateOffset;
-
- ////REVIEW: Should we do this only for events but not for InputState.Change()?
- // If noise filtering on .current is turned on and the device may have noise,
- // determine if the event carries signal or not.
- var noiseMask = device.noisy
- ? (byte*)InputStateBuffers.s_NoiseMaskBuffer + deviceStateOffset
- : null;
- // Compare the current state of the device to the newly received state but overlay
- // the comparison by the noise mask.
- var makeDeviceCurrent = !MemoryHelpers.MemCmpBitRegion(deviceStatePtr, statePtr,
- 0, stateSize * 8, mask: noiseMask);
-
- // Buffer flip.
- var flipped = FlipBuffersForDeviceIfNecessary(device, updateType);
-
- // Now write the state.
- #if UNITY_EDITOR
- if (updateType == InputUpdateType.Editor)
- {
- WriteStateChange(m_StateBuffers.m_EditorStateBuffers, deviceIndex, ref stateBlockOfDevice, stateOffsetInDevice,
- statePtr, stateSize, flipped);
- }
- else
- #endif
- {
- WriteStateChange(m_StateBuffers.m_PlayerStateBuffers, deviceIndex, ref stateBlockOfDevice,
- stateOffsetInDevice, statePtr, stateSize, flipped);
- }
-
- // Notify listeners.
- DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceStateChangeListeners,
- device, eventPtr, "InputSystem.onDeviceStateChange");
-
- // Now that we've committed the new state to memory, if any of the change
- // monitors fired, let the associated actions know.
- if (haveSignalledMonitors)
- FireStateChangeNotifications(deviceIndex, internalTime, eventPtr);
-
- return makeDeviceCurrent;
- }
-
- private unsafe void WriteStateChange(InputStateBuffers.DoubleBuffers buffers, int deviceIndex,
- ref InputStateBlock deviceStateBlock, uint stateOffsetInDevice, void* statePtr, uint stateSizeInBytes, bool flippedBuffers)
- {
- var frontBuffer = buffers.GetFrontBuffer(deviceIndex);
- Debug.Assert(frontBuffer != null);
-
- // If we're updating less than the full state, we need to preserve the parts we are not updating.
- // Instead of trying to optimize here and only copy what we really need, we just go and copy the
- // entire state of the device over.
- //
- // NOTE: This copying must only happen once, right after a buffer flip. Otherwise we may copy old,
- // stale input state from the back buffer over state that has already been updated with newer
- // data.
- var deviceStateSize = deviceStateBlock.sizeInBits / 8; // Always byte-aligned; avoid calling alignedSizeInBytes.
- if (flippedBuffers && deviceStateSize != stateSizeInBytes)
- {
- var backBuffer = buffers.GetBackBuffer(deviceIndex);
- Debug.Assert(backBuffer != null);
-
- UnsafeUtility.MemCpy(
- (byte*)frontBuffer + deviceStateBlock.byteOffset,
- (byte*)backBuffer + deviceStateBlock.byteOffset,
- deviceStateSize);
- }
-
- if (InputSettings.readValueCachingFeatureEnabled)
- {
- // if the buffers have just been flipped, and we're doing a full state update, then the state from the
- // previous update is now in the back buffer, and we should be comparing to that when checking what
- // controls have changed
- var buffer = (byte*)frontBuffer;
- if (flippedBuffers && deviceStateSize == stateSizeInBytes)
- buffer = (byte*)buffers.GetBackBuffer(deviceIndex);
-
- m_Devices[deviceIndex].WriteChangedControlStates(buffer + deviceStateBlock.byteOffset, statePtr,
- stateSizeInBytes, stateOffsetInDevice);
- }
-
- UnsafeUtility.MemCpy((byte*)frontBuffer + deviceStateBlock.byteOffset + stateOffsetInDevice, statePtr,
- stateSizeInBytes);
- }
-
- // Flip front and back buffer for device, if necessary. May flip buffers for more than just
- // the given update type.
- // Returns true if there was a buffer flip.
- private bool FlipBuffersForDeviceIfNecessary(InputDevice device, InputUpdateType updateType)
- {
- if (updateType == InputUpdateType.BeforeRender)
- {
- ////REVIEW: I think this is wrong; if we haven't flipped in the current dynamic or fixed update, we should do so now
- // We never flip buffers for before render. Instead, we already write
- // into the front buffer.
- return false;
- }
-
- #if UNITY_EDITOR
- ////REVIEW: should this use the editor update ticks as quasi-frame-boundaries?
- // Updates go to the editor only if the game isn't playing or does not have focus.
- // Otherwise we fall through to the logic that flips for the *next* dynamic and
- // fixed updates.
- if (updateType == InputUpdateType.Editor)
- {
- ////REVIEW: This isn't right. The editor does have update ticks which constitute the equivalent of player frames.
- // The editor doesn't really have a concept of frame-to-frame operation the
- // same way the player does. So we simply flip buffers on a device whenever
- // a new state event for it comes in.
- m_StateBuffers.m_EditorStateBuffers.SwapBuffers(device.m_DeviceIndex);
- return true;
- }
- #endif
-
- // Flip buffers if we haven't already for this frame.
- if (device.m_CurrentUpdateStepCount != InputUpdate.s_UpdateStepCount)
- {
- m_StateBuffers.m_PlayerStateBuffers.SwapBuffers(device.m_DeviceIndex);
- device.m_CurrentUpdateStepCount = InputUpdate.s_UpdateStepCount;
- return true;
- }
-
- return false;
- }
-
- // Domain reload survival logic. Also used for pushing and popping input system
- // state for testing.
-
- // Stuff everything that we want to survive a domain reload into
- // a m_SerializedState.
- #if UNITY_EDITOR || DEVELOPMENT_BUILD
- [Serializable]
- internal struct DeviceState
- {
- // Preserving InputDevices is somewhat tricky business. Serializing
- // them in full would involve pretty nasty work. We have the restriction,
- // however, that everything needs to be created from layouts (it partly
- // exists for the sake of reload survivability), so we should be able to
- // just go and recreate the device from the layout. This also has the
- // advantage that if the layout changes between reloads, the change
- // automatically takes effect.
- public string name;
- public string layout;
- public string variants;
- public string[] usages;
- public int deviceId;
- public int participantId;
- public InputDevice.DeviceFlags flags;
- public InputDeviceDescription description;
-
- public void Restore(InputDevice device)
- {
- var usageCount = usages.LengthSafe();
- for (var i = 0; i < usageCount; ++i)
- device.AddDeviceUsage(new InternedString(usages[i]));
- device.m_ParticipantId = participantId;
- }
- }
-
- /// <summary>
- /// State we take across domain reloads.
- /// </summary>
- /// <remarks>
- /// Most of the state we re-recreate in-between reloads and do not store
- /// in this structure. In particular, we do not preserve anything from
- /// the various RegisterXXX().
- ///
- /// WARNING
- ///
- /// Making changes to serialized data format will likely to break upgrading projects from older versions.
- /// That is until you restart the editor, then we recreate everything from clean state.
- /// </remarks>
- [Serializable]
- internal struct SerializedState
- {
- public int layoutRegistrationVersion;
- public float pollingFrequency;
- public DeviceState[] devices;
- public AvailableDevice[] availableDevices;
- public InputStateBuffers buffers;
- public InputUpdate.SerializedState updateState;
- public InputUpdateType updateMask;
- public InputMetrics metrics;
- public InputSettings settings;
- public InputActionAsset actions;
-
- #if UNITY_ANALYTICS || UNITY_EDITOR
- public bool haveSentStartupAnalytics;
- #endif
- }
-
- internal SerializedState SaveState()
- {
- // Devices.
- var deviceCount = m_DevicesCount;
- var deviceArray = new DeviceState[deviceCount];
- for (var i = 0; i < deviceCount; ++i)
- {
- var device = m_Devices[i];
- string[] usages = null;
- if (device.usages.Count > 0)
- usages = device.usages.Select(x => x.ToString()).ToArray();
-
- var deviceState = new DeviceState
- {
- name = device.name,
- layout = device.layout,
- variants = device.variants,
- deviceId = device.deviceId,
- participantId = device.m_ParticipantId,
- usages = usages,
- description = device.m_Description,
- flags = device.m_DeviceFlags
- };
- deviceArray[i] = deviceState;
- }
-
- return new SerializedState
- {
- layoutRegistrationVersion = m_LayoutRegistrationVersion,
- pollingFrequency = m_PollingFrequency,
- devices = deviceArray,
- availableDevices = m_AvailableDevices?.Take(m_AvailableDeviceCount).ToArray(),
- buffers = m_StateBuffers,
- updateState = InputUpdate.Save(),
- updateMask = m_UpdateMask,
- metrics = m_Metrics,
- settings = m_Settings,
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- actions = m_Actions,
- #endif
-
- #if UNITY_ANALYTICS || UNITY_EDITOR
- haveSentStartupAnalytics = m_HaveSentStartupAnalytics,
- #endif
- };
- }
-
- internal void RestoreStateWithoutDevices(SerializedState state)
- {
- m_StateBuffers = state.buffers;
- m_LayoutRegistrationVersion = state.layoutRegistrationVersion + 1;
- updateMask = state.updateMask;
- m_Metrics = state.metrics;
- m_PollingFrequency = state.pollingFrequency;
-
- if (m_Settings != null)
- Object.DestroyImmediate(m_Settings);
- m_Settings = state.settings;
-
- #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
- // Note that we just reassign actions and never destroy them since always mapped to persisted asset
- // and hence ownership lies with ADB.
- m_Actions = state.actions;
- #endif
-
- #if UNITY_ANALYTICS || UNITY_EDITOR
- m_HaveSentStartupAnalytics = state.haveSentStartupAnalytics;
- #endif
-
- ////REVIEW: instead of accessing globals here, we could move this to when we re-create devices
-
- // Update state.
- InputUpdate.Restore(state.updateState);
- }
-
- // If these are set, we clear them out on the first input update.
- internal DeviceState[] m_SavedDeviceStates;
- internal AvailableDevice[] m_SavedAvailableDevices;
-
- /// <summary>
- /// Recreate devices based on the devices we had before a domain reload.
- /// </summary>
- /// <remarks>
- /// Note that device indices may change between domain reloads.
- ///
- /// We recreate devices using the layout information as it exists now as opposed to
- /// as it existed before the domain reload. This means we'll be picking up any changes that
- /// have happened to layouts as part of the reload (including layouts having been removed
- /// entirely).
- /// </remarks>
- internal void RestoreDevicesAfterDomainReload()
- {
- Profiler.BeginSample("InputManager.RestoreDevicesAfterDomainReload");
-
- using (InputDeviceBuilder.Ref())
- {
- DeviceState[] retainedDeviceStates = null;
- var deviceStates = m_SavedDeviceStates;
- var deviceCount = m_SavedDeviceStates.LengthSafe();
- m_SavedDeviceStates = null; // Prevent layout matcher registering themselves on the fly from picking anything off this list.
- for (var i = 0; i < deviceCount; ++i)
- {
- ref var deviceState = ref deviceStates[i];
-
- var device = TryGetDeviceById(deviceState.deviceId);
- if (device != null)
- continue;
-
- var layout = TryFindMatchingControlLayout(ref deviceState.description,
- deviceState.deviceId);
- if (layout.IsEmpty())
- {
- var previousLayout = new InternedString(deviceState.layout);
- if (m_Layouts.HasLayout(previousLayout))
- layout = previousLayout;
- }
- if (layout.IsEmpty() || !RestoreDeviceFromSavedState(ref deviceState, layout))
- ArrayHelpers.Append(ref retainedDeviceStates, deviceState);
- }
-
- // See if we can make sense of an available device now that we couldn't make sense of
- // before. This can be the case if there's new layout information that wasn't available
- // before.
- if (m_SavedAvailableDevices != null)
- {
- m_AvailableDevices = m_SavedAvailableDevices;
- m_AvailableDeviceCount = m_SavedAvailableDevices.LengthSafe();
- for (var i = 0; i < m_AvailableDeviceCount; ++i)
- {
- var device = TryGetDeviceById(m_AvailableDevices[i].deviceId);
- if (device != null)
- continue;
-
- if (m_AvailableDevices[i].isRemoved)
- continue;
-
- var layout = TryFindMatchingControlLayout(ref m_AvailableDevices[i].description,
- m_AvailableDevices[i].deviceId);
- if (!layout.IsEmpty())
- {
- try
- {
- AddDevice(layout, m_AvailableDevices[i].deviceId,
- deviceDescription: m_AvailableDevices[i].description,
- deviceFlags: m_AvailableDevices[i].isNative ? InputDevice.DeviceFlags.Native : 0);
- }
- catch (Exception)
- {
- // Just ignore. Simply means we still can't really turn the device into something useful.
- }
- }
- }
- }
-
- // Done. Discard saved arrays.
- m_SavedDeviceStates = retainedDeviceStates;
- m_SavedAvailableDevices = null;
- }
-
- Profiler.EndSample();
- }
-
- // We have two general types of devices we need to care about when recreating devices
- // after domain reloads:
- //
- // A) device with InputDeviceDescription
- // B) device created directly from specific layout
- //
- // A) should go through the normal matching process whereas B) should get recreated with
- // layout of same name (if still available).
- //
- // So we kick device recreation off from two points:
- //
- // 1) From RegisterControlLayoutMatcher to catch A)
- // 2) From RegisterControlLayout to catch B)
- //
- // Additionally, we have the complication that a layout a device was using was something
- // dynamically registered from onFindLayoutForDevice. We don't do anything special about that.
- // The first full input update will flush out the list of saved device states and at that
- // point, any onFindLayoutForDevice hooks simply have to be in place. If they are, devices
- // will get recreated appropriately.
- //
- // It would be much simpler to recreate all devices as the first thing in the first full input
- // update but that would mean that devices would become available only very late. They would
- // not, for example, be available when MonoBehaviour.Start methods are invoked.
-
- private bool RestoreDeviceFromSavedState(ref DeviceState deviceState, InternedString layout)
- {
- // We assign the same device IDs here to newly created devices that they had
- // before the domain reload. This is safe as device ID allocation is under the
- // control of the runtime and not expected to be affected by a domain reload.
-
- InputDevice device;
- try
- {
- device = AddDevice(layout,
- deviceDescription: deviceState.description,
- deviceId: deviceState.deviceId,
- deviceName: deviceState.name,
- deviceFlags: deviceState.flags,
- variants: new InternedString(deviceState.variants));
- }
- catch (Exception exception)
- {
- Debug.LogError(
- $"Could not recreate input device '{deviceState.description}' with layout '{deviceState.layout}' and variants '{deviceState.variants}' after domain reload");
- Debug.LogException(exception);
- return true; // Don't try again.
- }
-
- deviceState.Restore(device);
-
- return true;
- }
-
- #endif // UNITY_EDITOR || DEVELOPMENT_BUILD
- }
- }
|