2
0
Эх сурвалжийг харах

Fixes Slow Icon Rendering, Issue #18567 (#18601)

* Fixes Slow Icon Rendering, Issue #18567

Fixes issue https://github.com/o3de/o3de/issues/18567

This batch renders icons.  Note that the old API was retained, for backward
compatibility, so all existing code in other gems will still work
(none were found in the engine), but the new API is preferred.

After this change, I can't really notice a difference between icons on and off
in terms of rendering for Startergame (A couple thousand icons).

Before this change, it would drop to about 4ps, from 60.

I'm sure there's always a way to improve this further but just this change
alone is enough for the icon rendering to no longer matter in terms of
profiler performance, so other things should probably be looked at before
we return to this.

Signed-off-by: Nicholas Lawson <[email protected]>
Nicholas Lawson 6 сар өмнө
parent
commit
40cdf65119

+ 14 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorViewportIconDisplayInterface.h

@@ -60,9 +60,22 @@ namespace AzToolsFramework
             Error
         };
 
-        //! Draws an icon to a viewport given a set of draw parameters.
+        //! Draws an icon (immediately) to a viewport given a set of draw parameters.
         //! Requires an IconId retrieved from GetOrLoadIconForPath.
+        //! Note that this is an immediate draw request for backward compatability.
+        //! For more efficient rendering, use the AddIcon method.
         virtual void DrawIcon(const DrawParameters& drawParameters) = 0;
+
+        //! Adds an icon to the upcoming draw batch.
+        //! Use DrawIcons() after adding them all to actually commit them to the renderer.
+        //! DrawParameters.m_viewport must be set to a valid viewport and must be the same viewport
+        //! as all other invocations of AddIcon since the last call to DrawIcons (do one viewport at a time!)
+        virtual void AddIcon(const DrawParameters& drawParameters) = 0;
+
+        //! Call this after adding all of the icons via AddIcon.
+        //! This commits all of them to the renderer, in batches organized per icon texture.
+        virtual void DrawIcons() = 0;
+
         //! Retrieves a reusable IconId for an icon at a given path.
         //! This will load the icon, if it has not already been loaded.
         //! @param path should be a relative asset path to an icon image asset.

+ 2 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/Mocks/MockEditorViewportIconDisplayInterface.h

@@ -21,7 +21,9 @@ namespace UnitTest
 
         //! AzToolsFramework::EditorViewportIconDisplayInterface overrides ...
         MOCK_METHOD1(DrawIcon, void(const DrawParameters&));
+        MOCK_METHOD1(AddIcon, void(const DrawParameters&));
         MOCK_METHOD1(GetOrLoadIconForPath, IconId(AZStd::string_view path));
         MOCK_METHOD1(GetIconLoadStatus, IconLoadStatus(IconId icon));
+        MOCK_METHOD0(DrawIcons, void());
     };
 } // namespace UnitTest

+ 3 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp

@@ -372,11 +372,13 @@ namespace AzToolsFramework
                     EditorEntityIconComponentRequestBus::EventResult(
                         iconTextureId, entityId, &EditorEntityIconComponentRequestBus::Events::GetEntityIconTextureId);
 
-                    editorViewportIconDisplay->DrawIcon(EditorViewportIconDisplayInterface::DrawParameters{
+                    editorViewportIconDisplay->AddIcon(EditorViewportIconDisplayInterface::DrawParameters{
                         viewportInfo.m_viewportId, iconTextureId, iconHighlight, entityPosition,
                         EditorViewportIconDisplayInterface::CoordinateSpace::WorldSpace, AZ::Vector2(GetIconSize(distanceFromCamera)) });
                 }
             }
+
+            editorViewportIconDisplay->DrawIcons();
         }
     }
 

+ 5 - 0
Code/Framework/AzToolsFramework/Tests/EditorViewportIconTests.cpp

@@ -83,6 +83,9 @@ namespace UnitTest
             .WillByDefault(Return(AZ::Vector3(0.0f, insideNearClip, 0.0f)));
 
         EXPECT_CALL(*m_editorViewportIconDisplayMock, DrawIcon(_)).Times(0);
