Skip to content

Commit 8d0e12c

Browse files
Spruill-1Copilot
andcommitted
Output window FPS unification + click-persistent FPS flyout
Two related UX fixes folded together because they both touch the FPS display surface: 1) OutputWindow now matches the main windows FPS counter exactly. The per-window m_frameCount / m_fpsTime / m_timingText state is gone -- every render tick presents to all output windows synchronously, so the main windows FPS *is* every output windows FPS. New SetStatusText() + SetStatusTooltip() setters take the canonical strings; PresentOutputWindows pushes them each frame. Both windows now display "60 fps | 16.5 ms" with the same per-phase breakdown on hover. Factored BuildFpsStatusText() and BuildFpsTooltipText() out of UpdateFpsTooltip() as the single source of truth so any future FPS surface (debugger overlay, MCP /perf, etc.) can reuse them. 2) The per-phase frame-timing breakdown is now a click-persistent Flyout instead of a hover Tooltip. The previous hover-tooltip auto-dismissed ~1s after the cursor entered, making the multi-line text effectively unreadable. Now: click the FPS counter to open, click outside to dismiss, no auto-dismiss. The button itself has a short hover tooltip ("Click for per-phase frame timing breakdown") so the affordance is discoverable. FpsTooltipText (the inner monospace TextBlock) is x:Named the same way it was inside the old Tooltip, so UpdateFpsTooltip() still targets it without changes. 154/154 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b574cc8 commit 8d0e12c

5 files changed

Lines changed: 88 additions & 44 deletions

File tree

Controls/OutputWindow.cpp

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ namespace ShaderLab::Controls
2525
m_dxgiFactory = dxgiFactory;
2626
m_nodeId = nodeId;
2727
m_format = format;
28-
m_fpsTime = std::chrono::steady_clock::now();
2928

3029
try
3130
{
@@ -58,7 +57,7 @@ namespace ShaderLab::Controls
5857
MUXC::Grid::SetRow(statusBar, 1);
5958

6059
m_fpsText = MUXC::TextBlock();
61-
m_fpsText.Text(L"0 FPS");
60+
m_fpsText.Text(L"-- fps");
6261
m_fpsText.Foreground(MUX::Media::SolidColorBrush(
6362
winrt::Windows::UI::Color{ 255, 180, 180, 180 }));
6463
m_fpsText.FontSize(11);
@@ -310,20 +309,11 @@ namespace ShaderLab::Controls
310309
dc->SetDpi(oldDpiX, oldDpiY);
311310
dc->SetTransform(&oldTransform);
312311

313-
// Update FPS counter.
314-
m_frameCount++;
315-
auto now = std::chrono::steady_clock::now();
316-
auto elapsed = std::chrono::duration<double>(now - m_fpsTime).count();
317-
if (elapsed >= 1.0)
318-
{
319-
uint32_t fps = static_cast<uint32_t>(m_frameCount / elapsed);
320-
if (m_timingText.empty())
321-
m_fpsText.Text(std::to_wstring(fps) + L" FPS");
322-
else
323-
m_fpsText.Text(std::format(L"{} FPS | {}", fps, m_timingText));
324-
m_frameCount = 0;
325-
m_fpsTime = now;
326-
}
312+
// FPS / timing display is driven by SetStatusText from the main
313+
// window so all windows show the same numbers in the same
314+
// format. No per-window counter -- every render tick presents
315+
// to all output windows synchronously, so per-window FPS would
316+
// be redundant.
327317
}
328318
catch (const winrt::hresult_error& ex)
329319
{
@@ -362,9 +352,23 @@ namespace ShaderLab::Controls
362352
m_window.Title(winrt::hstring(title));
363353
}
364354

365-
void OutputWindow::SetTimingText(const std::wstring& text)
355+
void OutputWindow::SetStatusText(const std::wstring& text)
356+
{
357+
if (m_fpsText)
358+
m_fpsText.Text(winrt::hstring(text));
359+
}
360+
361+
void OutputWindow::SetStatusTooltip(const std::wstring& tooltip)
366362
{
367-
m_timingText = text;
363+
if (!m_fpsText) return;
364+
// Wrap the tooltip text in a monospace TextBlock for readability.
365+
namespace MUX = winrt::Microsoft::UI::Xaml;
366+
namespace MUXC = winrt::Microsoft::UI::Xaml::Controls;
367+
auto tb = MUXC::TextBlock();
368+
tb.FontFamily(MUX::Media::FontFamily(L"Cascadia Mono, Consolas, Courier New"));
369+
tb.FontSize(11);
370+
tb.Text(winrt::hstring(tooltip));
371+
MUXC::ToolTipService::SetToolTip(m_fpsText, tb);
368372
}
369373

