Frame Pacing from First Principles: Consistency Gap, Stutter Magnitude and the Fluidity Score

|By Assurd Engineering Lab|The Playbook

A frame rate is a summary. What the GPU actually produces is a sequence of frames, each delivered some number of milliseconds after the one before it. Two cards can share an average frame rate and feel completely different, because one delivers its frames at a steady cadence and the other delivers most of them quickly and a few of them very late. This post defines, from the frame times up, the three numbers an Assurd certificate uses to describe that difference: the consistency gap, the stutter magnitude and the Fluidity Score.

Every definition below is the one the Kairoscope backend computes (SOP-GPU-001 v1.1, Step 4.2). None of them is entered by a technician.

Frame times

For each benchmark run we record one pass of the title's built-in benchmark with CapFrameX, which logs PresentMon's per-frame timing. The raw material is a list of frame times

t1,t2,…,tN(milliseconds),t_1, t_2, \dots, t_N \quad \text{(milliseconds)},

where tit_i is the interval between successive presents. It is not display scan-out timing and it is not input latency; it is the cadence at which the GPU and driver hand finished frames to the display pipeline.

Frames with ti≤0t_i \le 0 carry no physical meaning (a zero-length frame cannot exist) and are dropped. A capture with no frame left is rejected and the run is repeated; it is never scored as 0 FPS.

Time-weighted averages

There are two ways to turn frame times into an "average FPS", and they do not agree.

The per-frame mean converts each frame to an instantaneous rate and averages the rates:

Fˉper-frame=1N∑i=1N1000ti.\bar{F}_{\text{per-frame}} = \frac{1}{N}\sum_{i=1}^{N} \frac{1000}{t_i}.

The time-weighted mean divides the number of frames by the time they took:

Fˉ=N∑iti/1000=1000tˉ,tˉ=1N∑i=1Nti.\begin{gathered} \bar{F} = \frac{N}{\sum_i t_i / 1000} = \frac{1000}{\bar{t}}, \\ \bar{t} = \frac{1}{N}\sum_{i=1}^{N} t_i . \end{gathered}

The second is the physically meaningful one: it is exactly the number of frames delivered per second of capture. The first is the arithmetic mean of 1000/ti1000/t_i, while the second is its harmonic counterpart, and by the AM–HM inequality

Fˉper-frame  ≥  Fˉ,\bar{F}_{\text{per-frame}} \;\ge\; \bar{F},

with equality only when every frame time is identical. The per-frame mean therefore overstates the frame rate, and it overstates it more the more uneven the frame times are, which is precisely the property we are trying to measure. A metric of smoothness cannot be built on an average that rewards unevenness.

A concrete case: 1000 frames, of which 990 take 8 ms, 9 take 20 ms and 1 takes 50 ms. The capture lasts 990⋅8+9⋅20+50=8150990 \cdot 8 + 9 \cdot 20 + 50 = 8150 ms.

DefinitionComputationResult
Time-weighted1000/8.15 ms1000 / 8.15\ \text{ms}122.7 FPS
Per-frame mean(990⋅125+9⋅50+1⋅20)/1000(990 \cdot 125 + 9 \cdot 50 + 1 \cdot 20) / 1000124.2 FPS

The capture delivered 1000 frames in 8.15 s. Only the first number says so. Every FPS figure on an Assurd certificate is time-weighted.

Lows from the slowest frames

The 1% and 0.1% lows apply the same time-weighted rule to the slowest frames of the run. Sort the frame times in descending order, t(1)≥t(2)≥⋯≥t(N)t_{(1)} \ge t_{(2)} \ge \dots \ge t_{(N)}, and take the slowest kk:

kp=max⁡ ⁣(1,⌈p⋅N⌉),Fp=10001kp∑j=1kpt(j),p∈{0.01, 0.001}.\begin{gathered} k_p = \max\!\bigl(1, \lceil p \cdot N \rceil\bigr), \\ F_p = \frac{1000}{\tfrac{1}{k_p}\sum_{j=1}^{k_p} t_{(j)}}, \\ p \in \{0.01,\ 0.001\}. \end{gathered}

So F1%F_{1\%} is the frame rate the run would have if every frame were as slow as the average of its slowest 1%, and F0.1%F_{0.1\%} likewise for the slowest 0.1%. This is a mean over a tail, not a single percentile value, so one outlier frame moves it in proportion to its weight rather than all or nothing.

Two properties follow directly from the definition and matter later:

  1. The mean of the slowest kk frames is never smaller than the mean of all frames, so F1%≤FˉF_{1\%} \le \bar{F} always holds.
  2. The slowest k0.1%k_{0.1\%} frames are a subset of the slowest k1%k_{1\%} frames, and they are the slowest of that subset, so F0.1%≤F1%F_{0.1\%} \le F_{1\%} always holds.