+        EXPECT_CALL(*m_editorViewportIconDisplayMock, AddIcon(_)).Times(0);
+        EXPECT_CALL(*m_editorViewportIconDisplayMock, DrawIcons()).Times(1);
+
 
         // when
         m_editorHelpers->DisplayHelpers(
@@ -104,6 +107,8 @@ namespace UnitTest
         ON_CALL(*m_entityVisibleEntityDataCacheMock, GetVisibleEntityPosition(_)).WillByDefault(Return(AZ::Vector3(0.0f, -1.0f, 0.0f)));
 
         EXPECT_CALL(*m_editorViewportIconDisplayMock, DrawIcon(_)).Times(0);
+        EXPECT_CALL(*m_editorViewportIconDisplayMock, AddIcon(_)).Times(0);
+        EXPECT_CALL(*m_editorViewportIconDisplayMock, DrawIcons()).Times(1);
 
         // when
         m_editorHelpers->DisplayHelpers(

+ 193 - 75
Gems/AtomLyIntegration/AtomViewportDisplayIcons/Code/Source/AtomViewportDisplayIconsSystemComponent.cpp

@@ -20,6 +20,8 @@
 #include <AzToolsFramework/Viewport/ViewportMessages.h>
 #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
 
+#include <AtomCore/Instance/Instance.h>
+
 #include <Atom/RPI.Public/View.h>
 #include <Atom/RPI.Public/Scene.h>
 #include <Atom/RPI.Public/ViewportContextBus.h>
@@ -94,7 +96,7 @@ namespace AZ::Render
         Data::AssetBus::Handler::BusDisconnect();
         Bootstrap::NotificationBus::Handler::BusDisconnect();
 
-        auto perViewportDynamicDrawInterface = AtomBridge::PerViewportDynamicDraw::Get();
+        AZ::AtomBridge::PerViewportDynamicDrawInterface* perViewportDynamicDrawInterface = AtomBridge::PerViewportDynamicDraw::Get();
         if (!perViewportDynamicDrawInterface)
         {
             return;
@@ -108,113 +110,229 @@ namespace AZ::Render
         AzToolsFramework::EditorViewportIconDisplay::Unregister(this);
     }
 
-    void AtomViewportDisplayIconsSystemComponent::DrawIcon(const DrawParameters& drawParameters)
+    void AtomViewportDisplayIconsSystemComponent::AddIcon(const DrawParameters& drawParameters)
     {
-        // Ensure we have a valid viewport context & dynamic draw interface
-        auto viewportContext = RPI::ViewportContextRequests::Get()->GetViewportContextById(drawParameters.m_viewport);
-        if (viewportContext == nullptr)
+        if (drawParameters.m_viewport == AzFramework::InvalidViewportId)
         {
+            AZ_WarningOnce("AtomViewportDisplayIconsSystemComponent", false, "Invalid viewport ID provided for icon draw request, discarded.");
             return;
         }
 
-        auto perViewportDynamicDrawInterface = AtomBridge::PerViewportDynamicDraw::Get();
-        if (!perViewportDynamicDrawInterface)
+        if (m_drawRequestViewportId == AzFramework::InvalidViewportId)
         {
-            return;
-        } 
-
-        RHI::Ptr<RPI::DynamicDrawContext> dynamicDraw =
-            perViewportDynamicDrawInterface->GetDynamicDrawContextForViewport(m_drawContextName, drawParameters.m_viewport);
-        if (dynamicDraw == nullptr)
+            // this is the first request, initialize m_drawRequestViewportId.
+            m_drawRequestViewportId = drawParameters.m_viewport;
+        }
+        else if (m_drawRequestViewportId != drawParameters.m_viewport)
         {
+            AZ_WarningOnce("AtomViewportDisplayIconsSystemComponent", false, "Multiple viewports provided for a single icon draw batch, discarded.");
             return;
         }
+        m_drawRequests[drawParameters.m_icon].emplace_back(drawParameters);
 
-        // Find our icon, falling back on a gray placeholder if its image is unavailable
-        AZ::Data::Instance<AZ::RPI::Image> image = AZ::RPI::ImageSystemInterface::Get()->GetSystemImage(AZ::RPI::SystemImage::Grey);
-        if (auto iconIt = m_iconData.find(drawParameters.m_icon); iconIt != m_iconData.end())
+        // the maximum we can batch at a time would be the largest index that can fit into a u16, (65535), and it eats 4 index values per icon
+        // since the indices go (0, 1, 2, 0, 2, 3), ie, 2 triangles making up 6 indices per quad but only using four actual index numbers (0,1,2,3) per.
+        // So we can only batch (max_uint16 / 4) icons at a time before the u16 would overflow (about 16k icons).
+        constexpr AZStd::vector<IconIndexData>::size_type maxQuads = (AZStd::numeric_limits<IconIndexData>::max() / 4) - 1;
+        if (m_drawRequests[drawParameters.m_icon].size() >= maxQuads)
         {
-            auto& iconData = iconIt->second;
-            if (iconData.m_image)
-            {
-                image = iconData.m_image;
-            }
+            DrawIcons(); // flush all buffers immediately.
         }
-        else
+    }
+
+    // create a SRG specific to the viewport dimensions and icon.
+    AZ::Data::Instance<AZ::RPI::ShaderResourceGroup> AtomViewportDisplayIconsSystemComponent::CreateIconSRG(AzFramework::ViewportId viewportId, AZ::Data::Instance<AZ::RPI::Image> image)
+    {
+        using namespace AZ;
+
+        RHI::Ptr<RPI::DynamicDrawContext> dynamicDraw = GetDynamicDrawContextForViewport(m_drawRequestViewportId);
+
+        AZ::RPI::ViewportContextPtr viewportContext = RPI::ViewportContextRequests::Get()->GetViewportContextById(viewportId);
+        if (viewportContext == nullptr)
         {
-            return;
+            return {};
         }
 
         const auto [viewportWidth, viewportHeight] = viewportContext->GetViewportSize();
         const auto viewportSize = AzFramework::ScreenSize(viewportWidth, viewportHeight);
 
-        // Initialize our shader
         AZ::Data::Instance<AZ::RPI::ShaderResourceGroup> drawSrg = dynamicDraw->NewDrawSrg();
         drawSrg->SetConstant(m_viewportSizeIndex, AzFramework::Vector2FromScreenSize(viewportSize));
         drawSrg->SetImageView(m_textureParameterIndex, image->GetImageView());
         drawSrg->Compile();
+        return drawSrg;
+    }
 
-        // Scale icons by screen DPI
-        float scalingFactor = 1.0f;
+    RHI::Ptr<RPI::DynamicDrawContext> AtomViewportDisplayIconsSystemComponent::GetDynamicDrawContextForViewport(AzFramework::ViewportId viewportId)
+    {
+        AZ::AtomBridge::PerViewportDynamicDrawInterface* perViewportDynamicDrawInterface = AtomBridge::PerViewportDynamicDraw::Get();
+        if (!perViewportDynamicDrawInterface)
         {
-            using ViewportRequestBus = AzToolsFramework::ViewportInteraction::ViewportInteractionRequestBus;
-            ViewportRequestBus::EventResult(
-                scalingFactor, drawParameters.m_viewport, &ViewportRequestBus::Events::DeviceScalingFactor);
+            return {};
         }
+        return perViewportDynamicDrawInterface->GetDynamicDrawContextForViewport(m_drawContextName, viewportId);
+    }
 
-        AZ::Vector3 screenPosition;
-        if (drawParameters.m_positionSpace == CoordinateSpace::ScreenSpace)
+    AZ::Data::Instance<AZ::RPI::Image> AtomViewportDisplayIconsSystemComponent::GetImageForIconId(IconId iconId)
+    {
+        if (auto iconIt = m_iconData.find(iconId); iconIt != m_iconData.end())
         {
-            screenPosition = drawParameters.m_position;
+            auto& iconData = iconIt->second;
+            if (iconData.m_image)
+            {
+                return iconData.m_image;
+            }
         }
-        else if (drawParameters.m_positionSpace == CoordinateSpace::WorldSpace)
+
+        return AZ::RPI::ImageSystemInterface::Get()->GetSystemImage(AZ::RPI::SystemImage::Grey);
+    }
+
+    void AtomViewportDisplayIconsSystemComponent::DrawIcons()
+    {
+        // the strategy for drawing icons here is to do the expensive stuff once, and then draw all of the icons using the same texture
+        // in one go.
+
+        // To achieve this, we initialize all the variables that are per-viewport just one time in this function,
+        // then we initialize the variables that are per-texture just once per texture,
+        // then we build the vertex list once per texture by accumulating all the quads.
+        // Note that the index cache is a special case - becuase the indexes for quads are always 0,1,2, 0,2,3, etc, we don't need to update
+        // them every frame, just make sure that the index cache has ENOUGH initialized data for the amount of quads we intend to render.
+        // This allows us to re-use the index cache even between viewports and textures, the only rapidly changing data is the vertex data,
+        // and we store that in a vector so that its memory stays stable.
+
+        using ViewportRequestBus = AzToolsFramework::ViewportInteraction::ViewportInteractionRequestBus;
+        
+        if (m_drawRequestViewportId == AzFramework::InvalidViewportId)
+        {
+            // its possible for the hash map to have entries in it (since they represent texture slots) with no currently rendering quads.
+            return;
+        }
+
+        if (m_drawRequests.empty())
         {
-            // Calculate the ndc point (0.0-1.0 range) including depth
-            const AZ::Vector3 ndcPoint = AzFramework::WorldToScreenNdc(
-                drawParameters.m_position, viewportContext->GetCameraViewMatrixAsMatrix3x4(),
-                viewportContext->GetCameraProjectionMatrix());
-
-            // Calculate our screen space position using the viewport size
-            // We want this instead of RenderViewportWidget::WorldToScreen which works in QWidget virtual coordinate space
-            const AzFramework::ScreenPoint screenPoint = AzFramework::ScreenPointFromNdc(AZ::Vector3ToVector2(ndcPoint), viewportSize);
-            screenPosition = AzFramework::Vector3FromScreenPoint(screenPoint, ndcPoint.GetZ());
+            return;
         }
 
-        struct Vertex
+        AZ_Assert(m_drawRequestViewportId != AzFramework::InvalidViewportId, "Viewport ID is somehow invalid despite icons being in the list.");
+
+        RHI::Ptr<RPI::DynamicDrawContext> dynamicDraw = GetDynamicDrawContextForViewport(m_drawRequestViewportId);
+        AZ::RPI::ViewportContextPtr viewportContext = RPI::ViewportContextRequests::Get()->GetViewportContextById(m_drawRequestViewportId);
+        if ((viewportContext == nullptr) || (dynamicDraw == nullptr))
         {
-            float m_position[3];
-            AZ::u32 m_color;
-            float m_uv[2];
-        };
-        using Indice = AZ::u16;
-
-        // Create a vertex offset from the position to draw from based on the icon size
-        // Vertex positions are in screen space coordinates
-        auto createVertex = [&](float offsetX, float offsetY, float u, float v) -> Vertex
+            // this is not an error or assert as we might be running headlessly.
+            m_drawRequests.clear();
+            m_drawRequestViewportId = AzFramework::InvalidViewportId;
+            return;
+        }
+        // Scale icons by screen DPI
+        float scalingFactor = 1.0f;
+        ViewportRequestBus::EventResult(scalingFactor, m_drawRequestViewportId, &ViewportRequestBus::Events::DeviceScalingFactor);
+
+        const auto [viewportWidth, viewportHeight] = viewportContext->GetViewportSize();
+        const auto viewportSize = AzFramework::ScreenSize(viewportWidth, viewportHeight);
+
+        for (auto &[iconId, drawIconRequests] : m_drawRequests)
         {
-            Vertex vertex;
-            screenPosition.StoreToFloat3(vertex.m_position);
-            vertex.m_position[0] += offsetX * drawParameters.m_size.GetX() * scalingFactor;
-            vertex.m_position[1] += offsetY * drawParameters.m_size.GetY() * scalingFactor;
-            vertex.m_color = drawParameters.m_color.ToU32();
-            vertex.m_uv[0] = u;
-            vertex.m_uv[1] = v;
-            return vertex;
-        };
-
-        AZStd::array<Vertex, 4> vertices = {
-            createVertex(-0.5f, -0.5f, 0.f, 0.f),
-            createVertex(0.5f,  -0.5f, 1.f, 0.f),
-            createVertex(0.5f,  0.5f,  1.f, 1.f),
-            createVertex(-0.5f, 0.5f,  0.f, 1.f)
-        };
-        AZStd::array<Indice, 6> indices = {0, 1, 2, 0, 2, 3};
-
-        dynamicDraw->SetSortKey(
-            aznumeric_cast<int64_t>(screenPosition.GetZ() * aznumeric_cast<float>(AZStd::numeric_limits<int64_t>::max())));
-        dynamicDraw->DrawIndexed(
-            &vertices, static_cast<uint32_t>(vertices.size()), &indices, static_cast<uint32_t>(indices.size()), RHI::IndexFormat::Uint16,
-            drawSrg);
+            // Find our icon, falling back on a gray placeholder if its image is unavailable
+            if (drawIconRequests.empty())
+            {
+                continue;
+            }
+
+            AZ::Data::Instance<AZ::RPI::Image> image = GetImageForIconId(iconId);
+            AZ::Data::Instance<AZ::RPI::ShaderResourceGroup> drawSrg = CreateIconSRG(m_drawRequestViewportId, image);
+
+            // add all of the icons to draw buffers.
+            m_vertexCache.clear();
+            m_vertexCache.reserve(drawIconRequests.size() * 4);
+            
+            float minZ = aznumeric_cast<float>(AZStd::numeric_limits<int64_t>::max());
+            float maxZ = aznumeric_cast<float>(AZStd::numeric_limits<int64_t>::min());
+
+            for (const DrawParameters& drawParameters : drawIconRequests)
+            {
+                AZ::Vector3 screenPosition;
+                if (drawParameters.m_positionSpace == CoordinateSpace::ScreenSpace)
+                {
+                    screenPosition = drawParameters.m_position;
+                }
+                else if (drawParameters.m_positionSpace == CoordinateSpace::WorldSpace)
+                {
+                    // Calculate the ndc point (0.0-1.0 range) including depth
+                    const AZ::Vector3 ndcPoint = AzFramework::WorldToScreenNdc(
+                        drawParameters.m_position, viewportContext->GetCameraViewMatrixAsMatrix3x4(),
+                        viewportContext->GetCameraProjectionMatrix());
+
+                    // Calculate our screen space position using the viewport size
+                    // We want this instead of RenderViewportWidget::WorldToScreen which works in QWidget virtual coordinate space
+                    const AzFramework::ScreenPoint screenPoint = AzFramework::ScreenPointFromNdc(AZ::Vector3ToVector2(ndcPoint), viewportSize);
+                    screenPosition = AzFramework::Vector3FromScreenPoint(screenPoint, ndcPoint.GetZ());
+                }
+                minZ = AZStd::GetMin(minZ, screenPosition.GetZ());
+                maxZ = AZStd::GetMin(maxZ, screenPosition.GetZ());
+
+                // Create a vertex offset from the position to draw from based on the icon size
+                // Vertex positions are in screen space coordinates
+                auto createVertex = [&](float offsetX, float offsetY, float u, float v) -> IconVertexData
+                {
+                    IconVertexData vertex;
+                    screenPosition.StoreToFloat3(vertex.m_position);
+                    vertex.m_position[0] += offsetX * drawParameters.m_size.GetX() * scalingFactor;
+                    vertex.m_position[1] += offsetY * drawParameters.m_size.GetY() * scalingFactor;
+                    vertex.m_color = drawParameters.m_color.ToU32();
+                    vertex.m_uv[0] = u;
+                    vertex.m_uv[1] = v;
+                    return vertex;
+                };
+
+                m_vertexCache.emplace_back(createVertex(-0.5f, -0.5f, 0.f, 0.f));
+                m_vertexCache.emplace_back(createVertex(0.5f,  -0.5f, 1.f, 0.f));
+                m_vertexCache.emplace_back(createVertex(0.5f,  0.5f,  1.f, 1.f));
+                m_vertexCache.emplace_back(createVertex(-0.5f, 0.5f,  0.f, 1.f));
+            }
+
+            if (!m_vertexCache.empty())
+            {
+                // the indexes are always the same (0,1,2,0,2,3, 4,5,6,4,6,7, etc) and thus don't need to be updated unless more quads are added
+                using IndexCacheSize = AZStd::vector<IconIndexData>::size_type;
+
+                IndexCacheSize numQuadsInVertexBuffer = m_vertexCache.size() / 4;
+                IndexCacheSize numIndicesRequired = numQuadsInVertexBuffer * 6;
+
+                IndexCacheSize currentIndexCacheSize = m_indexCache.size();
+                if (currentIndexCacheSize < numIndicesRequired)
+                {
+                    m_indexCache.resize_no_construct(numIndicesRequired);
+                    IconIndexData baseIndex = aznumeric_cast<IconIndexData>(currentIndexCacheSize / 6);
+                    while (currentIndexCacheSize < numIndicesRequired)
+                    {
+                        m_indexCache[currentIndexCacheSize++] = (baseIndex * 4) + 0;
+                        m_indexCache[currentIndexCacheSize++] = (baseIndex * 4) + 1;
+                        m_indexCache[currentIndexCacheSize++] = (baseIndex * 4) + 2;
+                        m_indexCache[currentIndexCacheSize++] = (baseIndex * 4) + 0;
+                        m_indexCache[currentIndexCacheSize++] = (baseIndex * 4) + 2;
+                        m_indexCache[currentIndexCacheSize++] = (baseIndex * 4) + 3;
+                        ++baseIndex;
+                    }
+                }
+
+                dynamicDraw->SetSortKey((maxZ - minZ) * 0.5f  * aznumeric_cast<float>(AZStd::numeric_limits<int64_t>::max()));
+                dynamicDraw->DrawIndexed( m_vertexCache.data(), static_cast<uint32_t>(m_vertexCache.size()),
+                                         m_indexCache.data(), static_cast<uint32_t>(numIndicesRequired), RHI::IndexFormat::Uint16,
+                    drawSrg);
+            }
+            drawIconRequests.clear(); // note - we don't remove the key, we just clear the vector and keep the memory allocated.
+        }
+        m_drawRequestViewportId = AzFramework::InvalidViewportId;
+    }
+
+
+    void AtomViewportDisplayIconsSystemComponent::DrawIcon(const DrawParameters& drawParameters)
+    {
+        AddIcon(drawParameters);
+        // Be careful when using this method as it does not support batching.
+        // Prefer using AddIcon, AddIcon, AddIcon, ..., DrawIcons() to render them in a batch.
+        DrawIcons();
     }
 
     QString AtomViewportDisplayIconsSystemComponent::FindAssetPath(const QString& path) const