370374
void OutputWindow::CreateSwapChain()

Controls/OutputWindow.h

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@ namespace ShaderLab::Controls
3131
bool IsReady() const { return m_swapChain != nullptr; }
3232
uint32_t NodeId() const { return m_nodeId; }
3333
void SetTitle(const std::wstring& title);
34-
void SetTimingText(const std::wstring& text);
34+
// Push the canonical "NN fps | NN.N ms" status string from the main
35+
// window. Each render tick presents to all output windows
36+
// synchronously, so the main window's FPS *is* this window's FPS --
37+
// no point recomputing it per-window.
38+
void SetStatusText(const std::wstring& text);
39+
// Push the multi-line per-phase breakdown the main window shows in
40+
// its FPS-tooltip flyout. Used as the hover tooltip on this
41+
// window's status bar.
42+
void SetStatusTooltip(const std::wstring& tooltip);
3543

3644
private:
3745
void CreateSwapChain();
@@ -78,10 +86,9 @@ namespace ShaderLab::Controls
7886
float m_panOriginX{ 0.0f };
7987
float m_panOriginY{ 0.0f };
8088

81-
// FPS counter.
82-
uint32_t m_frameCount{ 0 };
83-
std::chrono::steady_clock::time_point m_fpsTime;
84-
std::wstring m_timingText;
89+
// FPS counter -- driven by main window via SetStatusText/SetStatusTooltip.
90+
// No per-window state; every render tick presents to all output windows
91+
// synchronously so the main window's FPS *is* this window's FPS.
8592

8693
// Event tokens for cleanup.
8794
winrt::event_token m_sizeChangedToken{};

MainWindow.xaml

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -321,19 +321,30 @@
321321
Style="{StaticResource CaptionTextBlockStyle}" />
322322
</StackPanel>
323323

324-
<TextBlock x:Name="FpsText" Grid.Column="6"
325-
Text="FPS: --"
326-
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
327-
Style="{StaticResource CaptionTextBlockStyle}">
328-
<ToolTipService.ToolTip>
329-
<ToolTip x:Name="FpsTooltip">
324+
<Button x:Name="FpsButton" Grid.Column="6"
325+
Background="Transparent"
326+
BorderThickness="0"
327+
Padding="4,0,4,0"
328+
MinWidth="0" MinHeight="0"
329+
Foreground="{ThemeResource TextFillColorSecondaryBrush}">
330+
<TextBlock x:Name="FpsText"
331+
Text="FPS: --"
332+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
333+
Style="{StaticResource CaptionTextBlockStyle}" />
334+
<Button.Flyout>
335+
<Flyout x:Name="FpsFlyout" ShowMode="Standard">
330336
<TextBlock x:Name="FpsTooltipText"
331337
FontFamily="Cascadia Mono, Consolas, Courier New"
332338
FontSize="11"
333339
Text="(no data yet)" />
340+
</Flyout>
341+
</Button.Flyout>
342+
<ToolTipService.ToolTip>
343+
<ToolTip>
344+
<TextBlock Text="Click for per-phase frame timing breakdown" />
334345
</ToolTip>
335346
</ToolTipService.ToolTip>
336-
</TextBlock>
347+
</Button>
337348
<TextBlock x:Name="AppVersionText" Grid.Column="7"
338349
Visibility="Collapsed" />
339350
</Grid>

MainWindow.xaml.cpp

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5393,10 +5393,11 @@ namespace winrt::ShaderLab::implementation
53935393
auto* dc = m_renderEngine.D2DDeviceContext();
53945394
if (!dc) return;
53955395

