Skip to content

The Edge of the Cyber World See the latest

Apps

CapFrameX Tutorial: 13 Steps, 50 Min [2026]

Average FPS hides the moments that actually ruin a game. A card can post a high average and still hitch every few seconds, and a counter in the corner of the screen will never tell you why. A proper CapFrameX run records every single frame, so you can see the stutter, measure it, and prove whether your latest tweak helped. This tutorial walks through a full frame-time benchmark in 13 steps, from install to a repeatable comparison script, and it works on an RTX 5090, a Radeon RX 9070 XT, or anything in between.

The key fact to understand first: CapFrameX is an analysis layer, not a separate measuring device. According to the PresentMon site’s own comparison, CapFrameX gathers its data through the PresentMon capture service, so the underlying frame data matches what Intel’s tool records for the same scene. That means you can learn the numbers in one tool and trust them in the other. If you plan to test an MSI Afterburner overclock or undervolt, this is the method that tells you whether the change was real or just noise.

Timing matters right now. ZDNET reported in early November 2026 that RTX 50 prices were rising again, and retail reports describe a similar squeeze on Radeon cards. When a GPU costs this much, you want evidence for every upgrade, power-limit change, and driver update. A repeatable frame-time test costs nothing but an hour of your time.

Why Frame Times Beat FPS for Judging a GPU

FPS is an average over time. A frame time is the gap between two consecutive frames, measured in milliseconds. If one frame takes 40 ms, you feel a hitch, yet the one-second average barely moves. Frame time data keeps that hitch visible, which is why reviewers publish low-percentile figures next to averages.

Frame pacing is the second reason. Two systems can post the same average FPS while one delivers frames at an even rhythm and the other swings between 7 ms and 16 ms. The second system feels worse. Only a per-frame capture exposes the difference, and CapFrameX draws it as a line you can read in seconds.

The third reason is honest comparison. If you want to know whether the flagship upgrade is worth it, raw averages leave out the cost side. A price-per-frame comparison of the RTX 5090 and 4090 only works when the frame data underneath is captured the same way on both cards. Consistent capture rules turn opinions into numbers.

What a Capture Actually Contains

Each row in a PresentMon-style capture describes one presented frame: the application name, a timestamp, the frame time in milliseconds, and in newer builds extra columns such as CPU busy time and GPU time. CapFrameX reads those rows, adds sensor telemetry such as clocks, temperatures, and power, and calculates the percentile statistics for you. Everything in this guide builds on that one table of numbers.

CapFrameX vs PresentMon vs FrameView: Pick the Right Tool

Three names show up in almost every benchmark thread. PresentMon is Intel’s free, open-source measurement engine, and in its classic form it is a console app that writes a CSV. CapFrameX is a full Windows application built on top of that engine. NVIDIA FrameView is a third option that, according to the same PresentMon comparison, also builds on PresentMon technology. They differ in the experience layered on top, not in the frame data underneath.

Tool Best for Interface Scripting Built-in charts
PresentMon console Automated runs, test benches, raw data Command line, one small executable Strong, flags work in batch files No, analyze the CSV yourself
CapFrameX Regular benchmarking and run-to-run comparison Windows desktop app with overlay Limited, mostly interactive Yes, frame time plots and percentile views
NVIDIA FrameView Readers who prefer NVIDIA’s own tool, built on PresentMon technology Desktop app See NVIDIA’s documentation See NVIDIA’s documentation
PresentMon 2.x app Casual live stats with an overlay Intel overlay and capture service Use the console build for scripts Overlay graphs only
Spreadsheet or Python on the CSV Custom statistics and automation Whatever you build Full control Only what you plot

This tutorial uses both layers on purpose. CapFrameX handles the interactive capture and visual inspection. A small Python project, built later in the guide, handles repeatable batch runs and summary tables. That pairing gives you a chart for quick judgment and a median-of-three number for the final verdict.

Prerequisites: Hardware, Software, and Versions