+ 26 - 1
Gems/AtomLyIntegration/AtomViewportDisplayIcons/Code/Source/AtomViewportDisplayIconsSystemComponent.h

@@ -8,9 +8,11 @@
 #pragma once
 
 #include <AzCore/Component/Component.h>
+#include <AzCore/Asset/AssetCommon.h> // for AssetBus
+#include <AzCore/std/containers/unordered_map.h>
+#include <AzCore/std/containers/vector.h>
 
 #include <AzToolsFramework/API/EditorViewportIconDisplayInterface.h>
-
 #include <Atom/RPI.Public/DynamicDraw/DynamicDrawInterface.h>
 #include <Atom/Bootstrap/BootstrapNotificationBus.h>
 
@@ -47,6 +49,9 @@ namespace AZ
 
             // AzToolsFramework::EditorViewportIconDisplayInterface overrides...
             void DrawIcon(const DrawParameters& drawParameters) override;
+            void AddIcon(const DrawParameters& drawParameters) override;
+            void DrawIcons() override;
+
             IconId GetOrLoadIconForPath(AZStd::string_view path) override;
             IconLoadStatus GetIconLoadStatus(IconId icon) override;
 
@@ -64,6 +69,9 @@ namespace AZ
             QString FindAssetPath(const QString& path) const;
             QImage RenderSvgToImage(const QString& svgPath) const;
             AZ::Data::Instance<AZ::RPI::Image> ConvertToAtomImage(AZ::Uuid assetId, QImage image) const;