5396-
auto& ft = m_frameTiming;
5397-
std::wstring timingStr = std::format(L"{:.1f}ms (eval {:.1f} + compute {:.1f} + draw {:.1f})",
5398-
ft.totalUs / 1000.0, ft.evaluateUs / 1000.0,
5399-
ft.deferredComputeUs / 1000.0, ft.drawUs / 1000.0 + ft.presentUs / 1000.0);
5396+
// Canonical status string + tooltip pushed to every output window so
5397+
// they all show identical numbers in identical formatting to the main
5398+
// window's FPS counter.
5399+
std::wstring statusText = BuildFpsStatusText();
5400+
std::wstring tooltipText = BuildFpsTooltipText();
54005401

54015402
for (auto& window : m_outputWindows)
54025403
{
@@ -5407,7 +5408,8 @@ namespace winrt::ShaderLab::implementation
54075408
auto* node = m_graph.FindNode(window->NodeId());
54085409
if (node)
54095410
window->SetTitle(node->name);
5410-
window->SetTimingText(timingStr);
5411+
window->SetStatusText(statusText);
5412+
window->SetStatusTooltip(tooltipText);
54115413

54125414
auto* image = ResolveDisplayImage(window->NodeId());
54135415
window->Present(dc, image);
@@ -5454,15 +5456,11 @@ namespace winrt::ShaderLab::implementation
54545456
}
54555457
}
54565458

5457-
void MainWindow::UpdateFpsTooltip()
5459+
std::wstring MainWindow::BuildFpsTooltipText() const
54585460
{
5459-
// Refresh the TextBlock inside the FPS counter's tooltip with a
5460-
// fresh per-phase breakdown. The TextBlock's Text property is
5461-
// observable, so updating it while the tooltip is open re-renders
5462-
// in place -- giving the user a real-time view of where each
5463-
// millisecond is going. Sub-phases sum to <= totalUs (= 1000/fps);
5464-
// the remainder is dispatcher idle / OS overhead between ticks.
5465-
if (!FpsTooltipText()) return;
5461+
// Builds the multi-line per-phase breakdown shown in the FPS tooltip
5462+
// / flyout. Used by the main window's FPS counter and by every
5463+
// output window's status-bar hover tooltip so they all stay in sync.
54665464
const auto& ft = m_frameTiming;
54675465
double fps = m_lastFps;
54685466
double total = ft.totalUs / 1000.0;
@@ -5508,6 +5506,26 @@ namespace winrt::ShaderLab::implementation
55085506
if (m_lastVideoFps > 0.1f)
55095507
text += std::format(L"\n\n video decode {:>6.0f} fps", m_lastVideoFps);
55105508

5511-
FpsTooltipText().Text(winrt::hstring(text));
5509+
return text;
5510+
}
5511+
5512+
std::wstring MainWindow::BuildFpsStatusText() const
5513+
{
5514+
// Single-line "60 fps | 16.5 ms" canonical status string. Shared by
5515+
// the main FPS counter and every output window's status bar.
5516+
return std::format(L"{:.0f} fps | {:.1f} ms",
5517+
m_lastFps, m_frameTiming.totalUs / 1000.0);
5518+
}
5519+
5520+
void MainWindow::UpdateFpsTooltip()
5521+
{
5522+
// Refresh the TextBlock inside the FPS counter's tooltip with a
5523+
// fresh per-phase breakdown. The TextBlock's Text property is
5524+
// observable, so updating it while the tooltip is open re-renders
5525+
// in place -- giving the user a real-time view of where each
5526+
// millisecond is going. Sub-phases sum to <= totalUs (= 1000/fps);
5527+
// the remainder is dispatcher idle / OS overhead between ticks.
5528+
if (!FpsTooltipText()) return;
5529+
FpsTooltipText().Text(winrt::hstring(BuildFpsTooltipText()));
55125530
}
55135531
}

MainWindow.xaml.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,10 @@ namespace winrt::ShaderLab::implementation
471471
void UpdateMcpActivityIndicator();
472472
void ResetMcpActivityState();
473473
void UpdateFpsTooltip();
474+
// Canonical FPS / timing strings shared by the main status bar and
475+
// every output window. Single source of truth.
476+
std::wstring BuildFpsStatusText() const;
477+
std::wstring BuildFpsTooltipText() const;
474478

475479
// Column splitter drag state.
476480
bool m_isDraggingSplitter{ false };

0 commit comments

Comments
 (0)