These are theorems, not tendencies. A record that violates either is not an unusual card; it is a corrupted or hand-edited record.

Sample size. The tail is small. A 60-second pass at 120 FPS yields about 7200 frames, so k1%=72k_{1\%} = 72 and k0.1%=8k_{0.1\%} = 8. A pass of 1000 frames or fewer has k0.1%=1k_{0.1\%} = 1: the 0.1% low is then the single slowest frame of the run. That is an extreme-value statistic and it is noisy by nature, which is one reason the re-run rule exists.

Frames NNk1%k_{1\%}k0.1%k_{0.1\%}
60061
1000101
7200728

Two dimensionless ratios

Absolute frame rates depend on the title, the resolution and the card's class. To describe the shape of the frame-time distribution independently of its scale, we take ratios. Below, tˉ1%\bar{t}_{1\%} and tˉ0.1%\bar{t}_{0.1\%} are the mean frame times of the slowest k1%k_{1\%} and k0.1%k_{0.1\%} frames. The second form of each equation follows because every FPS figure is 10001000 divided by a mean frame time, so the 1000s cancel.

Consistency gap

The consistency gap measures how far the slowest 1% of frames fall below the run as a whole:

g=1−F1%Fˉ=1−tˉtˉ1%.g = 1 - \frac{F_{1\%}}{\bar{F}} = 1 - \frac{\bar{t}}{\bar{t}_{1\%}} .

It is about sustained unevenness: is the slow end of the distribution broadly slower than the bulk? Rearranged, tˉ1%=tˉ/(1−g)\bar{t}_{1\%} = \bar{t} / (1 - g). A gap of 0.25 means the slowest 1% of frames take 4/34/3 of the mean frame time.

Stutter magnitude

The stutter magnitude measures how far the slowest 0.1% fall below the slowest 1%:

s=1−F0.1%F1%=1−tˉ1%tˉ0.1%.s = 1 - \frac{F_{0.1\%}}{F_{1\%}} = 1 - \frac{\bar{t}_{1\%}}{\bar{t}_{0.1\%}} .

It is about isolated spikes: within the slow tail, is there a still-slower sliver? tˉ0.1%=tˉ1%/(1−s)\bar{t}_{0.1\%} = \bar{t}_{1\%} / (1 - s). A stutter of 1/3 means the slowest 0.1% take 1.5× as long as the slowest 1%.

Properties

By the two properties in Lows from the slowest frames, 0≤g<10 \le g < 1 and 0≤s<10 \le s < 1. Both are lower-is-better, and both are stored as fractions (the certificate shows them ×100 as a percentage).

A run can have a small gap and a large stutter: a steady cadence broken by a handful of long frames. That pattern is the one a single average hides best, which is why we measure it separately.

Ratios are scale-free but not workload-free: a demanding scene can widen the gap on a healthy card. That is why runs are only ever compared within a cohort of identical configurations (see Cohorts and ranking).

Fluidity Score

The Fluidity Score combines the two ratios into one number on a 0–100 scale:

SF=100 (1−(0.7 g+0.3 s)).S_F = 100\,\bigl(1 - (0.7\,g + 0.3\,s)\bigr).

SF=100S_F = 100 exactly when g=s=0g = s = 0, a perfectly uniform cadence, and it falls linearly as either ratio grows. Its partial derivatives are constant:

∂SF∂g=−70,∂SF∂s=−30,\frac{\partial S_F}{\partial g} = -70, \quad \frac{\partial S_F}{\partial s} = -30 ,

so each percentage point of consistency gap costs 0.7 points of score, and each percentage point of stutter costs 0.3. The 0.7 / 0.3 weighting expresses a design judgement: sustained unevenness across the slowest 1% is felt for longer than a sliver of 0.1% spikes. The weights are engineering priors, not values fitted to perception data.

Because the score is linear, its grade thresholds are straight lines in the (g,s)(g, s) plane:

SF≥88  ⟺  0.7 g+0.3 s≤0.12,SF≥95  ⟺  0.7 g+0.3 s≤0.05.\begin{gathered} S_F \ge 88 \iff 0.7\,g + 0.3\,s \le 0.12, \\ S_F \ge 95 \iff 0.7\,g + 0.3\,s \le 0.05 . \end{gathered}
ThresholdLargest gg (with s=0s = 0)Largest ss (with g=0g = 0)
SF≥88S_F \ge 880.17140.4000
SF≥95S_F \ge 950.07140.1667

The Rules Engine applies the score per resolution tier, using the mean over that tier's runs that have a score:

  • PLATINUM requires at least one resolution tier with SF≥95S_F \ge 95.
  • GOLD and PLATINUM require every measured resolution tier with SF≥88S_F \ge 88.
  • A certificate with no measured gaming tier is capped at SILVER.

