diff --git a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py index 7ed08867a9..3f3885a028 100644 --- a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py +++ b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py @@ -49,7 +49,6 @@ class TestViewMenuOptions(EditorTestHelper): ("Viewport", "Center on Selection"), ("Viewport", "Go to Location"), ("Viewport", "Remember Location"), - ("Viewport", "Change Move Speed"), ("Viewport", "Switch Camera"), ("Viewport", "Show/Hide Helpers"), ("Refresh Style",), diff --git a/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py b/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py index b45a087c79..982f3617b5 100644 --- a/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py +++ b/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py @@ -89,7 +89,6 @@ class TestMenus(object): "Center on Selection Action triggered", "Go to Location Action triggered", "Remember Location Action triggered", - "Change Move Speed Action triggered", "Switch Camera Action triggered", "Show/Hide Helpers Action triggered", "Refresh Style Action triggered", diff --git a/Code/Editor/EditorViewportWidget.cpp b/Code/Editor/EditorViewportWidget.cpp index 1cfc3b6dd0..1b461e1356 100644 --- a/Code/Editor/EditorViewportWidget.cpp +++ b/Code/Editor/EditorViewportWidget.cpp @@ -714,7 +714,7 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event) if (m_renderViewport) { - m_renderViewport->GetControllerList()->SetEnabled(false); + m_renderViewport->SetInputProcessingEnabled(false); } } break; @@ -738,7 +738,7 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event) if (m_renderViewport) { - m_renderViewport->GetControllerList()->SetEnabled(true); + m_renderViewport->SetInputProcessingEnabled(true); } break; diff --git a/Code/Editor/LegacyViewportCameraController.cpp b/Code/Editor/LegacyViewportCameraController.cpp index 0750db8d97..f437196548 100644 --- a/Code/Editor/LegacyViewportCameraController.cpp +++ b/Code/Editor/LegacyViewportCameraController.cpp @@ -404,7 +404,7 @@ bool LegacyViewportCameraControllerInstance::HandleInputChannelEvent(const AzFra } else if (auto key = GetKeyboardKey(event.m_inputChannel); key != Qt::Key_unknown) { - if (state == InputChannel::State::Ended) + if (!event.m_inputChannel.IsActive()) { m_pressedKeys.erase(key); } diff --git a/Code/Editor/ViewportManipulatorController.cpp b/Code/Editor/ViewportManipulatorController.cpp index c7d2885cb2..fb696a61f3 100644 --- a/Code/Editor/ViewportManipulatorController.cpp +++ b/Code/Editor/ViewportManipulatorController.cpp @@ -216,7 +216,8 @@ namespace SandboxEditor interactionHandled, AzToolsFramework::GetEntityContextId(), targetInteractionEvent, mouseInteractionEvent); } - return interactionHandled; + // Only filter button/key press events, not release events + return interactionHandled && event.m_inputChannel.IsActive(); } void ViewportManipulatorControllerInstance::ResetInputChannels() diff --git a/Code/Framework/AzFramework/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.cpp b/Code/Framework/AzFramework/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.cpp index 7625679d69..75346227ed 100644 --- a/Code/Framework/AzFramework/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.cpp +++ b/Code/Framework/AzFramework/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.cpp @@ -454,8 +454,8 @@ namespace AzFramework } //////////////////////////////////////////////////////////////////////////////////////////////// - InputDeviceKeyboard::InputDeviceKeyboard() - : InputDevice(Id) + InputDeviceKeyboard::InputDeviceKeyboard(AzFramework::InputDeviceId id) + : InputDevice(id) , m_modifierKeyStates(AZStd::make_shared()) , m_allChannelsById() , m_keyChannelsById() diff --git a/Code/Framework/AzFramework/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h b/Code/Framework/AzFramework/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h index ea0aa1beec..9f61ba96f7 100644 --- a/Code/Framework/AzFramework/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h +++ b/Code/Framework/AzFramework/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h @@ -240,7 +240,7 @@ namespace AzFramework //////////////////////////////////////////////////////////////////////////////////////////// //! Constructor - InputDeviceKeyboard(); + InputDeviceKeyboard(AzFramework::InputDeviceId id = Id); //////////////////////////////////////////////////////////////////////////////////////////// // Disable copying diff --git a/Code/Framework/AzFramework/AzFramework/Input/Devices/Mouse/InputDeviceMouse.cpp b/Code/Framework/AzFramework/AzFramework/Input/Devices/Mouse/InputDeviceMouse.cpp index a7180d7ec1..5cd940e989 100644 --- a/Code/Framework/AzFramework/AzFramework/Input/Devices/Mouse/InputDeviceMouse.cpp +++ b/Code/Framework/AzFramework/AzFramework/Input/Devices/Mouse/InputDeviceMouse.cpp @@ -95,8 +95,8 @@ namespace AzFramework } //////////////////////////////////////////////////////////////////////////////////////////////// - InputDeviceMouse::InputDeviceMouse() - : InputDevice(Id) + InputDeviceMouse::InputDeviceMouse(AzFramework::InputDeviceId id) + : InputDevice(id) , m_allChannelsById() , m_buttonChannelsById() , m_movementChannelsById() diff --git a/Code/Framework/AzFramework/AzFramework/Input/Devices/Mouse/InputDeviceMouse.h b/Code/Framework/AzFramework/AzFramework/Input/Devices/Mouse/InputDeviceMouse.h index 7fc6a7a505..3feb79ac68 100644 --- a/Code/Framework/AzFramework/AzFramework/Input/Devices/Mouse/InputDeviceMouse.h +++ b/Code/Framework/AzFramework/AzFramework/Input/Devices/Mouse/InputDeviceMouse.h @@ -111,7 +111,7 @@ namespace AzFramework //////////////////////////////////////////////////////////////////////////////////////////// //! Constructor - explicit InputDeviceMouse(); + explicit InputDeviceMouse(AzFramework::InputDeviceId id = Id); //////////////////////////////////////////////////////////////////////////////////////////// // Disable copying diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp index 3658f9cd22..0516cdd591 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp +++ b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp @@ -747,19 +747,24 @@ namespace AzFramework return button == inputChannelId; }); - if (inputChannelId == InputDeviceMouse::Movement::X) + // accept active mouse channel updates, inactive movement channels will just have a 0 delta + if (inputChannel.IsActive()) { - return HorizontalMotionEvent{ aznumeric_cast(inputChannel.GetValue()) }; - } - else if (inputChannelId == InputDeviceMouse::Movement::Y) - { - return VerticalMotionEvent{ aznumeric_cast(inputChannel.GetValue()) }; - } - else if (inputChannelId == InputDeviceMouse::Movement::Z) - { - return ScrollEvent{ inputChannel.GetValue() }; + if (inputChannelId == InputDeviceMouse::Movement::X) + { + return HorizontalMotionEvent{ aznumeric_cast(inputChannel.GetValue()) }; + } + else if (inputChannelId == InputDeviceMouse::Movement::Y) + { + return VerticalMotionEvent{ aznumeric_cast(inputChannel.GetValue()) }; + } + else if (inputChannelId == InputDeviceMouse::Movement::Z) + { + return ScrollEvent{ inputChannel.GetValue() }; + } } - else if (wasMouseButton || InputDeviceKeyboard::IsKeyboardDevice(inputDeviceId)) + + if (wasMouseButton || InputDeviceKeyboard::IsKeyboardDevice(inputDeviceId)) { return DiscreteInputEvent{ inputChannelId, inputChannel.GetState() }; } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp new file mode 100644 index 0000000000..eb5034fdd5 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp @@ -0,0 +1,397 @@ +/* + * 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 +#include +#include +#include +#include + +namespace AzToolsFramework +{ + void QtEventToAzInputMapper::InitializeKeyMappings() + { + // This assumes modifier keys (ctrl/shift/alt) map to the left control/shift/alt keys as Qt provides no way to disambiguate + // in a platform agnostic manner. This could be expanded later with a PAL mapping from native scan codes acquired from + // QKeyEvents, if needed. + m_keyMappings = { { + { Qt::Key_0, AzFramework::InputDeviceKeyboard::Key::Alphanumeric0 }, + { Qt::Key_1, AzFramework::InputDeviceKeyboard::Key::Alphanumeric1 }, + { Qt::Key_2, AzFramework::InputDeviceKeyboard::Key::Alphanumeric2 }, + { Qt::Key_3, AzFramework::InputDeviceKeyboard::Key::Alphanumeric3 }, + { Qt::Key_4, AzFramework::InputDeviceKeyboard::Key::Alphanumeric4 }, + { Qt::Key_5, AzFramework::InputDeviceKeyboard::Key::Alphanumeric5 }, + { Qt::Key_6, AzFramework::InputDeviceKeyboard::Key::Alphanumeric6 }, + { Qt::Key_7, AzFramework::InputDeviceKeyboard::Key::Alphanumeric7 }, + { Qt::Key_8, AzFramework::InputDeviceKeyboard::Key::Alphanumeric8 }, + { Qt::Key_9, AzFramework::InputDeviceKeyboard::Key::Alphanumeric9 }, + { Qt::Key_A, AzFramework::InputDeviceKeyboard::Key::AlphanumericA }, + { Qt::Key_B, AzFramework::InputDeviceKeyboard::Key::AlphanumericB }, + { Qt::Key_C, AzFramework::InputDeviceKeyboard::Key::AlphanumericC }, + { Qt::Key_D, AzFramework::InputDeviceKeyboard::Key::AlphanumericD }, + { Qt::Key_E, AzFramework::InputDeviceKeyboard::Key::AlphanumericE }, + { Qt::Key_F, AzFramework::InputDeviceKeyboard::Key::AlphanumericF }, + { Qt::Key_G, AzFramework::InputDeviceKeyboard::Key::AlphanumericG }, + { Qt::Key_H, AzFramework::InputDeviceKeyboard::Key::AlphanumericH }, + { Qt::Key_I, AzFramework::InputDeviceKeyboard::Key::AlphanumericI }, + { Qt::Key_J, AzFramework::InputDeviceKeyboard::Key::AlphanumericJ }, + { Qt::Key_K, AzFramework::InputDeviceKeyboard::Key::AlphanumericK }, + { Qt::Key_L, AzFramework::InputDeviceKeyboard::Key::AlphanumericL }, + { Qt::Key_M, AzFramework::InputDeviceKeyboard::Key::AlphanumericM }, + { Qt::Key_N, AzFramework::InputDeviceKeyboard::Key::AlphanumericN }, + { Qt::Key_O, AzFramework::InputDeviceKeyboard::Key::AlphanumericO }, + { Qt::Key_P, AzFramework::InputDeviceKeyboard::Key::AlphanumericP }, + { Qt::Key_Q, AzFramework::InputDeviceKeyboard::Key::AlphanumericQ }, + { Qt::Key_R, AzFramework::InputDeviceKeyboard::Key::AlphanumericR }, + { Qt::Key_S, AzFramework::InputDeviceKeyboard::Key::AlphanumericS }, + { Qt::Key_T, AzFramework::InputDeviceKeyboard::Key::AlphanumericT }, + { Qt::Key_U, AzFramework::InputDeviceKeyboard::Key::AlphanumericU }, + { Qt::Key_V, AzFramework::InputDeviceKeyboard::Key::AlphanumericV }, + { Qt::Key_W, AzFramework::InputDeviceKeyboard::Key::AlphanumericW }, + { Qt::Key_X, AzFramework::InputDeviceKeyboard::Key::AlphanumericX }, + { Qt::Key_Y, AzFramework::InputDeviceKeyboard::Key::AlphanumericY }, + { Qt::Key_Z, AzFramework::InputDeviceKeyboard::Key::AlphanumericZ }, + { Qt::Key_Backspace, AzFramework::InputDeviceKeyboard::Key::EditBackspace }, + { Qt::Key_CapsLock, AzFramework::InputDeviceKeyboard::Key::EditCapsLock }, + { Qt::Key_Enter, AzFramework::InputDeviceKeyboard::Key::EditEnter }, + { Qt::Key_Space, AzFramework::InputDeviceKeyboard::Key::EditSpace }, + { Qt::Key_Tab, AzFramework::InputDeviceKeyboard::Key::EditTab }, + { Qt::Key_Escape, AzFramework::InputDeviceKeyboard::Key::Escape }, + { Qt::Key_F1, AzFramework::InputDeviceKeyboard::Key::Function01 }, + { Qt::Key_F2, AzFramework::InputDeviceKeyboard::Key::Function02 }, + { Qt::Key_F3, AzFramework::InputDeviceKeyboard::Key::Function03 }, + { Qt::Key_F4, AzFramework::InputDeviceKeyboard::Key::Function04 }, + { Qt::Key_F5, AzFramework::InputDeviceKeyboard::Key::Function05 }, + { Qt::Key_F6, AzFramework::InputDeviceKeyboard::Key::Function06 }, + { Qt::Key_F7, AzFramework::InputDeviceKeyboard::Key::Function07 }, + { Qt::Key_F8, AzFramework::InputDeviceKeyboard::Key::Function08 }, + { Qt::Key_F9, AzFramework::InputDeviceKeyboard::Key::Function09 }, + { Qt::Key_F10, AzFramework::InputDeviceKeyboard::Key::Function10 }, + { Qt::Key_F11, AzFramework::InputDeviceKeyboard::Key::Function11 }, + { Qt::Key_F12, AzFramework::InputDeviceKeyboard::Key::Function12 }, + { Qt::Key_F13, AzFramework::InputDeviceKeyboard::Key::Function13 }, + { Qt::Key_F14, AzFramework::InputDeviceKeyboard::Key::Function14 }, + { Qt::Key_F15, AzFramework::InputDeviceKeyboard::Key::Function15 }, + { Qt::Key_F16, AzFramework::InputDeviceKeyboard::Key::Function16 }, + { Qt::Key_F17, AzFramework::InputDeviceKeyboard::Key::Function17 }, + { Qt::Key_F18, AzFramework::InputDeviceKeyboard::Key::Function18 }, + { Qt::Key_F19, AzFramework::InputDeviceKeyboard::Key::Function19 }, + { Qt::Key_F20, AzFramework::InputDeviceKeyboard::Key::Function20 }, + { Qt::Key_Alt, AzFramework::InputDeviceKeyboard::Key::ModifierAltL }, + { Qt::Key_Control, AzFramework::InputDeviceKeyboard::Key::ModifierCtrlL }, + { Qt::Key_Shift, AzFramework::InputDeviceKeyboard::Key::ModifierShiftL }, + { Qt::Key_Super_L, AzFramework::InputDeviceKeyboard::Key::ModifierSuperL }, + { Qt::Key_Super_R, AzFramework::InputDeviceKeyboard::Key::ModifierSuperR }, + { Qt::Key_Down, AzFramework::InputDeviceKeyboard::Key::NavigationArrowDown }, + { Qt::Key_Left, AzFramework::InputDeviceKeyboard::Key::NavigationArrowLeft }, + { Qt::Key_Right, AzFramework::InputDeviceKeyboard::Key::NavigationArrowRight }, + { Qt::Key_Up, AzFramework::InputDeviceKeyboard::Key::NavigationArrowUp }, + { Qt::Key_Delete, AzFramework::InputDeviceKeyboard::Key::NavigationDelete }, + { Qt::Key_End, AzFramework::InputDeviceKeyboard::Key::NavigationEnd }, + { Qt::Key_Home, AzFramework::InputDeviceKeyboard::Key::NavigationHome }, + { Qt::Key_Insert, AzFramework::InputDeviceKeyboard::Key::NavigationInsert }, + { Qt::Key_PageDown, AzFramework::InputDeviceKeyboard::Key::NavigationPageDown }, + { Qt::Key_PageUp, AzFramework::InputDeviceKeyboard::Key::NavigationPageUp }, + { Qt::Key_Apostrophe, AzFramework::InputDeviceKeyboard::Key::PunctuationApostrophe }, + { Qt::Key_Backslash, AzFramework::InputDeviceKeyboard::Key::PunctuationBackslash }, + { Qt::Key_BracketLeft, AzFramework::InputDeviceKeyboard::Key::PunctuationBracketL }, + { Qt::Key_BracketRight, AzFramework::InputDeviceKeyboard::Key::PunctuationBracketR }, + { Qt::Key_Comma, AzFramework::InputDeviceKeyboard::Key::PunctuationComma }, + { Qt::Key_Equal, AzFramework::InputDeviceKeyboard::Key::PunctuationEquals }, + { Qt::Key_hyphen, AzFramework::InputDeviceKeyboard::Key::PunctuationHyphen }, + { Qt::Key_Period, AzFramework::InputDeviceKeyboard::Key::PunctuationPeriod }, + { Qt::Key_Semicolon, AzFramework::InputDeviceKeyboard::Key::PunctuationSemicolon }, + { Qt::Key_Slash, AzFramework::InputDeviceKeyboard::Key::PunctuationSlash }, + { Qt::Key_QuoteLeft, AzFramework::InputDeviceKeyboard::Key::PunctuationTilde }, + { Qt::Key_Pause, AzFramework::InputDeviceKeyboard::Key::WindowsSystemPause }, + { Qt::Key_Print, AzFramework::InputDeviceKeyboard::Key::WindowsSystemPrint }, + { Qt::Key_ScrollLock, AzFramework::InputDeviceKeyboard::Key::WindowsSystemScrollLock }, + } }; + } + + void QtEventToAzInputMapper::InitializeMouseButtonMappings() + { + m_mouseButtonMappings = { { + { Qt::MouseButton::LeftButton, AzFramework::InputDeviceMouse::Button::Left }, + { Qt::MouseButton::RightButton, AzFramework::InputDeviceMouse::Button::Right }, + { Qt::MouseButton::MiddleButton, AzFramework::InputDeviceMouse::Button::Middle }, + { Qt::MouseButton::ExtraButton1, AzFramework::InputDeviceMouse::Button::Other1 }, + { Qt::MouseButton::ExtraButton2, AzFramework::InputDeviceMouse::Button::Other2 }, + } }; + } + + // Currently this is only set for modifier keys. + // This should only be expanded sparingly, any keys handled here will not be bubbled up to the shortcut system. + // ex: If Key_S was here, the viewport would consume S key presses before the application could process a QAction with a Ctrl+S + // shortcut. + void QtEventToAzInputMapper::InitializeHighPriorityKeys() + { + m_highPriorityKeys = { Qt::Key_Alt, Qt::Key_Control, Qt::Key_Shift, Qt::Key_Super_L, Qt::Key_Super_R }; + } + + QtEventToAzInputMapper::EditorQtKeyboardDevice::EditorQtKeyboardDevice(AzFramework::InputDeviceId id) + : AzFramework::InputDeviceKeyboard(id) + { + // Disable all platform native processing in favor of our Qt event handling + SetImplementation(nullptr); + } + + QtEventToAzInputMapper::EditorQtMouseDevice::EditorQtMouseDevice(AzFramework::InputDeviceId id) + : AzFramework::InputDeviceMouse(id) + { + // Disable all platform native processing in favor of our Qt event handling + SetImplementation(nullptr); + } + + QtEventToAzInputMapper::QtEventToAzInputMapper(QWidget* sourceWidget, int syntheticDeviceId) + : QObject(sourceWidget) + , m_sourceWidget(sourceWidget) + , m_keyboardModifiers(AZStd::make_shared()) + , m_cursorPosition(AZStd::make_shared()) + { + InitializeKeyMappings(); + InitializeMouseButtonMappings(); + InitializeHighPriorityKeys(); + + // Add an arbitrary offset to our device index to avoid collision with real physical device index. + // We still have to use the keyboard and mouse device channel names because input channels are only addressed + // by their own name and their device index, so overlapping input channels between devices would conflict. + constexpr AZ::u32 syntheticDeviceOffset = 1000; + const AzFramework::InputDeviceId keyboardDeviceId( + AzFramework::InputDeviceKeyboard::Id.GetName(), syntheticDeviceId + syntheticDeviceOffset); + const AzFramework::InputDeviceId mouseDeviceId( + AzFramework::InputDeviceMouse::Id.GetName(), syntheticDeviceId + syntheticDeviceOffset); + + m_keyboardDevice = AZStd::make_unique(keyboardDeviceId); + m_mouseDevice = AZStd::make_unique(mouseDeviceId); + + AddChannels(m_keyboardDevice->m_allChannelsById); + AddChannels(m_mouseDevice->m_allChannelsById); + + // Install a global event filter to ensure we don't miss mouse and key release events. + QApplication::instance()->installEventFilter(this); + } + + bool QtEventToAzInputMapper::HandlesInputEvent(const AzFramework::InputChannel& channel) const + { + const AzFramework::InputChannelId& channelId = channel.GetInputChannelId(); + if (channelId == AzFramework::InputDeviceMouse::Movement::X || channelId == AzFramework::InputDeviceMouse::Movement::Y) + { + return false; + } + + // We map keyboard and mouse events from Qt, so flag all events coming from those devices + // as handled by our synthetic event system. + const AzFramework::InputDeviceId& deviceId = channel.GetInputDevice().GetInputDeviceId(); + return deviceId.GetNameCrc32() == AzFramework::InputDeviceMouse::Id.GetNameCrc32() || + deviceId.GetNameCrc32() == AzFramework::InputDeviceKeyboard::Id.GetNameCrc32(); + } + + void QtEventToAzInputMapper::SetEnabled(bool enabled) + { + m_enabled = enabled; + if (!enabled) + { + // Send an internal focus change event to reset our input state to fresh if we're disabled. + HandleFocusChange(nullptr); + } + } + + bool QtEventToAzInputMapper::eventFilter(QObject* object, QEvent* event) + { + // Abort if processing isn't enabled. + if (!m_enabled) + { + return false; + } + + // Because there's no "end" to mouse movement and wheel events, we reset mouse movement channels that have been opened + // during the next processed non-mouse event. + if (m_mouseChannelsNeedUpdate && event->type() != QEvent::Type::MouseMove && event->type() != QEvent::Type::Wheel) + { + m_cursorPosition->m_normalizedPositionDelta = AZ::Vector2::CreateZero(); + ProcessPendingMouseEvents(); + m_mouseChannelsNeedUpdate = false; + } + + // Only accept mouse & key release events that originate from an object that is not our target widget, + // as we don't want to erroneously intercept user input meant for another component. + if (object != m_sourceWidget && event->type() != QEvent::Type::KeyRelease && event->type() != QEvent::Type::MouseButtonRelease) + { + return false; + } + + // If our focus changes, go ahead and reset all input devices. + if (event->type() == QEvent::FocusIn || event->type() == QEvent::FocusOut) + { + HandleFocusChange(event); + } + // Map key events to input channels. + // ShortcutOverride is used in lieu of KeyPress for high priority input channels like Alt + // that need to be accepted and stopped before they bubble up and cause unintended behavior. + else if ( + event->type() == QEvent::Type::KeyPress || event->type() == QEvent::Type::KeyRelease || + event->type() == QEvent::Type::ShortcutOverride) + { + QKeyEvent* keyEvent = static_cast(event); + HandleKeyEvent(keyEvent); + } + // Map mouse events to input channels. + else if (event->type() == QEvent::Type::MouseButtonPress || event->type() == QEvent::Type::MouseButtonRelease || event->type() == QEvent::Type::MouseButtonDblClick) + { + QMouseEvent* mouseEvent = static_cast(event); + HandleMouseButtonEvent(mouseEvent); + } + // Map mouse movement to the movement input channels. + // This includes SystemCursorPosition alongside Movement::X and Movement::Y. + else if (event->type() == QEvent::Type::MouseMove) + { + QMouseEvent* mouseEvent = static_cast(event); + HandleMouseMoveEvent(mouseEvent); + } + // Map wheel events to the mouse Z movement channel. + else if (event->type() == QEvent::Type::Wheel) + { + QWheelEvent* wheelEvent = static_cast(event); + HandleWheelEvent(wheelEvent); + } + + return false; + } + + void QtEventToAzInputMapper::NotifyUpdateChannelIfNotIdle(const AzFramework::InputChannel* channel, QEvent* event) + { + if (channel->GetState() != AzFramework::InputChannel::State::Idle) + { + emit InputChannelUpdated(channel, event); + } + } + + void QtEventToAzInputMapper::ProcessPendingMouseEvents() + { + auto systemCursorChannel = + GetInputChannel(AzFramework::InputDeviceMouse::SystemCursorPosition); + auto mouseWheelChannel = + GetInputChannel(AzFramework::InputDeviceMouse::Movement::Z); + + systemCursorChannel->ProcessRawInputEvent(m_cursorPosition->m_normalizedPositionDelta.GetLength()); + mouseWheelChannel->ProcessRawInputEvent(0.f); + + NotifyUpdateChannelIfNotIdle(systemCursorChannel, nullptr); + NotifyUpdateChannelIfNotIdle(mouseWheelChannel, nullptr); + } + + void QtEventToAzInputMapper::HandleMouseButtonEvent(QMouseEvent* mouseEvent) + { + const Qt::MouseButton button = mouseEvent->button(); + + if (auto buttonIt = m_mouseButtonMappings.find(button); buttonIt != m_mouseButtonMappings.end()) + { + auto buttonChannel = GetInputChannel(buttonIt->second); + + if (buttonChannel) + { + if (mouseEvent->type() != QEvent::Type::MouseButtonRelease) + { + buttonChannel->UpdateState(true); + } + else + { + buttonChannel->UpdateState(false); + } + + NotifyUpdateChannelIfNotIdle(buttonChannel, mouseEvent); + } + } + } + + void QtEventToAzInputMapper::HandleMouseMoveEvent(QMouseEvent* mouseEvent) + { + const QPoint mousePos = mouseEvent->pos(); + const float normalizedX = aznumeric_cast(mousePos.x()) / aznumeric_cast(m_sourceWidget->width()); + const float normalizedY = aznumeric_cast(mousePos.y()) / aznumeric_cast(m_sourceWidget->height()); + const AZ::Vector2 normalizedPosition(normalizedX, normalizedY); + m_cursorPosition->m_normalizedPositionDelta = normalizedPosition - m_cursorPosition->m_normalizedPosition; + m_cursorPosition->m_normalizedPosition = normalizedPosition; + ProcessPendingMouseEvents(); + m_mouseChannelsNeedUpdate = true; + } + + void QtEventToAzInputMapper::HandleKeyEvent(QKeyEvent* keyEvent) + { + // Ignore key repeat events, they're unrelated to actual physical button presses. + if (keyEvent->isAutoRepeat()) + { + return; + } + + const Qt::Key key = static_cast(keyEvent->key()); + + // For ShortcutEvent, only continue processing if we're in the HighPriorityKeys set. + if (keyEvent->type() != QEvent::Type::ShortcutOverride || m_highPriorityKeys.find(key) != m_highPriorityKeys.end()) + { + if (auto keyIt = m_keyMappings.find(key); keyIt != m_keyMappings.end()) + { + auto keyChannel = GetInputChannel(keyIt->second); + + if (keyChannel) + { + if (keyEvent->type() == QEvent::Type::KeyPress || keyEvent->type() == QEvent::Type::ShortcutOverride) + { + keyChannel->UpdateState(true); + } + else + { + keyChannel->UpdateState(false); + } + + NotifyUpdateChannelIfNotIdle(keyChannel, keyEvent); + } + } + } + } + + void QtEventToAzInputMapper::HandleWheelEvent(QWheelEvent* wheelEvent) + { + auto cursorZChannel = + GetInputChannel(AzFramework::InputDeviceMouse::Movement::Z); + const QPoint angleDelta = wheelEvent->angleDelta(); + // Check both angles, as the alt modifier can change the wheel direction. + int wheelAngle = angleDelta.x(); + if (wheelAngle == 0) + { + wheelAngle = angleDelta.y(); + } + cursorZChannel->ProcessRawInputEvent(aznumeric_cast(wheelAngle)); + NotifyUpdateChannelIfNotIdle(cursorZChannel, wheelEvent); + m_mouseChannelsNeedUpdate = true; + } + + void QtEventToAzInputMapper::HandleFocusChange(QEvent* event) + { + for (auto& channelData : m_channels) + { + // If resetting the input device changed the channel state, submit it to the mapped channel list + // for processing. + if (channelData.second->IsActive()) + { + channelData.second->UpdateState(false); + NotifyUpdateChannelIfNotIdle(channelData.second, event); + } + } + m_mouseChannelsNeedUpdate = false; + } +} // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h new file mode 100644 index 0000000000..6d95f3a4b0 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h @@ -0,0 +1,152 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#endif //! defined(Q_MOC_RUN) + +class QWidget; +class QKeyEvent; +class QMouseEvent; +class QWheelEvent; + +namespace AzToolsFramework +{ + //! Maps events from the Qt input system to synthetic InputChannels in AzFramework + //! that can be used by AzFramework::ViewportControllers. + class QtEventToAzInputMapper final : public QObject + { + Q_OBJECT + + public: + QtEventToAzInputMapper(QWidget* sourceWidget, int syntheticDeviceId = 0); + ~QtEventToAzInputMapper() = default; + + //! Queries whether a given input channel has a synthetic equivalent mapped + //! by this system. + //! \returns true if the channel is handled by MapQtEventToAzInput. + bool HandlesInputEvent(const AzFramework::InputChannel& channel) const; + + //! Sets whether or not this input mapper should be updating its input channels from Qt events. + void SetEnabled(bool enabled); + + // QObject overrides... + bool eventFilter(QObject* object, QEvent* event) override; + + signals: + //! This signal fires whenever the state of the specified input channel changes. + //! This is determined by Qt events dispatched to the source widget. + //! \param channel The AZ input channel that has been updated. + //! \param event The underlying Qt event that triggered this change, if applicable. + void InputChannelUpdated(const AzFramework::InputChannel* channel, QEvent* event); + + private: + // Gets an input channel of the specified type by ID. + template + TInputChannel* GetInputChannel(const AzFramework::InputChannelId& id) + { + auto channelIt = m_channels.find(id); + if (channelIt != m_channels.end()) + { + return static_cast(channelIt->second); + } + return nullptr; + } + + // Adds channels from the specified channel container to our input channel ID -> input channel lookup table. + // Used for rapid lookup. + template + void AddChannels(const TContainer& container) + { + for (const auto& channelData : container) + { + // Break const as we're taking these input channels from devices we own. + m_channels.emplace(channelData.first, const_cast(channelData.second)); + } + } + + // Our synthetic Keyboard device, does no internal keyboard handling and instead listens to this class for updates. + class EditorQtKeyboardDevice : public AzFramework::InputDeviceKeyboard + { + public: + EditorQtKeyboardDevice(AzFramework::InputDeviceId id); + + friend class QtEventToAzInputMapper; + }; + + // Our synthetic Mouse device, does no internal keyboard handling and instead listens to this class for updates. + class EditorQtMouseDevice : public AzFramework::InputDeviceMouse + { + public: + EditorQtMouseDevice(AzFramework::InputDeviceId id); + + friend class QtEventToAzInputMapper; + }; + + // Emits InputChannelUpdated if channel has transitioned in state (i.e. has gone from active to inactive or vice versa). + void NotifyUpdateChannelIfNotIdle(const AzFramework::InputChannel* channel, QEvent* event); + + // Processes any pending mouse movement events, this allows mouse movement channels to close themselves. + void ProcessPendingMouseEvents(); + + // Handle mouse click events. + void HandleMouseButtonEvent(QMouseEvent* mouseEvent); + // Handle mouse move events. + void HandleMouseMoveEvent(QMouseEvent* mouseEvent); + // Handles key press / release events (or ShortcutOverride events for keys listed in m_highPriorityKeys). + void HandleKeyEvent(QKeyEvent* keyEvent); + // Handles mouse wheel events. + void HandleWheelEvent(QWheelEvent* wheelEvent); + // Handles focus change events. + void HandleFocusChange(QEvent* event); + + // Populates m_keyMappings. + void InitializeKeyMappings(); + // Populates m_mouseButtonMappings. + void InitializeMouseButtonMappings(); + // Populates m_highPriorityKeys. + void InitializeHighPriorityKeys(); + + // The current keyboard modifier state used by our synthetic key input channels. + AZStd::shared_ptr m_keyboardModifiers; + // The current normalized cursor position used by our synthetic system cursor event. + AZStd::shared_ptr m_cursorPosition; + // A lookup table for Qt key -> AZ input channel. + AZStd::unordered_map m_keyMappings; + // A lookup table for Qt mouse button -> AZ input channel. + AZStd::unordered_map m_mouseButtonMappings; + // A set of high priority keys that need to be processed at the ShortcutOverride level instead of the + // KeyEvent level. This prevents e.g. the main menu bar from processing a press of the "alt" key when the + // viewport consumes the event. + AZStd::unordered_set m_highPriorityKeys; + // A lookup table for AZ input channel ID -> physical input channel on our mouse or keyboard device. + AZStd::unordered_map m_channels; + // The source widget to map events from, used to calculate the relative mouse position within the widget bounds. + QWidget* m_sourceWidget; + // Flags when mouse movement channels have been opened and may need to be closed (as there are no movement ended events). + bool m_mouseChannelsNeedUpdate = false; + // Flags whether or not Qt events should currently be processed. + bool m_enabled = true; + + // Our viewport-specific AZ devices. We control their internal input channel states. + AZStd::unique_ptr m_mouseDevice; + AZStd::unique_ptr m_keyboardDevice; + }; +} // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake index b5b2c028a5..7fbde67ce4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake @@ -724,6 +724,8 @@ set(FILES PythonTerminal/ScriptTermDialog.cpp PythonTerminal/ScriptTermDialog.h PythonTerminal/ScriptTermDialog.ui + Input/QtEventToAzInputManager.h + Input/QtEventToAzInputManager.cpp ) # Prevent the following files from being grouped in UNITY builds diff --git a/Code/LauncherUnified/Platform/iOS/Launcher_iOS.mm b/Code/LauncherUnified/Platform/iOS/Launcher_iOS.mm index 480a1b0be1..b5401b8d80 100644 --- a/Code/LauncherUnified/Platform/iOS/Launcher_iOS.mm +++ b/Code/LauncherUnified/Platform/iOS/Launcher_iOS.mm @@ -6,7 +6,7 @@ */ #import - +#include int main(int argc, char* argv[]) { diff --git a/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp b/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp index a69ad3f3af..b31fbca6f2 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp @@ -229,6 +229,7 @@ namespace AZ { m_clearContainers = false; + m_stackLevel = 0; m_cachedTimeRegionMap.clear(); m_timeRegionStack.clear(); m_cachedTimeRegions.clear(); diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h index c649031280..50378c15bd 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +82,12 @@ namespace AtomToolsFramework //! Gets the default camera that's been automatically registered to our ViewportContext. AZ::RPI::ViewPtr GetDefaultCamera(); AZ::RPI::ConstViewPtr GetDefaultCamera() const; + //! Sets whether or not input processing is enabled for this RenderViewportWidget. + //! While input processing is enabled, synthetic input events may appear in OnInputChannelEventFiltered + //! due to internal viewport input mapping via QtEventToAzInputMapper, so it may be desirable to disable + //! camera controller input processing wholesale to avoid competing input messages. + //! Input processing is enabled by default. + void SetInputProcessingEnabled(bool enabled); // AzToolsFramework::ViewportInteraction::ViewportInteractionRequestBus::Handler ... AzFramework::CameraState GetCameraState() override; @@ -130,7 +137,6 @@ namespace AtomToolsFramework private: void SendWindowResizeEvent(); - bool CanInputGrantFocus(const AzFramework::InputChannel& inputChannel) const; // The underlying ViewportContext, our entry-point to the Atom RPI. AZ::RPI::ViewportContextPtr m_viewportContext; @@ -157,5 +163,7 @@ namespace AtomToolsFramework AZStd::optional m_lastCursorPosition; // The viewport settings (e.g. grid snapping, grid size) for this viewport. const AzToolsFramework::ViewportInteraction::ViewportSettings* m_viewportSettings = nullptr; + // Maps our internal Qt events into AzFramework InputChannels for our ViewportControllerList. + AzToolsFramework::QtEventToAzInputMapper* m_inputChannelMapper = nullptr; }; } //namespace AtomToolsFramework diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp index 529652e1a9..9fcd93f069 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp @@ -81,6 +81,23 @@ namespace AtomToolsFramework AZ::TickBus::Handler::BusConnect(); AzFramework::WindowRequestBus::Handler::BusConnect(params.windowHandle); + m_inputChannelMapper = new AzToolsFramework::QtEventToAzInputMapper(this, id); + + // Forward input events to our controller list. + QObject::connect(m_inputChannelMapper, &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, this, + [this](const AzFramework::InputChannel* inputChannel, QEvent* event) + { + AzFramework::NativeWindowHandle windowId = reinterpret_cast(winId()); + if (m_controllerList->HandleInputChannelEvent({GetId(), windowId, *inputChannel})) + { + // If the controller handled the input event, mark the event as accepted so it doesn't continue to propagate. + if (event) + { + event->setAccepted(true); + } + } + }); + return true; } @@ -149,86 +166,24 @@ namespace AtomToolsFramework return m_defaultCamera; } - static bool IsMouseButtonEvent(const AzFramework::InputChannel& inputChannel) - { - const auto& mouseButtons = AzFramework::InputDeviceMouse::Button::All; - return AZStd::find(mouseButtons.begin(), mouseButtons.end(), inputChannel.GetInputChannelId()) != mouseButtons.end(); - } - - static bool IsMouseMoveEvent(const AzFramework::InputChannel& inputChannel) - { - return inputChannel.GetInputChannelId() == AzFramework::InputDeviceMouse::SystemCursorPosition; - } - - static bool IsMouseButtonOrWheelEvent(const AzFramework::InputChannel& inputChannel) - { - return IsMouseButtonEvent(inputChannel) || inputChannel.GetInputChannelId() == AzFramework::InputDeviceMouse::Movement::Z; - } - - bool RenderViewportWidget::CanInputGrantFocus(const AzFramework::InputChannel& inputChannel) const - { - // Only take focus from a mouse event if the cursor is currently within the viewport - if (!m_mouseOver) - { - return false; - } - - // Only mouse button down events (clicks) can grant focus - if (inputChannel.GetState() != AzFramework::InputChannel::State::Began) - { - return false; - } - - // Only mouse button events can grant focus - return IsMouseButtonEvent(inputChannel); - } - bool RenderViewportWidget::OnInputChannelEventFiltered(const AzFramework::InputChannel& inputChannel) { - bool shouldConsumeEvent = true; - - // Grab keyboard focus if we've been clicked on. - // Qt normally handles this for us, but we're filtering native events before they get - // synthesized into QMouseEvents. - if (!hasFocus() && CanInputGrantFocus(inputChannel)) - { - setFocus(); - } - - // Don't consume new input events if we don't currently have focus. - // We do forward Ended events, as they may be relevant to our current state - // (e.g. a key gets released after we lose focus, it shouldn't remain "stuck"). if (!hasFocus()) - { - if (inputChannel.GetState() == AzFramework::InputChannel::State::Ended) - { - // Forward the input ended event to our controllers, but don't prevent other viewports from receiving it. - shouldConsumeEvent = false; - } - else - { - // Not an event we should listen to, abort. - return false; - } - } - - // If we receive a mouse button event from outside of our viewport, ignore it even if we have focus. - if (!m_mouseOver - && inputChannel.GetState() == AzFramework::InputChannel::State::Began - && IsMouseButtonOrWheelEvent(inputChannel)) { return false; } - // Don't forward system cursor position updates, we'll do that ourselves for in-window movements once the result of - // ViewportCursorScreenPosition is guaranteed to be correct (see mouseMoveEvent). - if (IsMouseMoveEvent(inputChannel)) + // Only forward channels that aren't covered by our Qt -> AZ event mapper + if (!m_inputChannelMapper || m_inputChannelMapper->HandlesInputEvent(inputChannel)) { return false; } + bool shouldConsumeEvent = true; + AzFramework::NativeWindowHandle windowId = reinterpret_cast(winId()); const bool eventHandled = m_controllerList->HandleInputChannelEvent({GetId(), windowId, inputChannel}); + // If our controllers handled the event and it's one we can safely consume (i.e. it's not an Ended event that other viewports might need), consume it. return eventHandled && shouldConsumeEvent; } @@ -262,6 +217,7 @@ namespace AtomToolsFramework { SendWindowResizeEvent(); } + return QWidget::event(event); } @@ -279,23 +235,12 @@ namespace AtomToolsFramework { m_mousePosition = event->localPos(); - // Now that we've looked a viewport local mouse position, - // we can go ahead and broadcast the system cursor input event to the controllers. - // This allows any controllers not listening to pure mouse deltas to consistently - // look up the mouse position in viewport screen coordinates. - const AzFramework::InputDevice* mouseInputDevice = nullptr; - if (AzFramework::InputDeviceRequestBus::EventResult( - mouseInputDevice, AzFramework::InputDeviceMouse::Id, &AzFramework::InputDeviceRequests::GetInputDevice); - mouseInputDevice != nullptr) - { - const AzFramework::NativeWindowHandle windowId = reinterpret_cast(winId()); - AzFramework::InputChannel syntheticInput(AzFramework::InputDeviceMouse::SystemCursorPosition, *mouseInputDevice); - m_controllerList->HandleInputChannelEvent({GetId(), windowId, syntheticInput}); - } - if (m_capturingCursor && m_lastCursorPosition.has_value()) { AzQtComponents::SetCursorPos(m_lastCursorPosition.value()); + // Even though we just set the cursor position, there are edge cases such as remote desktop that will leave + // the cursor position unchanged. For safety, we re-cache our last cursor position for delta generation. + m_lastCursorPosition = QCursor::pos(); } else { @@ -354,6 +299,12 @@ namespace AtomToolsFramework } } + void RenderViewportWidget::SetInputProcessingEnabled(bool enabled) + { + m_inputChannelMapper->SetEnabled(enabled); + m_controllerList->SetEnabled(enabled); + } + AzFramework::CameraState RenderViewportWidget::GetCameraState() { AZ::RPI::ViewPtr currentView = m_viewportContext->GetDefaultView(); diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h index 1dd2622603..0c019f19aa 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h @@ -7,8 +7,12 @@ #pragma once -#include +#include +#include + #include +#include + namespace AZ { @@ -19,24 +23,41 @@ namespace AZ namespace Render { - struct ThreadRegionEntry + struct ThreadRegionEntry { AZStd::thread_id m_threadId; AZStd::sys_time_t m_startTick = 0; AZStd::sys_time_t m_endTick = 0; }; + // Stores data about a region that is agreggated from all collected frames + // Data collection can be toggled on and off through m_record. + struct RegionStatistics + { + float CalcAverageTimeMs() const; + void RecordRegion(const AZ::RHI::CachedTimeRegion& region); + + bool m_draw = false; + bool m_record = true; + u64 m_invocations = 0; + AZStd::sys_time_t m_totalTicks = 0; + }; + //! Visual profiler for Cpu statistics. //! It uses ImGui as the library for displaying the Attachments and Heaps. //! It shows all heaps that are being used by the RHI and how the //! resources are allocated in each heap. class ImGuiCpuProfiler + : SystemTickBus::Handler { // Region Name -> Array of ThreadRegion entries using RegionEntryMap = AZStd::map>; // Group Name -> RegionEntryMap using GroupRegionMap = AZStd::map; + using TimeRegion = AZ::RHI::CachedTimeRegion; + using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName; + public: ImGuiCpuProfiler() = default; ~ImGuiCpuProfiler() = default; @@ -44,7 +65,13 @@ namespace AZ //! Draws the provided Cpu statistics. void Draw(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& cpuTimingStatistics); + //! Draws the CPU profiling visualizer in a new window. + void DrawVisualizer(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics); + private: + static constexpr float RowHeight = 50.0; + static constexpr int DefaultFramesToCollect = 50; + // Update the GroupRegionMap with the latest cached time regions void UpdateGroupRegionMap(); @@ -62,8 +89,73 @@ namespace AZ AZ::RHI::CpuTimingStatistics m_cpuTimingStatisticsWhenPause; AZStd::string m_lastCapturedFilePath; + + // Visualizer methods + + // Get the profiling data from the last frame, only called when the profiler is not paused. + void CollectFrameData(); + + // Cull old data from internal storage, only called when profiler is not paused. + void CullFrameData(const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics); + + // Draws a single block onto the timeline + void DrawBlock(const TimeRegion& block, u64 targetRow); + + // Draw horizontal lines between threads in the timeline + void DrawThreadSeparator(u64 threadBoundary, u64 maxDepth); + + // Draw the "Thread XXXXX" label onto the viewport + void DrawThreadLabel(u64 baseRow, AZStd::thread_id threadId); + + // Draws all active function statistics windows + void DrawRegionStatistics(); + + // Draw the vertical lines separating frames in the timeline + void DrawFrameBoundaries(); + + // Draw the ruler with frame time labels + void DrawRuler(); + + // Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling + float ConvertTickToPixelSpace(AZStd::sys_time_t tick) const; + + AZStd::sys_time_t GetViewportTickWidth() const; + + // Gets the color for a block using the GroupRegionName as a key into the cache + // Generates a random ImU32 if the block does not yet have a color + ImU32 GetBlockColor(const TimeRegion& block); + + // System tick bus overrides + virtual void OnSystemTick() override; + + // Visualizer state + + bool m_showVisualizer = false; + + int m_framesToCollect = DefaultFramesToCollect; + + // Tally of the number of saved profiling events so far + u64 m_savedRegionCount = 0; + + // Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects + AZStd::sys_time_t m_viewportStartTick; + AZStd::sys_time_t m_viewportEndTick; + + // Map to store each thread's TimeRegions, individual vectors are sorted by start tick + AZStd::unordered_map> m_savedData; + + // Region color cache + AZStd::unordered_map m_regionColorMap; + + // Tracks the frame boundaries + AZStd::vector m_frameEndTicks = { INT64_MIN }; + + // Main data structure for storing function statistics to be shown in the popup windows. + // For now we default allocate for all regions on the first render frame and then use RegionStatistics.m_draw to determine + // if we should draw the window or not. FIXME(ATOM-15948) this should be changed once RegionStatistics gets heavier. + AZStd::unordered_map m_regionStatisticsMap; }; } // namespace Render -} +} // namespace AZ #include "ImGuiCpuProfiler.inl" diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl index 7551cf50dc..daeb797c42 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl @@ -6,10 +6,16 @@ */ #include +#include +#include #include + #include -#include +#include #include +#include +#include + namespace AZ { @@ -38,14 +44,14 @@ namespace AZ AZ_Assert(ticksPerSecond >= 1000, "Error in converting ticks to ms, expected ticksPerSecond >= 1000"); return static_cast((ticks * 1000) / (ticksPerSecond / 1000)) / 1000.0f; } - } + } // namespace CpuProfilerImGuiHelper inline void ImGuiCpuProfiler::Draw(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics) { // Cache the value to detect if it was changed by ImGui(user pressed 'x') const bool cachedShowCpuProfiler = keepDrawing; - const ImVec2 windowSize(640.0f, 480.0f); + const ImVec2 windowSize(900.0f, 600.0f); ImGui::SetNextWindowSize(windowSize, ImGuiCond_Once); bool captureToFile = false; if (ImGui::Begin("Cpu Profiler", &keepDrawing, ImGuiWindowFlags_None)) @@ -114,9 +120,9 @@ namespace AZ } }; - const auto ShowRegionRow = [ticksPerSecond, &DrawRegionHoverMarker, &ShowTimeInMs](const char* regionLabel, - AZStd::vector regions, - AZStd::sys_time_t duration) + const auto ShowRegionRow = + [ticksPerSecond, &DrawRegionHoverMarker, + &ShowTimeInMs](const char* regionLabel, AZStd::vector regions, AZStd::sys_time_t duration) { // Draw the region label ImGui::Text(regionLabel); @@ -142,14 +148,15 @@ namespace AZ ImGui::NextColumn(); // Draw the time labels (max and then total) - const AZStd::string timeLabel = - AZStd::string::format("%.2f ms max, %.2f ms total", - CpuProfilerImGuiHelper::TicksToMs(duration), + const AZStd::string timeLabel = AZStd::string::format( + "%.2f ms max, %.2f ms total", CpuProfilerImGuiHelper::TicksToMs(duration), CpuProfilerImGuiHelper::TicksToMs(totalTime)); ImGui::Text(timeLabel.c_str()); ImGui::NextColumn(); }; + ImGui::Checkbox("Enable Visualizer", &m_showVisualizer); + // Set column settings. ImGui::Columns(2, "view", false); ImGui::SetColumnWidth(0, 660.0f); @@ -216,8 +223,8 @@ namespace AZ char resolvedPath[AZ::IO::MaxPathLength]; AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength); m_lastCapturedFilePath = resolvedPath; - AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics, - frameDataFilePath); + AZ::Render::ProfilingCaptureRequestBus::Broadcast( + &AZ::Render::ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics, frameDataFilePath); } // Toggle if the bool isn't the same as the cached value @@ -225,6 +232,11 @@ namespace AZ { AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(keepDrawing); } + + if (m_showVisualizer) + { + DrawVisualizer(m_showVisualizer, currentCpuTimingStatistics); + } } inline void ImGuiCpuProfiler::UpdateGroupRegionMap() @@ -251,5 +263,519 @@ namespace AZ } } } - } -} + + // -- CPU Visualizer -- + inline void ImGuiCpuProfiler::DrawVisualizer(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics) + { + ImGui::SetNextWindowSize({ 900, 600 }, ImGuiCond_Once); + if (ImGui::Begin("CPU Visualizer", &keepDrawing, ImGuiWindowFlags_None)) + { + // Get the instrumentation data for the last frame if active + if (!m_paused && m_groupRegionMap.size() != 0) + { + CollectFrameData(); // Also updates viewport bounds + + CullFrameData(currentCpuTimingStatistics); // Trim data if necessary + + if (!SystemTickBus::Handler::BusIsConnected()) + { + SystemTickBus::Handler::BusConnect(); + } + } + + // Options & Statistics + if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true)) + { + ImGui::Columns(3, "Options", true); + ImGui::Text("Frames To Collect:"); + ImGui::SliderInt("", &m_framesToCollect, 10, 100, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic); + + ImGui::NextColumn(); + + ImGui::Text("Viewport width: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(GetViewportTickWidth())); + ImGui::Text("Ticks [%lld , %lld]", m_viewportStartTick, m_viewportEndTick); + ImGui::Text("Recording %ld threads", RHI::CpuProfiler::Get()->GetTimeRegionMap().size()); + ImGui::Text("%llu profiling events saved", m_savedRegionCount); + + ImGui::NextColumn(); + + ImGui::TextWrapped( + "Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding ."); + } + + + ImGui::Columns(1, "RulerColumn", true); + + // Ruler + if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus)) + { + DrawRuler(); + } + ImGui::EndChild(); + + + ImGui::Columns(1, "TimelineColumn", true); + + // Timeline + if (ImGui::BeginChild( + "Timeline", { 0, 0 }, true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + // Find the next frame boundary after the viewport's right bound and draw until that tick + auto nextFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportEndTick); + if (nextFrameBoundaryItr == m_frameEndTicks.end() && + m_frameEndTicks.size() != 0) // lower_bound returns end() if not found + { + nextFrameBoundaryItr--; + } + const AZStd::sys_time_t nextFrameBoundary = *nextFrameBoundaryItr; + + // Find the start tick of the leftmost frame, which may be offscreen. + auto startTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); + if (startTickItr != m_frameEndTicks.begin()) + { + startTickItr--; + } + + // Main draw loop + u64 baseRow = 0; + for (const auto& [currentThreadId, singleThreadData] : m_savedData) + { + // Find the first TimeRegion that we should draw + auto regionItr = AZStd::lower_bound( + singleThreadData.begin(), singleThreadData.end(), *startTickItr, + [](const TimeRegion& wrapper, AZStd::sys_time_t target) + { + return wrapper.m_startTick < target; + }); + + // Draw all of the blocks for a given thread/row + u64 maxDepth = 0; + while (regionItr != singleThreadData.end()) + { + const TimeRegion& region = *regionItr; + + // Early out if we have drawn all the onscreen regions + if (region.m_startTick > nextFrameBoundary) + { + break; + } + u64 targetRow = region.m_stackDepth + baseRow; + maxDepth = AZStd::max(aznumeric_cast(region.m_stackDepth), maxDepth); + + DrawBlock(region, targetRow); + + regionItr++; + } + + // Draw UI details + DrawThreadLabel(baseRow, currentThreadId); + DrawThreadSeparator(baseRow, maxDepth); + + baseRow += maxDepth + 1; // Next draw loop should start one row down + } + + DrawRegionStatistics(); + DrawFrameBoundaries(); + + // Draw an invisible button to capture inputs + ImGui::InvisibleButton("Timeline Input", { ImGui::GetWindowContentRegionWidth(), baseRow * RowHeight }); + + // Controls + ImGuiIO& io = ImGui::GetIO(); + if (ImGui::IsWindowFocused() && ImGui::IsItemHovered()) + { + io.WantCaptureMouse = true; + if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling + { + auto [deltaX, deltaY] = io.MouseDelta; + if (deltaX != 0 || deltaY != 0) + { + // We want to maintain uniformity in scrolling (a click and drag should leave the cursor at the same spot + // relative to the objects on screen) + const float pixelDeltaNormalized = deltaX / ImGui::GetWindowWidth(); + auto tickDelta = aznumeric_cast(-1 * pixelDeltaNormalized * GetViewportTickWidth()); + m_viewportStartTick += tickDelta; + m_viewportEndTick += tickDelta; + + ImGui::SetScrollY(ImGui::GetScrollY() + deltaY * -1); + } + } + else if (io.MouseWheel != 0 && io.KeyCtrl) // Zooming + { + // We want zooming to be relative to the mouse's current position + const float mouseVel = io.MouseWheel; + const float mouseX = ImGui::GetMousePos().x; + + // Find the normalized position of the cursor relative to the window + const float percentWindow = (mouseX - ImGui::GetWindowPos().x) / ImGui::GetWindowWidth(); + + const auto overallTickDelta = aznumeric_cast(0.05 * io.MouseWheel * GetViewportTickWidth()); + + // Split the overall delta between the two bounds depending on mouse pos + const auto newStartTick = m_viewportStartTick + aznumeric_cast(percentWindow * overallTickDelta); + const auto newEndTick = m_viewportEndTick - aznumeric_cast((1-percentWindow) * overallTickDelta); + + // Avoid zooming too much, start tick should always be less than end tick + if (newStartTick < newEndTick) + { + m_viewportStartTick = newStartTick; + m_viewportEndTick = newEndTick; + } + } + } + } + ImGui::EndChild(); + } + ImGui::End(); + } + + inline void ImGuiCpuProfiler::CollectFrameData() + { + const RHI::CpuProfiler::TimeRegionMap& timeRegionMap = RHI::CpuProfiler::Get()->GetTimeRegionMap(); + + m_viewportStartTick = INT64_MAX; + m_viewportEndTick = INT64_MIN; + + // Iterate through the entire TimeRegionMap and copy the data since it will get deleted on the next frame + for (const auto& [threadId, singleThreadRegionMap] : timeRegionMap) + { + // The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949) + if (singleThreadRegionMap.size() == 0) + { + continue; + } + + // Now focus on just the data for the current thread + AZStd::vector newData; + newData.reserve(singleThreadRegionMap.size()); // Avoids reallocation in the normal case when each region only has one invocation + for (const auto& [regionName, regionVec] : singleThreadRegionMap) + { + for (const TimeRegion& region : regionVec) + { + newData.push_back(region); // Copies + + // Update running statistics if we want to record this region's data + if (m_regionStatisticsMap[region.m_groupRegionName].m_record) + { + m_regionStatisticsMap[region.m_groupRegionName].RecordRegion(region); + } + } + } + + // Sorting by start tick allows us to speed up some other processes (ex. finding the first block to draw) + // since we can binary search by start tick. + AZStd::sort( + newData.begin(), newData.end(), + [](const TimeRegion& lhs, const TimeRegion& rhs) + { + return lhs.m_startTick < rhs.m_startTick; + }); + + // Use the latest frame's data as the new bounds of the viewport + m_viewportStartTick = AZStd::min(newData.front().m_startTick, m_viewportStartTick); + m_viewportEndTick = AZStd::max(newData.back().m_endTick, m_viewportEndTick); + + m_savedRegionCount += newData.size(); + + // Move onto the end of the current thread's saved data, sorted order maintained + AZStd::vector& savedDataVec = m_savedData[threadId]; + savedDataVec.insert( + savedDataVec.end(), AZStd::make_move_iterator(newData.begin()), AZStd::make_move_iterator(newData.end())); + } + } + + inline void ImGuiCpuProfiler::CullFrameData(const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics) + { + const AZStd::sys_time_t frameToFrameTime = currentCpuTimingStatistics.m_frameToFrameTime; + const AZStd::sys_time_t deleteBeforeTick = AZStd::GetTimeNowTicks() - frameToFrameTime * m_framesToCollect; + + // Remove old frame boundary data + auto firstBoundaryToKeepItr = AZStd::upper_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), deleteBeforeTick); + m_frameEndTicks.erase(m_frameEndTicks.begin(), firstBoundaryToKeepItr); + + // Remove old region data for each thread + for (auto& [threadId, savedRegions] : m_savedData) + { + AZStd::size_t sizeBeforeRemove = savedRegions.size(); + + auto firstRegionToKeep = AZStd::lower_bound( + savedRegions.begin(), savedRegions.end(), deleteBeforeTick, + [](const TimeRegion& region, AZStd::sys_time_t target) + { + return region.m_startTick < target; + }); + savedRegions.erase(savedRegions.begin(), firstRegionToKeep); + + m_savedRegionCount -= sizeBeforeRemove - savedRegions.size(); + } + } + + inline void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, u64 targetRow) + { + float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY(); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const float startPixel = ConvertTickToPixelSpace(block.m_startTick); + const float endPixel = ConvertTickToPixelSpace(block.m_endTick); + + const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight }; + const ImVec2 endPoint = { endPixel, wy + targetRow * RowHeight + 40 }; + + const ImU32 blockColor = GetBlockColor(block); + + drawList->AddRectFilled(startPoint, endPoint, blockColor, 0); + + // Draw the region name if possible + // If the block's current width is too small, we skip drawing the label. + const float regionPixelWidth = endPixel - startPixel; + const float maxCharWidth = ImGui::CalcTextSize("M").x; // M is usually the largest character in most fonts (see CSS em) + if (regionPixelWidth > maxCharWidth) // We can draw at least one character + { + const AZStd::string label = + AZStd::string::format("%s/ %s", block.m_groupRegionName->m_groupName, block.m_groupRegionName->m_regionName); + const float textWidth = ImGui::CalcTextSize(label.c_str()).x; + + if (regionPixelWidth < textWidth) // Not enough space in the block to draw the whole name, draw clipped text. + { + // clipRect appears to only clip when a character is fully outside of its bounds which can lead to overflow + // for now subtract the width of a character + const ImVec4 clipRect = { startPoint.x, startPoint.y, endPoint.x - maxCharWidth, endPoint.y }; + const float fontSize = ImGui::GetFont()->FontSize; + + ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0); + } + else // We have enough space to draw the entire label, draw and center text. + { + const float remainingWidth = regionPixelWidth - textWidth; + const float offset = remainingWidth * .5; + + drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str()); + } + } + + // Tooltip and block highlighting + if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered()) + { + // Open function statistics map on click + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + const GroupRegionName* key = block.m_groupRegionName; + m_regionStatisticsMap[key].m_draw = true; + } + + // Hovering outline + drawList->AddRect(startPoint, endPoint, ImGui::GetColorU32({ 1, 1, 1, 1 }), 0.0, 0, 1.5); + + ImGui::BeginTooltip(); + ImGui::Text("%s::%s", block.m_groupRegionName->m_groupName, block.m_groupRegionName->m_regionName); + ImGui::Text("Execution time: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(block.m_endTick - block.m_startTick)); + ImGui::Text("Ticks %lld => %lld", block.m_startTick, block.m_endTick); + ImGui::EndTooltip(); + } + } + + inline ImU32 ImGuiCpuProfiler::GetBlockColor(const TimeRegion& block) + { + // Use the GroupRegionName pointer a key into the cache, equal regions will have equal pointers + const GroupRegionName* key = block.m_groupRegionName; + if (m_regionColorMap.contains(key)) // Cache hit + { + return ImGui::GetColorU32(m_regionColorMap[key]); + } + + // Cache miss, generate a new random color + AZ::SimpleLcgRandom rand(aznumeric_cast(AZStd::GetTimeNowTicks())); + const float r = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); + const float g = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); + const float b = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); + const ImVec4 randomColor = {r, g, b, .8}; + m_regionColorMap.emplace(key, randomColor); + return ImGui::GetColorU32(randomColor); + } + + inline void ImGuiCpuProfiler::DrawThreadSeparator(u64 baseRow, u64 maxDepth) + { + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + + auto [wx, wy] = ImGui::GetWindowPos(); + wy -= ImGui::GetScrollY(); + const float windowWidth = ImGui::GetWindowWidth(); + const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight - 5; + + ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 2.0f); + } + + inline void ImGuiCpuProfiler::DrawThreadLabel(u64 baseRow, AZStd::thread_id threadId) + { + auto [wx, wy] = ImGui::GetWindowPos(); + wy -= ImGui::GetScrollY(); + const AZStd::string threadIdText = AZStd::string::format("Thread %zu", static_cast(threadId.m_id)); + + ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight + 5 }, IM_COL32_WHITE, threadIdText.c_str()); + } + + inline void ImGuiCpuProfiler::DrawRegionStatistics() + { + for (auto& [groupRegionName, stat] : m_regionStatisticsMap) + { + if (stat.m_draw) + { + ImGui::SetNextWindowSize({300, 340}, ImGuiCond_FirstUseEver); + ImGui::Begin(groupRegionName->m_regionName, &stat.m_draw, 0); + + if (ImGui::Button(stat.m_record ? "Pause" : "Resume")) + { + stat.m_record = !stat.m_record; + } + + ImGui::Text("Invocations: %llu", stat.m_invocations); + ImGui::Text("Average time: %.3f ms", stat.CalcAverageTimeMs()); + + ImGui::Separator(); + + ImGui::ColorPicker4("Region color", &m_regionColorMap[groupRegionName].x); + ImGui::End(); + } + } + } + + inline void ImGuiCpuProfiler::DrawFrameBoundaries() + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const float wy = ImGui::GetWindowPos().y; + const float windowHeight = ImGui::GetWindowHeight(); + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + + // End ticks are sorted in increasing order, find the first frame bound to draw + auto endTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); + + while (endTickItr != m_frameEndTicks.end() && *endTickItr < m_viewportEndTick) + { + const float horizontalPixel = ConvertTickToPixelSpace(*endTickItr); + drawList->AddLine({ horizontalPixel, wy }, { horizontalPixel, wy + windowHeight }, red); + endTickItr++; + } + } + + inline void ImGuiCpuProfiler::DrawRuler() + { + // Use a pair of iterators to go through all saved frame boundaries and draw ruler lines + auto lastFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); + auto nextFrameBoundaryItr = lastFrameBoundaryItr; + if (lastFrameBoundaryItr != m_frameEndTicks.begin()) + { + lastFrameBoundaryItr--; + } + + const auto [wx, wy] = ImGui::GetWindowPos(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + while (nextFrameBoundaryItr != m_frameEndTicks.end()) + { + const AZStd::sys_time_t lastFrameBoundaryTick = *lastFrameBoundaryItr; + const AZStd::sys_time_t nextFrameBoundaryTick = *nextFrameBoundaryItr; + if (lastFrameBoundaryTick > m_viewportEndTick) + { + break; + } + + const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick); + const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick); + + const AZStd::string label = + AZStd::string::format("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick)); + const float labelWidth = ImGui::CalcTextSize(label.c_str()).x; + + // The label can fit between the two boundaries, center it and draw + if (labelWidth <= nextFrameBoundaryPixel - lastFrameBoundaryPixel) + { + const float offset = (nextFrameBoundaryPixel - lastFrameBoundaryPixel - labelWidth) /2; + const float textBeginPixel = lastFrameBoundaryPixel + offset; + const float textEndPixel = textBeginPixel + labelWidth; + + // Execution time label + drawList->AddText({ textBeginPixel, wy + ImGui::GetWindowHeight() / 4 }, IM_COL32_WHITE, label.c_str()); + + // Left side + drawList->AddLine( + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, + { textBeginPixel - 5, wy + ImGui::GetWindowHeight() / 2}, + IM_COL32_WHITE); + + // Right side + drawList->AddLine( + { textEndPixel, wy + ImGui::GetWindowHeight()/2 }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight()/2 }, + IM_COL32_WHITE); + } + else // Cannot fit inside, just draw a line between the two boundaries + { + drawList->AddLine( + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, + IM_COL32_WHITE); + } + + // Left bound + drawList->AddLine( + { lastFrameBoundaryPixel, wy }, + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + IM_COL32_WHITE); + + // Right bound + drawList->AddLine( + { nextFrameBoundaryPixel, wy }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + IM_COL32_WHITE); + + lastFrameBoundaryItr = nextFrameBoundaryItr; + nextFrameBoundaryItr++; + } + } + + inline AZStd::sys_time_t ImGuiCpuProfiler::GetViewportTickWidth() const + { + return m_viewportEndTick - m_viewportStartTick; + } + + inline float ImGuiCpuProfiler::ConvertTickToPixelSpace(AZStd::sys_time_t tick) const + { + const float wx = ImGui::GetWindowPos().x; + const float tickSpaceShifted = aznumeric_cast(tick - m_viewportStartTick); // This will be close to zero, so FP inaccuracy should not be too bad + const float tickSpaceNormalized = tickSpaceShifted / GetViewportTickWidth(); + const float pixelSpace = tickSpaceNormalized * ImGui::GetWindowWidth() + wx; + return pixelSpace; + } + + // System tick bus overrides + inline void ImGuiCpuProfiler::OnSystemTick() + { + m_frameEndTicks.push_back(AZStd::GetTimeNowTicks()); + + if (!m_showVisualizer || m_paused) + { + SystemTickBus::Handler::BusDisconnect(); + } + } + + // ----- RegionStatistics implementation ----- + + inline float RegionStatistics::CalcAverageTimeMs() const + { + if (m_invocations == 0) + { + return 0.0; + } + const double averageTicks = aznumeric_cast(m_totalTicks) / m_invocations; + return CpuProfilerImGuiHelper::TicksToMs(aznumeric_cast(averageTicks)); + } + + inline void RegionStatistics::RecordRegion(const AZ::RHI::CachedTimeRegion& region) + { + m_invocations++; + m_totalTicks += region.m_endTick - region.m_startTick; + } + } // namespace Render +} // namespace AZ