"""
wivexx_fps.py — real per-game FPS, read from RivaTuner Statistics Server (RTSS).
═══════════════════════════════════════════════════════════════════════════════
Measuring another process's frame rate on Windows normally needs an ETW trace
(PresentMon / Xbox Game Bar) which requires ADMIN — Wivexx runs per-user, so that
door is closed. But RTSS (which ships with MSI Afterburner, the tool the large
majority of PC gamers already run) publishes every app's live frame rate into a
named shared-memory block, "RTSSSharedMemoryV2", with a permissive ACL so any OSD
client can read it WITHOUT admin. We just read the numbers RTSS already computes —
no third-party code of ours, no elevation, no injected DLL.

  read_fps() -> float | None       # foreground game's FPS, or None if unavailable

Returns None (HUD shows a hint) when RTSS/Afterburner isn't running. Windows-only;
every failure path is swallowed → None.
"""

import ctypes
import struct
from ctypes import wintypes
from typing import Optional

_FILE_MAP_READ = 0x0004
_RTSS_SIG = 0x53535452          # 'RTSS' little-endian

# RTSS_SHARED_MEMORY_APP_ENTRY field offsets (from the RTSS SDK header):
#   dwProcessID 0 · szName[260] 4 · dwFlags 264 · dwTime0 268 · dwTime1 272 ·
#   dwFrames 276 · dwFrameTime 280
_ENT_READ = 284                # bytes we need from each entry


def _foreground_pid() -> Optional[int]:
    try:
        u = ctypes.windll.user32
        hwnd = u.GetForegroundWindow()
        if not hwnd:
            return None
        pid = wintypes.DWORD()
        u.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
        return int(pid.value) or None
    except Exception:
        return None


def read_fps() -> Optional[float]:
    """FPS of the foreground game (falls back to the most active app) via RTSS
    shared memory. None when RTSS isn't running or nothing is rendering."""
    try:
        k = ctypes.windll.kernel32
        k.OpenFileMappingW.restype = wintypes.HANDLE
        k.OpenFileMappingW.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.LPCWSTR]
        h = k.OpenFileMappingW(_FILE_MAP_READ, False, "RTSSSharedMemoryV2")
        if not h:
            return None
        try:
            k.MapViewOfFile.restype = ctypes.c_void_p
            k.MapViewOfFile.argtypes = [wintypes.HANDLE, wintypes.DWORD,
                                        wintypes.DWORD, wintypes.DWORD, ctypes.c_size_t]
            ptr = k.MapViewOfFile(h, _FILE_MAP_READ, 0, 0, 0)
            if not ptr:
                return None
            try:
                hdr = ctypes.string_at(ptr, 20)
                sig, _ver, ent_size, arr_off, arr_size = struct.unpack("<IIIII", hdr)
                if sig != _RTSS_SIG or ent_size < _ENT_READ or not arr_size:
                    return None
                fg = _foreground_pid()
                fg_fps = None
                best_fps, best_frames = None, -1
                for i in range(min(int(arr_size), 256)):
                    ent = ctypes.string_at(ptr + arr_off + i * ent_size, _ENT_READ)
                    pid = struct.unpack_from("<I", ent, 0)[0]
                    if not pid:
                        continue
                    t0 = struct.unpack_from("<I", ent, 268)[0]
                    t1 = struct.unpack_from("<I", ent, 272)[0]
                    frames = struct.unpack_from("<I", ent, 276)[0]
                    if t1 <= t0 or not frames:
                        continue
                    fps = 1000.0 * frames / (t1 - t0)
                    if not (0 < fps < 10000):
                        continue
                    if fg and pid == fg:
                        fg_fps = fps            # exact foreground match wins
                    if frames > best_frames:
                        best_fps, best_frames = fps, frames
                # prefer the focused game; else the busiest rendering app
                val = fg_fps if fg_fps is not None else best_fps
                return round(val, 0) if val is not None else None
            finally:
                k.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
                k.UnmapViewOfFile(ptr)
        finally:
            k.CloseHandle(h)
    except Exception:
        return None


if __name__ == "__main__":
    import time
    for _ in range(5):
        print("fps:", read_fps())
        time.sleep(1.0)