Worked example

Take a run with Fˉ=120\bar{F} = 120, F1%=90F_{1\%} = 90 and F0.1%=60F_{0.1\%} = 60 FPS. In frame-time terms:

tˉ=1000120=8.333 ms,tˉ1%=100090=11.111 ms,tˉ0.1%=100060=16.667 ms.\begin{gathered} \bar{t} = \frac{1000}{120} = 8.333\ \text{ms}, \\ \bar{t}_{1\%} = \frac{1000}{90} = 11.111\ \text{ms}, \\ \bar{t}_{0.1\%} = \frac{1000}{60} = 16.667\ \text{ms}. \end{gathered}

Consistency gap.

g=1−90120=1−0.75=0.25.g = 1 - \frac{90}{120} = 1 - 0.75 = 0.25 .

The slowest 1% of frames take 11.111/8.333=4/311.111 / 8.333 = 4/3 of the mean frame time.

Stutter magnitude.

s=1−6090=1−0.6667=0.3333.s = 1 - \frac{60}{90} = 1 - 0.6667 = 0.3333 .

The slowest 0.1% take 16.667/11.111=1.5×16.667 / 11.111 = 1.5\times as long as the slowest 1%.

Fluidity Score.

SF=100 (1−(0.7⋅0.25+0.3⋅0.3333))=100 (1−(0.175+0.1))=72.5.\begin{aligned} S_F &= 100\,\bigl(1 - (0.7 \cdot 0.25 + 0.3 \cdot 0.3333)\bigr) \\ &= 100\,(1 - (0.175 + 0.1)) \\ &= 72.5 . \end{aligned}

A run like this is unremarkable to an average-FPS reader: 120 FPS is a high frame rate. It is a long way from the grade lines, though. To reach 88, the weighted sum 0.7g+0.3s0.7g + 0.3s must fall from 0.275 to 0.12, less than half its current value. The frame rate is fine; the cadence is not.

The same arithmetic on the 1000-frame capture from Time-weighted averages (990 × 8 ms, 9 × 20 ms, 1 × 50 ms): Fˉ=122.7\bar{F} = 122.7, F1%=1000/23=43.5F_{1\%} = 1000/23 = 43.5, F0.1%=1000/50=20.0F_{0.1\%} = 1000/50 = 20.0 FPS, hence g=0.6455g = 0.6455, s=0.5402s = 0.5402 and SF=38.61S_F = 38.61. Ninety-nine percent of that run is a flawless 125 FPS; the tail alone takes the score to 38.61. That is the intended behaviour. A frame-pacing score that forgave the tail would be another average.

Test vectors

Both examples are fixed test vectors in SOP v1.1 Step 4.2 and in the backend test suite: 120 / 90 / 60 FPS gives gap 0.25, stutter 0.3333, score 72.5; the 1000-frame capture gives 122.7 / 43.5 / 20.0 FPS, gap 0.6455, stutter 0.5402, score 38.61. Any implementation that disagrees is wrong.

The stutter Soft Limit

A tier's score is a mean over runs, and a mean can dilute one bad run. Suppose a tier has four runs: three at SF=96S_F = 96, and one with g=0.05g = 0.05 and s=0.61s = 0.61, which scores

SF=100 (1−(0.7⋅0.05+0.3⋅0.61))=100 (1−0.218)=78.2.\begin{aligned} S_F &= 100\,\bigl(1 - (0.7 \cdot 0.05 + 0.3 \cdot 0.61)\bigr) \\ &= 100\,(1 - 0.218) \\ &= 78.2 . \end{aligned}

The tier mean is (3⋅96+78.2)/4=91.55(3 \cdot 96 + 78.2)/4 = 91.55, above the GOLD line, even though one run delivered its slowest 0.1% of frames at well under half the rate of its slowest 1%.

For this reason stutter is also checked per run, never on the mean. A recorded run with s>0.60s > 0.60 (strictly greater) raises a Soft Limit: it requires a written justification in the audit trail and a Phase 1 re-examination of VRM rail resistance, whatever the score says. Note that a run past this limit can never reach 88 on its own: 0.3⋅0.60=0.18>0.120.3 \cdot 0.60 = 0.18 > 0.12, so its score is below 82 even with a zero gap.

The Soft Limit is a trigger for investigation, not a diagnosis. Isolated long frames can come from failing power delivery, but they can also come from shader compilation on first launch, asset streaming, or background system activity. The SOP removes the most common of these before a run counts:

  • On first launch on a card, the technician waits at the main menu until shader compilation has finished.
  • If a capture is incomplete, or its stutter exceeds 0.60, that configuration is re-run once, immediately. The re-run is the recorded run; the first capture is archived and its SHA-256 is cited in the audit trail, so the re-run cannot quietly replace it.

