Files
gig-line/Game/GigLine/Source/GigLine/GigLineHUD.cpp
T
zwitschi bc2e9c3b8e feat(hud): F3 debug overlay — FPS, frame time, worst frame, state
A top-left readout for performance work: smoothed FPS and frame time, the worst
frame in a rolling two-second window, the active view, player speed, a frame
counter and the player's position in metres from the project origin.

- F3 toggles it, through the console variable gigline.Debug so a packaged build can
  be checked without a rebuild (IA_ToggleDebug).
- On automatically for -perf and -qaselftest, so a performance capture never depends
  on pressing a key first; off in the built demo, because a player should not see an
  FPS counter over Hollywood Boulevard.
- The first two seconds are a warm-up window excluded from the worst-frame figure.
  Without it the readout showed "worst 400 ms" for a healthy run: the startup shader
  and streaming hitches dominated the rolling maximum.

Measured cost on the self-test route at 1440p: 2.64 to 2.77 ms average, about 5 %,
still 6x the floor. The release-check number stays the overlay-off run; the
overlay-on series is committed beside it as
Docs/perf/0.2.0-walk-frametimes-overlay.csv.

Docs: controls (F3 row, overlay section, switch table), release checks (cost row),
architecture building block view (HUD and input list).
2026-11-09 12:41:01 +01:00

122 lines
4.3 KiB
C++

#include "GigLineHUD.h"
#include "Engine/Canvas.h"
#include "Engine/Engine.h"
#include "GameFramework/PlayerController.h"
#include "GigLineCharacter.h"
#include "HAL/IConsoleManager.h"
#include "Kismet/GameplayStatics.h"
// Console variable so a packaged build can be checked without a rebuild:
// gigline.Debug 1
// The HUD reads it every frame; the F3 binding toggles it.
static TAutoConsoleVariable<int32> CVarGigLineDebug(
TEXT("gigline.Debug"),
0,
TEXT("Draw the performance and state overlay (0=off, 1=on). F3 toggles it."),
ECVF_Cheat);
void AGigLineHUD::UpdateFrameStats(float DeltaSeconds)
{
if (DeltaSeconds <= 0.0f)
{
return;
}
++FramesDrawn;
const float Ms = DeltaSeconds * 1000.0f;
if (WarmupSeconds > 0.0f)
{
WarmupSeconds -= DeltaSeconds;
SmoothedFrameMs = FMath::Lerp(SmoothedFrameMs, Ms, 0.1f);
WorstFrameMs = Ms;
WorstWindowSeconds = 0.0f;
return;
}
// A 0.9/0.1 EMA settles in a few frames and still reacts to a stall.
SmoothedFrameMs = FMath::Lerp(SmoothedFrameMs, Ms, 0.1f);
WorstFrameMs = FMath::Max(WorstFrameMs, Ms);
// Rolling worst-frame window: a spike stays visible for two seconds, so a
// hitch is not missed between glances (and not averaged away).
WorstWindowSeconds += DeltaSeconds;
if (WorstWindowSeconds >= 2.0f)
{
WorstWindowSeconds = 0.0f;
WorstFrameMs = Ms;
}
}
void AGigLineHUD::DrawHUD()
{
Super::DrawHUD();
// Console variable is authoritative; the UPROPERTY is the per-instance default
// for tests that set it before BeginPlay.
if (IConsoleVariable* Var = IConsoleManager::Get().FindConsoleVariable(TEXT("gigline.Debug")))
{
bShowDebug = Var->GetInt() != 0;
}
UpdateFrameStats(GetWorld() ? GetWorld()->GetDeltaSeconds() : 0.0f);
if (!Canvas || AttributionLine.IsEmpty())
{
return;
}
// Bottom-left, above any letterbox: the standard place for map/data credits.
const float LineHeight = Canvas->ClipY > 0 ? 22.0f * TextScale : 22.0f * TextScale;
const float X = MarginPx;
const float Y = Canvas->ClipY - MarginPx - LineHeight * 2.1f;
const FLinearColor Shadow(0.0f, 0.0f, 0.0f, 0.55f);
const FLinearColor Ink(1.0f, 1.0f, 1.0f, 0.92f);
// Shadow pass for legibility over bright pavement and sky.
DrawText(AttributionLine, Shadow, X + 1.0f, Y + 1.0f, nullptr, TextScale);
DrawText(LicenceLine, Shadow, X + 1.0f, Y + LineHeight + 1.0f, nullptr, TextScale * 0.82f);
DrawText(AttributionLine, Ink, X, Y, nullptr, TextScale);
DrawText(LicenceLine, Ink, X, Y + LineHeight, nullptr, TextScale * 0.82f);
// Control legend, bottom-right (0.1.0 release check: controls shown in-game).
if (!ControlsLine.IsEmpty())
{
float IgnoredW = 0.0f, ControlsH = 0.0f;
GetTextSize(ControlsLine, IgnoredW, ControlsH, nullptr, TextScale * 0.82f);
const float CX = Canvas->ClipX - MarginPx - IgnoredW;
const float CY = Canvas->ClipY - MarginPx - ControlsH;
DrawText(ControlsLine, Shadow, CX + 1.0f, CY + 1.0f, nullptr, TextScale * 0.82f);
DrawText(ControlsLine, Ink, CX, CY, nullptr, TextScale * 0.82f);
}
if (!bShowDebug)
{
return;
}
// Performance and state readout, top-left. Everything here is cheap enough to
// leave on during a capture: four formatted lines and no additional passes.
const APawn* Pawn = UGameplayStatics::GetPlayerPawn(this, 0);
const AGigLineCharacter* Character = Cast<AGigLineCharacter>(Pawn);
const float Fps = SmoothedFrameMs > 0.0f ? 1000.0f / SmoothedFrameMs : 0.0f;
const FVector Where = Pawn ? Pawn->GetActorLocation() / 100.0f : FVector::ZeroVector;
const float Speed = Pawn ? Pawn->GetVelocity().Size2D() / 100.0f : 0.0f;
const FString Header = FString::Printf(TEXT("%.0f FPS %.2f ms worst %.2f ms"), Fps, SmoothedFrameMs, WorstFrameMs);
const FString State = FString::Printf(TEXT("%s %.2f m/s frames %lld"),
Character && Character->IsThirdPerson() ? TEXT("third person") : TEXT("first person"),
Speed, FramesDrawn);
const FString Place = FString::Printf(TEXT("x %.1f y %.1f z %.1f m"), Where.X, Where.Y, Where.Z);
const FString Hint = TEXT("F3 hide");
const FLinearColor DebugInk(0.85f, 1.0f, 0.55f, 0.95f);
const FLinearColor DebugShadow(0.0f, 0.0f, 0.0f, 0.6f);
const float DX = MarginPx;
float DY = MarginPx;
for (const FString& Line : { Header, State, Place, Hint })
{
DrawText(Line, DebugShadow, DX + 1.0f, DY + 1.0f, nullptr, TextScale * 0.9f);
DrawText(Line, DebugInk, DX, DY, nullptr, TextScale * 0.9f);
DY += 22.0f * TextScale * 0.9f;
}
}