All posts 中文版 →

Counting Frames Without Touching the Game: How Our FPS Overlay Works

You're mid-game, the overlay in the corner says you're pushing some FPS number, and a small question pops up: how does it even know that? You never installed a frame counter into the game. Nothing got plugged into it. So where is the number coming from?

Here's the part that surprises people: it never touches the game at all. It can't. The moment a tool reaches inside an online game to count frames, anti-cheat sees a stranger in the house — and the worst case isn't a crash, it's your account getting flagged. So we count from the outside, and it turns out Windows was quietly keeping the tally the whole time.

This post walks you through how that works — the OS feature doing the counting, why it's safe to run next to ranked play, and the very educational bug that once made one game proudly report 600 FPS while it was really running 200.

The constraint: never touch the game

The standard way to measure FPS is to get inside the game: tools like RTSS inject a DLL into the game process and hook its Present calls — every hook fires, you count one frame. It works beautifully, and it's also exactly what anti-cheat systems are built to hate. For a tool people run alongside online games, injection isn't an option: the realistic worst case isn't a crash, it's someone's account getting flagged.

So the requirement became: count another process's frames without ever touching that process.

Windows already counts them for you

The answer is ETW — Event Tracing for Windows, the OS-wide telemetry bus that most people only meet through Task Manager's nicer cousins. The graphics stack is instrumented: the Microsoft-Windows-DXGI provider emits an event every time any process presents a frame (Event 42, Present::Start). Subscribe to that stream from your own process, filter by the foreground game's PID, count events per second — that's FPS, measured from the outside. It's the same data source Intel's PresentMon uses. Older DirectX 9 titles present through a different path, so we also listen to Microsoft-Windows-D3D9 Event 1, with a flag so a frame routed through both layers isn't counted twice.

Think of an office tower with a security desk at the lobby. To count how many times a tenant walks out the door, you don't break into their apartment and hide a camera by the doorway — you just read the lobby's CCTV feed, which already logs everyone passing through the front entrance. ETW is that building-wide CCTV: Windows is already recording every "frame went out the door", so we just watch the tape instead of barging into the game.

The bug: the game that ran at 600 FPS

First version shipped, and a player reported the overlay reading 500–600 FPS in a game that was actually rendering around 200. Not jitter — consistently, confidently triple the truth.

The cause is obvious in hindsight: a modern game is not one stream of frames. Many engines embed a Chromium-based UI layer (menus, cash shop, map overlays), and that browser presents its own frames through its own swapchain. Our counter was summing every Present event from the PID — main render at ~200 plus a busy web overlay — and reporting the total. The fix is what PresentMon does: bucket events per swapchain, then report the busiest one, which is the main render. The chatty UI overlay gets its own bucket and stops polluting the number.

Our counter was like measuring traffic on a highway by standing at a junction and counting every car on every lane at once — main road plus the busy on-ramp from a shopping mall — then reporting one giant number. No wonder it looked like 600. The fix was to count each lane separately and report the main road only. A game's menu and shop screen are that mall on-ramp: their own steady stream of "cars" that has nothing to do with how fast the game itself is running.

Then the second trap fired: the event payload field that identifies the swapchain has different names across Windows versions (pIDXGISwapChain on some builds, variants on others). Read the wrong field name and every event lands in one bucket — silently reproducing the original bug. The build that finally stuck scans the payload for any pointer-sized field whose name contains "swap", and writes a once-per-second per-swapchain breakdown to a diagnostic log, so when a user's machine names the field something new, the log shows it and we can adapt without guessing.

What this approach can't do

Honesty about the trade-offs of staying outside the process:

  • It follows the foreground window. Alt-tab away and the overlay measures whatever you switched to — by design, but it surprises multi-monitor users.
  • It counts presents; it doesn't time them. You get a solid FPS number, not frametime percentiles or 1% lows. For deep stutter analysis, use PresentMon or CapFrameX — genuinely better tools for that job.
  • ETW sessions need elevation — PowerDoze's engine already runs elevated for power management, so the overlay rides along free.

Why this matters in a power tool

The overlay exists to answer one question at a glance: am I paying for frames I can't see? A 165 Hz monitor showing a menu at 400 FPS is pure heat. With FPS and Hz side by side, capping the GPU (and the power-limit trick barely costs performance) stops being a leap of faith — you watch the number, and it either holds or it doesn't.

Download PowerDoze free →

Nisonxi

I'm Nisonxi, the developer behind PowerDoze. I built it because my own Windows desktop idled all day at near-full power and no existing tool could read the situation and switch on its own. This blog is my notebook from the journey.

About the author and PowerDoze →