Only if the re-run still exceeds 0.60 does the Soft Limit apply.

Absent data stays absent

The ratios are only defined when their inputs are. The backend's null rule is:

  • If Fˉ\bar{F} or F1%F_{1\%} is 0 or absent, gg and SFS_F are null.
  • If F1%F_{1\%} is 0 or absent, or F0.1%F_{0.1\%} is absent, ss and SFS_F are null.
  • Out-of-order inputs, F1%>FˉF_{1\%} > \bar{F} or F0.1%>F1%F_{0.1\%} > F_{1\%}, give null. Lows from the slowest frames shows why: they cannot come from a real capture.

Nothing is defaulted to 0 and nothing is interpolated. A run that cannot execute, for example a ray-traced title on a GPU without hardware ray tracing, is recorded as not run and disclosed on the certificate; it is never a 0 FPS run that would drag a tier mean down.

Rounding. The three FPS figures are rounded to one decimal place. The gap and stutter are computed from those rounded FPS figures and then rounded to 4 decimal places. The Fluidity Score is computed from the unrounded ratios and rounded to 2 decimal places. Anyone who re-derives the score from the FPS figures printed on a certificate therefore gets the printed score.

Cohorts and ranking

A ratio is only comparable across runs of the same workload. Each run is ranked only against runs with an identical configuration:

cohort=(title, output resolution,preset, ray tracing,upscaler, upscaler mode,frame generation).\begin{aligned} \text{cohort} = (&\text{title},\ \text{output resolution}, \\ &\text{preset},\ \text{ray tracing}, \\ &\text{upscaler},\ \text{upscaler mode}, \\ &\text{frame generation}). \end{aligned}

Upscaled and native runs are never compared, and nor are DLSS, FSR and XeSS runs with each other.

A percentile is published only when the cohort holds at least n=30n = 30 runs. Below that, the rank, the cohort mean and the z-score are null and the certificate shows "Provisional · n = X", with X the real count. When a rank is shown, it reads "better than PP% of the cohort", already adjusted so that higher is always better, even for lower-is-better metrics like gg and ss.

Certificate means

The certificate's fleet section also reports the consistency gap (mean) and stutter (mean): the means over the card's RR runs that have a value,

gˉ=1R∑r=1Rgr,sˉ=1R∑r=1Rsr.\begin{gathered} \bar{g} = \frac{1}{R}\sum_{r=1}^{R} g_r, \\ \bar{s} = \frac{1}{R}\sum_{r=1}^{R} s_r . \end{gathered}

Runs of different titles and resolutions are never pooled into one cohort: each run is ranked within its own cohort, and the certificate reports the median of those per-run percentiles. Because the runs come from different cohorts, there is no single cohort mean to show beside gˉ\bar{g} and sˉ\bar{s}, so that column reads "—".

Calibration status

Every threshold in this post is an engineering prior pending fleet calibration (n ≈ 300): the 0.7 / 0.3 weights, the 88 and 95 grade lines, the 0.60 stutter Soft Limit and the 30-run ranking minimum. They follow from the reasoning above, not from a fitted population, and we do not describe them as empirical or statistically validated. Once the fleet holds roughly 300 certified cards, the thresholds will be reviewed against the observed distributions, and any change will be versioned in the SOP.

The definitions themselves, the time-weighted averages, the slowest-kk lows, the two ratios and the null rule, are not priors. They are arithmetic, and they will not change with calibration.

See it on a certificate

The sample certificate shows these metrics as they appear on a real Assurd certificate, per run and per resolution tier, with the grade reasoning that follows from the fluidity gates. Its numbers are illustrative: they come from synthetic frame-time logs run through the production parser, not from a measured card.

Notation

SymbolMeaningRange
tit_iFrame time of frame ii, ms (ti≤0t_i \le 0 dropped)>0\gt 0
tˉ\bar{t}Mean frame time over all NN framesms
kpk_pmax⁡(1,⌈pN⌉)\max(1, \lceil pN \rceil), frames in the slowest-pp tailinteger
Fˉ\bar{F}Average FPS, 1000/tˉ1000/\bar{t}FPS
F1%, F0.1%F_{1\%},\ F_{0.1\%}1000 / mean frame time of the slowest k1%k_{1\%}, k0.1%k_{0.1\%} framesFPS
ggConsistency gap, 1−F1%/Fˉ1 - F_{1\%}/\bar{F}0–1, lower better
ssStutter magnitude, 1−F0.1%/F1%1 - F_{0.1\%}/F_{1\%}0–1, lower better
SFS_FFluidity Score, 100 (1−(0.7g+0.3s))100\,(1 - (0.7g + 0.3s))0–100, higher better