You need a Windows 10 or Windows 11 PC, because PresentMon relies on Windows event tracing to see presented frames. A discrete GPU from any vendor works. For context, NVIDIA’s own GeForce page lists the GeForce RTX 5090 as the top of its current desktop line, and AMD’s Radeon RX 9070 XT leads AMD’s desktop Radeon lineup, so those are the cards most readers will compare. Check the RTX 5090 product page if you want the current official specs. Older cards benchmark fine with the same steps, which helps because NVIDIA’s decision to skip new gaming GPUs means many readers are still testing last cycle’s hardware.

  • Operating system: Windows 11 (current build) or Windows 10, 64-bit, with an administrator account.
  • GPU driver: the latest from NVIDIA or AMD. Record the exact version before testing.
  • CapFrameX: the latest stable release from the official CapFrameX site or its GitHub releases page. Install whatever the release page marks as stable, and skip beta or development builds unless you want to test them.
  • PresentMon: Intel’s console build for the scripted part. Use the newest official release from Intel. Builds bundled with other tools are maintained separately and are not official Intel releases.
  • Python: a current Python release for the analysis project.
  • RivaTuner Statistics Server (RTSS): optional, used for the classic CapFrameX overlay. The CapFrameX site states the overlay is provided by RTSS.
  • Runtimes: the runtime and Visual C++ packages the CapFrameX installer asks for. Follow the prompts for the build you download.
  • A repeatable test scene: a built-in benchmark, or a 60-second route you can walk the same way each time.
  • Time: about 50 minutes for the first full pass, and 15 minutes for each later comparison.

One safety note before you download anything. Fake installers for popular tuning tools turn up regularly, and the same risk applies to every benchmarking tool. Download from the project’s own site or its official GitHub releases, never from a mirror that wraps the installer in an extra downloader.

Phase 1: Install and Configure (Steps 1 to 4)

Step 1: Record Your Baseline System Details

Before you install anything, write down what you are testing. A benchmark without context is worthless a month later. Open an elevated PowerShell window and run the commands below to capture the GPU name, driver version, current power limit, and active power plan. Paste the output into a text file named after the date.

# GPU name and driver version for the test log
Get-CimInstance Win32_VideoController | Select-Object Name, DriverVersion

# NVIDIA only: driver, power limit, clocks
nvidia-smi --query-gpu=name,driver_version,power.limit,clocks.gr,clocks.mem --format=csv

# Active Windows power plan
powercfg /getactivescheme

# Hash of the downloaded archive, to compare against the release page
Get-FileHash .CapFrameX_download.zip -Algorithm SHA256

Also note the CPU model, RAM speed, Windows build, game version, resolution, and refresh rate. Handheld testers go further. The HandheldAtlas guide on benchmarking a handheld PC with CapFrameX insists on logging the exact device, power mode, and graphics settings, and its reasoning applies to desktops too. If a variable is unknown, leave the field blank instead of guessing.

Step 2: Download CapFrameX From the Official Source

Go to the CapFrameX site, open the Download page, and grab the current stable release. The project describes itself as a frame-time capture and analysis tool based on Intel’s PresentMon, with an overlay from RivaTuner Statistics Server. The site also carries a Donate link, which fits its free, open-source model. Run the installer, accept the runtime prompts, and launch the app once so it creates its configuration folder.

If you downloaded a zip, compare its hash (the Get-FileHash line from Step 1) against the value published on the release page when one is listed. A mismatch means you should delete the file and download again. Skipping this check is an easy way to end up with a tampered tool.

Step 3: Install RTSS and Set Up the Overlay

CapFrameX uses RTSS to draw the on-screen display in many setups. Install RTSS if the installer did not bundle it, then start it before the game. Open the overlay section of CapFrameX and enable only a few readouts: frame time graph, average FPS, GPU load, GPU temperature, and GPU power. A cluttered overlay costs a small amount of performance and distracts you from the test route.

Newer releases changed this area. Reporting describes a refreshed interface and a built-in overlay that does not need a hook, and a later build reportedly restored an experimental in-game overlay for DirectX 11, DirectX 12, and Vulkan games. Menu names can differ between builds, so search the interface for the overlay settings instead of expecting an exact layout. If an overlay misbehaves in one game, a custom per-game profile is the intended fix according to the beta notes.

Step 4: Install the PresentMon Console Build

The scripted part of this project calls Intel’s console executable directly. Download the console release from the official PresentMon project, extract it to C:ToolsPresentMon, and rename the file to PresentMon.exe so the scripts do not break when a new version appears. Run it with the help flag first to confirm which options your build supports.

cd C:ToolsPresentMon
.PresentMon.exe --help
.PresentMon.exe --process_name Game.exe --output_file .capturestest_run1.csv --delay 10 --timed 60 --terminate_after_timed --stop_existing_session

Older documentation shows single-dash flags such as -process_name and -timed, while 2.x builds document double-dash forms. The help output is the final word. PresentMon needs access to Windows event tracing, so run it from an elevated shell or add your account to the Performance Log Users group. Without that, the tool starts and then captures nothing.

