From 1b2730adf46ae9e5a7a5d1f3ce856ec0ad3da866 Mon Sep 17 00:00:00 2001 From: Marcos Slomp Date: Tue, 15 Sep 2026 19:21:55 -0700 Subject: [PATCH 1/5] add ability to "pin" timeline tracks (queues, threads and plots) --- .../src/profiler/TracyTimelineController.cpp | 111 +++++++++++++++--- .../src/profiler/TracyTimelineController.hpp | 2 + profiler/src/profiler/TracyTimelineItem.cpp | 6 + profiler/src/profiler/TracyTimelineItem.hpp | 7 ++ .../src/profiler/TracyTimelineItemPlot.hpp | 2 + 5 files changed, 114 insertions(+), 14 deletions(-) diff --git a/profiler/src/profiler/TracyTimelineController.cpp b/profiler/src/profiler/TracyTimelineController.cpp index 8ad815f7..b3c99bd9 100644 --- a/profiler/src/profiler/TracyTimelineController.cpp +++ b/profiler/src/profiler/TracyTimelineController.cpp @@ -16,6 +16,7 @@ TimelineController::TimelineController( View& view, Worker& worker, bool threadi , m_scroll( 0 ) , m_centerItemkey( nullptr ) , m_centerItemOffsetY( 0 ) + , m_pinnedTop( 0 ) , m_firstFrame( true ) , m_view( view ) , m_worker( worker ) @@ -53,16 +54,28 @@ void TimelineController::UpdateCenterItem() const auto timelineMousePosY = mousePos.y - ImGui::GetWindowPos().y; int centerY = timelineMousePosY + ImGui::GetScrollY(); - int yBegin = 0; - int yEnd = 0; + // Pinned items are fixed to the viewport and excluded from the scrolling + // flow, so centering only considers the normal items in the middle band. + const void* firstNormalKey = nullptr; + const void* lastNormalKey = nullptr; for( auto& item : m_items ) { + if( item->IsPinned() ) continue; + if( !firstNormalKey ) firstNormalKey = item->GetKey(); + lastNormalKey = item->GetKey(); + } + + int yBegin = 0; + int yEnd = m_pinnedTop; + for( auto& item : m_items ) + { + if( item->IsPinned() ) continue; m_centerItemkey = item->GetKey(); yBegin = yEnd; yEnd += item->GetHeight(); - const auto inLowerBounds = m_centerItemkey == m_items.front()->GetKey() || yBegin <= centerY; - const auto inUpperBounds = m_centerItemkey == m_items.back()->GetKey() || centerY < yEnd; + const auto inLowerBounds = m_centerItemkey == firstNormalKey || yBegin <= centerY; + const auto inUpperBounds = m_centerItemkey == lastNormalKey || centerY < yEnd; if( inLowerBounds && inUpperBounds ) { @@ -83,9 +96,10 @@ std::optional TimelineController::CalculateScrollPosition() const const auto timelineMousePosY = mousePos.y - ImGui::GetWindowPos().y; int yBegin = 0; - int yEnd = 0; + int yEnd = m_pinnedTop; for( auto& item : m_items ) { + if( item->IsPinned() ) continue; yBegin = yEnd; yEnd += item->GetHeight(); @@ -101,6 +115,19 @@ std::optional TimelineController::CalculateScrollPosition() const void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool vcenter, float yMin, float yMax ) { + // Pinned tracks are lifted out of the scrolling flow and drawn fixed to the + // top (queues/threads) or bottom (plots) of the viewport. Heights are the + // previous frame's values (zero on the first frame); items still animate. + int pinnedTop = 0; + int pinnedBottom = 0; + for( auto& item : m_items ) + { + if( !item->IsPinned() ) continue; + if( item->PinToBottom() ) pinnedBottom += item->GetHeight(); + else pinnedTop += item->GetHeight(); + } + m_pinnedTop = pinnedTop; + auto shouldUpdateCenterItem = [&] () { const auto imguiChangedScroll = m_scroll != ImGui::GetScrollY(); const auto& mouseDelta = ImGui::GetIO().MouseDelta; @@ -138,28 +165,84 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool ctx.wpos = wpos; ctx.hover = hover; - int yOffset = 0; + const int curScrollY = (int)ImGui::GetScrollY(); + const int windowHeight = (int)ImGui::GetWindowHeight(); + const int bottomStart = curScrollY + windowHeight - pinnedBottom; + + // Content-space Y offset an item is drawn at. Pinned items compensate for the + // scroll position so they stay fixed in the viewport; normal items flow in the + // middle band, below the pinned-top band. + auto itemYOffset = [&] ( TimelineItem* item, int topRun, int normalRun, int bottomRun ) -> int { + if( !item->IsPinned() ) return pinnedTop + normalRun; + return item->PinToBottom() ? bottomStart + bottomRun : curScrollY + topRun; + }; + + int topOffset = 0, normalOffset = 0, bottomOffset = 0; for( auto& item : m_items ) { + const int off = itemYOffset( item, topOffset, normalOffset, bottomOffset ); if( item->WantPreprocess() && item->IsVisible() ) { - const auto yPos = wpos.y + yOffset; - const bool visible = m_firstFrame || ( yPos < yMax && yPos + item->GetHeight() >= yMin ); + const auto yPos = wpos.y + off; + const bool visible = item->IsPinned() || m_firstFrame || ( yPos < yMax && yPos + item->GetHeight() >= yMin ); item->Preprocess( ctx, m_td, visible, yPos ); } - yOffset += m_firstFrame ? 0 : item->GetHeight(); + const int h = m_firstFrame ? 0 : item->GetHeight(); + if( !item->IsPinned() ) normalOffset += h; + else if( item->PinToBottom() ) bottomOffset += h; + else topOffset += h; } m_td.Sync(); - yOffset = 0; + auto draw = ImGui::GetWindowDrawList(); + const auto bgColor = ImGui::GetColorU32( ImGuiCol_WindowBg ); + + // Normal (middle) items. Cull headers to the band between the pinned regions, + // and suppress interaction whenever the mouse is over a pinned band, so tracks + // scrolling underneath a pinned band stay inert. + TimelineContext ctxNormal = ctx; + if( pinnedTop > 0 ) ctxNormal.yMin = std::max( ctx.yMin, wpos.y + curScrollY + pinnedTop ); + if( pinnedBottom > 0 ) ctxNormal.yMax = std::min( ctx.yMax, wpos.y + bottomStart ); + const auto mouseY = ImGui::GetMousePos().y; + const bool mouseInTopBand = pinnedTop > 0 && mouseY >= wpos.y + curScrollY && mouseY < wpos.y + curScrollY + pinnedTop; + const bool mouseInBottomBand = pinnedBottom > 0 && mouseY >= wpos.y + bottomStart && mouseY < wpos.y + curScrollY + windowHeight; + ctxNormal.hover = ctx.hover && !mouseInTopBand && !mouseInBottomBand; + int normalRunning = 0; for( auto& item : m_items ) { - auto currentFrameItemHeight = item->GetHeight(); - item->Draw( m_firstFrame, ctx, yOffset ); - if( m_firstFrame ) currentFrameItemHeight = item->GetHeight(); - yOffset += currentFrameItemHeight; + if( item->IsPinned() ) continue; + auto h = item->GetHeight(); + item->Draw( m_firstFrame, ctxNormal, pinnedTop + normalRunning ); + if( m_firstFrame ) h = item->GetHeight(); + normalRunning += h; } + // Pinned-top band (queues/threads), fixed at the viewport top. + if( pinnedTop > 0 ) draw->AddRectFilled( ImVec2( wpos.x, wpos.y + curScrollY ), ImVec2( wpos.x + ctx.w, wpos.y + curScrollY + pinnedTop ), bgColor ); + int topRunning = 0; + for( auto& item : m_items ) + { + if( !item->IsPinned() || item->PinToBottom() ) continue; + auto h = item->GetHeight(); + item->Draw( m_firstFrame, ctx, curScrollY + topRunning ); + if( m_firstFrame ) h = item->GetHeight(); + topRunning += h; + } + + // Pinned-bottom band (plots), fixed at the viewport bottom. + if( pinnedBottom > 0 ) draw->AddRectFilled( ImVec2( wpos.x, wpos.y + bottomStart ), ImVec2( wpos.x + ctx.w, wpos.y + curScrollY + windowHeight ), bgColor ); + int bottomRunning = 0; + for( auto& item : m_items ) + { + if( !item->IsPinned() || !item->PinToBottom() ) continue; + auto h = item->GetHeight(); + item->Draw( m_firstFrame, ctx, bottomStart + bottomRunning ); + if( m_firstFrame ) h = item->GetHeight(); + bottomRunning += h; + } + + int yOffset = pinnedTop + normalRunning + pinnedBottom; + if( const auto scrollY = CalculateScrollPosition() ) { int clampedScrollY = std::min( *scrollY, std::max( yOffset - ImGui::GetWindowHeight(), 0 ) ); diff --git a/profiler/src/profiler/TracyTimelineController.hpp b/profiler/src/profiler/TracyTimelineController.hpp index aebc6073..d6d5b179 100644 --- a/profiler/src/profiler/TracyTimelineController.hpp +++ b/profiler/src/profiler/TracyTimelineController.hpp @@ -58,6 +58,8 @@ private: const void* m_centerItemkey; int m_centerItemOffsetY; + int m_pinnedTop; + bool m_firstFrame; View& m_view; diff --git a/profiler/src/profiler/TracyTimelineItem.cpp b/profiler/src/profiler/TracyTimelineItem.cpp index d44f26c1..0591b245 100644 --- a/profiler/src/profiler/TracyTimelineItem.cpp +++ b/profiler/src/profiler/TracyTimelineItem.cpp @@ -12,6 +12,7 @@ namespace tracy TimelineItem::TimelineItem( View& view, Worker& worker, const void* key, bool wantPreprocess ) : m_visible( true ) , m_showFull( true ) + , m_pinned( false ) , m_height( 0 ) , m_wantPreprocess( wantPreprocess ) , m_key( key ) @@ -121,6 +122,11 @@ void TimelineItem::Draw( bool firstFrame, const TimelineContext& ctx, int yOffse SetVisible( false ); ImGui::CloseCurrentPopup(); } + if( ImGui::MenuItem( m_pinned ? ICON_FA_THUMBTACK_SLASH " Unpin" : ICON_FA_THUMBTACK " Pin" ) ) + { + m_pinned = !m_pinned; + ImGui::CloseCurrentPopup(); + } DrawExtraPopupItems(); ImGui::EndPopup(); } diff --git a/profiler/src/profiler/TracyTimelineItem.hpp b/profiler/src/profiler/TracyTimelineItem.hpp index a1ec22a1..71297fa3 100644 --- a/profiler/src/profiler/TracyTimelineItem.hpp +++ b/profiler/src/profiler/TracyTimelineItem.hpp @@ -30,6 +30,12 @@ public: virtual void SetVisible( bool visible ) { m_visible = visible; } virtual bool IsVisible() const { return m_visible; } + void SetPinned( bool pinned ) { m_pinned = pinned; } + bool IsPinned() const { return m_pinned; } + // Pinned items are lifted out of the scrolling flow: false keeps them fixed + // at the top of the viewport (queues/threads), true at the bottom (plots). + virtual bool PinToBottom() const { return false; } + void SetShowFull( bool showFull ) { m_showFull = showFull; } // returns 0 instead of the correct value for the first frame @@ -58,6 +64,7 @@ protected: bool m_visible; bool m_showFull; + bool m_pinned; private: void AdjustThreadHeight( bool firstFrame, int yBegin, int yEnd ); diff --git a/profiler/src/profiler/TracyTimelineItemPlot.hpp b/profiler/src/profiler/TracyTimelineItemPlot.hpp index a57e98eb..410f3e83 100644 --- a/profiler/src/profiler/TracyTimelineItemPlot.hpp +++ b/profiler/src/profiler/TracyTimelineItemPlot.hpp @@ -19,6 +19,8 @@ protected: uint32_t HeaderLineColor() const override { return 0x8844DDDD; } const char* HeaderLabel() const override; + bool PinToBottom() const override { return true; } + int64_t RangeBegin() const override; int64_t RangeEnd() const override; From 9b1a7c72ba396431a40332566fbfceb09bf86302 Mon Sep 17 00:00:00 2001 From: Marcos Slomp Date: Wed, 16 Sep 2026 10:34:57 -0700 Subject: [PATCH 2/5] Addressing code review. --- NEWS | 1 + manual/tracy.tex | 1 + .../src/profiler/TracyTimelineController.cpp | 37 +++++++++---------- .../src/profiler/TracyTimelineController.hpp | 6 +-- profiler/src/profiler/TracyTimelineItem.cpp | 4 +- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/NEWS b/NEWS index df8fe542..2bd66f18 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ here. vx.xx.x (2026-xx-xx) -------------------- +- Ability to pin/anchor threads and plots to the viewport. - API break: various lock-related "AfterUnlock" instrumentation methods were renamed to "BeforeUnlock". Call sites need to be adjusted with regard to the placement around the lock unlock calls, to match the name. diff --git a/manual/tracy.tex b/manual/tracy.tex index 593ed949..2c586caf 100644 --- a/manual/tracy.tex +++ b/manual/tracy.tex @@ -4134,6 +4134,7 @@ Labels accompanied by the \faCaretDown{}~symbol can be collapsed out of the view \begin{itemize} \item \emph{\faEyeSlash{}~Hide} -- Hides the label along with the content associated to it. To make the label visible again, you must find it in the options menu (section~\ref{options}). +\item \emph{\faThumbtack{}~Pin} -- Pins the track so that it remains visible while scrolling the timeline view vertically. Pinned threads and GPU queues are anchored to the top of the view, while pinned plots are anchored to the bottom. Selecting \emph{Unpin} on a pinned track releases it. \end{itemize} Under the \faCaretDown{}~symbol are a series of points that allow to limit the depth of the zones displayed. Hover the~\faArrowPointer{}~mouse pointer over a circle to display a line visualizing the cutting point, then click the \MMB{}~middle mouse button to apply or remove a zone depth limit. diff --git a/profiler/src/profiler/TracyTimelineController.cpp b/profiler/src/profiler/TracyTimelineController.cpp index b3c99bd9..75cb4e56 100644 --- a/profiler/src/profiler/TracyTimelineController.cpp +++ b/profiler/src/profiler/TracyTimelineController.cpp @@ -16,7 +16,6 @@ TimelineController::TimelineController( View& view, Worker& worker, bool threadi , m_scroll( 0 ) , m_centerItemkey( nullptr ) , m_centerItemOffsetY( 0 ) - , m_pinnedTop( 0 ) , m_firstFrame( true ) , m_view( view ) , m_worker( worker ) @@ -42,7 +41,7 @@ void TimelineController::Begin() m_items.clear(); } -void TimelineController::UpdateCenterItem() +void TimelineController::UpdateCenterItem( int pinnedTop ) { ImVec2 mousePos = ImGui::GetMousePos(); @@ -66,7 +65,7 @@ void TimelineController::UpdateCenterItem() } int yBegin = 0; - int yEnd = m_pinnedTop; + int yEnd = pinnedTop; for( auto& item : m_items ) { if( item->IsPinned() ) continue; @@ -85,7 +84,7 @@ void TimelineController::UpdateCenterItem() } } -std::optional TimelineController::CalculateScrollPosition() const +std::optional TimelineController::CalculateScrollPosition( int pinnedTop ) const { if( !m_centerItemkey ) return std::nullopt; @@ -96,7 +95,7 @@ std::optional TimelineController::CalculateScrollPosition() const const auto timelineMousePosY = mousePos.y - ImGui::GetWindowPos().y; int yBegin = 0; - int yEnd = m_pinnedTop; + int yEnd = pinnedTop; for( auto& item : m_items ) { if( item->IsPinned() ) continue; @@ -110,14 +109,16 @@ std::optional TimelineController::CalculateScrollPosition() const return scrollY; } + // A "just-pinned" center item is gone from the normal run but will be + // "re-picked" on next mouse move. return std::nullopt; } void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool vcenter, float yMin, float yMax ) { - // Pinned tracks are lifted out of the scrolling flow and drawn fixed to the - // top (queues/threads) or bottom (plots) of the viewport. Heights are the - // previous frame's values (zero on the first frame); items still animate. + // GetHeight() is 0 on the first frame, so the scroll extent would be shorter + // if a track was pinned at load, but would "self-correct" in the next frame. + // (Also, nothing is pinned at load anyway.) int pinnedTop = 0; int pinnedBottom = 0; for( auto& item : m_items ) @@ -126,7 +127,6 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool if( item->PinToBottom() ) pinnedBottom += item->GetHeight(); else pinnedTop += item->GetHeight(); } - m_pinnedTop = pinnedTop; auto shouldUpdateCenterItem = [&] () { const auto imguiChangedScroll = m_scroll != ImGui::GetScrollY(); @@ -144,7 +144,7 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool } else if( shouldUpdateCenterItem() ) { - UpdateCenterItem(); + UpdateCenterItem( pinnedTop ); } const auto& viewData = m_view.GetViewData(); @@ -167,11 +167,11 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool const int curScrollY = (int)ImGui::GetScrollY(); const int windowHeight = (int)ImGui::GetWindowHeight(); - const int bottomStart = curScrollY + windowHeight - pinnedBottom; + // Keep the pinned bands from overlapping (top wins), otherwise bottom items + // would cover top ones while leaving them clickable underneath. + const int bottomStart = std::max( curScrollY + pinnedTop, curScrollY + windowHeight - pinnedBottom ); - // Content-space Y offset an item is drawn at. Pinned items compensate for the - // scroll position so they stay fixed in the viewport; normal items flow in the - // middle band, below the pinned-top band. + // Pinned items add the scroll offset so they stay fixed as content scrolls. auto itemYOffset = [&] ( TimelineItem* item, int topRun, int normalRun, int bottomRun ) -> int { if( !item->IsPinned() ) return pinnedTop + normalRun; return item->PinToBottom() ? bottomStart + bottomRun : curScrollY + topRun; @@ -197,9 +197,8 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool auto draw = ImGui::GetWindowDrawList(); const auto bgColor = ImGui::GetColorU32( ImGuiCol_WindowBg ); - // Normal (middle) items. Cull headers to the band between the pinned regions, - // and suppress interaction whenever the mouse is over a pinned band, so tracks - // scrolling underneath a pinned band stay inert. + // Keep tracks scrolling under a pinned band inert (cull their headers to the + // middle region and drop hover while the mouse is over a band). TimelineContext ctxNormal = ctx; if( pinnedTop > 0 ) ctxNormal.yMin = std::max( ctx.yMin, wpos.y + curScrollY + pinnedTop ); if( pinnedBottom > 0 ) ctxNormal.yMax = std::min( ctx.yMax, wpos.y + bottomStart ); @@ -217,7 +216,6 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool normalRunning += h; } - // Pinned-top band (queues/threads), fixed at the viewport top. if( pinnedTop > 0 ) draw->AddRectFilled( ImVec2( wpos.x, wpos.y + curScrollY ), ImVec2( wpos.x + ctx.w, wpos.y + curScrollY + pinnedTop ), bgColor ); int topRunning = 0; for( auto& item : m_items ) @@ -229,7 +227,6 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool topRunning += h; } - // Pinned-bottom band (plots), fixed at the viewport bottom. if( pinnedBottom > 0 ) draw->AddRectFilled( ImVec2( wpos.x, wpos.y + bottomStart ), ImVec2( wpos.x + ctx.w, wpos.y + curScrollY + windowHeight ), bgColor ); int bottomRunning = 0; for( auto& item : m_items ) @@ -243,7 +240,7 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool int yOffset = pinnedTop + normalRunning + pinnedBottom; - if( const auto scrollY = CalculateScrollPosition() ) + if( const auto scrollY = CalculateScrollPosition( pinnedTop ) ) { int clampedScrollY = std::min( *scrollY, std::max( yOffset - ImGui::GetWindowHeight(), 0 ) ); ImGui::SetScrollY( clampedScrollY ); diff --git a/profiler/src/profiler/TracyTimelineController.hpp b/profiler/src/profiler/TracyTimelineController.hpp index d6d5b179..30983c2b 100644 --- a/profiler/src/profiler/TracyTimelineController.hpp +++ b/profiler/src/profiler/TracyTimelineController.hpp @@ -46,8 +46,8 @@ public: } private: - void UpdateCenterItem(); - std::optional CalculateScrollPosition() const; + void UpdateCenterItem( int pinnedTop ); + std::optional CalculateScrollPosition( int pinnedTop ) const; std::vector m_items; unordered_flat_map> m_itemMap; @@ -58,8 +58,6 @@ private: const void* m_centerItemkey; int m_centerItemOffsetY; - int m_pinnedTop; - bool m_firstFrame; View& m_view; diff --git a/profiler/src/profiler/TracyTimelineItem.cpp b/profiler/src/profiler/TracyTimelineItem.cpp index 0591b245..189a9208 100644 --- a/profiler/src/profiler/TracyTimelineItem.cpp +++ b/profiler/src/profiler/TracyTimelineItem.cpp @@ -122,9 +122,9 @@ void TimelineItem::Draw( bool firstFrame, const TimelineContext& ctx, int yOffse SetVisible( false ); ImGui::CloseCurrentPopup(); } - if( ImGui::MenuItem( m_pinned ? ICON_FA_THUMBTACK_SLASH " Unpin" : ICON_FA_THUMBTACK " Pin" ) ) + if( ImGui::MenuItem( IsPinned() ? ICON_FA_THUMBTACK_SLASH " Unpin" : ICON_FA_THUMBTACK " Pin" ) ) { - m_pinned = !m_pinned; + SetPinned( !IsPinned() ); ImGui::CloseCurrentPopup(); } DrawExtraPopupItems(); From ac4dee10649adc8bb3f4a19e9a75dc5316566f40 Mon Sep 17 00:00:00 2001 From: Marcos Slomp Date: Mon, 21 Sep 2026 08:27:00 -0700 Subject: [PATCH 3/5] addressing code review --- .../src/profiler/TracyTimelineController.cpp | 125 +++++++++--------- .../src/profiler/TracyTimelineController.hpp | 3 + profiler/src/profiler/TracyView.hpp | 2 +- profiler/src/profiler/TracyView_CpuData.cpp | 2 +- .../src/profiler/TracyView_ZoneTimeline.cpp | 6 +- 5 files changed, 69 insertions(+), 69 deletions(-) diff --git a/profiler/src/profiler/TracyTimelineController.cpp b/profiler/src/profiler/TracyTimelineController.cpp index 75cb4e56..f2d32c08 100644 --- a/profiler/src/profiler/TracyTimelineController.cpp +++ b/profiler/src/profiler/TracyTimelineController.cpp @@ -55,20 +55,14 @@ void TimelineController::UpdateCenterItem( int pinnedTop ) // Pinned items are fixed to the viewport and excluded from the scrolling // flow, so centering only considers the normal items in the middle band. - const void* firstNormalKey = nullptr; - const void* lastNormalKey = nullptr; - for( auto& item : m_items ) - { - if( item->IsPinned() ) continue; - if( !firstNormalKey ) firstNormalKey = item->GetKey(); - lastNormalKey = item->GetKey(); - } + if( m_normalItems.empty() ) return; + const void* firstNormalKey = m_normalItems.front()->GetKey(); + const void* lastNormalKey = m_normalItems.back()->GetKey(); int yBegin = 0; int yEnd = pinnedTop; - for( auto& item : m_items ) + for( auto& item : m_normalItems ) { - if( item->IsPinned() ) continue; m_centerItemkey = item->GetKey(); yBegin = yEnd; yEnd += item->GetHeight(); @@ -96,9 +90,8 @@ std::optional TimelineController::CalculateScrollPosition( int pinnedTop ) int yBegin = 0; int yEnd = pinnedTop; - for( auto& item : m_items ) + for( auto& item : m_normalItems ) { - if( item->IsPinned() ) continue; yBegin = yEnd; yEnd += item->GetHeight(); @@ -116,16 +109,23 @@ std::optional TimelineController::CalculateScrollPosition( int pinnedTop ) void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool vcenter, float yMin, float yMax ) { + // Snapshot the pin classification once per frame before any Draw runs. + // Toggling a pin flips its state mid-frame and since re-checking it in + // the draw passes would draw the item twice (normal and band passes), + // the grouping here defers the change to the next frame. // GetHeight() is 0 on the first frame, so the scroll extent would be shorter - // if a track was pinned at load, but would "self-correct" in the next frame. - // (Also, nothing is pinned at load anyway.) + // if a track was pinned at load, but self-corrects next frame (and nothing is + // pinned at load anyway). + m_normalItems.clear(); + m_pinnedTopItems.clear(); + m_pinnedBottomItems.clear(); int pinnedTop = 0; int pinnedBottom = 0; for( auto& item : m_items ) { - if( !item->IsPinned() ) continue; - if( item->PinToBottom() ) pinnedBottom += item->GetHeight(); - else pinnedTop += item->GetHeight(); + if( !item->IsPinned() ) m_normalItems.push_back( item ); + else if( item->PinToBottom() ) { m_pinnedBottomItems.push_back( item ); pinnedBottom += item->GetHeight(); } + else { m_pinnedTopItems.push_back( item ); pinnedTop += item->GetHeight(); } } auto shouldUpdateCenterItem = [&] () { @@ -171,75 +171,72 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool // would cover top ones while leaving them clickable underneath. const int bottomStart = std::max( curScrollY + pinnedTop, curScrollY + windowHeight - pinnedBottom ); - // Pinned items add the scroll offset so they stay fixed as content scrolls. - auto itemYOffset = [&] ( TimelineItem* item, int topRun, int normalRun, int bottomRun ) -> int { - if( !item->IsPinned() ) return pinnedTop + normalRun; - return item->PinToBottom() ? bottomStart + bottomRun : curScrollY + topRun; - }; + // Screen-space band edges, shared by culling, the mouse tests, and the fills. + const float topBandBegin = wpos.y + curScrollY; + const float topBandEnd = topBandBegin + pinnedTop; + const float bottomBandBegin = wpos.y + bottomStart; + const float bottomBandEnd = topBandBegin + windowHeight; + TimelineContext ctxNormal = ctx; + if( pinnedTop > 0 ) ctxNormal.yMin = std::max( ctx.yMin, topBandEnd ); + if( pinnedBottom > 0 ) ctxNormal.yMax = std::min( ctx.yMax, bottomBandBegin ); + const auto mouseY = ImGui::GetMousePos().y; + const bool mouseInTopBand = pinnedTop > 0 && mouseY >= topBandBegin && mouseY < topBandEnd; + const bool mouseInBottomBand = pinnedBottom > 0 && mouseY >= bottomBandBegin && mouseY < bottomBandEnd; + ctxNormal.hover = ctx.hover && !mouseInTopBand && !mouseInBottomBand; + + // Preprocess runs before any Draw, so live pin state is stable here. Normal + // items are culled to the middle band and pinned items are always on screen. int topOffset = 0, normalOffset = 0, bottomOffset = 0; for( auto& item : m_items ) { - const int off = itemYOffset( item, topOffset, normalOffset, bottomOffset ); + const bool pinned = item->IsPinned(); + const bool toBottom = pinned && item->PinToBottom(); + const int off = !pinned ? pinnedTop + normalOffset : toBottom ? bottomStart + bottomOffset : curScrollY + topOffset; if( item->WantPreprocess() && item->IsVisible() ) { const auto yPos = wpos.y + off; - const bool visible = item->IsPinned() || m_firstFrame || ( yPos < yMax && yPos + item->GetHeight() >= yMin ); + const bool visible = pinned || m_firstFrame || ( yPos < ctxNormal.yMax && yPos + item->GetHeight() >= ctxNormal.yMin ); item->Preprocess( ctx, m_td, visible, yPos ); } const int h = m_firstFrame ? 0 : item->GetHeight(); - if( !item->IsPinned() ) normalOffset += h; - else if( item->PinToBottom() ) bottomOffset += h; + if( !pinned ) normalOffset += h; + else if( toBottom ) bottomOffset += h; else topOffset += h; } m_td.Sync(); auto draw = ImGui::GetWindowDrawList(); + // Matches the timeline background in normal builds: the root-window build + // overrides WindowBg per-window, so the fill can be a slightly off shade there. const auto bgColor = ImGui::GetColorU32( ImGuiCol_WindowBg ); - // Keep tracks scrolling under a pinned band inert (cull their headers to the - // middle region and drop hover while the mouse is over a band). - TimelineContext ctxNormal = ctx; - if( pinnedTop > 0 ) ctxNormal.yMin = std::max( ctx.yMin, wpos.y + curScrollY + pinnedTop ); - if( pinnedBottom > 0 ) ctxNormal.yMax = std::min( ctx.yMax, wpos.y + bottomStart ); - const auto mouseY = ImGui::GetMousePos().y; - const bool mouseInTopBand = pinnedTop > 0 && mouseY >= wpos.y + curScrollY && mouseY < wpos.y + curScrollY + pinnedTop; - const bool mouseInBottomBand = pinnedBottom > 0 && mouseY >= wpos.y + bottomStart && mouseY < wpos.y + curScrollY + windowHeight; - ctxNormal.hover = ctx.hover && !mouseInTopBand && !mouseInBottomBand; - int normalRunning = 0; - for( auto& item : m_items ) - { - if( item->IsPinned() ) continue; - auto h = item->GetHeight(); - item->Draw( m_firstFrame, ctxNormal, pinnedTop + normalRunning ); - if( m_firstFrame ) h = item->GetHeight(); - normalRunning += h; - } + // Draws a group at (base + accumulated height) with respect to the + // first-frame height bootstrap, and returns the total height drawn. + auto drawRun = [&]( std::vector& items, const TimelineContext& c, int base ) -> int { + int running = 0; + for( auto& item : items ) + { + auto h = item->GetHeight(); + item->Draw( m_firstFrame, c, base + running ); + if( m_firstFrame ) h = item->GetHeight(); + running += h; + } + return running; + }; - if( pinnedTop > 0 ) draw->AddRectFilled( ImVec2( wpos.x, wpos.y + curScrollY ), ImVec2( wpos.x + ctx.w, wpos.y + curScrollY + pinnedTop ), bgColor ); - int topRunning = 0; - for( auto& item : m_items ) - { - if( !item->IsPinned() || item->PinToBottom() ) continue; - auto h = item->GetHeight(); - item->Draw( m_firstFrame, ctx, curScrollY + topRunning ); - if( m_firstFrame ) h = item->GetHeight(); - topRunning += h; - } + const int normalRunning = drawRun( m_normalItems, ctxNormal, pinnedTop ); - if( pinnedBottom > 0 ) draw->AddRectFilled( ImVec2( wpos.x, wpos.y + bottomStart ), ImVec2( wpos.x + ctx.w, wpos.y + curScrollY + windowHeight ), bgColor ); - int bottomRunning = 0; - for( auto& item : m_items ) - { - if( !item->IsPinned() || !item->PinToBottom() ) continue; - auto h = item->GetHeight(); - item->Draw( m_firstFrame, ctx, bottomStart + bottomRunning ); - if( m_firstFrame ) h = item->GetHeight(); - bottomRunning += h; - } + // Opaque fills so tracks scrolling under a band do not show through. These also + // hide parent-list overlays drawn before the child. + if( pinnedTop > 0 ) draw->AddRectFilled( ImVec2( wpos.x, topBandBegin ), ImVec2( wpos.x + ctx.w, topBandEnd ), bgColor ); + drawRun( m_pinnedTopItems, ctx, curScrollY ); + if( pinnedBottom > 0 ) draw->AddRectFilled( ImVec2( wpos.x, bottomBandBegin ), ImVec2( wpos.x + ctx.w, bottomBandEnd ), bgColor ); + drawRun( m_pinnedBottomItems, ctx, bottomStart ); int yOffset = pinnedTop + normalRunning + pinnedBottom; + // pinnedTop is a pre-Draw height, so vertical-centre compensation lags one frame if( const auto scrollY = CalculateScrollPosition( pinnedTop ) ) { int clampedScrollY = std::min( *scrollY, std::max( yOffset - ImGui::GetWindowHeight(), 0 ) ); diff --git a/profiler/src/profiler/TracyTimelineController.hpp b/profiler/src/profiler/TracyTimelineController.hpp index 30983c2b..4c7791d0 100644 --- a/profiler/src/profiler/TracyTimelineController.hpp +++ b/profiler/src/profiler/TracyTimelineController.hpp @@ -50,6 +50,9 @@ private: std::optional CalculateScrollPosition( int pinnedTop ) const; std::vector m_items; + std::vector m_normalItems; + std::vector m_pinnedTopItems; + std::vector m_pinnedBottomItems; unordered_flat_map> m_itemMap; float m_height; diff --git a/profiler/src/profiler/TracyView.hpp b/profiler/src/profiler/TracyView.hpp index c68ec688..633326c0 100644 --- a/profiler/src/profiler/TracyView.hpp +++ b/profiler/src/profiler/TracyView.hpp @@ -327,7 +327,7 @@ private: void DrawTimeline(); void DrawSampleList( const TimelineContext& ctx, const std::vector& drawList, const Vector& vec, int offset, uint64_t tid ); void DrawZoneList( const TimelineContext& ctx, const std::vector& drawList, int offset, uint64_t tid, int maxDepth, double margin ); - void DrawThreadCropper( const int depth, const uint64_t tid, const float xPos, const float yPos, const float ostep, const float cropperWidth, const bool hasCtxSwitches ); + void DrawThreadCropper( const int depth, const uint64_t tid, const float xPos, const float yPos, const float ostep, const float cropperWidth, const bool hasCtxSwitches, const bool hover ); void DrawContextSwitchList( const TimelineContext& ctx, const std::vector& drawList, const Vector& ctxSwitch, int offset, int endOffset, bool isFiber, uint64_t tid ); int DispatchGpuZoneLevel( const Vector>& vec, bool hover, double pxns, int64_t nspx, const ImVec2& wpos, int offset, int depth, uint64_t thread, float yMin, float yMax, int64_t begin, int drift ); template diff --git a/profiler/src/profiler/TracyView_CpuData.cpp b/profiler/src/profiler/TracyView_CpuData.cpp index bde2b96d..df31b69b 100644 --- a/profiler/src/profiler/TracyView_CpuData.cpp +++ b/profiler/src/profiler/TracyView_CpuData.cpp @@ -445,7 +445,7 @@ bool View::DrawCpuData( const TimelineContext& ctx, const std::vector 0 ) DrawThreadCropper( depth, thread.id, croppperPosX, yPos, ostep, cropperWidth, hasCtxSwitch ); + if( depth > 0 ) DrawThreadCropper( depth, thread.id, croppperPosX, yPos, ostep, cropperWidth, hasCtxSwitch, ctx.hover ); } } @@ -640,7 +640,7 @@ void View::DrawZoneList( const TimelineContext& ctx, const std::vectorAddCircle( center, hradius, 0xFFFFFFFF, 0, hoverCircleThickness ); const float wPosX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMin().x; From 88357187ff23fd6bb3053150d1871d0b2f5d873a Mon Sep 17 00:00:00 2001 From: Marcos Slomp Date: Mon, 5 Oct 2026 14:38:09 -0700 Subject: [PATCH 4/5] addressing pin tyrack color issues --- profiler/src/profiler/TracyTimelineController.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/profiler/src/profiler/TracyTimelineController.cpp b/profiler/src/profiler/TracyTimelineController.cpp index f2d32c08..37d8e90a 100644 --- a/profiler/src/profiler/TracyTimelineController.cpp +++ b/profiler/src/profiler/TracyTimelineController.cpp @@ -207,9 +207,6 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool m_td.Sync(); auto draw = ImGui::GetWindowDrawList(); - // Matches the timeline background in normal builds: the root-window build - // overrides WindowBg per-window, so the fill can be a slightly off shade there. - const auto bgColor = ImGui::GetColorU32( ImGuiCol_WindowBg ); // Draws a group at (base + accumulated height) with respect to the // first-frame height bootstrap, and returns the total height drawn. @@ -225,13 +222,12 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool return running; }; + // Clip (rather than paint over) so parent-window overlays stay visible inside the bands. + ImGui::PushClipRect( ImVec2( draw->GetClipRectMin().x, topBandEnd ), ImVec2( draw->GetClipRectMax().x, bottomBandBegin ), true ); const int normalRunning = drawRun( m_normalItems, ctxNormal, pinnedTop ); + ImGui::PopClipRect(); - // Opaque fills so tracks scrolling under a band do not show through. These also - // hide parent-list overlays drawn before the child. - if( pinnedTop > 0 ) draw->AddRectFilled( ImVec2( wpos.x, topBandBegin ), ImVec2( wpos.x + ctx.w, topBandEnd ), bgColor ); drawRun( m_pinnedTopItems, ctx, curScrollY ); - if( pinnedBottom > 0 ) draw->AddRectFilled( ImVec2( wpos.x, bottomBandBegin ), ImVec2( wpos.x + ctx.w, bottomBandEnd ), bgColor ); drawRun( m_pinnedBottomItems, ctx, bottomStart ); int yOffset = pinnedTop + normalRunning + pinnedBottom; From 2c8971809f3310cc2c51a7f1e1ad5ce09a3043b2 Mon Sep 17 00:00:00 2001 From: Marcos Slomp Date: Sat, 10 Oct 2026 12:49:48 -0700 Subject: [PATCH 5/5] adding border and pin icon to pinned tracks --- profiler/src/profiler/TracyTimelineController.cpp | 8 ++++++-- profiler/src/profiler/TracyTimelineItem.cpp | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/profiler/src/profiler/TracyTimelineController.cpp b/profiler/src/profiler/TracyTimelineController.cpp index 37d8e90a..060886a1 100644 --- a/profiler/src/profiler/TracyTimelineController.cpp +++ b/profiler/src/profiler/TracyTimelineController.cpp @@ -227,8 +227,12 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool const int normalRunning = drawRun( m_normalItems, ctxNormal, pinnedTop ); ImGui::PopClipRect(); - drawRun( m_pinnedTopItems, ctx, curScrollY ); - drawRun( m_pinnedBottomItems, ctx, bottomStart ); + const int topRunning = drawRun( m_pinnedTopItems, ctx, curScrollY ); + const int bottomRunning = drawRun( m_pinnedBottomItems, ctx, bottomStart ); + + const auto pinBorderColor = 0xFF66FFFF; + if( topRunning > 0 ) draw->AddRect( ImVec2( wpos.x + 0.5f, topBandBegin + 0.5f ), ImVec2( wpos.x + ctx.w - 0.5f, topBandBegin + topRunning - 0.5f ), pinBorderColor ); + if( bottomRunning > 0 ) draw->AddRect( ImVec2( wpos.x + 0.5f, bottomBandBegin + 0.5f ), ImVec2( wpos.x + ctx.w - 0.5f, bottomBandBegin + bottomRunning - 0.5f ), pinBorderColor ); int yOffset = pinnedTop + normalRunning + pinnedBottom; diff --git a/profiler/src/profiler/TracyTimelineItem.cpp b/profiler/src/profiler/TracyTimelineItem.cpp index 189a9208..1d117c08 100644 --- a/profiler/src/profiler/TracyTimelineItem.cpp +++ b/profiler/src/profiler/TracyTimelineItem.cpp @@ -83,13 +83,15 @@ void TimelineItem::Draw( bool firstFrame, const TimelineContext& ctx, int yOffse DrawTextContrast( draw, wpos + ImVec2( 0, hdrOffset ), colorInactive, ICON_FA_CARET_RIGHT ); } const auto label = HeaderLabel(); - labelWidth = ImGui::CalcTextSize( label ).x; + const auto pinWidth = IsPinned() ? ImGui::CalcTextSize( ICON_FA_THUMBTACK " " ).x : 0.f; + labelWidth = pinWidth + ImGui::CalcTextSize( label ).x; if( m_showFull ) { DrawLine( draw, dpos + ImVec2( 0, hdrOffset + ty - 1 ), dpos + ImVec2( w, hdrOffset + ty - 1 ), HeaderLineColor() ); HeaderExtraContents( ctx, hdrOffset, labelWidth ); } - DrawTextContrast( draw, wpos + ImVec2( ty, hdrOffset ), m_showFull ? color : colorInactive, label ); + if( IsPinned() ) DrawTextContrast( draw, wpos + ImVec2( ty, hdrOffset ), m_showFull ? color : colorInactive, ICON_FA_THUMBTACK ); + DrawTextContrast( draw, wpos + ImVec2( ty + pinWidth, hdrOffset ), m_showFull ? color : colorInactive, label ); if( ctx.hover && ImGui::IsMouseHoveringRect( wpos + ImVec2( 0, hdrOffset ), wpos + ImVec2( ty + labelWidth, hdrOffset + ty ) ) ) {