mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-13 09:39:33 +01:00
docs: How lag sync paces, and how to benchmark it on a platform
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c0826fab63
commit
746ddfa818
2 files changed
+106
No files matched your search
@@ -38,6 +38,7 @@ for it:
|
||||
| [docs/kernel-hle-review.md](docs/kernel-hle-review.md) | Findings from a review pass over `Core/HLE/sceKernel*.cpp`, and what was verified clean |
|
||||
| [docs/shader-compilation.md](docs/shader-compilation.md) | Shader/pipeline compile stutter: where the time goes on MoltenVK (Metal's MSL compile, not glslang), the caches involved, how game-specific shaders are, and how to measure a cold start |
|
||||
| [docs/metal-backend.md](docs/metal-backend.md) | The Metal backend: shader translation, the render manager (recording, replay, pass merging), programmable blending, how it was tested against Vulkan, and what isn't done |
|
||||
| [docs/lag-sync.md](docs/lag-sync.md) | How lag sync ("Force real clock sync") paces emulation every millisecond and falls back to bursting, and how to benchmark it on a platform (speed near the host's limit, CPU cost, sleep lateness) |
|
||||
| [docs/async-savestates.md](docs/async-savestates.md) | How savestate compression and file I/O run on a worker thread: the save/load/rewind pipelines, ordering, memory rules, what stays synchronous, and testing slots with the `game.state.*` debugger commands |
|
||||
|
||||
## General instructions
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Lag sync: how it works, and benchmarking it
|
||||
|
||||
Lag sync ("Force real clock sync", `bForceLagSync`, `--lag-sync` on the command line) keeps emulated
|
||||
time in step with the wall clock every emulated millisecond, instead of running a frame flat out and
|
||||
then waiting at the end of it. Spread-out emulation means lower input, network and audio latency: with
|
||||
bursts, a whole frame of audio arrives at once, so the audio buffer has to hold a frame plus slack.
|
||||
|
||||
The code is `hleLagSync` and `UpdateLagSyncMode` in `Core/HLE/sceDisplay.cpp`.
|
||||
|
||||
- Each wait is measured from a fixed base (`lagSyncRealBase`/`lagSyncEmuBase`), so waking up late
|
||||
doesn't add up. That's why the sleep doesn't need to spin: it uses `sleep_approx`
|
||||
(`Common/TimeUtil.cpp`), which never spins, rather than `sleep_precise`, which on Windows spins the
|
||||
last 1.02 ms.
|
||||
- It only sleeps when at least `LAG_SYNC_MIN_SLEEP` (2 ms) ahead, and asks for `lagSyncWakeLate` less:
|
||||
a running average of how late the OS wakes it.
|
||||
- More than 100 ms behind or ahead, it starts over from now.
|
||||
- **Fallback to bursting.** Every second of wall time it measures the share of time spent waiting, in
|
||||
lag sync or in the frame throttle. Under 10% idle it stops pacing and lets the frame throttle pace
|
||||
per frame, exactly as with lag sync off. It goes back after two windows over 25% idle.
|
||||
|
||||
## Benchmarking
|
||||
|
||||
What to find out on a platform:
|
||||
|
||||
1. **Speed.** Near the host's limit, lag sync must not run slower than bursting (lag sync off).
|
||||
2. **Cost.** At 1x, how much more CPU lag sync uses than bursting.
|
||||
3. **Lateness.** How late its sleeps wake up on average (`late` below).
|
||||
4. **Fallback.** That it falls back when the host is nearly flat out, and stays pacing otherwise.
|
||||
|
||||
Use headless with a game and a savestate in real gameplay, Vulkan, a Release build, and the firmware
|
||||
memstick (see the AGENTS.md section on game runs). Ask the user which game and state to use. Compare the
|
||||
branch against a build of `master` from the same commit the branch starts from, run one at a time.
|
||||
`--frame-stats` prints, at the end of the run:
|
||||
|
||||
```
|
||||
Frame stats: 422 flips in 14 emulated seconds, wall 15.1s, per second: 32 30 30 ...
|
||||
Lag sync: 5092 sleeps, 12.63s slept, 1us late on average, 0 resets, 1 fallbacks to bursting (bursting now)
|
||||
```
|
||||
|
||||
The second line only appears with lag sync on, and only from the branch build.
|
||||
|
||||
### Speed near the host's limit
|
||||
|
||||
`--speed=N` (percent, with `--realtime`) runs the game N% faster than a PSP, which is the easy way to
|
||||
turn a fast host into a slow one. First find the game's ceiling by running it unthrottled (leave out
|
||||
`--realtime`) for a fixed wall time and dividing emulated seconds by wall seconds. Then run speeds
|
||||
around it, for example 60%, 80%, 95% and 105% of the ceiling, each with and without `--lag-sync`:
|
||||
|
||||
```
|
||||
PPSSPPHeadless --memstick=<memstick> --graphics=vulkan --realtime --speed=2800 --lag-sync --frame-stats
|
||||
--timeout-emulated=1000 --timeout-wall=12 --log <game.iso> --state=<state.ppst>
|
||||
```
|
||||
|
||||
Compare emulated seconds reached in the 12 wall seconds. `--log` shows the mode switches (`Lag sync: 4%
|
||||
idle, bursting instead`, `... pacing again`). Expected: the same emulated seconds as without lag sync, or
|
||||
slightly more; staying in pacing mode below about 80% load; falling back above about 90%.
|
||||
|
||||
Reference, M1 Mac, GTA VCS (ceiling about 30x): at 15x and 18x both modes reached the same emulated
|
||||
time and lag sync stayed pacing; at 24x, 28x and 32x it fell back and reached 284/320/330 emulated
|
||||
seconds against 284/317/319 for bursting.
|
||||
|
||||
### Cost at 1x
|
||||
|
||||
Run 15 emulated seconds at 1x (`--realtime --timeout-emulated=15`) with and without `--lag-sync`, two or
|
||||
three times each, alternating, and compare the process's CPU time. On the Mac, `/usr/bin/time -l` gives
|
||||
user/sys time, instructions and cycles. On Windows, from PowerShell:
|
||||
|
||||
```powershell
|
||||
$p = Start-Process -FilePath $exe -ArgumentList $argList -PassThru -NoNewWindow -RedirectStandardError err.txt
|
||||
$p.WaitForExit()
|
||||
"{0:N2}s CPU" -f $p.TotalProcessorTime.TotalSeconds
|
||||
Select-String -Path err.txt -Pattern 'Frame stats|Lag sync:'
|
||||
```
|
||||
|
||||
Read `TotalProcessorTime` only after `WaitForExit()`. Reference, M1 Mac: bursting about 2.8 s of CPU,
|
||||
lag sync about 3.2 s (about 1% of a core more), the same for the old and new lag sync.
|
||||
|
||||
On Windows, also run `master` with `--lag-sync`: its waits spun (see above), so it should cost clearly
|
||||
more CPU than the branch. That's the main thing this change is expected to fix there.
|
||||
|
||||
### Lateness
|
||||
|
||||
`late` is the average of how much later than its goal each sleep woke up, after the `lagSyncWakeLate`
|
||||
correction, so it should be near 0. The M1 Mac gives 1-10 us. On Windows it depends on whether
|
||||
`CreateWaitableTimerExW(CREATE_WAITABLE_TIMER_HIGH_RESOLUTION)` worked (Windows 10 1803 and later); with
|
||||
the fallback `Sleep()`, expect lateness around a millisecond. Lateness much over 1 ms, or many resets,
|
||||
is worth reporting.
|
||||
|
||||
### Things that skew the numbers
|
||||
|
||||
- **Background QoS on macOS** (`taskpolicy -c background`) gives timers huge slack: it is not a slow-CPU
|
||||
simulation. Bursting lost most of its speed under it (12 emulated seconds in 40), and lag sync woke
|
||||
48 ms late on average yet kept up (14 in 15.6), because its waits don't add up.
|
||||
- The first second includes loading the state and compiling JIT blocks and shaders, and can make the
|
||||
first window fall back to bursting. That's expected, and it goes back to pacing two seconds later if
|
||||
there's idle time.
|
||||
- Laptops on battery throttle unpredictably; plug in. Don't run two builds at once.
|
||||
|
||||
## Still to do
|
||||
|
||||
- With lag sync pacing, audio arrives about every 1.5 ms rather than once per frame, so the
|
||||
resampler's target buffer (`TARGET_BUFSIZE_DEFAULT`, 40 ms, in `Core/HW/StereoResampler.cpp`) could be
|
||||
much smaller, going back up on a fallback to bursting.
|
||||
- Measure on Windows and on Android (where CPU frequency scaling may react to spread-out work), then
|
||||
decide whether lag sync should be on by default.
|
||||
Reference in new issue
Block a user