Phase 2: Control the Test Variables (Steps 5 to 7)

Step 5: Lock Every Variable You Can

A benchmark answers one question at a time. If you change the driver, the power limit, and the in-game preset together, you cannot say which one moved the result. Decide the single variable you are testing, then freeze the rest. The table lists the usual suspects.

Variable How to lock it Why it matters
GPU driver Do not update mid-test, log the version Driver updates change frame pacing and sometimes performance
Power limit and clocks Reset to stock unless that is the test Undervolts and overclocks shift both averages and lows
Windows power plan Use one plan for every run Balanced and performance plans can throttle CPU boost differently
Background apps Close launchers, browsers, and updaters Background tasks create random spikes in the slowest frames
Resolution and refresh rate Fix them and disable dynamic resolution Changing the pixel load changes GPU limits
V-Sync and frame cap Turn off for raw tests, or keep identical A cap flattens the data and hides headroom
Upscaler and frame generation Fix the mode, label the run These change which frames exist and how they are paced
Room and GPU temperature Warm the card for 5 minutes first A cold card boosts higher than a heat-soaked one

Pay special attention to the last row. A first run on a cold GPU often scores a few percent higher than the third run, and that gap tricks people into believing a change made things worse. Warm up first, then record. If you want a refresher on reading temperatures during that warm-up, the guide on checking GPU temperature covers the sensors worth watching.

Step 6: Pick a Repeatable Test Scene

The best scene is a built-in benchmark, because the game plays the same sequence every time. If the game lacks one, build a route: load the same save, stand at the same spot, and walk a fixed loop for 60 seconds. Avoid scenes with random weather, crowds, or enemy spawns. Pick one that stresses your GPU in a typical way, such as a dense city street, rather than an empty corridor that flatters every card.

Choose the duration deliberately. Sixty seconds yields several thousand frames at high refresh rates, enough for a stable low-percentile figure. Thirty seconds works for a quick check. Anything under 15 seconds gives too few slow frames for the percentile maths to mean anything, and the analysis script in this guide rejects captures with fewer than 100 frames for that reason.

Step 7: Warm Up and Set a Capture Delay

Play the route once without recording. This loads shaders, fills the texture cache, and heats the GPU. Many modern games compile shaders on the first pass, so a cold first run shows huge spikes that never appear again. After the warm-up, restart the route from the same position.

Set a capture delay of about 10 seconds so you have time to switch back into the game and start moving. In CapFrameX, look for the capture time and capture delay fields in the capture settings, and assign a hotkey that does not conflict with the game or other overlays. In the console script, the --delay flag does the same job. Trim the opening frames in analysis if you still see a settling period.

Phase 3: Capture the Runs (Steps 8 to 10)

Step 8: Make Your First Capture in CapFrameX

Start RTSS if you use it, launch CapFrameX, then start the game and load the save. Confirm the overlay shows frame times. Press your capture hotkey, run the route for the configured duration, and wait for CapFrameX to confirm the capture ended. The app stores the run in its capture list with the game name, so give it a clear comment right away, such as the preset and the date.

Open the new capture in the analysis view. You should see a frame time graph, an FPS summary, and percentile figures. Skim the shape of the graph before reading any number. A flat line near the target frame time means smooth pacing. Sharp vertical spikes mean stutter, and a slow upward drift often points to thermal throttling or a memory leak.

Step 9: Repeat the Capture Three Times

One run proves nothing. Run-to-run variance in a real game can reach a few percent even on a well-controlled machine, so a single result that differs by only a small margin may sit inside the noise. Capture at least three runs per configuration and use the median. Wait a short while between runs so temperatures settle, and reload the save each time so the scene starts identically.

Label every run with the configuration it belongs to. Use short, consistent names such as stock, undervolt, or driver-new. The scripts in the project below depend on a label_runN.csv naming pattern, and consistent names make later comparisons painless.

Step 10: Automate the Same Test With PresentMon

Clicking hotkeys three times per configuration gets tedious, and human timing adds variance. The PowerShell harness below wraps the PresentMon console build. It checks for administrator rights, asks you to stage the scene, counts down, captures for a fixed time, and writes one CSV per run. Save it as run_bench.ps1 in a folder called benchkit.

