Add filtering and hierarchical view to plots selection

- Filter text allows restricting plots to ones matching the name
- Hierarchical view displays plots as a tree, separating levels based on
  slash-separated names, i.e.: "GPU/Counters/Draws" and
  "GPU/Counters/Primitives" will be leaf nodes under GPU > Counters
This commit is contained in:
Camille Martel
2026-10-09 16:54:39 -07:00
parent d55cee0601
commit f6850efbf0
4 changed files with 156 additions and 9 deletions

View File

@@ -177,6 +177,7 @@ bool UserData::Save()
{ "frameTarget", m_viewData.frameTarget },
{ "shortenName", (int)m_viewData.shortenName },
{ "plotHeight", m_viewData.plotHeight },
{ "plotsHierarchy", m_viewData.plotsHierarchy },
} },
};
@@ -278,6 +279,7 @@ bool UserData::Load()
LoadValue( options, "frameTarget", m_viewData.frameTarget );
LoadValueCast( options, "shortenName", m_viewData.shortenName );
LoadValue( options, "plotHeight", m_viewData.plotHeight );
LoadValue( options, "plotsHierarchy", m_viewData.plotsHierarchy );
}
if( json.contains( "sourceSubstitutions" ) )

View File

@@ -637,6 +637,7 @@ private:
ImGuiTextFilter m_statisticsFilter;
ImGuiTextFilter m_statisticsImageFilter;
ImGuiTextFilter m_userTextFilter;
ImGuiTextFilter m_plotFilter;
unordered_flat_set<Worker::ZoneThreadData*> m_filteredZones;
Region m_highlight;

View File

@@ -54,6 +54,7 @@ struct ViewData
uint8_t inheritParentColors = true;
uint8_t forceColors = false;
uint8_t ghostZones = true;
uint8_t plotsHierarchy = false;
ShortenName shortenName = ShortenName::NoSpaceAndNormalize;
uint32_t frameTarget = 60;

View File

