你正在打遊戲,角落的 overlay 顯示你現在跑幾 FPS,腦中忽然冒出一個小問題:它到底是怎麼知道的?你沒在遊戲裡裝過幀數計數器,也沒往遊戲裡插任何東西。那這個數字,是從哪冒出來的?
真相會讓你意外:它根本沒碰遊戲,也不能碰。一個工具只要伸手進線上遊戲裡數幀,防作弊就會看到「家裡來了陌生人」——而最壞的結果不是當機,是你的帳號被標記。所以我們從外面數,而且你會發現,Windows 其實一直默默幫你記著這筆帳。
這篇就帶你看清楚這是怎麼辦到的:負責數幀的那個作業系統功能、為什麼它可以安心跟排位賽一起跑,還有那個超有教育意義的 bug——它曾經讓一款遊戲很自信地報出 600 FPS,而它其實只跑 200。
前提約束:絕不碰遊戲本體
量 FPS 的標準做法是「進到遊戲裡面量」:RTSS 這類工具把 DLL 注入遊戲程序、hook 它的 Present 呼叫——每攔到一次就是一幀。效果很好,但這正是防作弊系統的頭號獵物。對一個會跟線上遊戲一起跑的工具來說,注入不是選項:最壞情況不是當機,是使用者的帳號被標記。
所以需求變成:完全不碰目標程序,數別人的幀。
Windows 其實已經幫你數好了
答案是 ETW——Event Tracing for Windows,作業系統內建的全域遙測匯流排。圖形堆疊本身就有埋點:Microsoft-Windows-DXGI provider 會在任何程序送出一幀時發事件(Event 42,Present::Start)。從自己的程序訂閱這條流、用前景遊戲的 PID 過濾、每秒數事件數——就是 FPS,從外面量出來的。這跟 Intel 的 PresentMon 用的是同一個資料來源。老的 DirectX 9 遊戲走另一條路徑,所以我們同時聽 Microsoft-Windows-D3D9 的 Event 1,並用旗標防止同一幀從兩邊各算一次。
那個跑出 600 FPS 的 bug
第一版上線後,玩家回報:遊戲實際大約 200 幀,overlay 卻顯示 500–600。不是抖動——是穩定、自信地三倍於真相。
事後看原因很明顯:現代遊戲不是只有一條幀流。很多引擎內嵌 Chromium 做 UI(選單、商城、地圖介面),那個瀏覽器有自己的 swapchain、自己送自己的幀。我們的計數器把該 PID 的所有 Present 事件加總——主渲染約 200 幀,加上一個很忙的網頁 UI——報出總和。修法就是 PresentMon 的做法:按 swapchain 分桶,回報最忙的那一桶(主渲染),愛講話的 UI overlay 自己一桶,不再污染數字。
然後第二個坑引爆:事件 payload 裡指向 swapchain 的欄位,在不同 Windows 版本叫不同名字(有的叫 pIDXGISwapChain,有的是變體)。讀錯欄位名,所有事件全進同一桶——悄悄重現原 bug。最後站穩的版本改成名稱無關掃描:找 payload 裡任何名字含「swap」的指標欄位,並每秒把逐 swapchain 的分桶明細寫進診斷 log——哪天某台機器的欄位又叫新名字,log 會直接告訴我們,不用猜。
這個做法做不到的事
站在程序外面的代價,誠實列出:
- 跟著前景視窗走。Alt-Tab 切走,overlay 就改量你切過去的東西——設計如此,但多螢幕使用者會驚訝。
- 數的是 present 次數,不是每幀耗時。FPS 數字紮實,但沒有 frametime 百分位數、沒有 1% low。要深度分析卡頓,請用 PresentMon 或 CapFrameX——那件事它們真的做得更好。
- ETW session 需要管理員權限——PowerDoze 引擎本來就以系統管理員身分跑電源管理,overlay 搭便車不用多付。
電源工具為什麼需要這個數字
這個 overlay 存在的目的,是讓你一眼回答一個問題:我是不是在為看不到的幀付電費?165 Hz 的螢幕上選單跑 400 FPS,那是純發熱。FPS 和 Hz 並排放著,壓 GPU 功耗(power limit 那招幾乎不掉性能)就不再是賭注——你盯著數字,它撐住或撐不住,一翻兩瞪眼。