param(
    [Parameter(Mandatory)] [string] $Label,
    [Parameter(Mandatory)] [string] $Process,
    [string] $PresentMon = "C:ToolsPresentMonPresentMon.exe",
    [int] $Runs = 3,
    [int] $Delay = 10,
    [int] $Seconds = 60,
    [string] $OutDir = ".captures"
)

$ErrorActionPreference = "Stop"

$identity = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $identity.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    throw "Open an elevated PowerShell window. PresentMon needs ETW access."
}
if (-not (Test-Path $PresentMon)) {
    throw "PresentMon not found at $PresentMon"
}

New-Item -ItemType Directory -Force -Path $OutDir | Out-Null

for ($i = 1; $i -le $Runs; $i++) {
    $file = Join-Path $OutDir ("{0}_run{1}.csv" -f $Label, $i)
    Write-Host "Run $i of $Runs for '$Label'. Load the same save and stand at the route start."
    Read-Host "Press Enter, then click back into the game within $Delay seconds"
    & $PresentMon --process_name $Process --output_file $file --delay $Delay --timed $Seconds --terminate_after_timed --stop_existing_session --no_console_stats
    Write-Host "Saved $file"
    Start-Sleep -Seconds 20
}
Write-Host "Done. Next: python summarize_runs.py $OutDir --app $Process"

Run it once per configuration. The prompt tells you when to load the save, and the delay gives you time to click back into the game. Because the harness never touches the game, it works with any title, any API, and either GPU vendor.

# Elevated PowerShell, from the benchkit folder
.run_bench.ps1 -Label stock -Process Game.exe
.run_bench.ps1 -Label undervolt -Process Game.exe

# Any shell with Python 3.10+
python summarize_runs.py .captures --app Game.exe --baseline stock

Phase 4: Analyze and Compare (Steps 11 to 13)

Step 11: Read Averages and Percentile Lows

Percentile lows describe the slow tail of the run. The standard low asks how fast the slowest frames were. The deeper low zooms further into the rare, ugly spikes. Tools calculate these in two common ways, so always state which method you used. The table separates them, and the Python script reports both.

Metric How it is calculated What it tells you Common trap
Average FPS Frames divided by total time Overall throughput Hides stutter completely
Percentile low Frame time at the 99th percentile, converted to FPS Typical worst-case smoothness Ignores how bad the spikes beyond the cutoff are
Low (average) Mean of the slowest frames beyond the cutoff, converted to FPS Severity of the slow tail Not comparable to a percentile figure from another tool
Deep tail low Same idea, pushed further into the slowest tail Rare hitches Needs long captures, short runs make it noisy
Frame time spikes Frames far above the median, such as double Count of visible hitches A single shader compile can dominate a short run
Frames over the 60 FPS frame time Share of frames slower than 60 FPS pace Time spent below 60 FPS Meaningless on a much higher refresh rate target

Notice in the sample output further down how the extreme low by average can sit far below the percentile version. That gap is the whole point of showing both. A big gap means a few severe spikes. A small gap means the tail is gentle.

Step 12: Build the Analysis Script

CapFrameX gives you charts. A script gives you numbers you can paste into a spreadsheet and rerun in five seconds. Save the code below as analyze.py. It reads your exported capture files, picks the frame time column automatically, filters by application name, trims the settling period, and prints every metric from the table above.

"""Frame-time analysis for PresentMon and CapFrameX CSV exports."""
import argparse
import csv
import math
import statistics
import sys
from pathlib import Path

FRAME_TIME_COLUMNS = ("FrameTime", "MsBetweenPresents", "msBetweenPresents")


def load_frame_times(path, app=None):
    with open(path, newline="", encoding="utf-8-sig") as handle:
        reader = csv.DictReader(handle)
        column = next((c for c in FRAME_TIME_COLUMNS if c in (reader.fieldnames or [])), None)
        if column is None:
            sys.exit(f"{path}: no frame time column found in {reader.fieldnames}")
        times = []
        for row in reader:
            if app and row.get("Application") != app:
                continue
            try:
                times.append(float(row[column]))
            except (TypeError, ValueError):
                continue
    if len(times) < 100:
        sys.exit(f"{path}: only {len(times)} frames, capture is too short to trust")
    return times


def percentile(values, pct):
    ordered = sorted(values)
    rank = max(0, min(len(ordered) - 1, math.ceil(pct / 100 * len(ordered)) - 1))
    return ordered[rank]


def worst_slice_fps(times, fraction):
    count = max(1, int(len(times) * fraction))
    worst = sorted(times, reverse=True)[:count]
    return 1000 / statistics.mean(worst)


