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));
}