CS2 Progressive FPS Degradation — Root Cause Analysis

Panorama UI Bugs Causing GPU Utilization Collapse

CS2 has a long-standing issue where FPS slowly degrades during
matches
. Many players report starting at 300–400 FPS, then
dropping to 60–120 FPS after ~30–45 minutes.

This post summarizes a technical investigation of the Panorama UI
code
extracted from CS2 game files. The analysis identifies five
concrete defects
that explain the behavior.

This is NOT a hardware issue. Reports exist across all GPU tiers
and CPU architectures
.


Executive Summary

Profiling data shows the GPU is not the bottleneck.

Instead, the Panorama UI (JavaScript + layout engine) consumes
massive CPU time on the main render thread, delaying frame
submission to the GPU.

During FPS degradation:

Total frame time        13ms → 133ms
JavaScript execution    <1ms → 51ms
Panorama layout         <1ms → 45ms
GPU render time         ~13ms (unchanged)
GPU utilization         95% → 30–40%

The GPU finishes its work quickly, then sits idle waiting for the UI
system
.


Hardware Reports (Examples)

Ryzen 7800X3D + RTX 4080     400 FPS → <100
Ryzen 5900X + RTX 4090       140 FPS → 60
i7‑13700 + RTX 4080          400 FPS → 200
i9‑10850K + RTX 3070         300 FPS → 60
Ryzen 5600X + RTX 3060 Ti    290 FPS → 170
RX 6700 XT systems           crashes / memory issues

The 7800X3D + 4080 case is important because:

  • single CCD CPU\
  • 96MB L3 cache\
  • 16GB VRAM

There is no hardware bottleneck here.


Root Causes Found in Panorama UI

1. Expensive Gaussian Blur Running Every Frame

In csgostyles.css:

@define hudWorldBlur: gaussian(2,2,2);

This expensive blur is applied to 24+ HUD elements every frame.

Across the UI code:

gaussian() usage:        109
mipmapgaussian() usage:   20

But mipmapgaussian() is already used elsewhere in the UI and is much
cheaper
.

Correct version:

@define hudWorldBlur: mipmapgaussian(2,2,2);

Estimated fix time: <1 hour


2. Scoreboard Performs Massive DOM Traversals

scoreboard.js repeatedly scans the UI tree.

Example:

let elLabel = elPanel.FindChildTraverse('label');

Every scoreboard refresh triggers:

  • 67 DOM traversals
  • ~1300 node visits
  • 3 full loops over the player list

None of the elements are cached.

Estimated fix: 1–2 days


3. Event Handler + Closure Leaks (V8 Heap Growth)

Multiple scripts register global handlers without ever unregistering
them
.

Example:

$.RegisterForUnhandledEvent('HideContentPanel', MainMenu.HideContentPanel);

There are 33 global handlers and none are removed.

Another issue:

$.Schedule(1, _LoopUpdateNotifications);

This creates a new closure every second.

Over a 45‑minute match:

~8000 orphaned closures

Heap growth estimate:

Round 1   ~5MB
Round 10  ~35MB
Round 20  ~70MB
Round 30  ~100MB+

Since V8 GC runs on the main thread, garbage collection pauses grow
over time, causing frame delays.


4. Panorama UI Textures Never Released

UI images are loaded with:

PopulateFromSteamID()
SetImage()

But no unload mechanism exists.

Example console output:

TEXTURESTREAMING: Extremely low memory
Available mem: 4.49 MB
Required: 98.20 MB

Texture stats:

1988MB total
867MB non‑evictable

Across the codebase:

216 image loads
0 explicit unloads

This causes VRAM exhaustion, especially on 6–8GB GPUs.


5. No CPU Thread Affinity for Modern CPUs

Source 2 does not pin threads for:

  • AMD dual‑CCD processors
  • Intel P‑core / E‑core CPUs

Cross‑CCD latency example:

same CCD:   ~15ns
cross CCD:  ~80ns

That is a ~5× penalty.


FPS Degradation Timeline

Typical competitive match progression:

Round 1
FPS: 300‑400
GPU util: ~95%

Round 10
FPS: 200‑300

Round 20
FPS: 90‑150
GPU util ~40‑50%

Round 30+
FPS: 60‑90
GPU util ~20‑40%

At this point:

  • GC pauses
  • UI layout stalls
  • VRAM pressure

All combine to starve the GPU.


Fix Priority

Immediate (<1 day)

  • Replace gaussian() with mipmapgaussian() in HUD blur
  • Replace remaining gaussian blur calls

Short term

  • Cache FindChildTraverse() results
  • Unregister event handlers
  • Fix closure creation loop

Medium term

  • Add UI texture lifecycle management
  • Add CPU thread affinity

Long term

Move Panorama layout + JavaScript off the main render thread.


Full Technical Analysis

Full detailed report, code references, and investigation:

https://github.com/ValveSoftware/csgo-osx-linux/issues/4361

Huge credit to the author of that GitHub investigation for compiling the
full analysis and extracting the Panorama UI source code.