def summarize(times):
    median = statistics.median(times)
    return {
        "frames": len(times),
        "avg_fps": 1000 * len(times) / sum(times),
        "p1_fps": 1000 / percentile(times, 99),
        "p01_fps": 1000 / percentile(times, 99.9),
        "low1_avg_fps": worst_slice_fps(times, 0.01),
        "low01_avg_fps": worst_slice_fps(times, 0.001),
        "max_ms": max(times),
        "over_16_7_pct": 100 * sum(t > 16.667 for t in times) / len(times),
        "stutters": sum(t > 2 * median for t in times),
    }


def print_report(label, stats):
    print(f"== {label}")
    print(f"frames           {stats['frames']}")
    print(f"average fps      {stats['avg_fps']:.1f}")
    print(f"1% low (p99)     {stats['p1_fps']:.1f}")
    print(f"1% low (avg)     {stats['low1_avg_fps']:.1f}")
    print(f"0.1% low (p99.9) {stats['p01_fps']:.1f}")
    print(f"0.1% low (avg)   {stats['low01_avg_fps']:.1f}")
    print(f"worst frame      {stats['max_ms']:.1f} ms")
    print(f"frames > 16.7 ms {stats['over_16_7_pct']:.2f}%")
    print(f"stutters (>2x median) {stats['stutters']}")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("files", nargs="+", type=Path)
    parser.add_argument("--app", help="Application column filter, for example Game.exe")
    parser.add_argument("--trim", type=int, default=0, help="frames to drop from the start of each run")
    args = parser.parse_args()

    results = []
    for path in args.files:
        times = load_frame_times(path, args.app)[args.trim:]
        stats = summarize(times)
        print_report(path.name, stats)
        results.append((path.name, stats))

    if len(results) > 1:
        base_name, base = results[0]
        print(f"n== change versus {base_name}")
        for name, stats in results[1:]:
            for key, label in (("avg_fps", "average"), ("p1_fps", "1% low"), ("p01_fps", "0.1% low")):
                delta = 100 * (stats[key] - base[key]) / base[key]
                print(f"{name}: {label} {delta:+.1f}%")


if __name__ == "__main__":
    main()

The output below comes from two synthetic files, generated by the sample-data script in the project section. They are not real GPU measurements, and the numbers exist only to show the report format. The first file models a run with six large spikes, the second a smoother run at a slightly lower average.

== stock_run1.csv
frames           7080
average fps      124.6
1% low (p99)     109.3
1% low (avg)     89.3
0.1% low (p99.9) 104.4
0.1% low (avg)   35.3
worst frame      49.1 ms
frames > 16.7 ms 0.06%
stutters (>2x median) 4
== undervolt_run1.csv
frames           7080
average fps      120.4
1% low (p99)     105.8
1% low (avg)     101.5
0.1% low (p99.9) 101.9
0.1% low (avg)   81.3
worst frame      26.3 ms
frames > 16.7 ms 0.01%
stutters (>2x median) 1

== change versus stock_run1.csv
undervolt_run1.csv: average -3.3%
undervolt_run1.csv: 1% low -3.2%
undervolt_run1.csv: 0.1% low -2.5%

Read the sample the way you would read a real test. The second file loses a little average FPS, yet its low by average is far higher because it has fewer spikes. A person staring only at the average would call the second configuration worse. A person looking at the tail would call it smoother. That is exactly the kind of trade-off an undervolt or a lower power limit can produce.

Step 13: Summarize Runs and Compare Configurations

The final script groups all captures by label, takes the median of each metric across runs, reports the spread between runs, and compares every configuration against a baseline. Save it as summarize_runs.py next to analyze.py. It warns you when the runs disagree by more than a small margin, which is the cue to repeat the test rather than trust the result.

"""Group captures named label_runN.csv and report the median of each metric per label."""
import argparse
import statistics
from collections import defaultdict
from pathlib import Path

from analyze import load_frame_times, summarize