+            AZ::Data::Instance<AZ::RPI::ShaderResourceGroup> CreateIconSRG(AzFramework::ViewportId viewportId, AZ::Data::Instance<AZ::RPI::Image> image);
+            RHI::Ptr<RPI::DynamicDrawContext> GetDynamicDrawContextForViewport(AzFramework::ViewportId viewportId);
+            AZ::Data::Instance<AZ::RPI::Image> GetImageForIconId(IconId iconId);
 
             Name m_drawContextName = Name("ViewportIconDisplay");
             bool m_shaderIndexesInitialized = false;
@@ -79,6 +87,23 @@ namespace AZ
             IconId m_currentId = 0;
 
             bool m_drawContextRegistered = false;
+
+            AZStd::unordered_map<IconId, AZStd::vector<DrawParameters>> m_drawRequests;
+            AzFramework::ViewportId m_drawRequestViewportId = AzFramework::InvalidViewportId;
+            
+            using IconIndexData = AZ::u16;
+            struct IconVertexData
+            {
+                float m_position[3];
+                AZ::u32 m_color;
+                float m_uv[2];
+            };
+
+
+            // re-used between frames so that we don't constantly allocate new memory
+            AZStd::vector<IconVertexData> m_vertexCache;
+            AZStd::vector<IconIndexData> m_indexCache;
+
         };
     } // namespace Render
 } // namespace AZ