/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "LyShine_precompiled.h" #include "UiCanvasManager.h" #include "UiCanvasFileObject.h" #include "UiCanvasComponent.h" #include "UiGameEntityContext.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _RELEASE #include #endif #if !ENABLE_CRY_PHYSICS #include #include #include #include #endif //////////////////////////////////////////////////////////////////////////////////////////////////// // Anonymous namespace //////////////////////////////////////////////////////////////////////////////////////////////////// namespace { //////////////////////////////////////////////////////////////////////////////////////////////// // Compare function used for sorting bool CompareCanvasDrawOrder(UiCanvasComponent* a, UiCanvasComponent* b) { return a->GetDrawOrder() < b->GetDrawOrder(); } //////////////////////////////////////////////////////////////////////////////////////////////// // Transform the pathname so that a) it works for opening a file that could be in a Gem or in // a pak file, and b) so that it is in a consistent form that can be used for string comparison AZStd::string GetAssePathFromUserDefinedPath(const AZStd::string& userPath) { if (userPath.empty()) { AZ_Warning("UI", false, "Given UI canvas path is empty.") return userPath; } AZStd::string assetPath = userPath; // Check if extension needs to be fixed up const AZStd::string canvasExtension("uicanvas"); bool validExtension = AzFramework::StringFunc::Path::IsExtension(assetPath.c_str(), canvasExtension.c_str(), true); if (!validExtension) { // Fix extension AZ_Warning("UI", !AzFramework::StringFunc::Path::HasExtension(assetPath.c_str()), "Given UI canvas path \"%s\" has an invalid extension. Replacing extension with \"%s\".", userPath.c_str(), canvasExtension.c_str()); AzFramework::StringFunc::Path::ReplaceExtension(assetPath, canvasExtension.c_str()); } // Normalize path EBUS_EVENT(AzFramework::ApplicationRequests::Bus, NormalizePath, assetPath); // Check for any leading slashes as the specified path should be a relative path to the @assets@ alias. // This eliminates inconsistencies between lower level file opens on different platforms int numCharsToErase = 0; for (; numCharsToErase < assetPath.length(); ++numCharsToErase) { if (assetPath[numCharsToErase] != '/') { break; } } if (numCharsToErase > 0) { // Remove leading slashes AZ_Warning("UI", false, "Given UI canvas path \"%s\" has invalid leading slashes that make the path not relative. " "Removing the invalid leading slashes.", userPath.c_str()); assetPath.erase(0, numCharsToErase); } return assetPath; } } //////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasManager::UiCanvasManager() : m_latestViewportSize(UiCanvasComponent::s_defaultCanvasSize) , m_localUserIdInputFilter(AzFramework::LocalUserIdAny) { UiCanvasManagerBus::Handler::BusConnect(); UiCanvasOrderNotificationBus::Handler::BusConnect(); UiCanvasEnabledStateNotificationBus::Handler::BusConnect(); FontNotificationBus::Handler::BusConnect(); AzFramework::AssetCatalogEventBus::Handler::BusConnect(); } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasManager::~UiCanvasManager() { UiCanvasManagerBus::Handler::BusDisconnect(); UiCanvasOrderNotificationBus::Handler::BusDisconnect(); UiCanvasEnabledStateNotificationBus::Handler::BusDisconnect(); FontNotificationBus::Handler::BusDisconnect(); AzFramework::AssetCatalogEventBus::Handler::BusDisconnect(); // destroy ALL the loaded canvases, whether loaded in game or in Editor for (auto canvas : (m_loadedCanvases)) { delete canvas->GetEntity(); } for (auto canvas : (m_loadedCanvasesInEditor)) { delete canvas->GetEntity(); } AZ_Assert(m_recursionGuardCount == 0, "Destroying the UiCanvasManager while it is processing canvases"); DeleteCanvasesQueuedForDeletion(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasManager::CreateCanvas() { // Prevent in-game canvas from being created when we are in the editor in a simulation mode // but not in game mode (ex. AI/Physics mode or Preview mode) if (gEnv && gEnv->IsEditor() && gEnv->IsEditing()) { return AZ::EntityId(); } UiGameEntityContext* entityContext = new UiGameEntityContext(); UiCanvasComponent* canvasComponent = UiCanvasComponent::CreateCanvasInternal(entityContext, false); m_loadedCanvases.push_back(canvasComponent); SortCanvasesByDrawOrder(); // The game entity context needs to know its corresponding canvas entity for instantiating dynamic slices entityContext->SetCanvasEntity(canvasComponent->GetEntityId()); // When we create a canvas in game we want it to have the correct viewport size from the first frame rather // than having to wait a frame to have it updated canvasComponent->SetTargetCanvasSize(true, m_latestViewportSize); return canvasComponent->GetEntityId(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasManager::LoadCanvas(const AZStd::string& assetIdPathname) { // Prevent canvas from being loaded when we are in the editor in a simulation mode // but not in game mode (ex. AI/Physics mode or Preview mode). // NOTE: Normal Preview mode load does not come through here since we clone the canvas rather than load it if (gEnv && gEnv->IsEditor() && gEnv->IsEditing()) { return AZ::EntityId(); } AZ_ASSET_NAMED_SCOPE(assetIdPathname.c_str()); UiGameEntityContext* entityContext = new UiGameEntityContext(); AZ::EntityId canvasEntityId = LoadCanvasInternal(assetIdPathname, false, "", entityContext); if (!canvasEntityId.IsValid()) { delete entityContext; } else { // The game entity context needs to know its corresponding canvas entity for instantiating dynamic slices entityContext->SetCanvasEntity(canvasEntityId); EBUS_EVENT(UiCanvasManagerNotificationBus, OnCanvasLoaded, canvasEntityId); } return canvasEntityId; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::UnloadCanvas(AZ::EntityId canvasEntityId) { ReleaseCanvasDeferred(canvasEntityId); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasManager::FindLoadedCanvasByPathName(const AZStd::string& assetIdPathName, bool loadIfNotFound) { // this is only used for finding canvases loaded in game UiCanvasComponent* canvasComponent = FindCanvasComponentByPathname(assetIdPathName.c_str()); AZ::EntityId canvasId = canvasComponent ? canvasComponent->GetEntityId() : AZ::EntityId(); if (!canvasId.IsValid() && loadIfNotFound) { canvasId = LoadCanvas(assetIdPathName); } return canvasId; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasManager::CanvasEntityList UiCanvasManager::GetLoadedCanvases() { CanvasEntityList list; for (auto canvasComponent : m_loadedCanvases) { list.push_back(canvasComponent->GetEntityId()); } return list; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::SetLocalUserIdInputFilterForAllCanvases(AzFramework::LocalUserId localUserId) { m_localUserIdInputFilter = localUserId; for (auto canvasComponent : m_loadedCanvases) { canvasComponent->SetLocalUserIdInputFilter(m_localUserIdInputFilter); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::OnCanvasDrawOrderChanged(AZ::EntityId canvasEntityId) { SortCanvasesByDrawOrder(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::OnCanvasEnabledStateChanged(AZ::EntityId canvasEntityId, bool enabled) { if (enabled) { bool isConsumingAllInputEvents = false; EBUS_EVENT_ID_RESULT(isConsumingAllInputEvents, canvasEntityId, UiCanvasBus, GetIsConsumingAllInputEvents); if (isConsumingAllInputEvents) { AzFramework::InputChannelRequestBus::Broadcast(&AzFramework::InputChannelRequests::ResetState); EBUS_EVENT(UiCanvasBus, ClearAllInteractables); } } // Update hover state for loaded canvases m_generateMousePositionInputEvent = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::GenerateMousePositionInputEvent() { const AzFramework::InputDevice* mouseDevice = AzFramework::InputDeviceRequests::FindInputDevice(AzFramework::InputDeviceMouse::Id); if (mouseDevice && mouseDevice->IsConnected()) { // Create a game input event for the system cursor position const AzFramework::InputChannel::Snapshot inputSnapshot(AzFramework::InputDeviceMouse::SystemCursorPosition, AzFramework::InputDeviceMouse::Id, AzFramework::InputChannel::State::Updated); // Get the current system cursor viewport position AZ::Vector2 systemCursorPositionNormalized(0.0f, 0.0f); AzFramework::InputSystemCursorRequestBus::EventResult(systemCursorPositionNormalized, AzFramework::InputDeviceMouse::Id, &AzFramework::InputSystemCursorRequests::GetSystemCursorPositionNormalized); AZ::Vector2 cursorViewportPos(systemCursorPositionNormalized.GetX() * m_latestViewportSize.GetX(), systemCursorPositionNormalized.GetY() * m_latestViewportSize.GetY()); // Handle the input event HandleInputEventForLoadedCanvases(inputSnapshot, cursorViewportPos, AzFramework::ModifierKeyMask::None, true); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::OnFontsReloaded() { m_fontTextureHasChanged = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::OnFontTextureUpdated(IFFont* font) { m_fontTextureHasChanged = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::OnCatalogAssetChanged(const AZ::Data::AssetId& assetId) { // get AssetInfo from asset id AZ::Data::AssetInfo assetInfo; EBUS_EVENT_RESULT(assetInfo, AZ::Data::AssetCatalogRequestBus, GetAssetInfoById, assetId); if (assetInfo.m_assetType != LyShine::CanvasAsset::TYPEINFO_Uuid()) { // this is not a UI canvas asset return; } // get pathname from asset id AZStd::string assetPath; EBUS_EVENT_RESULT(assetPath, AZ::Data::AssetCatalogRequestBus, GetAssetPathById, assetId); CanvasList reloadedCanvases; // keep track of the reloaded canvases and add them to m_loadedCanvases after the loop AZStd::vector unloadedCanvases; // also keep track of any canvases that fail to reload and are unloaded // loop over all canvases loaded in game and reload any canvases loaded from this canvas asset // NOTE: this could be improved by using AssetId for the comparison rather than pathnames auto iter = m_loadedCanvases.begin(); while (iter != m_loadedCanvases.end()) { UiCanvasComponent* canvasComponent = *iter; if (assetPath == canvasComponent->GetPathname()) { AZ::EntityId existingCanvasEntityId = canvasComponent->GetEntityId(); //Before unloading the existing canvas, make a copy of its mapping table AZ::SliceComponent::EntityIdToEntityIdMap existingRemapTable = canvasComponent->GetEditorToGameEntityIdMap(); // unload the canvas, just deleting the canvas entity does this AZ::Entity* existingCanvasEntity = canvasComponent->GetEntity(); delete existingCanvasEntity; // Remove the deleted canvas entry in the m_loadedCanvases vector and move the iterator to the next iter = m_loadedCanvases.erase(iter); // reload canvas with the same entity IDs (except for new entities, deleted entities etc) UiGameEntityContext* entityContext = new UiGameEntityContext(); string pathname(assetPath.c_str()); UiCanvasComponent* newCanvasComponent = UiCanvasComponent::LoadCanvasInternal(pathname, false, "", entityContext, &existingRemapTable, existingCanvasEntityId); if (!newCanvasComponent) { delete entityContext; unloadedCanvases.push_back(existingCanvasEntityId); } else { // The game entity context needs to know its corresponding canvas entity for instantiating dynamic slices entityContext->SetCanvasEntity(newCanvasComponent->GetEntityId()); reloadedCanvases.push_back(newCanvasComponent); } } else { ++iter; // we did not delete this canvas - move to next in list } } // add the successfully reloaded canvases at the end m_loadedCanvases.insert(m_loadedCanvases.end(), reloadedCanvases.begin(), reloadedCanvases.end()); // In case any draw orders changed resort SortCanvasesByDrawOrder(); // notify any listeners of any UI canvases that were reloaded for (auto reloadedCanvasComponent : reloadedCanvases) { EBUS_EVENT(UiCanvasManagerNotificationBus, OnCanvasReloaded, reloadedCanvasComponent->GetEntityId()); } // notify any listeners of any UI canvases that were unloaded for (auto unloadedCanvas : unloadedCanvases) { EBUS_EVENT(UiCanvasManagerNotificationBus, OnCanvasUnloaded, unloadedCanvas); } } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasManager::CreateCanvasInEditor(UiEntityContext* entityContext) { UiCanvasComponent* canvasComponent = UiCanvasComponent::CreateCanvasInternal(entityContext, true); m_loadedCanvasesInEditor.push_back(canvasComponent); return canvasComponent->GetEntityId(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasManager::LoadCanvasInEditor(const string& assetIdPathname, const string& sourceAssetPathname, UiEntityContext* entityContext) { return LoadCanvasInternal(assetIdPathname.c_str(), true, sourceAssetPathname.c_str(), entityContext); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasManager::ReloadCanvasFromXml(const AZStd::string& xmlString, UiEntityContext* entityContext) { // Load new canvas from xml AZ::IO::MemoryStream memoryStream(xmlString.c_str(), xmlString.size()); AZ::Entity* rootSliceEntity = nullptr; AZ::Entity* newCanvasEntity = UiCanvasFileObject::LoadCanvasEntitiesFromStream(memoryStream, rootSliceEntity); if (newCanvasEntity && rootSliceEntity) { // Find the old canvas to replace UiCanvasComponent* oldCanvasComponent = nullptr; for (auto canvas : (m_loadedCanvasesInEditor)) { if (canvas->GetEntityId() == newCanvasEntity->GetId()) { oldCanvasComponent = canvas; break; } } AZ_Assert(oldCanvasComponent, "Canvas not found"); if (oldCanvasComponent) { LyShine::CanvasId oldCanvasId = oldCanvasComponent->GetCanvasId(); const AZStd::string oldPathname = oldCanvasComponent->GetPathname(); AZ::Matrix4x4 oldCanvasToViewportMatrix = oldCanvasComponent->GetCanvasToViewportMatrix(); // Delete the old canvas. We assume this is for editor ReleaseCanvas(oldCanvasComponent->GetEntityId(), true); // Complete initialization of new canvas. We assume this is for editor UiCanvasComponent* newCanvasComponent = UiCanvasComponent::FixupReloadedCanvasForEditorInternal( newCanvasEntity, rootSliceEntity, entityContext, oldCanvasId, oldPathname); newCanvasComponent->SetCanvasToViewportMatrix(oldCanvasToViewportMatrix); // Add new canvas to the list of loaded canvases m_loadedCanvasesInEditor.push_back(newCanvasComponent); return newCanvasComponent->GetEntityId(); } else { delete newCanvasEntity; } } return AZ::EntityId(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::ReleaseCanvas(AZ::EntityId canvasEntityId, bool forEditor) { if(!canvasEntityId.IsValid()) { AZ_Warning("UI", false, "%s has been invoked with an Invalid Canvas Entity ID. No Canvas can be released", AZ_FUNCTION_SIGNATURE); return; } // if we are currently processing canvases for input handling or update then defer the deletion of the canvas if (!forEditor && m_recursionGuardCount > 0) { ReleaseCanvasDeferred(canvasEntityId); return; } AZ::Entity* canvasEntity = nullptr; EBUS_EVENT_RESULT(canvasEntity, AZ::ComponentApplicationBus, FindEntity, canvasEntityId); AZ_Assert(canvasEntity, "Canvas entity not found by ID"); if (canvasEntity) { UiCanvasComponent* canvasComponent = canvasEntity->FindComponent(); AZ_Assert(canvasComponent, "Canvas entity has no canvas component"); if (canvasComponent) { if (forEditor) { stl::find_and_erase(m_loadedCanvasesInEditor, canvasComponent); delete canvasEntity; } else { stl::find_and_erase(m_loadedCanvases, canvasComponent); delete canvasEntity; EBUS_EVENT(UiCanvasManagerNotificationBus, OnCanvasUnloaded, canvasEntityId); // Update hover state for loaded canvases m_generateMousePositionInputEvent = true; } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::ReleaseCanvasDeferred(AZ::EntityId canvasEntityId) { if (!canvasEntityId.IsValid()) { AZ_Warning("UI", false, "%s has been invoked with an Invalid Canvas Entity ID. No Canvas can be released", AZ_FUNCTION_SIGNATURE); return; } AZ::Entity* canvasEntity = nullptr; EBUS_EVENT_RESULT(canvasEntity, AZ::ComponentApplicationBus, FindEntity, canvasEntityId); AZ_Assert(canvasEntity, "Canvas entity not found by ID"); if (canvasEntity) { UiCanvasComponent* canvasComponent = canvasEntity->FindComponent(); AZ_Assert(canvasComponent, "Canvas entity has no canvas component"); if (canvasComponent) { // Remove canvas component from list of loaded canvases stl::find_and_erase(m_loadedCanvases, canvasComponent); // Deactivate elements of the canvas canvasComponent->DeactivateElements(); // Deactivate the canvas element canvasEntity->Deactivate(); EBUS_EVENT(UiCanvasManagerNotificationBus, OnCanvasUnloaded, canvasEntityId); // Queue UI canvas deletion. This is because this function could have been triggered in input processing of // a component within the canvas. i.e. there could be a member function of the canvas or one of it's child entities // on the callstack. Unfortunately, just delaying until the next tick is not enough - pressing a button could cause // unloading of an entire level which could flush the tick bus. So we have to use our own queue. QueueCanvasForDeletion(canvasEntityId); // Update hover state for loaded canvases m_generateMousePositionInputEvent = true; } } } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasManager::FindCanvasById(LyShine::CanvasId id) { // this is only used for finding canvases loaded in game for (auto canvas : m_loadedCanvases) { if (canvas->GetCanvasId() == id) { return canvas->GetEntityId(); } } return AZ::EntityId(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::SetTargetSizeForLoadedCanvases(AZ::Vector2 viewportSize) { for (auto canvas : m_loadedCanvases) { canvas->SetTargetCanvasSize(true, viewportSize); } m_latestViewportSize = viewportSize; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::UpdateLoadedCanvases(float deltaTime) { // make a temporary copy of the list in case the update code ends up releasing or loading canvases during iterating over the list auto loadedCanvases = m_loadedCanvases; // Update all the canvases loaded in game. // It is unlikely this will call out to client code that could remove a canvas but we have no // control over what custom components do so we increment the count that will defer all canvas deletion m_recursionGuardCount++; if (m_generateMousePositionInputEvent) { // Update hover state for loaded canvases m_generateMousePositionInputEvent = false; GenerateMousePositionInputEvent(); } for (auto canvas : loadedCanvases) { canvas->UpdateCanvas(deltaTime, true); } m_recursionGuardCount--; // If not being called recursively from other canvas processing then immediately do any deferred canvas deletes DeleteCanvasesQueuedForDeletion(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::RenderLoadedCanvases() { // Render all the canvases loaded in game // canvases loaded in editor are rendered by the viewport window // If any font texture has changed we force all canvases to rebuild the render graph. Individual text components // that use this font will also have got the notification and will have set a flag in their render cache // to indicate that the font texture has changed. This allows them to regenerate the quads with no reallocation. if (m_fontTextureHasChanged) { for (auto canvas : m_loadedCanvases) { canvas->MarkRenderGraphDirty(); } m_fontTextureHasChanged = false; } // clear the stencil buffer before rendering the loaded canvases - required for masking // NOTE: We want to use ClearTargetsImmediately instead of ClearTargetsLater since we will not be setting the render target ColorF viewportBackgroundColor(0, 0, 0, 0); // if clearing color we want to set alpha to zero also gEnv->pRenderer->ClearTargetsImmediately(FRT_CLEAR_STENCIL, viewportBackgroundColor); for (auto canvas : m_loadedCanvases) { if (!canvas->GetIsRenderToTexture()) { // Rendering in game full screen so the viewport size and target canvas size are the same AZ::Vector2 viewportSize = canvas->GetTargetCanvasSize(); canvas->RenderCanvas(true, viewportSize); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::DestroyLoadedCanvases(bool keepCrossLevelCanvases) { // Delete all the canvases loaded in game (but not loaded in editor) for (auto iter = m_loadedCanvases.begin(); iter != m_loadedCanvases.end(); ++iter) { auto canvas = *iter; bool removed = false; if (!(keepCrossLevelCanvases && canvas->GetKeepLoadedOnLevelUnload())) { // no longer used by game so delete the canvas delete canvas->GetEntity(); *iter = nullptr; // mark for removal from container } } // now remove the nullptr entries m_loadedCanvases.erase( std::remove(m_loadedCanvases.begin(), m_loadedCanvases.end(), nullptr), m_loadedCanvases.end()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::OnLoadScreenUnloaded() { // Mark all render graphs dirty in case the loaded canvases were already rendered before their textures were // done loading. This happens when a load screen is being rendered during a level load. When other canvases // associated with the level are loaded, they also get rendered by the UiLoadScreenComponent, but their texture // loading is delayed until further down the level load process. Once a canvas is rendered, its render graph's // dirty flag is cleared, so the render graph needs to be marked dirty again after the textures are loaded for (auto canvas : m_loadedCanvases) { canvas->MarkRenderGraphDirty(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasManager::HandleInputEventForLoadedCanvases(const AzFramework::InputChannel& inputChannel) { // Take a snapshot of the input channel instead of just passing through the channel itself. // This is necessary because UI input is currently simulated in the editor's UI Preview mode // by constructing 'fake' input events, which we can do with snapshots but not input channels. // Long term we should look to update the AzFramework input system while in UI Editor Preview // mode so that it works exactly the same as in-game input, but this is a larger task for later. const AzFramework::InputChannel::Snapshot inputSnapshot(inputChannel); // De-normalize the position (if any) of the input event, as the UI system expects it relative // to the viewport from here on. AZ::Vector2 viewportPos(0.0f, 0.0f); const AzFramework::InputChannel::PositionData2D* positionData2D = inputChannel.GetCustomData(); if (positionData2D) { viewportPos.SetX(positionData2D->m_normalizedPosition.GetX() * m_latestViewportSize.GetX()); viewportPos.SetY(positionData2D->m_normalizedPosition.GetY() * m_latestViewportSize.GetY()); } // Get the active modifier keys (if any) of the input event. Will only exist for keyboard keys. const AzFramework::ModifierKeyStates* modifierKeyStates = inputChannel.GetCustomData(); const AzFramework::ModifierKeyMask activeModifierKeys = modifierKeyStates ? modifierKeyStates->GetActiveModifierKeys() : AzFramework::ModifierKeyMask::None; bool handled = HandleInputEventForLoadedCanvases(inputSnapshot, viewportPos, activeModifierKeys, positionData2D ? true : false); return handled; } bool UiCanvasManager::HandleInputEventForLoadedCanvases(const AzFramework::InputChannel::Snapshot& inputSnapshot, const AZ::Vector2& viewportPos, AzFramework::ModifierKeyMask activeModifierKeys, bool isPositional) { bool handled = false; if (isPositional) { m_generateMousePositionInputEvent = false; } // make a temporary copy of the list in case the input handling ends up releasing or loading canvases during iterating over the list auto loadedCanvases = m_loadedCanvases; // reverse iterate over the loaded canvases so that the front most canvas gets first chance to // handle the event bool areAnyInWorldInputCanvasesLoaded = false; // HandleInputEvent is likely to call user code and scripts that could potentially cause a canvas to be released. // Setting this flag will cause any canvas deletions to be deferred. Due to the weird behavior when switching levels this function // (HandleInputEventForLoadedCanvases) can actually be called recursively because it can flush the input events. m_recursionGuardCount++; for (auto iter = loadedCanvases.rbegin(); iter != loadedCanvases.rend(); ++iter) { UiCanvasComponent* canvas = *iter; if (canvas->GetIsRenderToTexture() && canvas->GetIsPositionalInputSupported()) { // keep track of whether any canvases are rendering to texture. Positional events for these // are ignored in HandleInputEvent and handled later in this function by HandleInputEventForInWorldCanvases areAnyInWorldInputCanvasesLoaded = true; } if (canvas->HandleInputEvent(inputSnapshot, &viewportPos, activeModifierKeys)) { handled = true; break; } } m_recursionGuardCount--; // If not being called recursively from other canvas processing then immediately do any deferred canvas deletes DeleteCanvasesQueuedForDeletion(); // if there are any canvases loaded that are rendering to texture we handle them seperately after the screen canvases // only do this for input events that are actually associated with a position if (!handled && areAnyInWorldInputCanvasesLoaded && isPositional) { if (HandleInputEventForInWorldCanvases(inputSnapshot, viewportPos)) { handled = true; } } return handled; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasManager::HandleTextEventForLoadedCanvases(const AZStd::string& textUTF8) { // reverse iterate over the loaded canvases so that the front most canvas gets first chance to // handle the event for (auto iter = m_loadedCanvases.rbegin(); iter != m_loadedCanvases.rend(); ++iter) { if ((*iter)->HandleTextEvent(textUTF8)) { return true; } } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::SortCanvasesByDrawOrder() { std::stable_sort(m_loadedCanvases.begin(), m_loadedCanvases.end(), CompareCanvasDrawOrder); } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent* UiCanvasManager::FindCanvasComponentByPathname(const AZStd::string& name) { UiCanvasComponent* match = nullptr; AZStd::string adjustedSearchName = GetAssePathFromUserDefinedPath(name); for (auto canvas : (m_loadedCanvases)) { if (adjustedSearchName == canvas->GetPathname()) { return canvas; } } return nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent* UiCanvasManager::FindEditorCanvasComponentByPathname(const AZStd::string& name) { UiCanvasComponent* match = nullptr; for (auto canvas : (m_loadedCanvasesInEditor)) { if (name == canvas->GetPathname()) { return canvas; } } return nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasManager::HandleInputEventForInWorldCanvases(const AzFramework::InputChannel::Snapshot& inputSnapshot, const AZ::Vector2& viewportPos) { // First we need to construct a ray from the either the center of the screen or the mouse position. // This requires knowledge of the camera // for initial testing we will just use a ray in the center of the viewport const CCamera& cam = GetISystem()->GetIRenderer()->GetCamera(); // construct a ray from the camera position in the view direction of the camera const float rayLength = 5000.0f; Vec3 rayOrigin(cam.GetPosition()); Vec3 rayDirection = cam.GetViewdir() * rayLength; // If the mouse cursor is visible we will assume that the ray should be in the direction of the // mouse pointer. This is a temporary solution. A better solution is to be able to configure the // LyShine system to say how ray input should be handled. bool isCursorVisible = false; UiCursorBus::BroadcastResult(isCursorVisible, &UiCursorInterface::IsUiCursorVisible); if (isCursorVisible) { // for some reason Unproject seems to work when given the viewport pos with (0,0) at the // bottom left as opposed to the top left - even though that function specifically sets top left // to (0,0). const float viewportYInverted = cam.GetViewSurfaceZ() - viewportPos.GetY(); // Unproject to get the screen position in world space, use arbitrary Z that is within the depth range Vec3 flippedViewportRayOrigin(viewportPos.GetX(), viewportYInverted, 0.f); Vec3 flippedViewportRayForward(viewportPos.GetX(), viewportYInverted, 1.f); cam.Unproject(flippedViewportRayOrigin, rayOrigin); Vec3 unprojectedPosForward; cam.Unproject(flippedViewportRayForward, unprojectedPosForward); // We want a vector relative to the camera origin Vec3 rayVec = unprojectedPosForward - rayOrigin; // we want to ensure that the ray is a certain length so normalize it and scale it rayVec.NormalizeSafe(); rayDirection = rayVec * rayLength; } #if ENABLE_CRY_PHYSICS // do a ray world intersection test ray_hit rayhit; // NOTE: these flags may need some tuning. After a fix from physics setup rwi_colltype_any may work. unsigned int flags = rwi_stop_at_pierceable | rwi_ignore_noncolliding; if (gEnv->pPhysicalWorld->RayWorldIntersection(rayOrigin, rayDirection, ent_all, flags, &rayhit, 1)) { // if the ray collided with a component entity then call a bus on that entity to process the event const AZ::EntityId entityId = AZ::EntityId(rayhit.pCollider->GetForeignData(PHYS_FOREIGN_ID_COMPONENT_ENTITY)); if (entityId.IsValid()) { // first get the UI canvas entity from the hit entity - we do this to see if it supports // automatic input. AZ::EntityId canvasEntityId; EBUS_EVENT_ID_RESULT(canvasEntityId, entityId, UiCanvasRefBus, GetCanvas); if (canvasEntityId.IsValid()) { // Checkif the UI canvas referenced by the hit entity supports automatic input bool doesCanvasSupportInput = false; EBUS_EVENT_ID_RESULT(doesCanvasSupportInput, canvasEntityId, UiCanvasBus, GetIsPositionalInputSupported); if (doesCanvasSupportInput) { // set the hit details to the hit entity, it will convert into canvas coords and send to canvas bool result = false; EBUS_EVENT_ID_RESULT(result, entityId, UiCanvasOnMeshBus, ProcessRayHitInputEvent, inputSnapshot, rayhit); if (result) { return true; } } } } } #else AzFramework::EntityContextId gameContextId; AzFramework::GameEntityContextRequestBus::BroadcastResult(gameContextId, &AzFramework::GameEntityContextRequests::GetGameEntityContextId); AzFramework::RenderGeometry::RayRequest request; request.m_startWorldPosition = LYVec3ToAZVec3(rayOrigin); request.m_endWorldPosition = LYVec3ToAZVec3(rayOrigin + rayDirection); AzFramework::RenderGeometry::RayResult rayResult; AzFramework::RenderGeometry::IntersectorBus::EventResult(rayResult, gameContextId, &AzFramework::RenderGeometry::IntersectorInterface::RayIntersect, request); if (rayResult) { AZ::EntityId hitEntity = rayResult.m_entityAndComponent.GetEntityId(); if (hitEntity.IsValid()) { AZ::EntityId canvasEntityId; UiCanvasRefBus::EventResult(canvasEntityId, hitEntity, &UiCanvasRefInterface::GetCanvas); if (canvasEntityId.IsValid()) { // Checkif the UI canvas referenced by the hit entity supports automatic input bool doesCanvasSupportInput = false; UiCanvasBus::EventResult(doesCanvasSupportInput, canvasEntityId, &UiCanvasInterface::GetIsPositionalInputSupported); if (doesCanvasSupportInput) { // set the hit details to the hit entity, it will convert into canvas coords and send to canvas bool handled = false; UiCanvasOnMeshBus::EventResult(handled, hitEntity, &UiCanvasOnMeshInterface::ProcessHitInputEvent, inputSnapshot, rayResult); if (handled) { return true; } } } } } #endif // ENABLE_CRY_PHYSICS return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasManager::LoadCanvasInternal(const AZStd::string& assetIdPathname, bool forEditor, const AZStd::string& fullSourceAssetPathname, UiEntityContext* entityContext, const AZ::SliceComponent::EntityIdToEntityIdMap* previousRemapTable, AZ::EntityId previousCanvasId) { // Fix up user defined asset path for runtime. For editor, the asset path should already be valid AZStd::string assetIdPath = forEditor ? assetIdPathname : GetAssePathFromUserDefinedPath(assetIdPathname); // If loading from the editor we load the source asset path. // If loading in game this could be a path that a developer typed into a script. // However, it has to be a valid asset ID path. E.g. it can be resolved from the asset root // since at runtime we cannot convert from an arbitrary dev asset path to an asset ID const AZStd::string& pathToOpen = forEditor ? fullSourceAssetPathname : assetIdPath; // if the canvas is already loaded in the editor and we are running in game then we clone the // editor version so that the user can test their canvas without saving it UiCanvasComponent* canvasComponent = FindEditorCanvasComponentByPathname(assetIdPath); // This scope opened here intentionally to control the lifetime of the AZ_ASSET_NAMED_SCOPE { AZ_ASSET_NAMED_SCOPE(pathToOpen.c_str()); if (canvasComponent) { // this canvas is already loaded in the editor if (forEditor) { // should never load a canvas in Editor if it is already loaded. The Editor should avoid loading the // same canvas twice in Editor. If the game is running it is not possible to load a canvas // from the editor. gEnv->pSystem->Warning(VALIDATOR_MODULE_SHINE, VALIDATOR_WARNING, VALIDATOR_FLAG_FILE, pathToOpen.c_str(), "UI canvas file: %s is already loaded", pathToOpen.c_str()); return AZ::EntityId(); } else { // we are loading from the game, the canvas is already open in the editor, so // we clone the canvas that is open in the editor. canvasComponent = canvasComponent->CloneAndInitializeCanvas(entityContext, assetIdPath); } } else { // not already loaded in editor, attempt to load... canvasComponent = UiCanvasComponent::LoadCanvasInternal(pathToOpen.c_str(), forEditor, assetIdPath.c_str(), entityContext, previousRemapTable, previousCanvasId); } if (canvasComponent) { // canvas loaded OK (or cloned from Editor canvas OK) // add to the list of loaded canvases if (forEditor) { m_loadedCanvasesInEditor.push_back(canvasComponent); } else { if (canvasComponent->GetEnabled() && canvasComponent->GetIsConsumingAllInputEvents()) { AzFramework::InputChannelRequestBus::Broadcast(&AzFramework::InputChannelRequests::ResetState); EBUS_EVENT(UiCanvasBus, ClearAllInteractables); } m_loadedCanvases.push_back(canvasComponent); SortCanvasesByDrawOrder(); // Update hover state for loaded canvases m_generateMousePositionInputEvent = true; } canvasComponent->SetLocalUserIdInputFilter(m_localUserIdInputFilter); } } return (canvasComponent) ? canvasComponent->GetEntityId() : AZ::EntityId(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::QueueCanvasForDeletion(AZ::EntityId canvasEntityId) { m_canvasesQueuedForDeletion.push_back(canvasEntityId); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::DeleteCanvasesQueuedForDeletion() { // In weird cases like level unload HandleInputEventForLoadedCanvases can get called recursively // so do not delete any canvases until there is no recursion if (m_recursionGuardCount == 0) { for (auto canvasEntityId : m_canvasesQueuedForDeletion) { AZ::Entity* canvasEntity = nullptr; EBUS_EVENT_RESULT(canvasEntity, AZ::ComponentApplicationBus, FindEntity, canvasEntityId); delete canvasEntity; } m_canvasesQueuedForDeletion.clear(); } } #ifndef _RELEASE //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::DebugDisplayCanvasData(int setting) const { bool onlyShowEnabledCanvases = (setting == 2) ? true : false; IDraw2d* draw2d = Draw2dHelper::GetDraw2d(); draw2d->BeginDraw2d(false); float xOffset = 20.0f; float yOffset = 20.0f; float xSpacing = 20.0f; const int elementNameFieldLength = 20; float opacity = 1.0f; int blackTexture = gEnv->pRenderer->GetBlackTextureId(); float textOpacity = 1.0f; float backgroundRectOpacity = 0.75f; const AZ::Vector3 white(1.0f, 1.0f, 1.0f); const AZ::Vector3 grey(0.5f, 0.5f, 0.5f); const AZ::Vector3 red(1.0f, 0.3f, 0.3f); const AZ::Vector3 blue(0.3f, 0.3f, 1.0f); // If the viewport is narrow then a font size of 16 might be too large, so we use a size between 12 and 16 depending // on the viewport width. float fontSize(draw2d->GetViewportWidth() / 75.f); fontSize = AZ::GetClamp(fontSize, 12.f, 16.f); const float lineSpacing = fontSize; // local function to write a line of text (with a background rect) and increment Y offset AZStd::function WriteLine = [&](const char* buffer, const AZ::Vector3& color) { IDraw2d::TextOptions textOptions = draw2d->GetDefaultTextOptions(); textOptions.color = color; AZ::Vector2 textSize = draw2d->GetTextSize(buffer, fontSize, &textOptions); AZ::Vector2 rectTopLeft = AZ::Vector2(xOffset - 2, yOffset); AZ::Vector2 rectSize = AZ::Vector2(textSize.GetX() + 4, lineSpacing); draw2d->DrawImage(blackTexture, rectTopLeft, rectSize, backgroundRectOpacity); draw2d->DrawText(buffer, AZ::Vector2(xOffset, yOffset), fontSize, textOpacity, &textOptions); yOffset += lineSpacing; }; char buffer[200]; sprintf_s(buffer, "There are %d loaded UI canvases", static_cast(m_loadedCanvases.size())); WriteLine(buffer, white); sprintf_s(buffer, "NN: %20s %2s %2s %2s %11s %5s %5s %5s %5s %5s %5s %5s %5s %5s %5s %20s %20s", "Name", "En", "Po", "Na", "DrawOrder", "nElem", "nEnab", "nRend", "nRCtl", "nImg", "nText", "nMask", "nFadr", "nIntr", "nUpdt", "ActiveInt", "HoverInt"); WriteLine(buffer, white); int totalEnabled = 0; int totalPositionalInputs = 0; int totalNavigable = 0; int totalElements = 0; int totalEnabledElements = 0; int totalEnabledRenderables = 0; int totalEnabledRCtls = 0; int totalEnabledImages = 0; int totalEnabledTexts = 0; int totalEnabledMasks = 0; int totalEnabledFaders = 0; int totalEnabledIntrs = 0; int totalEnabledUpdates = 0; int i = 0; for (auto canvas : m_loadedCanvases) { // Name const string pathname = canvas->GetPathname().c_str(); size_t lastDot = pathname.find_last_of("."); size_t lastSlash = pathname.find_last_of("/"); string leafName = pathname; if (lastDot > lastSlash) { leafName = pathname.substr(lastSlash+1, lastDot-lastSlash-1); } leafName = leafName.substr(0, 20); // Enabled bool isCanvasEnabled = canvas->GetEnabled(); if (onlyShowEnabledCanvases && !isCanvasEnabled) { continue; } const char* enabledString = isCanvasEnabled ? "Y" : "N"; totalEnabled += isCanvasEnabled ? 1 : 0; bool posEnabled = canvas->GetIsPositionalInputSupported(); const char* posEnabledString = posEnabled ? "Y" : "N"; totalPositionalInputs += posEnabled ? 1 : 0; bool navEnabled = canvas->GetIsNavigationSupported(); const char* navEnabledString = navEnabled ? "Y" : "N"; totalNavigable += navEnabled ? 1 : 0; // Draw order int drawOrder = canvas->GetDrawOrder(); // Active and hover AZ::EntityId activeInteractableId; AZ::EntityId hoverInteractableId; canvas->GetDebugInfoInteractables(activeInteractableId, hoverInteractableId); AZStd::string activeInteractableName = "None"; AZStd::string hoverInteractableName = "None"; if (activeInteractableId.IsValid()) { activeInteractableName = DebugGetElementName(activeInteractableId, elementNameFieldLength); } if (hoverInteractableId.IsValid()) { hoverInteractableName = DebugGetElementName(hoverInteractableId, elementNameFieldLength); } // Num elements UiCanvasComponent::DebugInfoNumElements info; canvas->GetDebugInfoNumElements(info); sprintf_s(buffer, "%2d: %20s %2s %2s %2s %11d %5d %5d %5d %5d %5d %5d %5d %5d %5d %5d %20s %20s", i, leafName.c_str(), enabledString, posEnabledString, navEnabledString, drawOrder, info.m_numElements, info.m_numEnabledElements, info.m_numRenderElements, info.m_numRenderControlElements, info.m_numImageElements, info.m_numTextElements, info.m_numMaskElements, info.m_numFaderElements, info.m_numInteractableElements,info.m_numUpdateElements, activeInteractableName.c_str(), hoverInteractableName.c_str()); const AZ::Vector3& color = isCanvasEnabled ? white : grey; WriteLine(buffer, color); ++i; totalElements += info.m_numElements; totalEnabledElements += info.m_numEnabledElements; totalEnabledRenderables += info.m_numRenderElements; totalEnabledRCtls += info.m_numRenderControlElements; totalEnabledImages += info.m_numImageElements; totalEnabledTexts += info.m_numTextElements; totalEnabledMasks += info.m_numMaskElements; totalEnabledFaders += info.m_numFaderElements; totalEnabledIntrs += info.m_numInteractableElements; totalEnabledUpdates += info.m_numUpdateElements; } sprintf_s(buffer, "Totals: %16s %2d %2d %2d %11s %5d %5d %5d %5d %5d %5d %5d %5d %5d %5d", "", totalEnabled, totalPositionalInputs, totalNavigable, "", totalElements, totalEnabledElements, totalEnabledRenderables, totalEnabledRCtls, totalEnabledImages, totalEnabledTexts, totalEnabledMasks, totalEnabledFaders, totalEnabledIntrs, totalEnabledUpdates); WriteLine(buffer, red); draw2d->EndDraw2d(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::DebugDisplayDrawCallData() const { IDraw2d* draw2d = Draw2dHelper::GetDraw2d(); draw2d->BeginDraw2d(false); float xOffset = 20.0f; float yOffset = 20.0f; float xSpacing = 20.0f; float opacity = 1.0f; int blackTexture = gEnv->pRenderer->GetBlackTextureId(); float textOpacity = 1.0f; float backgroundRectOpacity = 0.75f; const float lineSpacing = 20.0f; const AZ::Vector3 white(1,1,1); const AZ::Vector3 red(1,0.3f,0.3f); const AZ::Vector3 blue(0.3f,0.3f,1); const AZ::Vector3 green(0.3f,1,0.3f); const AZ::Vector3 yellow(0.7f,0.7f,0.2f); // local function to write a line of text (with a background rect) and increment Y offset AZStd::function WriteLine = [&](const char* buffer, const AZ::Vector3& color) { IDraw2d::TextOptions textOptions = draw2d->GetDefaultTextOptions(); textOptions.color = color; AZ::Vector2 textSize = draw2d->GetTextSize(buffer, 16, &textOptions); AZ::Vector2 rectTopLeft = AZ::Vector2(xOffset - 2, yOffset); AZ::Vector2 rectSize = AZ::Vector2(textSize.GetX() + 4, lineSpacing); draw2d->DrawImage(blackTexture, rectTopLeft, rectSize, backgroundRectOpacity); draw2d->DrawText(buffer, AZ::Vector2(xOffset, yOffset), 16, textOpacity, &textOptions); yOffset += lineSpacing; }; char buffer[200]; sprintf_s(buffer, "NN: %20s %5s %5s %5s %5s %5s %5s %5s %5s %5s %5s %5s %5s", "Canvas name", "nDraw", "nPrim", "nTris", "nMask", "nRTs", "nUTex", "XMask", "XRT", "XBlnd", "XSrgb", "XMaxV", "XTex"); WriteLine(buffer, blue); int totalRenderNodes = 0; int totalPrimitives = 0; int totalTriangles = 0; int totalMasks = 0; int totalRTs = 0; int totalDueToMask = 0; int totalDueToRT = 0; int totalDueToBlendMode = 0; int totalDueToSrgb = 0; int totalDueToMaxVerts = 0; int totalDueToTextures = 0; int i = 0; for (auto canvas : m_loadedCanvases) { // Name const string pathname = canvas->GetPathname().c_str(); size_t lastDot = pathname.find_last_of("."); size_t lastSlash = pathname.find_last_of("/"); string leafName = pathname; if (lastDot > lastSlash) { leafName = pathname.substr(lastSlash+1, lastDot-lastSlash-1); } // Enabled bool isCanvasEnabled = canvas->GetEnabled(); if (!isCanvasEnabled) { continue; } // Num elements LyShineDebug::DebugInfoRenderGraph info; canvas->GetDebugInfoRenderGraph(info); sprintf_s(buffer, "%2d: %20s %5d %5d %5d %5d %5d %5d %5d %5d %5d %5d %5d %5d", i, leafName.c_str(), info.m_numRenderNodes, info.m_numPrimitives, info.m_numTriangles, info.m_numMasks, info.m_numRTs, info.m_numUniqueTextures, info.m_numNodesDueToMask, info.m_numNodesDueToRT, info.m_numNodesDueToBlendMode, info.m_numNodesDueToSrgb, info.m_numNodesDueToMaxVerts, info.m_numNodesDueToTextures); AZ::u64 timeSinceBuiltMs = AZStd::GetTimeUTCMilliSecond() - info.m_timeGraphLastBuiltMs; if (timeSinceBuiltMs > 1000) { timeSinceBuiltMs = 1000; } float percentageOfSecSinceLastBuilt = static_cast(timeSinceBuiltMs) / 1000.0f; AZ::Vector3 color; if (info.m_wasBuiltThisFrame) { color = white; // white used if the render graph was rebuilt this frame } else { if (info.m_isReusingRenderTargets) { color = yellow; // yellow used if the render graph was not rebuilt and render targets were reused } else { color = green; // green used if render graph not regenerated this frame and no render targets reused } // When the render graph switches to not being built each frame we take 1 second to interpolate from white to // the desired color, otherwise it is not possible to see when the rendergraph gets rebuilt at high frame rates color = white + (color - white) * percentageOfSecSinceLastBuilt; } WriteLine(buffer, color); ++i; totalRenderNodes += info.m_numRenderNodes; totalPrimitives += info.m_numPrimitives; totalTriangles += info.m_numTriangles; totalMasks += info.m_numMasks; totalRTs += info.m_numRTs; totalDueToMask += info.m_numNodesDueToMask; totalDueToRT += info.m_numNodesDueToRT; totalDueToBlendMode += info.m_numNodesDueToBlendMode; totalDueToSrgb += info.m_numNodesDueToSrgb; totalDueToMaxVerts += info.m_numNodesDueToMaxVerts; totalDueToTextures += info.m_numNodesDueToTextures; } sprintf_s(buffer, "Totals: %5d %5d %5d %5d %5d %5d %5d %5d %5d %5d %5d", totalRenderNodes, totalPrimitives, totalTriangles, totalMasks, totalRTs, totalDueToMask, totalDueToRT, totalDueToBlendMode, totalDueToSrgb, totalDueToMaxVerts, totalDueToTextures); WriteLine(buffer, red); draw2d->EndDraw2d(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZStd::string UiCanvasManager::DebugGetElementName(AZ::EntityId entityId, int maxLength) const { AZStd::string name = "None"; AZ::Entity* entity = nullptr; EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, entityId); if (entity) { name = entity->GetName(); if (name.length() < maxLength) { AZ::Entity* parent = nullptr; EBUS_EVENT_ID_RESULT(parent, entityId, UiElementBus, GetParent); if (parent) { name = parent->GetName() + "/" + name; if (name.length() > maxLength) { AZStd::string maxString = name.substr(name.length() - maxLength); name = maxString; } } } } return name; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::DebugReportDrawCalls(const AZStd::string& name) const { AZStd::string logFolder = AZStd::string::format("@log@/LyShine"); AZ::IO::LocalFileIO::GetInstance()->CreatePath(logFolder.c_str()); AZStd::string logFileLeafName = "DrawCallReport"; if (!name.empty()) { logFileLeafName += "_"; logFileLeafName += name; } AZStd::string logFile = AZStd::string::format("%s/%s.txt", logFolder.c_str(), logFileLeafName.c_str()); AZ::IO::HandleType logHandle; AZ::IO::Result result = AZ::IO::LocalFileIO::GetInstance()->Open(logFile.c_str(), AZ::IO::OpenMode::ModeWrite, logHandle); if (!result) { AZ_TracePrintf("UI", "Failed to open file for Draw Call Report at %s\n", logFile.c_str()); return; } AZStd::string logLine = AZStd::string::format("Draw call report for \'%s\'\r\n", name.c_str()); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = AZStd::string::format("Output by the ui_ReportDrawCalls console command\r\n\r\n"); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); int numEnabledCanvases = 0; for (auto canvas : m_loadedCanvases) { if (canvas->GetEnabled()) { numEnabledCanvases++; } } logLine = AZStd::string::format("There are %zu loaded UI canvases, %d of which are enabled.\r\nThe below report only includes the enabled canvases\r\n", m_loadedCanvases.size(), numEnabledCanvases); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); LyShineDebug::DebugInfoDrawCallReport reportInfo; for (auto canvas : m_loadedCanvases) { // Check enabled bool isCanvasEnabled = canvas->GetEnabled(); if (!isCanvasEnabled) { continue; } // Name of canvas const string pathname = canvas->GetPathname().c_str(); logLine = "\r\n=====================================================================================\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = AZStd::string::format("Canvas: %s\r\n", pathname.c_str()); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = "=====================================================================================\r\n\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); // Get canvas summary data LyShineDebug::DebugInfoRenderGraph renderGraphInfo; canvas->GetDebugInfoRenderGraph(renderGraphInfo); // Output a summary report if (renderGraphInfo.m_numRenderNodes > 0) { logLine = AZStd::string::format("Canvas has %d draw calls and %d primitives with a total of %d triangles\r\n", renderGraphInfo.m_numRenderNodes, renderGraphInfo.m_numPrimitives, renderGraphInfo.m_numTriangles); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = AZStd::string::format("There are %d unique textures used, %d mask render nodes and %d render target render nodes\r\n", renderGraphInfo.m_numUniqueTextures, renderGraphInfo.m_numMasks, renderGraphInfo.m_numRTs); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = AZStd::string::format( "Extra draw calls caused by... Masks: %d, RenderTargets: %d, BlendModes: %d, Srgb: %d, MaxVerts: %d, MaxTextures: %d\r\n\r\n", renderGraphInfo.m_numNodesDueToMask, renderGraphInfo.m_numNodesDueToRT, renderGraphInfo.m_numNodesDueToBlendMode, renderGraphInfo.m_numNodesDueToSrgb, renderGraphInfo.m_numNodesDueToMaxVerts, renderGraphInfo.m_numNodesDueToTextures); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); } // output the details on each draw call and gather info for all canvases canvas->DebugReportDrawCalls(logHandle, reportInfo, canvas); } AZStd::string fontTexturePrefix = "$AutoFont"; logLine = "\r\n\r\n--------------------------------------------------------------------------------------------\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = AZStd::string::format("Textures used on multiple canvases that are causing extra draw calls\r\n"); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = "--------------------------------------------------------------------------------------------\r\n\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); for (LyShineDebug::DebugInfoTextureUsage& reportTextureUsage : reportInfo.m_textures) { if (reportTextureUsage.m_numCanvasesUsed > 1 && reportTextureUsage.m_numDrawCallsWhereExceedingMaxTextures) { AZStd::string textureName = reportTextureUsage.m_texture->GetName(); if (textureName.compare(0, fontTexturePrefix.length(), fontTexturePrefix) != 0) { logLine = AZStd::string::format("%s\r\n", textureName.c_str()); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); } } } logLine = "\r\n\r\n--------------------------------------------------------------------------------------------\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = AZStd::string::format("Per canvas report of textures used on only that canvas that are causing extra draw calls\r\n"); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = "--------------------------------------------------------------------------------------------\r\n\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); for (auto canvas : m_loadedCanvases) { // Check enabled bool isCanvasEnabled = canvas->GetEnabled(); if (!isCanvasEnabled) { continue; } bool loggedCanvasHeader = false; for (LyShineDebug::DebugInfoTextureUsage& reportTextureUsage : reportInfo.m_textures) { if (reportTextureUsage.m_numCanvasesUsed == 1 && reportTextureUsage.m_lastContextUsed == canvas && reportTextureUsage.m_numDrawCallsWhereExceedingMaxTextures) { AZStd::string textureName = reportTextureUsage.m_texture->GetName(); // exclude font textures if (textureName.compare(0, fontTexturePrefix.length(), fontTexturePrefix) != 0) { if (!loggedCanvasHeader) { const string pathname = canvas->GetPathname().c_str(); logLine = AZStd::string::format("\r\nCanvas: %s\r\n\r\n", pathname.c_str()); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); loggedCanvasHeader = true; } logLine = AZStd::string::format("%s\r\n", textureName.c_str()); AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); } } } } logLine = "\r\n--------------------------------------------------------------------------------------------\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = "End of report\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); logLine = "--------------------------------------------------------------------------------------------\r\n"; AZ::IO::LocalFileIO::GetInstance()->Write(logHandle, logLine.c_str(), logLine.size()); AZ::IO::LocalFileIO::GetInstance()->Close(logHandle); AZ_TracePrintf("UI", "Wrote Draw Call Report to %s\n", logFile.c_str()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasManager::DebugDisplayElemBounds(int canvasIndexFilter) const { IDraw2d* draw2d = Draw2dHelper::GetDraw2d(); draw2d->BeginDraw2d(false); int canvasIndex = 0; for (auto canvas : m_loadedCanvases) { // Enabled bool isCanvasEnabled = canvas->GetEnabled(); if (!isCanvasEnabled) { continue; } // filter canvas index if (canvasIndexFilter == -1 || canvasIndexFilter == canvasIndex) { // Display the elem bounds canvas->DebugDisplayElemBounds(draw2d); } ++canvasIndex; // only increments for enabled canvases so index matches "ui_DisplayCanvasData 2" } draw2d->EndDraw2d(); } #endif