diff --git a/Code/Framework/AzCore/AzCore/std/containers/fixed_unordered_map.h b/Code/Framework/AzCore/AzCore/std/containers/fixed_unordered_map.h index 8da0d84a23..19ad46dc62 100644 --- a/Code/Framework/AzCore/AzCore/std/containers/fixed_unordered_map.h +++ b/Code/Framework/AzCore/AzCore/std/containers/fixed_unordered_map.h @@ -120,6 +120,11 @@ namespace AZStd base_type::insert(*first); } } + fixed_unordered_map(const AZStd::initializer_list& list, const hasher& hash = hasher(), + const key_eq& keyEqual = key_eq()) + : fixed_unordered_map(list.begin(), list.end(), hash, keyEqual) + { + } AZ_FORCE_INLINE pair_iter_bool insert(const value_type& value) { @@ -241,6 +246,12 @@ namespace AZStd base_type::insert(*first); } } + fixed_unordered_multimap(const AZStd::initializer_list& list, const hasher& hash = hasher(), + const key_eq& keyEqual = key_eq()) + : fixed_unordered_multimap(list.begin(), list.end(), hash, keyEqual) + { + } + AZ_FORCE_INLINE iterator insert(const value_type& value) { return base_type::insert_impl(value).first; diff --git a/Gems/BarrierInput/CMakeLists.txt b/Gems/BarrierInput/CMakeLists.txt new file mode 100644 index 0000000000..de2b439e68 --- /dev/null +++ b/Gems/BarrierInput/CMakeLists.txt @@ -0,0 +1,9 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +add_subdirectory(Code) diff --git a/Gems/BarrierInput/Code/CMakeLists.txt b/Gems/BarrierInput/Code/CMakeLists.txt new file mode 100644 index 0000000000..96231cfdbf --- /dev/null +++ b/Gems/BarrierInput/Code/CMakeLists.txt @@ -0,0 +1,45 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +ly_add_target( + NAME BarrierInput.Static STATIC + NAMESPACE Gem + FILES_CMAKE + barrierinput_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + BUILD_DEPENDENCIES + PUBLIC + AZ::AzCore + AZ::AzFramework + AZ::AtomCore + Gem::Atom_RPI.Public + RUNTIME_DEPENDENCIES + Gem::Atom_RPI.Private +) + +ly_add_target( + NAME BarrierInput ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + barrierinput_shared_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + BUILD_DEPENDENCIES + PRIVATE + Gem::BarrierInput.Static +) + +# Barrier Input is only needed for the client: +ly_create_alias(NAME BarrierInput.Clients NAMESPACE Gem TARGETS Gem::BarrierInput) diff --git a/Gems/BarrierInput/Code/Include/BarrierInput/RawInputNotificationBus_Barrier.h b/Gems/BarrierInput/Code/Include/BarrierInput/RawInputNotificationBus_Barrier.h new file mode 100644 index 0000000000..17a326e806 --- /dev/null +++ b/Gems/BarrierInput/Code/Include/BarrierInput/RawInputNotificationBus_Barrier.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace BarrierInput +{ + //////////////////////////////////////////////////////////////////////////////////////////////// + //! Barrier keyboard modifier bit mask + enum ModifierMask + { + ModifierMask_None = 0x0000, + ModifierMask_Shift = 0x0001, + ModifierMask_Ctrl = 0x0002, + ModifierMask_AltL = 0x0004, + ModifierMask_Windows = 0x0010, + ModifierMask_AltR = 0x0020, + ModifierMask_CapsLock = 0x1000, + ModifierMask_NumLock = 0x2000, + ModifierMask_ScrollLock = 0x4000, + }; + + //////////////////////////////////////////////////////////////////////////////////////////////// + //! EBus interface used to listen for raw Barrier input as broadcast by the BarrierClient. + //! + //! It's possible to receive multiple events per button/key per frame, and it's very likely that + //! Barrier input events will not be dispatched from the main thread, so care should be taken to + //! ensure thread safety when implementing event handlers that connect to this Barrier event bus. + //! + //! This EBus is intended primarily for the BarrierClient to send raw input to Barrier devices. + //! Most systems that need to process input should use the generic AzFramework input interfaces, + //! but if necessary it is perfectly valid to connect directly to this EBus for Barrier events. + class RawInputNotificationsBarrier : public AZ::EBusTraits + { + public: + //////////////////////////////////////////////////////////////////////////////////////////// + //! EBus Trait: raw input notifications are addressed to a single address + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! EBus Trait: raw input notifications can be handled by multiple listeners + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Default destructor + virtual ~RawInputNotificationsBarrier() = default; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Process raw mouse button down events (assumed to be dispatched from any thread) + //! \param[in] buttonIndex The index of the button that was pressed down + virtual void OnRawMouseButtonDownEvent([[maybe_unused]]uint32_t buttonIndex) {} + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Process raw mouse button up events (assumed to be dispatched from any thread) + //! \param[in] buttonIndex The index of the button that was released up + virtual void OnRawMouseButtonUpEvent([[maybe_unused]]uint32_t buttonIndex) {} + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Process raw mouse movement events (assumed to be dispatched from any thread) + //! \param[in] movementX The x movement of the mouse + //! \param[in] movementY The y movement of the mouse + virtual void OnRawMouseMovementEvent([[maybe_unused]]float movementX, [[maybe_unused]]float movementY) {} + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Process raw mouse position events (assumed to be dispatched from any thread) + //! \param[in] positionX The x position of the mouse + //! \param[in] positionY The y position of the mouse + virtual void OnRawMousePositionEvent([[maybe_unused]]float positionX, [[maybe_unused]]float positionY) {} + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Process raw keyboard key down events (assumed to be dispatched from any thread) + //! \param[in] scanCode The scan code of the key that was pressed down + //! \param[in] activeModifiers The bit mask of currently active modifier keys + virtual void OnRawKeyboardKeyDownEvent([[maybe_unused]]uint32_t scanCode, [[maybe_unused]]ModifierMask activeModifiers) {} + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Process raw keyboard key up events (assumed to be dispatched from any thread) + //! \param[in] scanCode The scan code of the key that was released up + //! \param[in] activeModifiers The bit mask of currently active modifier keys + virtual void OnRawKeyboardKeyUpEvent([[maybe_unused]]uint32_t scanCode, [[maybe_unused]]ModifierMask activeModifiers) {} + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Process raw keyboard key repeat events (assumed to be dispatched from any thread) + //! \param[in] scanCode The scan code of the key that was repeatedly held down + //! \param[in] activeModifiers The bit mask of currently active modifier keys + virtual void OnRawKeyboardKeyRepeatEvent([[maybe_unused]]uint32_t scanCode, [[maybe_unused]]ModifierMask activeModifiers) {} + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Process raw clipboard events (assumed to be dispatched from any thread) + //! \param[in] clipboardContents The contents of the clipboard + virtual void OnRawClipboardEvent([[maybe_unused]]const char* clipboardContents) {} + }; + using RawInputNotificationBusBarrier = AZ::EBus; +} // namespace BarrierInput diff --git a/Gems/BarrierInput/Code/Source/BarrierInputClient.cpp b/Gems/BarrierInput/Code/Source/BarrierInputClient.cpp new file mode 100644 index 0000000000..b1ca6cb7d7 --- /dev/null +++ b/Gems/BarrierInput/Code/Source/BarrierInputClient.cpp @@ -0,0 +1,398 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include +#include + +#include +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// The majority of this file was resurrected from legacy code, and could use some love, but it works. +namespace BarrierInput +{ + struct Stream + { + explicit Stream(int size) + { + buffer = (AZ::u8*)malloc(size); + end = data = buffer; + bufferSize = size; + packet = nullptr; + } + + ~Stream() + { + free(buffer); + } + + AZ::u8* data; + AZ::u8* end; + AZ::u8* buffer; + AZ::u8* packet; + int bufferSize; + + void Rewind() { data = buffer; } + int GetBufferSize() { return bufferSize; } + + char* GetBuffer() { return (char*)buffer; } + char* GetData() { return (char*)data; } + + void SetLength(int len) { end = data + len; } + int GetLength() { return (int)(end - data); } + + int ReadU32() { int ret = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; data += 4; return ret; } + int ReadU16() { int ret = (data[0] << 8) | data[1]; data += 2; return ret; } + int ReadU8() { int ret = data[0]; data += 1; return ret; } + void Eat(int len) { data += len; } + + void InsertString(const char* str) { int len = strlen(str); memcpy(end, str, len); end += len; } + void InsertU32(int a) { end[0] = a >> 24; end[1] = a >> 16; end[2] = a >> 8; end[3] = a; end += 4; } + void InsertU16(int a) { end[0] = a >> 8; end[1] = a; end += 2; } + void InsertU8(int a) { end[0] = a; end += 1; } + void OpenPacket() { packet = end; end += 4; } + void ClosePacket() { int len = GetLength() - sizeof(AZ::u32); packet[0] = len >> 24; packet[1] = len >> 16; packet[2] = len >> 8; packet[3] = len; packet = NULL; } + }; + + enum ArgType + { + ARG_END = 0, + ARG_UINT8, + ARG_UINT16, + ARG_UINT32 + }; + constexpr int MAX_ARGS = 16; + + typedef bool (*packetCallback)(BarrierClient* pContext, int* pArgs, Stream* pStream, int streamLeft); + + struct Packet + { + const char* pattern; + ArgType args[MAX_ARGS + 1]; + packetCallback callback; + }; + + static bool barrierSendFunc(BarrierClient* pContext, const char* buffer, int length) + { + int ret = AZ::AzSock::Send(pContext->GetSocket(), buffer, length, 0); + return (ret == length) ? true : false; + } + + static bool barrierPacket(BarrierClient* pContext, [[maybe_unused]]int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + Stream stream(256); + stream.OpenPacket(); + stream.InsertString("Barrier"); + stream.InsertU16(1); + stream.InsertU16(4); + stream.InsertU32(pContext->GetClientScreenName().length()); + stream.InsertString(pContext->GetClientScreenName().c_str()); + stream.ClosePacket(); + return barrierSendFunc(pContext, stream.GetBuffer(), stream.GetLength()); + } + + static bool barrierQueryInfo(BarrierClient* pContext, [[maybe_unused]]int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + Stream stream(256); + stream.OpenPacket(); + stream.InsertString("DINF"); + stream.InsertU16(0); + stream.InsertU16(0); + + auto atomViewportRequests = AZ::Interface::Get(); + AZ::RPI::ViewportContextPtr viewportContext = atomViewportRequests->GetDefaultViewportContext(); + if (viewportContext) + { + const AzFramework::WindowSize windowSize = viewportContext->GetViewportSize(); + stream.InsertU16(windowSize.m_width); + stream.InsertU16(windowSize.m_height); + } + else + { + stream.InsertU16(1920); + stream.InsertU16(1080); + } + stream.InsertU16(0); + stream.InsertU16(0); + stream.InsertU16(0); + stream.ClosePacket(); + return barrierSendFunc(pContext, stream.GetBuffer(), stream.GetLength()); + } + + static bool barrierKeepAlive(BarrierClient* pContext, [[maybe_unused]]int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + Stream stream(256); + stream.OpenPacket(); + stream.InsertString("CALV"); + stream.ClosePacket(); + return barrierSendFunc(pContext, stream.GetBuffer(), stream.GetLength()); + } + + static bool barrierEnterScreen([[maybe_unused]]BarrierClient* pContext, int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + const float positionX = static_cast(pArgs[0]); + const float positionY = static_cast(pArgs[1]); + RawInputNotificationBusBarrier::Broadcast(&RawInputNotificationsBarrier::OnRawMousePositionEvent, + positionX, + positionY); + return true; + } + + static bool barrierExitScreen([[maybe_unused]]BarrierClient* pContext, [[maybe_unused]]int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + return true; + } + + static bool barrierMouseMove([[maybe_unused]]BarrierClient* pContext, int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + const float positionX = static_cast(pArgs[0]); + const float positionY = static_cast(pArgs[1]); + RawInputNotificationBusBarrier::Broadcast(&RawInputNotificationsBarrier::OnRawMousePositionEvent, + positionX, + positionY); + return true; + } + + static bool barrierMouseMoveRelative([[maybe_unused]]BarrierClient* pContext, int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + const float movementX = static_cast(pArgs[0]); + const float movementY = static_cast(pArgs[1]); + RawInputNotificationBusBarrier::Broadcast(&RawInputNotificationsBarrier::OnRawMouseMovementEvent, + movementX, + movementY); + return true; + } + + static bool barrierMouseButtonDown([[maybe_unused]]BarrierClient* pContext, int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + const uint32_t buttonIndex = pArgs[0]; + RawInputNotificationBusBarrier::Broadcast(&RawInputNotificationsBarrier::OnRawMouseButtonDownEvent, buttonIndex); + return true; + } + + static bool barrierMouseButtonUp([[maybe_unused]]BarrierClient* pContext, int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + const uint32_t buttonIndex = pArgs[0]; + RawInputNotificationBusBarrier::Broadcast(&RawInputNotificationsBarrier::OnRawMouseButtonUpEvent, buttonIndex); + return true; + } + + static bool barrierKeyboardDown([[maybe_unused]]BarrierClient* pContext, int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + const uint32_t scanCode = pArgs[2]; + const ModifierMask activeModifiers = static_cast(pArgs[1]); + RawInputNotificationBusBarrier::Broadcast(&RawInputNotificationsBarrier::OnRawKeyboardKeyDownEvent, scanCode, activeModifiers); + return true; + } + + static bool barrierKeyboardUp([[maybe_unused]]BarrierClient* pContext, int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + const uint32_t scanCode = pArgs[2]; + const ModifierMask activeModifiers = static_cast(pArgs[1]); + RawInputNotificationBusBarrier::Broadcast(&RawInputNotificationsBarrier::OnRawKeyboardKeyUpEvent, scanCode, activeModifiers); + return true; + } + + static bool barrierKeyboardRepeat([[maybe_unused]]BarrierClient* pContext, int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + const uint32_t scanCode = pArgs[2]; + const ModifierMask activeModifiers = static_cast(pArgs[1]); + RawInputNotificationBusBarrier::Broadcast(&RawInputNotificationsBarrier::OnRawKeyboardKeyRepeatEvent, scanCode, activeModifiers); + return true; + } + + static bool barrierClipboard([[maybe_unused]]BarrierClient* pContext, int* pArgs, Stream* pStream, [[maybe_unused]]int streamLeft) + { + for (int i = 0; i < pArgs[3]; i++) + { + int format = pStream->ReadU32(); + int size = pStream->ReadU32(); + if (format == 0) // Is text + { + char* clipboardContents = new char[size]; + memcpy(clipboardContents, pStream->GetData(), size); + clipboardContents[size] = '\0'; + RawInputNotificationBusBarrier::Broadcast(&RawInputNotificationsBarrier::OnRawClipboardEvent, clipboardContents); + delete[] clipboardContents; + } + pStream->Eat(size); + } + return true; + } + + static bool barrierBye([[maybe_unused]]BarrierClient* pContext, [[maybe_unused]]int* pArgs, [[maybe_unused]]Stream* pStream, [[maybe_unused]]int streamLeft) + { + AZLOG_INFO("BarrierClient: Server said bye. Disconnecting\n"); + return false; + } + + static Packet s_packets[] = { + { "Barrier", { ARG_UINT16, ARG_UINT16 }, barrierPacket }, + { "QINF", {}, barrierQueryInfo }, + { "CALV", {}, barrierKeepAlive }, + { "CINN", { ARG_UINT16, ARG_UINT16, ARG_UINT32, ARG_UINT16 }, barrierEnterScreen }, + { "COUT", { }, barrierExitScreen }, + { "CBYE", { }, barrierBye }, + { "DMMV", { ARG_UINT16, ARG_UINT16 }, barrierMouseMove }, + { "DMRM", { ARG_UINT16, ARG_UINT16 }, barrierMouseMoveRelative }, + { "DMDN", { ARG_UINT8 }, barrierMouseButtonDown }, + { "DMUP", { ARG_UINT8 }, barrierMouseButtonUp }, + { "DKDN", { ARG_UINT16, ARG_UINT16, ARG_UINT16 }, barrierKeyboardDown }, + { "DKUP", { ARG_UINT16, ARG_UINT16, ARG_UINT16 }, barrierKeyboardUp }, + { "DKRP", { ARG_UINT16, ARG_UINT16, ARG_UINT16, ARG_UINT16 }, barrierKeyboardRepeat }, + { "DCLP", { ARG_UINT8, ARG_UINT32, ARG_UINT32, ARG_UINT32 }, barrierClipboard } + }; + + static bool ProcessPackets(BarrierClient* pContext, Stream& stream) + { + while (stream.data < stream.end) + { + const int packetLength = stream.ReadU32(); + const int streamLength = stream.GetLength(); + const char* packetStart = stream.GetData(); + if (packetLength > streamLength) + { + AZLOG_INFO("BarrierClient: Packet overruns buffer (Packet Length: %d Buffer Length: %d), probably lots of data on clipboard?\n", packetLength, streamLength); + return false; + } + + const int numPackets = sizeof(s_packets) / sizeof(s_packets[0]); + int i; + for (i = 0; i < numPackets; ++i) + { + const int len = strlen(s_packets[i].pattern); + if (packetLength >= len && memcmp(stream.GetData(), s_packets[i].pattern, len) == 0) + { + bool bDone = false; + int numArgs = 0; + int args[MAX_ARGS]; + stream.Eat(len); + while (!bDone) + { + switch (s_packets[i].args[numArgs]) + { + case ARG_UINT8: + args[numArgs++] = stream.ReadU8(); + break; + case ARG_UINT16: + args[numArgs++] = stream.ReadU16(); + break; + case ARG_UINT32: + args[numArgs++] = stream.ReadU32(); + break; + case ARG_END: + bDone = true; + break; + } + } + if (s_packets[i].callback) + { + if (!s_packets[i].callback(pContext, args, &stream, packetLength - (int)(stream.GetData() - packetStart))) + { + return false; + } + } + stream.Eat(packetLength - (int)(stream.GetData() - packetStart)); + break; + } + } + if (i == numPackets) + { + stream.Eat(packetLength); + } + } + return true; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + BarrierClient::BarrierClient(const char* clientScreenName, const char* serverHostName, AZ::u32 connectionPort) + : m_clientScreenName(clientScreenName) + , m_serverHostName(serverHostName) + , m_connectionPort(connectionPort) + , m_socket(AZ_SOCKET_INVALID) + , m_threadHandle() + , m_threadQuit(false) + { + AZStd::thread_desc threadDesc; + threadDesc.m_name = "BarrierInputClientThread"; + m_threadHandle = AZStd::thread(AZStd::bind(&BarrierClient::Run, this), &threadDesc); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + BarrierClient::~BarrierClient() + { + if (AZ::AzSock::IsAzSocketValid(m_socket)) + { + AZ::AzSock::CloseSocket(m_socket); + } + m_threadQuit = true; + m_threadHandle.join(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void BarrierClient::Run() + { + Stream stream(4 * 1024); + bool connected = false; + while (!m_threadQuit) + { + if (!connected) + { + connected = ConnectToServer(); + continue; + } + + const int lengthReceived = AZ::AzSock::Recv(m_socket, stream.GetBuffer(), stream.GetBufferSize(), 0); + if (lengthReceived <= 0) + { + AZLOG_INFO("BarrierClient: Receive failed, reconnecting.\n"); + connected = false; + continue; + } + + stream.Rewind(); + stream.SetLength(lengthReceived); + if (!ProcessPackets(this, stream)) + { + AZLOG_INFO("BarrierClient: Packet processing failed, reconnecting.\n"); + connected = false; + continue; + } + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + bool BarrierClient::ConnectToServer() + { + if (AZ::AzSock::IsAzSocketValid(m_socket)) + { + AZ::AzSock::CloseSocket(m_socket); + } + + m_socket = AZ::AzSock::Socket(); + if (AZ::AzSock::IsAzSocketValid(m_socket)) + { + AZ::AzSock::AzSocketAddress socketAddress; + if (socketAddress.SetAddress(m_serverHostName.c_str(), m_connectionPort)) + { + const int result = AZ::AzSock::Connect(m_socket, socketAddress); + if (!AZ::AzSock::SocketErrorOccured(result)) + { + return true; + } + } + AZ::AzSock::CloseSocket(m_socket); + m_socket = AZ_SOCKET_INVALID; + } + + return false; + } +} // namespace BarrierInput diff --git a/Gems/BarrierInput/Code/Source/BarrierInputClient.h b/Gems/BarrierInput/Code/Source/BarrierInputClient.h new file mode 100644 index 0000000000..ba37cb1008 --- /dev/null +++ b/Gems/BarrierInput/Code/Source/BarrierInputClient.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace BarrierInput +{ + //////////////////////////////////////////////////////////////////////////////////////////////// + //! Barrier client that manages a connection with a Barrier server. + class BarrierClient + { + public: + //////////////////////////////////////////////////////////////////////////////////////////// + static constexpr AZ::u32 DEFAULT_BARRIER_CONNECTION_PORT_NUMBER = 24800; + + //////////////////////////////////////////////////////////////////////////////////////////// + // Allocator + AZ_CLASS_ALLOCATOR(BarrierClient, AZ::SystemAllocator, 0); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Constructor + //! \param[in] clientScreenName Name of the Barrier client screen this class implements + //! \param[in] serverHostName Name of the Barrier server host this client connects to + //! \param[in] connectionPort Port number over which to connect to the Barrier server + BarrierClient(const char* clientScreenName, + const char* serverHostName, + AZ::u32 connectionPort = DEFAULT_BARRIER_CONNECTION_PORT_NUMBER); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Destructor + ~BarrierClient(); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Access to the Barrier client screen this class implements + //! \return Name of the Barrier client screen this class implements + const AZStd::string& GetClientScreenName() const { return m_clientScreenName; } + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Access to the Barrier server host this client connects to + //! \return Name of the Barrier server host this client connects to + const AZStd::string& GetServerHostName() const { return m_serverHostName; } + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Access to the socket the Barrier client is communicating over + //! \return The socket the Barrier client is communicating over + const AZSOCKET& GetSocket() const { return m_socket; } + + protected: + //////////////////////////////////////////////////////////////////////////////////////////// + //! The client connection loop that runs in it's own thread + void Run(); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Try to connect to the Barrier server + //! \return True if we're connected to the Barrier server, false otherwise + bool ConnectToServer(); + + private: + //////////////////////////////////////////////////////////////////////////////////////////// + // Variables + AZStd::string m_clientScreenName; + AZStd::string m_serverHostName; + AZ::u32 m_connectionPort; + AZStd::thread m_threadHandle; + AZStd::atomic_bool m_threadQuit; + AZSOCKET m_socket; + }; +} // namespace BarrierInput diff --git a/Gems/BarrierInput/Code/Source/BarrierInputKeyboard.cpp b/Gems/BarrierInput/Code/Source/BarrierInputKeyboard.cpp new file mode 100644 index 0000000000..5e486df680 --- /dev/null +++ b/Gems/BarrierInput/Code/Source/BarrierInputKeyboard.cpp @@ -0,0 +1,260 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +#include + +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace BarrierInput +{ + using namespace AzFramework; + + //////////////////////////////////////////////////////////////////////////////////////////////// + InputDeviceKeyboard::Implementation* InputDeviceKeyboardBarrier::Create(InputDeviceKeyboard& inputDevice) + { + return aznew InputDeviceKeyboardBarrier(inputDevice); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + InputDeviceKeyboardBarrier::InputDeviceKeyboardBarrier(InputDeviceKeyboard& inputDevice) + : InputDeviceKeyboard::Implementation(inputDevice) + , m_threadAwareRawKeyEventQueuesById() + , m_threadAwareRawKeyEventQueuesByIdMutex() + , m_threadAwareRawTextEventQueue() + , m_threadAwareRawTextEventQueueMutex() + , m_hasTextEntryStarted(false) + { + RawInputNotificationBusBarrier::Handler::BusConnect(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + InputDeviceKeyboardBarrier::~InputDeviceKeyboardBarrier() + { + RawInputNotificationBusBarrier::Handler::BusDisconnect(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + bool InputDeviceKeyboardBarrier::IsConnected() const + { + // We could check the validity of the socket connection to the Barrier server + return true; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + bool InputDeviceKeyboardBarrier::HasTextEntryStarted() const + { + return m_hasTextEntryStarted; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceKeyboardBarrier::TextEntryStart(const InputTextEntryRequests::VirtualKeyboardOptions&) + { + m_hasTextEntryStarted = true; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceKeyboardBarrier::TextEntryStop() + { + m_hasTextEntryStarted = false; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceKeyboardBarrier::TickInputDevice() + { + { + // Queue all key events that were received in the other thread + AZStd::scoped_lock lock(m_threadAwareRawKeyEventQueuesByIdMutex); + for (const auto& keyEventQueuesById : m_threadAwareRawKeyEventQueuesById) + { + const InputChannelId& inputChannelId = keyEventQueuesById.first; + for (bool rawKeyState : keyEventQueuesById.second) + { + QueueRawKeyEvent(inputChannelId, rawKeyState); + } + } + m_threadAwareRawKeyEventQueuesById.clear(); + } + + { + // Queue all text events that were received in the other thread + AZStd::scoped_lock lock(m_threadAwareRawTextEventQueueMutex); + for (const AZStd::string& rawTextEvent : m_threadAwareRawTextEventQueue) + { + #if !defined(ALWAYS_DISPATCH_KEYBOARD_TEXT_INPUT) + if (!m_hasTextEntryStarted) + { + continue; + } + #endif // !defined(ALWAYS_DISPATCH_KEYBOARD_TEXT_INPUT) + QueueRawTextEvent(rawTextEvent); + } + m_threadAwareRawTextEventQueue.clear(); + } + + // Process raw event queues once each frame + ProcessRawEventQueues(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceKeyboardBarrier::OnRawKeyboardKeyDownEvent(uint32_t scanCode, + ModifierMask activeModifiers) + { + // Queue key events and text events + ThreadSafeQueueRawKeyEvent(scanCode, true); + if (char asciiChar = TranslateRawKeyEventToASCIIChar(scanCode, activeModifiers)) + { + const AZStd::string text(1, asciiChar); + ThreadSafeQueueRawTextEvent(text); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceKeyboardBarrier::OnRawKeyboardKeyUpEvent(uint32_t scanCode, + [[maybe_unused]]ModifierMask activeModifiers) + { + // Queue key events, not text events + ThreadSafeQueueRawKeyEvent(scanCode, false); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceKeyboardBarrier::OnRawKeyboardKeyRepeatEvent(uint32_t scanCode, + ModifierMask activeModifiers) + { + // Don't queue key events, only text events + if (char asciiChar = TranslateRawKeyEventToASCIIChar(scanCode, activeModifiers)) + { + const AZStd::string text(1, asciiChar); + ThreadSafeQueueRawTextEvent(text); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceKeyboardBarrier::ThreadSafeQueueRawKeyEvent(uint32_t scanCode, bool rawKeyState) + { + // From observation, Barrier scan codes in the: + // - Range 0x0-0x7F (0-127) correspond to windows scan codes without the extended bit set + // - Range 0x100-0x17F (256-383) correspond to windows scan codes with the extended bit set + const InputChannelId* inputChannelId = nullptr; + if (scanCode < InputChannelIdByScanCodeTable.size()) + { + inputChannelId = InputChannelIdByScanCodeTable[scanCode]; + } + else if (0 <= (scanCode - 0x100) && scanCode < InputChannelIdByScanCodeWithExtendedPrefixTable.size()) + { + inputChannelId = InputChannelIdByScanCodeWithExtendedPrefixTable[scanCode - 0x100]; + } + + if (inputChannelId) + { + AZStd::scoped_lock lock(m_threadAwareRawKeyEventQueuesByIdMutex); + m_threadAwareRawKeyEventQueuesById[*inputChannelId].push_back(rawKeyState); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceKeyboardBarrier::ThreadSafeQueueRawTextEvent(const AZStd::string& textUTF8) + { + AZStd::scoped_lock lock(m_threadAwareRawTextEventQueueMutex); + m_threadAwareRawTextEventQueue.push_back(textUTF8); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + char InputDeviceKeyboardBarrier::TranslateRawKeyEventToASCIIChar(uint32_t scanCode, + ModifierMask activeModifiers) + { + // Map ASCII character pairs keyed by their keyboard scan code, assuming an ANSI mechanical + // keyboard layout with a standard QWERTY key mapping. The first element of the pair is the + // character that should be produced if the key is pressed while no shift or caps modifiers + // are active, while the second element is the character that should be produced if the key + // is pressed while a shift or caps modifier is active. Required because Barrier only sends + // raw key events, not translated text input. While we would ideally support the full range + // of UTF-8 text input, that is beyond the scope of this debug/development only class. Note + // that this function assumes an ANSI mechanical keyboard layout with a standard QWERTY key + // mapping, and will not produce correct results if used with other key layouts or mappings. + static const AZStd::fixed_unordered_map, 16, 64> ScanCodeToASCIICharMap = + { + { 2, { '1', '!' } }, + { 3, { '2', '@' } }, + { 4, { '3', '#' } }, + { 5, { '4', '$' } }, + { 6, { '5', '%' } }, + { 7, { '6', '^' } }, + { 8, { '7', '&' } }, + { 9, { '8', '*' } }, + { 10, { '9', '(' } }, + { 11, { '0', ')' } }, + { 12, { '-', '_' } }, + { 13, { '=', '+' } }, + { 15, { '\t', '\t' } }, + { 16, { 'q', 'Q' } }, + { 17, { 'w', 'W' } }, + { 18, { 'e', 'E' } }, + { 19, { 'r', 'R' } }, + { 20, { 't', 'T' } }, + { 21, { 'y', 'Y' } }, + { 22, { 'u', 'U' } }, + { 23, { 'i', 'I' } }, + { 24, { 'o', 'O' } }, + { 25, { 'p', 'P' } }, + { 26, { '[', '{' } }, + { 27, { ']', '}' } }, + { 30, { 'a', 'A' } }, + { 31, { 's', 'S' } }, + { 32, { 'd', 'D' } }, + { 33, { 'f', 'F' } }, + { 34, { 'g', 'G' } }, + { 35, { 'h', 'H' } }, + { 36, { 'j', 'J' } }, + { 37, { 'k', 'K' } }, + { 38, { 'l', 'L' } }, + { 39, { ';', ':' } }, + { 40, { '\'', '"' } }, + { 41, { '`', '~' } }, + { 43, { '\\', '|' } }, + { 44, { 'z', 'Z' } }, + { 45, { 'x', 'X' } }, + { 46, { 'c', 'C' } }, + { 47, { 'v', 'V' } }, + { 48, { 'b', 'B' } }, + { 49, { 'n', 'N' } }, + { 50, { 'm', 'M' } }, + { 51, { ',', '<' } }, + { 52, { '.', '>' } }, + { 53, { '/', '?' } }, + { 55, { '*', '*' } }, + { 57, { ' ', ' ' } }, + { 71, { '7', '7' } }, + { 72, { '8', '8' } }, + { 73, { '9', '9' } }, + { 74, { '-', '-' } }, + { 75, { '4', '4' } }, + { 76, { '5', '5' } }, + { 77, { '6', '6' } }, + { 78, { '+', '+' } }, + { 79, { '1', '1' } }, + { 80, { '2', '2' } }, + { 81, { '3', '3' } }, + { 82, { '0', '0' } }, + { 83, { '.', '.' } }, + { 309, { '/', '/' } } + }; + + const auto& it = ScanCodeToASCIICharMap.find(scanCode); + if (it == ScanCodeToASCIICharMap.end()) + { + return '\0'; + } + + const bool shiftOrCapsLockActive = (activeModifiers & ModifierMask_Shift) || + (activeModifiers & ModifierMask_CapsLock); + return shiftOrCapsLockActive ? it->second.second : it->second.first; + } +} // namespace BarrierInput diff --git a/Gems/BarrierInput/Code/Source/BarrierInputKeyboard.h b/Gems/BarrierInput/Code/Source/BarrierInputKeyboard.h new file mode 100644 index 0000000000..d7321faaec --- /dev/null +++ b/Gems/BarrierInput/Code/Source/BarrierInputKeyboard.h @@ -0,0 +1,109 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +#include + +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace BarrierInput +{ + //////////////////////////////////////////////////////////////////////////////////////////////// + //! Barrier specific implementation for keyboard input devices. + class InputDeviceKeyboardBarrier : public AzFramework::InputDeviceKeyboard::Implementation + , public RawInputNotificationBusBarrier::Handler + { + public: + //////////////////////////////////////////////////////////////////////////////////////////// + // Allocator + AZ_CLASS_ALLOCATOR(InputDeviceKeyboardBarrier, AZ::SystemAllocator, 0); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Custom factory create function + //! \param[in] inputDevice Reference to the input device being implemented + static Implementation* Create(AzFramework::InputDeviceKeyboard& inputDevice); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Constructor + //! \param[in] inputDevice Reference to the input device being implemented + InputDeviceKeyboardBarrier(AzFramework::InputDeviceKeyboard& inputDevice); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Destructor + ~InputDeviceKeyboardBarrier() override; + + private: + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceKeyboard::Implementation::IsConnected + bool IsConnected() const override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceKeyboard::Implementation::HasTextEntryStarted + bool HasTextEntryStarted() const override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceKeyboard::Implementation::TextEntryStart + void TextEntryStart(const AzFramework::InputTextEntryRequests::VirtualKeyboardOptions& options) override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceKeyboard::Implementation::TextEntryStop + void TextEntryStop() override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceKeyboard::Implementation::TickInputDevice + void TickInputDevice() override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref RawInputNotificationsBarrier::OnRawKeyboardKeyDownEvent + virtual void OnRawKeyboardKeyDownEvent(uint32_t scanCode, ModifierMask activeModifiers); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref RawInputNotificationsBarrier::OnRawKeyboardKeyUpEvent + virtual void OnRawKeyboardKeyUpEvent(uint32_t scanCode, ModifierMask activeModifiers); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref RawInputNotificationsBarrier::OnRawKeyboardKeyRepeatEvent + virtual void OnRawKeyboardKeyRepeatEvent(uint32_t scanCode, ModifierMask activeModifiers); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Thread safe method to queue raw key events to be processed in the main thread update + //! \param[in] scanCode The scan code of the key + //! \param[in] rawKeyState The raw key state + void ThreadSafeQueueRawKeyEvent(uint32_t scanCode, bool rawKeyState); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Thread safe method to queue raw text events to be processed in the main thread update + //! \param[in] textUTF8 The text to queue (encoded using UTF-8) + void ThreadSafeQueueRawTextEvent(const AZStd::string& textUTF8); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Translate a key event to an ASCII character. This is required because Barrier only sends + //! raw key events, not translated text input. While we would ideally support the full range + //! of UTF-8 text input, that is beyond the scope of this debug/development only class. Note + //! that this function assumes an ANSI mechanical keyboard layout with a standard QWERTY key + //! mapping, and will not produce correct results if used with other key layouts or mappings. + //! \param[in] scanCode The scan code of the key + //! \param[in] activeModifiers The bit mask of currently active modifier keys + //! \return If the scan code and active modifiers produce a valid ASCII character + char TranslateRawKeyEventToASCIIChar(uint32_t scanCode, ModifierMask activeModifiers); + + //////////////////////////////////////////////////////////////////////////////////////////// + // Variables + RawKeyEventQueueByIdMap m_threadAwareRawKeyEventQueuesById; + AZStd::mutex m_threadAwareRawKeyEventQueuesByIdMutex; + + AZStd::vector m_threadAwareRawTextEventQueue; + AZStd::mutex m_threadAwareRawTextEventQueueMutex; + + bool m_hasTextEntryStarted; + }; +} // namespace BarrierInput diff --git a/Gems/BarrierInput/Code/Source/BarrierInputModule.cpp b/Gems/BarrierInput/Code/Source/BarrierInputModule.cpp new file mode 100644 index 0000000000..716dfb4704 --- /dev/null +++ b/Gems/BarrierInput/Code/Source/BarrierInputModule.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include + +namespace BarrierInput +{ + class BarrierInputModule + : public AZ::Module + { + public: + AZ_RTTI(BarrierInputModule, "{C338BB3B-EA09-4FC8-AD49-840F8A22837F}", AZ::Module); + AZ_CLASS_ALLOCATOR(BarrierInputModule, AZ::SystemAllocator, 0); + + BarrierInputModule() + : AZ::Module() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + m_descriptors.insert(m_descriptors.end(), { + BarrierInputSystemComponent::CreateDescriptor(), + }); + } + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + }; + } + }; +} + +// DO NOT MODIFY THIS LINE UNLESS YOU RENAME THE GEM +// The first parameter should be GemName_GemIdLower +// The second should be the fully qualified name of the class above +AZ_DECLARE_MODULE_CLASS(Gem_BarrierInput, BarrierInput::BarrierInputModule) diff --git a/Gems/BarrierInput/Code/Source/BarrierInputMouse.cpp b/Gems/BarrierInput/Code/Source/BarrierInputMouse.cpp new file mode 100644 index 0000000000..b0053f5ba5 --- /dev/null +++ b/Gems/BarrierInput/Code/Source/BarrierInputMouse.cpp @@ -0,0 +1,195 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +#include +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace BarrierInput +{ + using namespace AzFramework; + + //////////////////////////////////////////////////////////////////////////////////////////////// + InputDeviceMouse::Implementation* InputDeviceMouseBarrier::Create(InputDeviceMouse& inputDevice) + { + return aznew InputDeviceMouseBarrier(inputDevice); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + InputDeviceMouseBarrier::InputDeviceMouseBarrier(InputDeviceMouse& inputDevice) + : InputDeviceMouse::Implementation(inputDevice) + , m_systemCursorState(SystemCursorState::Unknown) + , m_systemCursorPositionNormalized(0.5f, 0.5f) + , m_threadAwareRawButtonEventQueuesById() + , m_threadAwareRawButtonEventQueuesByIdMutex() + , m_threadAwareRawMovementEventQueuesById() + , m_threadAwareRawMovementEventQueuesByIdMutex() + , m_threadAwareSystemCursorPosition(0.0f, 0.0f) + , m_threadAwareSystemCursorPositionMutex() + { + RawInputNotificationBusBarrier::Handler::BusConnect(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + InputDeviceMouseBarrier::~InputDeviceMouseBarrier() + { + RawInputNotificationBusBarrier::Handler::BusDisconnect(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + bool InputDeviceMouseBarrier::IsConnected() const + { + // We could check the validity of the socket connection to the Barrier server + return true; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceMouseBarrier::SetSystemCursorState(SystemCursorState systemCursorState) + { + // This doesn't apply when using Barrier, but we'll store it so it can be queried + m_systemCursorState = systemCursorState; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + SystemCursorState InputDeviceMouseBarrier::GetSystemCursorState() const + { + return m_systemCursorState; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceMouseBarrier::SetSystemCursorPositionNormalized(AZ::Vector2 positionNormalized) + { + // This will simply get overridden by the next call to OnRawMousePositionEvent, but there's + // not much we can do about it, and Barrier mouse input is only for debug purposes anyway. + m_systemCursorPositionNormalized = positionNormalized; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + AZ::Vector2 InputDeviceMouseBarrier::GetSystemCursorPositionNormalized() const + { + return m_systemCursorPositionNormalized; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceMouseBarrier::TickInputDevice() + { + { + // Queue all mouse button events that were received in the other thread + AZStd::scoped_lock lock(m_threadAwareRawButtonEventQueuesByIdMutex); + for (const auto& buttonEventQueuesById : m_threadAwareRawButtonEventQueuesById) + { + const InputChannelId& inputChannelId = buttonEventQueuesById.first; + for (bool rawButtonState : buttonEventQueuesById.second) + { + QueueRawButtonEvent(inputChannelId, rawButtonState); + } + } + m_threadAwareRawButtonEventQueuesById.clear(); + } + + bool receivedRawMovementEvents = false; + { + // Queue all mouse movement events that were received in the other thread + AZStd::scoped_lock lock(m_threadAwareRawMovementEventQueuesByIdMutex); + for (const auto& movementEventQueuesById : m_threadAwareRawMovementEventQueuesById) + { + const InputChannelId& inputChannelId = movementEventQueuesById.first; + for (float rawMovementDelta : movementEventQueuesById.second) + { + QueueRawMovementEvent(inputChannelId, rawMovementDelta); + receivedRawMovementEvents = true; + } + } + m_threadAwareRawMovementEventQueuesById.clear(); + } + + // Update the system cursor position + auto atomViewportRequests = AZ::Interface::Get(); + AZ::RPI::ViewportContextPtr viewportContext = atomViewportRequests->GetDefaultViewportContext(); + if (viewportContext) + { + const AzFramework::WindowSize windowSize = viewportContext->GetViewportSize(); + const float windowWidth = static_cast(windowSize.m_width); + const float windowHeight = static_cast(windowSize.m_height); + const AZ::Vector2 oldSystemCursorPositionNormalized = m_systemCursorPositionNormalized; + + AZStd::scoped_lock lock(m_threadAwareSystemCursorPositionMutex); + { + const AZ::Vector2 normalizedPosition(m_threadAwareSystemCursorPosition.GetX() / windowWidth, + m_threadAwareSystemCursorPosition.GetY() / windowHeight); + m_systemCursorPositionNormalized = normalizedPosition; + } + + // In theory Barrier should send relative mouse movement events as 'DMRM' messages, which are + // forwarded to InputDeviceMouseBarrier::OnRawMouseMovementEvent, but this does not appear to + // be happening, so if we didn't receive any relative mouse movement events this frame we can + // just approximate the movement ourselves. Unlike other mouse implementations where movement + // events are sent 'raw' before any operating system ballistics/smoothing is applied, Barrier + // seems to calculate relative mouse movement events by taking the delta between the previous + // system cursor position and the current one, so we should obtain the same result regardless. + if (!receivedRawMovementEvents) + { + const AZ::Vector2 mouseMovementDelta = m_systemCursorPositionNormalized - oldSystemCursorPositionNormalized; + QueueRawMovementEvent(InputDeviceMouse::Movement::X, mouseMovementDelta.GetX() * windowWidth); + QueueRawMovementEvent(InputDeviceMouse::Movement::Y, mouseMovementDelta.GetY() * windowHeight); + } + } + + // Process raw event queues once each frame + ProcessRawEventQueues(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceMouseBarrier::OnRawMouseButtonDownEvent(uint32_t buttonIndex) + { + ThreadSafeQueueRawButtonEvent(buttonIndex, true); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceMouseBarrier::OnRawMouseButtonUpEvent(uint32_t buttonIndex) + { + ThreadSafeQueueRawButtonEvent(buttonIndex, false); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceMouseBarrier::OnRawMouseMovementEvent(float movementX, float movementY) + { + AZStd::scoped_lock lock(m_threadAwareRawMovementEventQueuesByIdMutex); + m_threadAwareRawMovementEventQueuesById[InputDeviceMouse::Movement::X].push_back(movementX); + m_threadAwareRawMovementEventQueuesById[InputDeviceMouse::Movement::Y].push_back(movementY); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceMouseBarrier::OnRawMousePositionEvent(float positionX, + float positionY) + { + AZStd::scoped_lock lock(m_threadAwareSystemCursorPositionMutex); + m_threadAwareSystemCursorPosition = AZ::Vector2(positionX, positionY); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void InputDeviceMouseBarrier::ThreadSafeQueueRawButtonEvent(uint32_t buttonIndex, + bool rawButtonState) + { + const InputChannelId* inputChannelId = nullptr; + switch (buttonIndex) + { + case 1: { inputChannelId = &InputDeviceMouse::Button::Left; } break; + case 2: { inputChannelId = &InputDeviceMouse::Button::Middle; } break; + case 3: { inputChannelId = &InputDeviceMouse::Button::Right; } break; + } + + if (inputChannelId) + { + AZStd::scoped_lock lock(m_threadAwareRawButtonEventQueuesByIdMutex); + m_threadAwareRawButtonEventQueuesById[*inputChannelId].push_back(rawButtonState); + } + } +} // namespace BarrierInput diff --git a/Gems/BarrierInput/Code/Source/BarrierInputMouse.h b/Gems/BarrierInput/Code/Source/BarrierInputMouse.h new file mode 100644 index 0000000000..0d160c8769 --- /dev/null +++ b/Gems/BarrierInput/Code/Source/BarrierInputMouse.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +#include + +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace BarrierInput +{ + //////////////////////////////////////////////////////////////////////////////////////////////// + //! Barrier specific implementation for mouse input devices. + class InputDeviceMouseBarrier : public AzFramework::InputDeviceMouse::Implementation + , public RawInputNotificationBusBarrier::Handler + { + public: + //////////////////////////////////////////////////////////////////////////////////////////// + // Allocator + AZ_CLASS_ALLOCATOR(InputDeviceMouseBarrier, AZ::SystemAllocator, 0); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Custom factory create function + //! \param[in] inputDevice Reference to the input device being implemented + static Implementation* Create(AzFramework::InputDeviceMouse& inputDevice); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Constructor + //! \param[in] inputDevice Reference to the input device being implemented + InputDeviceMouseBarrier(AzFramework::InputDeviceMouse& inputDevice); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Destructor + ~InputDeviceMouseBarrier() override; + + private: + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::IsConnected + bool IsConnected() const override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::SetSystemCursorState + void SetSystemCursorState(AzFramework::SystemCursorState systemCursorState) override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::GetSystemCursorState + AzFramework::SystemCursorState GetSystemCursorState() const override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::SetSystemCursorPositionNormalized + void SetSystemCursorPositionNormalized(AZ::Vector2 positionNormalized) override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::GetSystemCursorPositionNormalized + AZ::Vector2 GetSystemCursorPositionNormalized() const override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AzFramework::InputDeviceMouse::Implementation::TickInputDevice + void TickInputDevice() override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref RawInputNotificationsBarrier::OnRawMouseButtonDownEvent + void OnRawMouseButtonDownEvent(uint32_t buttonIndex) override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref RawInputNotificationsBarrier::OnRawMouseButtonUpEvent + void OnRawMouseButtonUpEvent(uint32_t buttonIndex) override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref RawInputNotificationsBarrier::OnRawMouseMovementEvent + void OnRawMouseMovementEvent(float movementX, float movementY) override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref RawInputNotificationsBarrier::OnRawMousePositionEvent + void OnRawMousePositionEvent(float positionX, float positionY) override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Thread safe method to queue raw button events to be processed in the main thread update + //! \param[in] buttonIndex The index of the button + //! \param[in] rawButtonState The raw button state + void ThreadSafeQueueRawButtonEvent(uint32_t buttonIndex, bool rawButtonState); + + //////////////////////////////////////////////////////////////////////////////////////////// + // Variables + AzFramework::SystemCursorState m_systemCursorState; + AZ::Vector2 m_systemCursorPositionNormalized; + + RawButtonEventQueueByIdMap m_threadAwareRawButtonEventQueuesById; + AZStd::mutex m_threadAwareRawButtonEventQueuesByIdMutex; + + RawMovementEventQueueByIdMap m_threadAwareRawMovementEventQueuesById; + AZStd::mutex m_threadAwareRawMovementEventQueuesByIdMutex; + + AZ::Vector2 m_threadAwareSystemCursorPosition; + AZStd::mutex m_threadAwareSystemCursorPositionMutex; + }; +} // namespace BarrierInput diff --git a/Gems/BarrierInput/Code/Source/BarrierInputSystemComponent.cpp b/Gems/BarrierInput/Code/Source/BarrierInputSystemComponent.cpp new file mode 100644 index 0000000000..a3c9fb3cc7 --- /dev/null +++ b/Gems/BarrierInput/Code/Source/BarrierInputSystemComponent.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +#include +#include +#include + +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace BarrierInput +{ + //////////////////////////////////////////////////////////////////////////////////////////////// + template + void OnBarrierConnectionCVarChanged(const T&) + { + BarrierInputConnectionNotificationBus::Broadcast(&BarrierInputConnectionNotifications::OnBarrierConnectionCVarChanged); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + AZ_CVAR(AZ::CVarFixedString, + barrier_clientScreenName, + "", + OnBarrierConnectionCVarChanged, + AZ::ConsoleFunctorFlags::DontReplicate, + "The Barrier screen name assigned to this client."); + + //////////////////////////////////////////////////////////////////////////////////////////////// + AZ_CVAR(AZ::CVarFixedString, + barrier_serverHostName, + "", + OnBarrierConnectionCVarChanged, + AZ::ConsoleFunctorFlags::DontReplicate, + "The IP or hostname of the Barrier server to connect to."); + + //////////////////////////////////////////////////////////////////////////////////////////////// + AZ_CVAR(AZ::u32, + barrier_connectionPort, + BarrierClient::DEFAULT_BARRIER_CONNECTION_PORT_NUMBER, + OnBarrierConnectionCVarChanged, + AZ::ConsoleFunctorFlags::DontReplicate, + "The port number over which to connect to the Barrier server."); + + //////////////////////////////////////////////////////////////////////////////////////////////// + void BarrierInputSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0); + + if (AZ::EditContext* ec = serialize->GetEditContext()) + { + ec->Class("BarrierInput", "Provides functionality related to Barrier input.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ; + } + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void BarrierInputSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC("BarrierInputService")); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void BarrierInputSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC("BarrierInputService")); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void BarrierInputSystemComponent::Activate() + { + TryCreateBarrierClientAndInputDeviceImplementations(); + BarrierInputConnectionNotificationBus::Handler::BusConnect(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void BarrierInputSystemComponent::Deactivate() + { + BarrierInputConnectionNotificationBus::Handler::BusDisconnect(); + DestroyBarrierClientAndInputDeviceImplementations(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void BarrierInputSystemComponent::OnBarrierConnectionCVarChanged() + { + TryCreateBarrierClientAndInputDeviceImplementations(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void BarrierInputSystemComponent::TryCreateBarrierClientAndInputDeviceImplementations() + { + // Destroy any existing Barrier client and input device implementations. + DestroyBarrierClientAndInputDeviceImplementations(); + + const AZ::CVarFixedString barrierClientScreenNameCVar = static_cast(barrier_clientScreenName); + const AZ::CVarFixedString barrierServerHostNameCVar = static_cast(barrier_serverHostName); + const AZ::u32 barrierConnectionPort = static_cast(barrier_connectionPort); + if (!barrierClientScreenNameCVar.empty() && !barrierServerHostNameCVar.empty() && barrierConnectionPort) + { + // Enable the Barrier keyboard/mouse input device implementations. + AzFramework::InputDeviceImplementationRequest::Bus::Event( + AzFramework::InputDeviceKeyboard::Id, + &AzFramework::InputDeviceImplementationRequest::SetCustomImplementation, + BarrierInput::InputDeviceKeyboardBarrier::Create); + AzFramework::InputDeviceImplementationRequest::Bus::Event( + AzFramework::InputDeviceMouse::Id, + &AzFramework::InputDeviceImplementationRequest::SetCustomImplementation, + BarrierInput::InputDeviceMouseBarrier::Create); + + // Create the Barrier client instance. + m_barrierClient = AZStd::make_unique(barrierClientScreenNameCVar.c_str(), barrierServerHostNameCVar.c_str(), barrierConnectionPort); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void BarrierInputSystemComponent::DestroyBarrierClientAndInputDeviceImplementations() + { + if (m_barrierClient) + { + // Destroy the Barrier client instance. + m_barrierClient.reset(); + + // Reset to the default keyboard/mouse input device implementations. + AzFramework::InputDeviceImplementationRequest::Bus::Event( + AzFramework::InputDeviceKeyboard::Id, + &AzFramework::InputDeviceImplementationRequest::SetCustomImplementation, + AzFramework::InputDeviceKeyboard::Implementation::Create); + AzFramework::InputDeviceImplementationRequest::Bus::Event( + AzFramework::InputDeviceMouse::Id, + &AzFramework::InputDeviceImplementationRequest::SetCustomImplementation, + AzFramework::InputDeviceMouse::Implementation::Create); + } + } +} // namespace BarrierInput diff --git a/Gems/BarrierInput/Code/Source/BarrierInputSystemComponent.h b/Gems/BarrierInput/Code/Source/BarrierInputSystemComponent.h new file mode 100644 index 0000000000..d602d66711 --- /dev/null +++ b/Gems/BarrierInput/Code/Source/BarrierInputSystemComponent.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include + +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace BarrierInput +{ + //////////////////////////////////////////////////////////////////////////////////////////////// + //! EBus interface used to listen for changes to Barrier connection related CVars. + class BarrierInputConnectionNotifications : public AZ::EBusTraits + { + public: + //////////////////////////////////////////////////////////////////////////////////////////// + //! Called when a CVar relating to the Barrier input connection changes. + virtual void OnBarrierConnectionCVarChanged() {} + }; + using BarrierInputConnectionNotificationBus = AZ::EBus; + + //////////////////////////////////////////////////////////////////////////////////////////////// + //! A system component providing functionality related to Barrier input. + class BarrierInputSystemComponent : public AZ::Component + , public BarrierInputConnectionNotificationBus::Handler + { + public: + //////////////////////////////////////////////////////////////////////////////////////////// + // AZ::Component Setup + AZ_COMPONENT(BarrierInputSystemComponent, "{720B6420-8A76-46F9-80C7-0DBF0CD467C2}"); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::ComponentDescriptor::Reflect + static void Reflect(AZ::ReflectContext* context); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::ComponentDescriptor::GetProvidedServices + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::ComponentDescriptor::GetIncompatibleServices + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Default constructor + BarrierInputSystemComponent() = default; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Default destructor + ~BarrierInputSystemComponent() override = default; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::Component::Activate + void Activate() override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::Component::Deactivate + void Deactivate() override; + + protected: + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref BarrierInput::BarrierInputConnectionNotifications::OnBarrierConnectionCVarChanged + void OnBarrierConnectionCVarChanged() override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Try to create the Barrier client and input device implementations. + void TryCreateBarrierClientAndInputDeviceImplementations(); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Destroy the Barrier client and input device implementations (if they've been created). + void DestroyBarrierClientAndInputDeviceImplementations(); + + private: + //////////////////////////////////////////////////////////////////////////////////////////// + //! The Barrier client instance. + AZStd::unique_ptr m_barrierClient; + }; +} // namespace BarrierInput diff --git a/Gems/BarrierInput/Code/barrierinput_files.cmake b/Gems/BarrierInput/Code/barrierinput_files.cmake new file mode 100644 index 0000000000..e82944c288 --- /dev/null +++ b/Gems/BarrierInput/Code/barrierinput_files.cmake @@ -0,0 +1,19 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Include/BarrierInput/RawInputNotificationBus_Barrier.h + Source/BarrierInputClient.cpp + Source/BarrierInputClient.h + Source/BarrierInputKeyboard.cpp + Source/BarrierInputKeyboard.h + Source/BarrierInputMouse.cpp + Source/BarrierInputMouse.h + Source/BarrierInputSystemComponent.cpp + Source/BarrierInputSystemComponent.h +) diff --git a/Gems/BarrierInput/Code/barrierinput_shared_files.cmake b/Gems/BarrierInput/Code/barrierinput_shared_files.cmake new file mode 100644 index 0000000000..e8b7ed7a24 --- /dev/null +++ b/Gems/BarrierInput/Code/barrierinput_shared_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/BarrierInputModule.cpp +) diff --git a/Gems/BarrierInput/gem.json b/Gems/BarrierInput/gem.json new file mode 100644 index 0000000000..738d644d84 --- /dev/null +++ b/Gems/BarrierInput/gem.json @@ -0,0 +1,12 @@ +{ + "gem_name": "BarrierInput", + "display_name": "Barrier Input", + "license": "Apache-2.0 Or MIT", + "origin": "Open 3D Engine - o3de.org", + "type": "Code", + "summary": "The Barrier Input Gem allows the Open 3D Engine to function as a Barrier client so that it can receive input from a remote Barrier server.", + "canonical_tags": ["Gem"], + "user_tags": ["Input", "Barrier", "Synergy"], + "icon_path": "preview.png", + "requirements": "" +} diff --git a/Gems/BarrierInput/preview.png b/Gems/BarrierInput/preview.png new file mode 100644 index 0000000000..a8457c7f6e --- /dev/null +++ b/Gems/BarrierInput/preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7917fbf6e4e3a89e3432b8f48822b660bb245d2b84bb8efdf9f715593c0973df +size 38792 diff --git a/engine.json b/engine.json index 07b4b7baa2..5d862779c0 100644 --- a/engine.json +++ b/engine.json @@ -19,6 +19,7 @@ "Gems/AWSCore", "Gems/AWSGameLift", "Gems/AWSMetrics", + "Gems/BarrierInput", "Gems/Blast", "Gems/Camera", "Gems/CameraFramework",