How the Frame Limiter Works
The first system of Bloodborne replaced in C++ is the one that decides how long each frame lasts. This page walks through it as the original does it, and as the replacement now does it, exactly the same way.
Where it sits
Every frame, frame_timing_frame_step asks the window whether the game is still running, and on the very first frame creates the game's frame-pacing object, the SprjFlipper. Its constructor, frame_timing_flipper_init, reads one value from the game's configuration: Game.FlipMode. Then the frame step calls the flipper's limiter, frame_timing_pace_frame, and only after it returns lets the game do its frame of work.
So the limiter runs at the start of every frame, and its job is to make sure the previous frame took long enough.
Choosing the pace
The flip mode picks one of five settings. Each sets two things in the flipper: the frame interval (a float, in seconds) and the sync interval (how many vblanks a flip waits for).
| Mode | Frame interval | Sync interval |
|---|---|---|
| 0, 1 | 1/30 s | 2 |
| 2 | 1/60 s | 1 |
| 3, 4 | 1/30 s | 1 |
A pending mode can replace the current one at the start of a frame; a few flags reset the pacing state, for example after a load.
Waiting out the frame
The limiter reads the time with gettimeofday, works out how much of the frame's budget is left, and waits. The game carries two ways of waiting: one that sleeps in chunks and spins the last few milliseconds, and one that only spins, reading the clock again and again until the time is up. A switch compiled into this build always picks the spinning one, which is why a quick frame can read the clock hundreds of thousands of times. It is precise, and it keeps one CPU core busy.
The budget is not always the full interval. The flipper keeps the last 32 frame times in a ring, marking the frames that ran late; if recent frames ran over, the next budget is shortened to catch up, but never below a third of the interval.
Afterwards
The limiter records the frame's time in the ring, decides whether this frame counts as late (and if so drops the sync interval to 0 so the next flip does not wait for a vblank), and keeps a 16-frame history from which it computes the frame rate the game reports. If a flag asks for it, it also tells another object that the game now targets 60 frames a second.
The replacement
The C++ version follows the original instruction by instruction, including how it converts between integers and floating point and how each comparison treats a value that is not a number. It was compared with the original on every frame recorded in two game runs, including frames that spin on the clock for hundreds of thousands of reads, and on hand-written cases for every mode and flag; four deliberately wrong versions all failed.
It also carries the port's first setting, BB_TARGET_FPS: unset, nothing changes; 30, 60 and uncapped do what the community frame-rate patches do here (a fixed interval of 1/30, 1/60 or 1/240 s, no overrides), and the frame step passes the game the real frame time, as those patches do. Each part is checked against the patch itself. See the frame timing page.