METRICS = (("avg_fps", "avg fps"), ("p1_fps", "1% low"), ("p01_fps", "0.1% low"))


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("folder", type=Path)
    parser.add_argument("--app", default=None)
    parser.add_argument("--trim", type=int, default=120)
    parser.add_argument("--baseline", help="label to compare every other label against")
    args = parser.parse_args()

    groups = defaultdict(list)
    for csv_file in sorted(args.folder.glob("*_run*.csv")):
        label = csv_file.stem.rsplit("_run", 1)[0]
        times = load_frame_times(csv_file, args.app)[args.trim:]
        groups[label].append(summarize(times))

    medians = {}
    print(f"{'label':<16}{'runs':>5}{'avg fps':>10}{'1% low':>9}{'0.1% low':>10}{'spread':>9}")
    for label, runs in groups.items():
        med = {key: statistics.median(r[key] for r in runs) for key, _ in METRICS}
        avgs = [r["avg_fps"] for r in runs]
        spread = 100 * (max(avgs) - min(avgs)) / med["avg_fps"]
        medians[label] = med
        print(f"{label:<16}{len(runs):>5}{med['avg_fps']:>10.1f}{med['p1_fps']:>9.1f}"
              f"{med['p01_fps']:>10.1f}{spread:>8.1f}%")
        if spread > 3:
            print(f"  warning: {label} runs differ by {spread:.1f}%, repeat the test")

    if args.baseline and args.baseline in medians:
        base = medians[args.baseline]
        print(f"nchange versus {args.baseline}")
        for label, med in medians.items():
            if label == args.baseline:
                continue
            parts = [f"{name} {100 * (med[key] - base[key]) / base[key]:+.1f}%" for key, name in METRICS]
            print(f"{label:<16}" + "  ".join(parts))


if __name__ == "__main__":
    main()

Running it against nine synthetic captures (three runs for each of three labels) produces a compact verdict table. Again, the data is generated, so treat the figures as a format demonstration only.

label            runs   avg fps   1% low  0.1% low   spread
oc                  3     131.3    115.4     110.4     0.2%
stock               3     124.6    109.3     104.0     0.2%
undervolt           3     120.4    105.8     101.9     0.1%

change versus stock
oc              avg fps +5.4%  1% low +5.5%  0.1% low +6.1%
undervolt       avg fps -3.3%  1% low -3.2%  0.1% low -2.0%

The spread column is your confidence meter. Here it reads very low, so a clear difference between labels is believable. The same grouping works for comparing cards, as in the RTX 5070 vs RX 9070 XT matchup, where each card becomes one label with three runs. In a real game, a visible spread between runs means you should treat anything under that gap as a tie.

Complete Working Project: The Bench Kit Folder

Everything above fits into a small folder that you can copy to any Windows machine. Here is the final layout. Nothing needs installing beyond Python and the PresentMon executable.

  • run_bench.ps1 captures repeated runs through the PresentMon console build.
  • analyze.py prints the full metric report for one or more captures.
  • summarize_runs.py takes the median across runs and compares labels to a baseline.
  • make_sample.py writes synthetic CSV files so you can test the pipeline before a real session.
  • captures/ holds the CSV files, named label_runN.csv.

The sample generator is the fifth file, and it earns its place. Run it first to confirm Python, the scripts, and the folder structure work. If the synthetic test passes and the real one fails, you know the problem is capture permissions or the process name, not your analysis code.

"""Write a synthetic PresentMon-style CSV so you can test analyze.py without a game."""
import csv
import random
import sys

