From 080de40eac10f59ba7202841991da03db5b204f4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 10 Jun 2026 15:13:59 +0200 Subject: [PATCH] Add debug preview to selected resource in GPU Memory tab --- .../CustomEditors/CustomEditorPresenter.cs | 2 +- Source/Editor/GUI/Row.cs | 11 +- Source/Editor/Windows/Profiler/MemoryGPU.cs | 369 ++++++++++++++++-- Source/Engine/Graphics/GPUContext.cpp | 2 +- 4 files changed, 338 insertions(+), 46 deletions(-) diff --git a/Source/Editor/CustomEditors/CustomEditorPresenter.cs b/Source/Editor/CustomEditors/CustomEditorPresenter.cs index c7832cfe1..ceb5574aa 100644 --- a/Source/Editor/CustomEditors/CustomEditorPresenter.cs +++ b/Source/Editor/CustomEditors/CustomEditorPresenter.cs @@ -352,7 +352,7 @@ namespace FlaxEditor.CustomEditors /// The undo. It's optional. /// The custom text to display when no object is selected. Default is No selection. /// The owner of the presenter. - public CustomEditorPresenter(Undo undo, string noSelectionText = null, IPresenterOwner owner = null) + public CustomEditorPresenter(Undo undo = null, string noSelectionText = null, IPresenterOwner owner = null) { Undo = undo; Owner = owner; diff --git a/Source/Editor/GUI/Row.cs b/Source/Editor/GUI/Row.cs index dac08b2a5..f256e0263 100644 --- a/Source/Editor/GUI/Row.cs +++ b/Source/Editor/GUI/Row.cs @@ -14,6 +14,11 @@ namespace FlaxEditor.GUI { private Table _table; + /// + /// True if row is selected by the user. + /// + public bool IsSelected; + /// /// Gets the parent table that owns this row. /// @@ -55,7 +60,11 @@ namespace FlaxEditor.GUI var style = Style.Current; - if (IsMouseOver) + if (IsSelected) + { + Render2D.FillRectangle(new Rectangle(Float2.Zero, Size), style.BackgroundSelected); + } + else if (IsMouseOver) { Render2D.FillRectangle(new Rectangle(Float2.Zero, Size), style.BackgroundHighlighted * 0.7f); } diff --git a/Source/Editor/Windows/Profiler/MemoryGPU.cs b/Source/Editor/Windows/Profiler/MemoryGPU.cs index 11b8a1980..a65d3a420 100644 --- a/Source/Editor/Windows/Profiler/MemoryGPU.cs +++ b/Source/Editor/Windows/Profiler/MemoryGPU.cs @@ -3,8 +3,10 @@ #if USE_PROFILER using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; +using FlaxEditor.CustomEditors; using FlaxEditor.GUI; using FlaxEngine; using FlaxEngine.GUI; @@ -17,23 +19,157 @@ namespace FlaxEditor.Windows.Profiler /// internal sealed class MemoryGPU : ProfilerMode { + private enum ResourceTypes + { + Texture, + Buffer, + RenderTarget, + DepthBuffer, + VolumeTexture, + CubeTexture, + TextureArray, + VertexBuffer, + IndexBuffer, + MAX + } + private class Resource { public string Name; public string Tooltip; - public GPUResourceType Type; + public ResourceTypes Type; public ulong MemoryUsage; public Guid AssetId; public bool IsAssetItem; + public GPUResource Reference; + } + + [CustomEditor(typeof(Resource))] + private sealed class ResourceEditor : CustomEditor + { + public override void Initialize(LayoutElementsContainer layout) + { + var resource = (Resource)Values[0]; + + // Resource name + var style = FlaxEngine.GUI.Style.Current; + var nameSplit = resource.Name.LastIndexOf('/'); + var label = layout.Label(nameSplit == -1 ? resource.Name : resource.Name.Substring(nameSplit + 1), TextAlignment.Center).Label; + label.AutoFitText = true; + label.Font = new FontReference(style.FontLarge); + + var memoryUsage = Utilities.Utils.FormatBytesCount(resource.MemoryUsage); + if (resource.Reference is GPUTexture texture && texture) + { + // Texture preview + var desc = texture.Description; + var canSave = false; + // TODO: custom viewers for non-2d textures + if (desc.IsShaderResource && !desc.IsArray && !desc.IsVolume && !desc.IsCubeMap) + { + var image = layout.Image(texture); + image.Image.Size = new Float2(layout.Presenter.Panel.Width - Utilities.Constants.UIMargin * 2); + canSave = true; + } + + // Texture info + layout.AddPropertyItem("Format").Label(desc.Format.ToString()); + string size; + if (desc.IsVolume) + size = $"{desc.Width}x{desc.Height}x{desc.Depth}"; + else + size = $"{desc.Width}x{desc.Height}"; + if (desc.IsArray) + size += $"[{desc.ArraySize}]"; + layout.AddPropertyItem("Size").Label(size); + var residentMipLevels = texture.ResidentMipLevels; + layout.AddPropertyItem("Mips").Label(residentMipLevels == desc.MipLevels ? desc.MipLevels.ToString() : $"{residentMipLevels} / {desc.MipLevels}"); + if (desc.IsMultiSample) + layout.AddPropertyItem("MSAA").Label(desc.MultiSampleLevel.ToString()); + layout.AddPropertyItem("Memory Size").Label(memoryUsage); + var asset = FlaxEngine.Content.LoadAsync(resource.AssetId) as TextureBase; + if (asset) + { + // Texture asset info + var path = asset.Path; + if (!asset.IsVirtual && File.Exists(path)) + layout.AddPropertyItem("Disk Size").Label(Utilities.Utils.FormatBytesCount((ulong)new FileInfo(path).Length)); + var textureGroup = asset.TextureGroup; + if (textureGroup >= 0) + { + var textureGroups = Streaming.TextureGroups; + if (textureGroup < textureGroups.Length) + layout.AddPropertyItem("Texture Group").Label(textureGroups[textureGroup].Name); + } + layout.AddPropertyItem("Refs").Label(asset.ReferencesCount.ToString()); + } + + if (canSave) + { + var buttonPanel = layout.HorizontalPanel(); + buttonPanel.Panel.Size = new Float2(0, Button.DefaultHeight); + buttonPanel.Panel.Margin = Margin.Zero; + buttonPanel.Panel.Spacing = Utilities.Constants.UIMargin; + var button = buttonPanel.Button("Save", "Downloads the texture from the GPU and saves it to file inside project Screenshots folder"); + button.Button.Width = 100; + button.Button.Clicked += OnSave; + } + } + else if (resource.Reference is GPUBuffer buffer && buffer) + { + var desc = buffer.Description; + + // Buffer info + layout.AddPropertyItem("Memory Usage").Label(memoryUsage); + layout.AddPropertyItem("Stride").Label($"{desc.Stride} bytes"); + layout.AddPropertyItem("Elements").Label(desc.ElementsCount.ToString("###,###,###")); + if (desc.Format != PixelFormat.Unknown) + layout.AddPropertyItem("Format").Label(desc.Format.ToString()); + layout.AddPropertyItem("Usage").Label(desc.Usage.ToString()); + var asset = FlaxEngine.Content.LoadAsync(resource.AssetId) as ModelBase; + if (asset) + { + // Model asset info + layout.AddPropertyItem("Refs").Label(asset.ReferencesCount.ToString()); + } + if (desc.VertexLayout) + { + var group = layout.Group("Vertex Layout"); + var elements = desc.VertexLayout.Elements; + foreach (var e in elements) + group.Label($" > {e.Type}, {e.Format} ({PixelFormatExtensions.SizeInBytes(e.Format)} bytes), offset {e.Offset}").Label.Height = 14; + } + } + else + { + // Unknown resource or broken reference (eg. object deleted) + layout.Label("Memory Usage: " + memoryUsage); + label = layout.Label(resource.Tooltip).Label; + label.AutoHeight = true; + } + } + + private void OnSave() + { + var resource = (Resource)Values[0]; + if (resource.Reference is GPUTexture texture && texture) + { + Screenshot.Capture(texture); + } + } } private readonly SingleChart _memoryUsageChart; private readonly Table _table; + private readonly Panel _tablePanel; + private readonly Panel _resourcePanel; + private readonly CustomEditorPresenter _resourceProperties; private SamplesBuffer _resources; private List _tableRowsCache; private string[] _resourceTypesNames; private Dictionary _assetPathToId; private Dictionary _resourceCache; + private List _resourceList; private StringBuilder _stringBuilder; private GPUResource[] _gpuResourcesCached; @@ -47,7 +183,7 @@ namespace FlaxEditor.Windows.Profiler Offsets = Margin.Zero, Parent = this, }; - + // Chart _memoryUsageChart = new SingleChart { @@ -59,11 +195,24 @@ namespace FlaxEditor.Windows.Profiler Parent = mainPanel, }; _memoryUsageChart.SelectedSampleChanged += OnSelectedSampleChanged; + var chartsBottom = _memoryUsageChart.Height + Utilities.Constants.UIMargin; - var panel = new Panel(ScrollBars.Vertical) + // Selected resource info + _resourcePanel = new Panel(ScrollBars.Vertical) + { + AnchorPreset = AnchorPresets.VerticalStretchRight, + Offsets = new Margin(0, 0, chartsBottom, 0), + Visible = false, + Parent = mainPanel, + }; + _resourceProperties = new CustomEditorPresenter(); + _resourceProperties.Panel.Parent = _resourcePanel; + + // Table panel + _tablePanel = new Panel(ScrollBars.Vertical) { AnchorPreset = AnchorPresets.StretchAll, - Offsets = new Margin(0, 0, _memoryUsageChart.Height + 2, 0), + Offsets = new Margin(0, 0, chartsBottom, 0), Parent = mainPanel, }; var layout = new VerticalPanel @@ -72,7 +221,7 @@ namespace FlaxEditor.Windows.Profiler Offsets = Margin.Zero, Pivot = Float2.Zero, IsScrollable = true, - Parent = panel, + Parent = _tablePanel, }; // Table @@ -123,6 +272,7 @@ namespace FlaxEditor.Windows.Profiler _resources?.Clear(); _assetPathToId?.Clear(); _resourceCache?.Clear(); + _resourceList?.Clear(); } /// @@ -130,8 +280,11 @@ namespace FlaxEditor.Windows.Profiler { _memoryUsageChart.AddSample(sharedData.Stats.MemoryGPU.Used); + // Lazy-init cache data if (_resourceCache == null) _resourceCache = new Dictionary(); + if (_resourceList == null) + _resourceList = new List(); if (_assetPathToId == null) _assetPathToId = new Dictionary(); if (_stringBuilder == null) @@ -140,31 +293,22 @@ namespace FlaxEditor.Windows.Profiler // Capture current GPU resources usage info var contentDatabase = Editor.Instance.ContentDatabase; GPUDevice.Instance.GetResources(ref _gpuResourcesCached, out var count); - var resources = new Resource[count]; var sb = _stringBuilder; + _resourceList.Clear(); + _resourceList.EnsureCapacity(count); for (int i = 0; i < count; i++) { var gpuResource = _gpuResourcesCached[i]; - ref var resource = ref resources[i]; - if (!gpuResource) + if (!gpuResource || gpuResource.MemoryUsage < 100) // Skip invalid, unallocated or very small resources continue; // Try to reuse cached resource info var gpuResourceId = gpuResource.ID; - if (!_resourceCache.TryGetValue(gpuResourceId, out resource)) + if (!_resourceCache.TryGetValue(gpuResourceId, out var resource)) { - resource = new Resource - { -#if !BUILD_RELEASE - Name = gpuResource.Name, -#endif - Type = gpuResource.ResourceType, - }; - if (resource.Name == null) - resource.Name = string.Empty; - // Create tooltip sb.Clear(); + ResourceTypes type; if (gpuResource is GPUTexture gpuTexture) { var desc = gpuTexture.Description; @@ -180,6 +324,18 @@ namespace FlaxEditor.Windows.Profiler sb.Append("MSAA: ").Append('x').Append((int)desc.MultiSampleLevel).AppendLine(); sb.Append("Flags: ").Append(desc.Flags).AppendLine(); sb.Append("Usage: ").Append(desc.Usage); + if (desc.Flags.HasFlag(GPUTextureFlags.RenderTarget)) + type = ResourceTypes.RenderTarget; + else if (desc.Flags.HasFlag(GPUTextureFlags.DepthStencil)) + type = ResourceTypes.DepthBuffer; + else if (desc.IsVolume) + type = ResourceTypes.VolumeTexture; + else if (desc.IsCubeMap) + type = ResourceTypes.CubeTexture; + else if (desc.IsArray) + type = ResourceTypes.TextureArray; + else + type = ResourceTypes.Texture; } else if (gpuResource is GPUBuffer gpuBuffer) { @@ -189,8 +345,28 @@ namespace FlaxEditor.Windows.Profiler sb.Append("Elements: ").Append(desc.ElementsCount).AppendLine(); sb.Append("Flags: ").Append(desc.Flags).AppendLine(); sb.Append("Usage: ").Append(desc.Usage); + if (desc.Flags.HasFlag(GPUBufferFlags.VertexBuffer)) + type = ResourceTypes.VertexBuffer; + else if (desc.Flags.HasFlag(GPUBufferFlags.IndexBuffer)) + type = ResourceTypes.IndexBuffer; + else + type = ResourceTypes.Buffer; } - resource.Tooltip = _stringBuilder.ToString(); + else + { + // Ignore internal resources (not useful for user) + continue; + } + resource = new Resource + { +#if !BUILD_RELEASE + Name = gpuResource.Name ?? string.Empty, +#else + Name = string.Empty, +#endif + Tooltip = _stringBuilder.ToString(), + Type = type, + }; // Detect asset path in the resource name int ext = resource.Name.LastIndexOf(".flax", StringComparison.OrdinalIgnoreCase); @@ -216,15 +392,83 @@ namespace FlaxEditor.Windows.Profiler } resource.MemoryUsage = gpuResource.MemoryUsage; - if (resource.MemoryUsage == 1) - resource.MemoryUsage = 0; // Sometimes GPU backend fakes memory usage as 1 to mark as allocated but not resided in actual GPU memory + resource.Reference = gpuResource; + _resourceList.Add(resource); } if (_resources == null) _resources = new SamplesBuffer(); - _resources.Add(resources); + _resources.Add(_resourceList.ToArray()); Array.Clear(_gpuResourcesCached); } + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (base.OnKeyDown(key)) + return true; + + // Input control over resource table + if (_table.ContainsFocus) + { + var selectedRow = GetSelectedRow(); + switch (key) + { + case KeyboardKeys.Return: + // Open selected resource + if (selectedRow != null && selectedRow.RowDoubleClick != null) + { + selectedRow.RowDoubleClick(selectedRow); + } + break; + case KeyboardKeys.Escape: + // Deselect all rows + if (selectedRow != null) + { + selectedRow.IsSelected = false; + ShowResourcePanel(false); + _resourceProperties.Deselect(); + } + break; + case KeyboardKeys.ArrowUp: + // Select the previous row + if (selectedRow != null) + { + var prevIndex = _table.Children.IndexOf(selectedRow) - 1; + if (prevIndex >= 0 && _table.Children[prevIndex] is ClickableRow prevRow) + { + selectedRow.IsSelected = false; + selectedRow = prevRow; + selectedRow.IsSelected = true; + selectedRow.Focus(); + _tablePanel.ScrollViewTo(selectedRow); + var e = (Resource)selectedRow.Tag; + _resourceProperties.Select(e); + } + } + break; + case KeyboardKeys.ArrowDown: + // Select the next row + if (selectedRow != null) + { + var nextIndex = _table.Children.IndexOf(selectedRow) + 1; + if (nextIndex < _table.Children.Count && _table.Children[nextIndex] is ClickableRow nextRow) + { + selectedRow.IsSelected = false; + selectedRow = nextRow; + selectedRow.IsSelected = true; + selectedRow.Focus(); + _tablePanel.ScrollViewTo(selectedRow); + var e = (Resource)selectedRow.Tag; + _resourceProperties.Select(e); + } + } + break; + } + } + + return false; + } + /// public override void UpdateView(int selectedFrame, bool showOnlyLastUpdateEvents) { @@ -235,19 +479,11 @@ namespace FlaxEditor.Windows.Profiler if (_tableRowsCache == null) _tableRowsCache = new List(); if (_resourceTypesNames == null) - _resourceTypesNames = new string[(int)GPUResourceType.MAX] - { - "Render Target", - "Texture", - "Cube Texture", - "Volume Texture", - "Buffer", - "Shader", - "Pipeline State", - "Descriptor", - "Query", - "Sampler", - }; + { + _resourceTypesNames = new string[(int)ResourceTypes.MAX]; + for (int i = 0; i < _resourceTypesNames.Length; i++) + _resourceTypesNames[i] = ((ResourceTypes)i).ToString(); + } UpdateTable(); } @@ -264,6 +500,16 @@ namespace FlaxEditor.Windows.Profiler base.OnDestroy(); } + private ClickableRow GetSelectedRow() + { + foreach (var child in _table.Children) + { + if (child is ClickableRow row && row.IsSelected) + return row; + } + return null; + } + private void UpdateTable() { _table.IsLayoutLocked = true; @@ -273,6 +519,12 @@ namespace FlaxEditor.Windows.Profiler var child = _table.Children[idx]; if (child is ClickableRow row) { + if (row.IsSelected) + { + row.IsSelected = false; + ShowResourcePanel(false); + _resourceProperties.Deselect(); + } _tableRowsCache.Add(row); child.Parent = null; } @@ -316,7 +568,11 @@ namespace FlaxEditor.Windows.Profiler else { // Allocate new row - row = new ClickableRow { Values = new object[3] }; + row = new ClickableRow + { + Values = new object[3], + RowLeftClick = OnRowLeftClick + }; } // Setup row data @@ -327,11 +583,7 @@ namespace FlaxEditor.Windows.Profiler // Setup row interactions row.Tag = e; row.TooltipText = e.Tooltip; - row.RowDoubleClick = null; - if (e.IsAssetItem) - { - row.RowDoubleClick = OnRowDoubleClickAsset; - } + row.RowDoubleClick = e.IsAssetItem ? OnRowDoubleClickAsset : null; // Add row to the table row.Width = _table.Width; @@ -341,11 +593,42 @@ namespace FlaxEditor.Windows.Profiler } } + private void OnRowLeftClick(ClickableRow row) + { + if (!row.IsSelected) + { + // Deselect all other rows + foreach (var child in _table.Children) + { + if (child is ClickableRow r) + r.IsSelected = false; + } + } + row.IsSelected = !row.IsSelected; + ShowResourcePanel(row.IsSelected); + row.Focus(); + var e = (Resource)row.Tag; + _resourceProperties.Select(e); + } + private void OnRowDoubleClickAsset(ClickableRow row) { var e = (Resource)row.Tag; var assetItem = Editor.Instance.ContentDatabase.FindAsset(e.AssetId); - Editor.Instance.ContentEditing.Open(assetItem); + if (assetItem != null) + Editor.Instance.ContentEditing.Open(assetItem); + } + + private void ShowResourcePanel(bool visible = true) + { + _resourcePanel.Visible = visible; + var parentSize = _resourcePanel.Parent.Size; + var split = visible ? parentSize.X * 0.3f : 0; + var chartsBottom = _memoryUsageChart.Height + Utilities.Constants.UIMargin; + _resourcePanel.Bounds = new Rectangle(parentSize.X - split, chartsBottom, split, parentSize.Y - chartsBottom); + var offsets = _tablePanel.Offsets; + offsets.Right = visible ? split + Utilities.Constants.UIMargin : 0; + _tablePanel.Offsets = offsets; } } } diff --git a/Source/Engine/Graphics/GPUContext.cpp b/Source/Engine/Graphics/GPUContext.cpp index c7d40a964..60e1bf567 100644 --- a/Source/Engine/Graphics/GPUContext.cpp +++ b/Source/Engine/Graphics/GPUContext.cpp @@ -78,7 +78,7 @@ void GPUContext::OnPresent() void GPUContext::BindSR(int32 slot, GPUTexture* t) { - ASSERT_LOW_LAYER(t == nullptr || t->ResidentMipLevels() == 0 || t->IsShaderResource()); + CHECK_DEBUG(t == nullptr || t->ResidentMipLevels() == 0 || t->IsShaderResource()); BindSR(slot, GET_TEXTURE_VIEW_SAFE(t)); }