There was a community event in a Discord I am part of: A few hours of playing Quake Live.
Having played well over 1000 hours of this game when I was younger, but not having touched it in a while, I wanted to make sure it runs fine on my PC and that my config was still set up to my liking. Looking up how to run the game at the maximal available refresh rate, I found a Steam community comment of someone claiming to be able to run the game at 1000 fps with a special com_maxfps of 250x.
com_maxfps is supposed to be capped at 250 and setting it to any value above it clamps it down to 250. But it actually worked. 250x lets the game run at the engine’s maximum of about 1000 FPS.
This had to be a bug. So I did what any normal person would do, opened Ghidra, and analyzed the binary.
Quake Live caps its frame rate by sleeping until a minimum number of milliseconds has passed.
It’s Com_Frame decompiles to roughly this:
minMsec = 1; // default: essentially uncapped
if (com_dedicated->integer == 0) {
int fps = com_maxfps->integer; // read the INTEGER field
if (0 < fps && fixedtime->integer == 0) { // only cap if fps > 0
if (unfocused->integer != 0) fps = 60;
minMsec = 1000 / fps; // e.g. 1000/250 = 4ms
}
}
...
do {
elapsed = now() - lastFrame;
if (minMsec - elapsed < 1) break; // spin until minMsec has passed
} while (...);
The gist of this is: setting /com_maxfps 0 would unlock the frame rate. You get whatever your machine can handle, up to the ~1000 FPS wall a 1ms floor implies.
This tracks with what Quake 3 does, according to the source.
But com_maxfps is clamped by the client to a range between 30 and 250.
So how does 250x circumvent the clamp?
When a cvar is set, it is stored in three different ways:
cvar->string = "250x";
cvar->value = atof("250x"); // = 250.0 OK
cvar->integer = Cvar_ParseInteger(); // = ? (this is what the limiter uses)
And here’s the integer parser in full:
int Cvar_ParseInteger(char *s) { // s = "250x"
if (strstr(s, "0x") != NULL) { // does the string contain "0x"?
int v = 0;
sscanf(s, "0x%08x", &v); // then parse it as hex
return v;
}
return atoi(s); // otherwise, plain decimal
}
The intent of this was: “If the user typed a hex literal like 0xFF, parse it as hex; otherwise use atoi.”
Going through the function with the string "250x":
strstr(s, "0x") looks for "0x" anywhere in the string (not just at the front), so the parser incorrectly decides it is
looking at hex and takes the hex branch.sscanf(s, "0x%08x", &v), which now does expect the string to start with "0x", so it fails to match and v is left at its initial value of 0, which is then returned.Side note: Turns out this bugged Cvar_ParseInteger() function was introduced in Quake Live, as the original Quake 3 source simply used atoi() to parse the integer.
Wouldn’t clamping catch this? It would, but the clamp compares against the float value (not the bugged integer), which is still 250.0.
I don’t think so, and I looked.
The constraints are:
cvar->integer must be consumed by code.Sending an AI agent to look through the code of a Quake Live Source Reconstruction Project, it found zero other cvars that match these constraints.