Add debug preview to selected resource in GPU Memory tab

This commit is contained in:
2026-06-10 15:13:59 +02:00
parent 7a0ba92808
commit 080de40eac
4 changed files with 338 additions and 46 deletions
@@ -352,7 +352,7 @@ namespace FlaxEditor.CustomEditors
/// <param name="undo">The undo. It's optional.</param>
/// <param name="noSelectionText">The custom text to display when no object is selected. Default is No selection.</param>
/// <param name="owner">The owner of the presenter.</param>
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;
+10 -1
View File
@@ -14,6 +14,11 @@ namespace FlaxEditor.GUI
{
private Table _table;
/// <summary>
/// True if row is selected by the user.
/// </summary>
public bool IsSelected;
/// <summary>
/// Gets the parent table that owns this row.
/// </summary>
@@ -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);
}
+326 -43
View File
@@ -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
/// <seealso cref="FlaxEditor.Windows.Profiler.ProfilerMode" />
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<Resource[]> _resources;
private List<ClickableRow> _tableRowsCache;
private string[] _resourceTypesNames;
private Dictionary<string, Guid> _assetPathToId;
private Dictionary<Guid, Resource> _resourceCache;
private List<Resource> _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();
}
/// <inheritdoc />
@@ -130,8 +280,11 @@ namespace FlaxEditor.Windows.Profiler
{
_memoryUsageChart.AddSample(sharedData.Stats.MemoryGPU.Used);
// Lazy-init cache data
if (_resourceCache == null)
_resourceCache = new Dictionary<Guid, Resource>();
if (_resourceList == null)
_resourceList = new List<Resource>();
if (_assetPathToId == null)
_assetPathToId = new Dictionary<string, Guid>();
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<Resource[]>();
_resources.Add(resources);
_resources.Add(_resourceList.ToArray());
Array.Clear(_gpuResourcesCached);
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public override void UpdateView(int selectedFrame, bool showOnlyLastUpdateEvents)
{
@@ -235,19 +479,11 @@ namespace FlaxEditor.Windows.Profiler
if (_tableRowsCache == null)
_tableRowsCache = new List<ClickableRow>();
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;
}
}
}
+1 -1
View File
@@ -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));
}