@@ -1,5 +1,8 @@
#include <inttypes.h>
#include <random>
#include <map>
#include <cstring>
#include <cstdio>
#include "TracyFilesystem.hpp"
#include "TracyImGui.hpp"
@@ -28,6 +31,102 @@ static void DefaultMarker( bool active, bool tooltip = true )
}
}
struct PlotTreeNode
{
std::map<std::string, PlotTreeNode> children;
std::vector<const PlotData*> plots;
};
static void BuildPlotTree( PlotTreeNode& root, const std::vector<const PlotData*>& plots, const Worker& worker )
{
for( const auto* p : plots )
{
const char* name = worker.GetString( p->name );
PlotTreeNode* node = &root;
const char* segStart = name;
for(;;)
{
const char* slash = strchr( segStart, '/' );
if( !slash )
{
node->plots.emplace_back( p );
break;
}
node = &node->children[std::string( segStart, slash - segStart )];
segStart = slash + 1;
}
}
}
static void CountPlotTreeVisibility( const PlotTreeNode& node, TimelineController& tc, size_t& total, size_t& visible )
{
for( const auto& child : node.children ) CountPlotTreeVisibility( child.second, tc, total, visible );
for( const auto* p : node.plots )
{
total++;
if( tc.GetItem( p ).IsVisible() ) visible++;
}
}
static void SetPlotTreeVisibility( PlotTreeNode& node, TimelineController& tc, bool visible )
{
for( auto& child : node.children ) SetPlotTreeVisibility( child.second, tc, visible );
for( const auto* p : node.plots )
{
tc.GetItem( p ).SetVisible( visible );
}
}
// fullPath is the '/'-joined path to this node, used as a stable ImGui ID (the tree is rebuilt every frame).
static void DrawPlotTreeNode( const std::string& label, const std::string& fullPath, PlotTreeNode& node, TimelineController& tc, const Worker& worker )
{
size_t total = 0, visibleCount = 0;
CountPlotTreeVisibility( node, tc, total, visibleCount );
bool allVisible = total > 0 && visibleCount == total;
const bool mixed = visibleCount > 0 && visibleCount < total;
const bool nodeExpand = ImGui::TreeNodeEx( fullPath.c_str(), ImGuiTreeNodeFlags_None, "" );
ImGui::SameLine();
const std::string checkboxId = "##hdr_" + fullPath;
ImGui::PushItemFlag( ImGuiItemFlags_MixedValue, mixed );
const bool clicked = SmallCheckbox( checkboxId.c_str(), &allVisible );
ImGui::PopItemFlag();
if( clicked )
{
SetPlotTreeVisibility( node, tc, allVisible );
}
ImGui::SameLine();
ImGui::TextUnformatted( label.c_str() );
if( nodeExpand )
{
for( auto& child : node.children )
{
DrawPlotTreeNode( child.first, fullPath + "/" + child.first, child.second, tc, worker );
}
for( const auto* p : node.plots )
{
const char* fullName = worker.GetString( p->name );
const char* slash = strrchr( fullName, '/' );
const char* leaf = slash ? slash + 1 : fullName;
SmallColorBox( GetPlotColor( *p, worker ) );
ImGui::SameLine();
bool visible = tc.GetItem( p ).IsVisible();
char leafId[256];
snprintf( leafId, sizeof( leafId ), "%s##%p", leaf, (const void*)p );
if( SmallCheckbox( leafId, &visible ) )
{
tc.GetItem( p ).SetVisible( visible );
}
ImGui::SameLine();
ImGui::TextDisabled( "%s data points", RealToString( p->data.size() ) );
}
ImGui::TreePop();
}
}
void View::DrawOptions()
{
static bool default_markers_active = false;
@@ -678,9 +777,25 @@ void View::DrawOptions()
if( expand )
{
ImGui::SameLine();
bool hierarchy = m_vd.plotsHierarchy;
if( ImGui::RadioButton( "Flat", !hierarchy ) ) m_vd.plotsHierarchy = false;
ImGui::SameLine();
if( ImGui::RadioButton( "Hierarchical", hierarchy ) ) m_vd.plotsHierarchy = true;
hierarchy = m_vd.plotsHierarchy;
std::vector<const PlotData*> filteredPlots;
filteredPlots.reserve( m_worker.GetPlots().size() );
for( const auto& p : m_worker.GetPlots() )
{
if( m_plotFilter.PassFilter( m_worker.GetString( p->name ) ) )
{
filteredPlots.emplace_back( p );
}
}
if( ImGui::SmallButton( "Select all" ) )
{
for( const auto& p : m_worker.GetPlots() )
for( const auto* p : filteredPlots )
{
m_tc.GetItem( p ).SetVisible( true );
}
@@ -688,19 +803,47 @@ void View::DrawOptions()
ImGui::SameLine();
if( ImGui::SmallButton( "Unselect all" ) )
{
for( const auto& p : m_worker.GetPlots() )
for( const auto* p : filteredPlots )
{
m_tc.GetItem( p ).SetVisible( false );
}
}
for( const auto& p : m_worker.GetPlots() )
ImGui::SameLine();
m_plotFilter.Draw( ICON_FA_FILTER "###plotFilter", 200 );
ImGui::SameLine();
if( ImGui::Button( ICON_FA_DELETE_LEFT " Clear###plotFilterClear" ) )
{
SmallColorBox( GetPlotColor( *p, m_worker ) );
ImGui::SameLine();
m_tc.GetItem( p ).VisibilityCheckbox();
ImGui::SameLine();
ImGui::TextDisabled( "%s data points", RealToString( p->data.size() ) );
m_plotFilter.Clear();
}
if( hierarchy )
{
PlotTreeNode root;
BuildPlotTree( root, filteredPlots, m_worker );
for( auto& child : root.children )
{
DrawPlotTreeNode( child.first, child.first, child.second, m_tc, m_worker );
}
for( const auto* p : root.plots )
{
SmallColorBox( GetPlotColor( *p, m_worker ) );
ImGui::SameLine();
m_tc.GetItem( p ).VisibilityCheckbox();
ImGui::SameLine();
ImGui::TextDisabled( "%s data points", RealToString( p->data.size() ) );
}
}
else
{
for( const auto* p : filteredPlots )
{
SmallColorBox( GetPlotColor( *p, m_worker ) );
ImGui::SameLine();
m_tc.GetItem( p ).VisibilityCheckbox();
ImGui::SameLine();
ImGui::TextDisabled( "%s data points", RealToString( p->data.size() ) );
}
}
ImGui::TreePop();
}