path, base_ms, spikes, seed = sys.argv[1], float(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
random.seed(seed)
rows = []
for i in range(7200):
    ft = random.gauss(base_ms, base_ms * 0.06)
    if spikes and i % (7200 // spikes) == 0:
        ft *= random.uniform(3, 6)
    rows.append(max(ft, 1.0))
with open(path, "w", newline="") as handle:
    writer = csv.writer(handle)
    writer.writerow(["Application", "FrameTime"])
    for ft in rows:
        writer.writerow(["Game.exe", f"{ft:.3f}"])

Generate a file with python make_sample.py captures/stock_run1.csv 8.0 6 1. The arguments are the output path, the base frame time in milliseconds, the number of injected spikes, and a random seed. After that, a full workflow looks like this: capture three runs per configuration with run_bench.ps1, run summarize_runs.py with a baseline label, then open the interesting captures in CapFrameX to see where the spikes sit on the timeline. The script finds the problem, and the chart explains it.

Benchmarking Upscaling and Frame Generation Honestly

Upscalers and frame generation complicate every frame-time test. NVIDIA reportedly says RTX 50 systems support Dynamic Multi Frame Generation with up to 6× frame multiplication. AMD offers its own frame generation on Radeon cards, and our guide to enabling AMD FSR Redstone shows how that side of the setup works. These features raise the displayed frame rate, but they change what a frame even means.

Treat every frame generation configuration as its own test with its own label. Never compare a frame-generated run directly against a native run and call the gap a performance gain, because the extra frames are synthesized and the input latency path differs. Capture the native render rate separately if you want to know what the GPU is really doing. Recent repository activity around PresentMon, including changes to how deferred frame-type presents are handled and fixes for stale timing data after frame generation changes, shows that this area is still moving, so keep your tools current and note the version in your log.

A Simple Labeling Scheme That Prevents Mistakes

Encode the upscaler and frame generation state directly in the label. Examples: native, quality-upscale, quality-upscale-fg. When the summary table lists them, nobody confuses a 2x frame-generated number with a native one. It takes ten seconds per capture and saves an embarrassing correction later.

Testing RTX 50 and Radeon RX 9000 Fairly

The same method applies to both vendors, and that is the strength of a PresentMon-based workflow. A head-to-head between an RTX 5070 and an RX 9070 XT only means something when both cards run identical scenes, identical settings, and identical capture rules. Fairness checks take minutes and prevent the usual forum arguments.

  • Use the same CPU, RAM, and Windows install, or at least the same test machine with a clean driver swap.
  • Remove the old vendor’s driver fully before installing the other, so leftover settings do not interfere.
  • Match the upscaler type where possible, or test native rendering for a pure silicon comparison.
  • Run the same number of warm-up passes and the same capture duration on both cards.
  • Record the board partner model, because factory clocks and cooler quality vary between cards with the same GPU name.
  • Log actual retail price on the day you test, not launch MSRP.

That last point matters more than usual in late 2026. Reporting on a fourth Radeon price hike due in November says AMD had not officially confirmed it. Retail prices are moving, so a performance-per-dollar chart is only valid for the date on which you built it. Date your charts.

Common Pitfalls That Ruin Frame-Time Tests

Most bad benchmarks fail for boring reasons, not exotic ones. These six mistakes account for the majority of wrong conclusions.

  1. Trusting a single run. One run can swing by a few percent because of boost behavior, background tasks, or a lucky stretch of the route. Use three runs and the median, and check the spread.
  2. Skipping the warm-up. Shader compilation and cold clocks distort the first pass. Always play the route once before you record.
  3. Changing two things at once. A new driver plus a new power limit gives you a result you cannot attribute. Change one variable per test.
  4. Comparing percentile lows from different tools. A low by percentile and a low by average are different numbers. Match the method before you compare.
  5. Letting a frame cap flatten the data. If V-Sync or a frame limiter pins the game at 60, every configuration looks identical. Remove the cap for raw tests.
  6. Ignoring the capture of generated frames. A frame-generation run is a different test. Label it and keep it out of native comparisons.
  7. Running with a cluttered overlay. Heavy overlays and recording software cost frames. Keep only the readouts you need.

A seventh trap deserves its own mention: confirmation bias. If you expected the undervolt to help, you will read ambiguous data as proof. Decide in advance what result would change your mind, for example a drop in your low figures beyond a margin you set, then apply that rule when the numbers arrive.

Troubleshooting: 10 Problems and Fixes

Capture tools sit close to the operating system, so they fail in odd ways. Work through these in order.

  1. Capture file is empty or has only a header. The process name is wrong or PresentMon lacks permission. Check the exact executable name in Task Manager (the Details tab), match its case, and run from an elevated shell.
  2. CapFrameX records nothing in one game. Some titles use a launcher that spawns the real process later. Select the actual game executable as the capture target, and if several processes appear, pick the one that is presenting frames.
  3. The overlay does not appear. Start RTSS before the game, confirm the game is not running as a different user, and try the per-game profile option.
  4. CapFrameX shows lower FPS than the in-game counter. The tools may calculate FPS differently. CapFrameX documents that it derives FPS from frame times, which can produce lower figures than a game counter that averages over a second. Compare like with like.
  5. Frame times freeze after toggling frame generation. Stale timing data after a mode switch has been a reported issue, and the CapFrameX-maintained build reportedly includes fixes. Restart the capture after any mode change and update to a current build.
  6. Anti-cheat blocks the overlay or the game. Some online games reject injected overlays. Use the hook-free overlay if your build offers it, or capture with the console tool, which does not inject anything into the game.
  7. The analysis script says no frame time column found. Open the CSV and check the header. The script looks for FrameTime, MsBetweenPresents, and msBetweenPresents. A custom export may use another name, so add it to the tuple at the top of analyze.py.
  8. The spread between runs is above 3%. The test is not controlled. Look for background apps, a game that streams new data each run, thermal build-up, or a route that differs slightly. Fix the cause and repeat. If you hear a buzzing card during long runs, the guide to fixing GPU coil whine covers a separate but common annoyance.
  9. Captures show periodic 100 ms spikes. This pattern usually comes from a background task such as an updater, antivirus scan, or cloud sync. Pause them and capture again. If the spikes persist, check storage and shader cache activity.
  10. CapFrameX or the installer is flagged by security software. Hook-based overlays trip heuristics. Confirm you downloaded from the official source and verified the hash, then add an exception only if you are sure of the origin. Never disable your protection globally.

When the Numbers Look Too Good

If a tweak seems to deliver a 15% gain for free, suspect the test before celebrating. Check that the resolution did not change, that the upscaler mode matches, that the scene loaded the same way, and that no frame cap kicked in. Real gains from software tweaks on current GPUs are usually modest. Dramatic results almost always trace back to a changed variable.

Advanced Tips for Serious Benchmarkers

Once the basic workflow feels routine, a few upgrades raise the quality of your data. First, log sensors alongside frame times. Clock speed, power draw, and temperature next to a spike often explain it in seconds, such as a clock dip when the power limit bites. CapFrameX shows telemetry beside the capture, and the project has even been adding Blackwell thermal work in its repository, according to a November 2026 code commit.

  • Use the CPU-bound versus GPU-bound check. If GPU usage sits well below 100% while frame times spike, the CPU or memory is the limit. Lowering resolution should raise FPS only when the GPU is the bottleneck.
  • Compare against yourself over time. Keep a dated results spreadsheet. A driver that quietly costs 4% will show up as a trend, not a single run.
  • Test at two resolutions. A card that looks fine at 1440p can stumble at 4K because of memory limits, and the low-percentile frame times usually reveal it first.
  • Automate the post-processing. Add a few lines to summarize_runs.py to write a CSV or markdown table, then paste it straight into your notes.
  • Plot your own frame time histogram. A short matplotlib script that bins frame times shows the shape of the distribution, which percentile numbers compress.
  • Keep an eye on the market. With a next-generation GPU delay reported, the cards you own now may have to last. Good measurement helps you squeeze them properly. More hardware guides live in the hardware section.

Version Discipline and the Future of the Tool

CapFrameX is under active development. Do not plan a workflow around features that have not shipped. Pin the exact CapFrameX and PresentMon versions in each results log, and re-run your baseline whenever you update, because capture changes can shift numbers slightly.

FAQ: CapFrameX and Frame-Time Benchmarking

Is CapFrameX free?

The project distributes the application openly and its site carries a Donate link, which points to a free, donation-supported model. No paid plan appeared in the project’s official pages at the time of writing, but check the download page for current terms before you rely on that.

What is the difference between CapFrameX and PresentMon?

PresentMon is the measurement engine from Intel, available as a console app and as a newer overlay app. CapFrameX wraps the same capture service in an analysis interface with charts, run comparison, and sensor data. Both see the same underlying frame data, so pick the one that fits your workflow, or use both as this guide does.

How long should a benchmark capture be?

Sixty seconds is a solid default. It produces thousands of frames at typical refresh rates, which stabilizes the low-percentile figures. Use a shorter run for quick checks, and go longer when you care about the extreme lows or thermal behavior over time.

Which is better, a low figure by percentile or by average?

Neither is universally better. The percentile method tells you the cutoff frame time of the slowest slice of frames, while the average method tells you how bad that slow slice is on average. Report one consistently, and show both if you can. Never compare a number from one method against a figure from the other.

Can I use CapFrameX on AMD and NVIDIA cards?

Yes. The PresentMon site describes its capture as working on compatible Windows applications across Intel, NVIDIA, and AMD hardware, and CapFrameX builds on the same service. The method in this guide works on an RTX 50 card, a Radeon RX 9000 card, or older hardware.

Why do my results change between runs?

Small variance is normal. Boost clocks, temperature, background tasks, and scene randomness all contribute. Keep the spread as small as you can by warming up, closing background apps, and using the same route each time. If the spread stays high, find the uncontrolled variable before drawing conclusions.

Does frame generation make a benchmark invalid?

No, but it makes the test a different one. Capture native and frame-generated runs separately, label them clearly, and never present the generated frame rate as raw GPU performance. Update your tools regularly, since capture handling for generated frames has been changing in recent builds.

Source: Tech Insider