Repository navigation
Hot Path Windows Short Waits
Lifecycle: Performance - Implemented · Type: Heuristic wait shortcut
State: Present in currentmain.
| Field | State at reviewed main
|
|---|---|
| Underlying route | Implemented as Windows/Xbox short-wait boolean |
| Current control |
Reduce CPU usage while waiting, live, default On |
| Planned control | Issue #98: Auto / Low latency / Lower CPU use
|
| Exact scope | Non-ppoll Xbox path and 0 < timeout < 1.25 ms
|
| Maintained reference | Busy-spin to deadline, then poll original handles with zero timeout |
| Evidence counters | Focused tests/source checks; shared #94 route artifact absent |
| Qualification | API/fault/readiness coverage; policy tradeoff remains workload-specific |
| Reviewed source |
322986f4c5502b3fa887a9605ac8cd88e9b2c579, 2026-09-14 |
| Primary history | Host-wait work around issues #19/#20 and PR #25; control PR #37; planned #98 |
For very short timeouts on the Windows non-ppoll path, current xemu historically busy-spins until the nanosecond deadline and then polls original handles with timeout zero. That preserves sub-millisecond timing but burns an active CPU core during the wait.
When the CPU-saving control is enabled, xemu first attempts a persistent thread-local high-resolution Windows waitable timer. It appends that timer to a private copy of the original GPollFD array and waits on both the timer and the original event handles. Event readiness can therefore end the wait before the timeout. Any unsupported, re-entrant, create/arm/cancel, or lifecycle condition falls back to the busy-spin reference route.
The control does not affect every qemu_poll_ns() call. Zero, negative/infinite, long, ppoll, and non-Windows paths follow their normal implementations.
The primary target is host CPU/power, not necessarily lower latency:
eligible short waits
× active busy-spin duration replaced by kernel wait
On a thread that repeatedly waits hundreds of microseconds, replacing spin time can reduce active CPU cycles, heat, fan/power use, and contention with the vCPU or renderer.
The tradeoff is wake behavior. Kernel scheduling and timer delivery can add latency or tail variance compared with spinning. A title already limited by another thread may gain nothing in guest progress even while CPU use falls.
Current main has focused Windows tests for sub-millisecond return, original event readiness, enable/disable behavior, failure/backoff, and cleanup. PR #37 qualified the integrated control path but did not claim a fresh exact-head game-speed improvement.
Historical work treated the route as a CPU-use policy. This distinction is essential: fewer active cycles in the helper do not prove a faster frame, and a lower mean can coexist with worse p99 wake intervals.
Direct host API: Microsoft documents CreateWaitableTimerExW and CREATE_WAITABLE_TIMER_HIGH_RESOLUTION for high-resolution timer objects, with explicit handle lifetime and access requirements: CreateWaitableTimerExW.
General host-wait precedent: Windows waitable timer objects allow threads to block until a due time rather than consume active CPU: Waitable timer objects.
The API does not guarantee that this policy is best for xemu. xemu additionally has to preserve GLib poll semantics, original event readiness, nanosecond deadline rounding, re-entry safety, and per-thread lifecycle.
QEMU/xemu main-loop or worker wait request
original event handles + nanosecond timeout
↓
host waiting strategy
busy-spin reference OR high-resolution timer + original handles
↓
required result
same ready handles, revents, return/error semantics, and no early timeout
This policy does not alter an emulated PIT, RTC, NV2A PTIMER, or other Xbox device timer. It changes how a host thread waits between emulator events.
| Location | Role | Owner |
|---|---|---|
util/qemu-timer.c::qemu_poll_ns() |
Scope gate, reference spin, normal poll paths | Calling thread |
XboxHighResolutionPoll TLS state |
Timer handle, private poll array, re-entry/backoff/lifecycle | One instance per calling thread |
xbox_high_resolution_poll_ns() |
Create/reuse, arm, wait, copy readiness, cancel/drain | Calling thread |
qemu_poll_set/get_cpu_saving() |
Current atomic boolean | UI publishes; poll threads read |
include/qemu/timer.h |
Public wait-policy API surface | QEMU utility layer |
| issue #98 | Planned typed policy/counters | Not implemented at reviewed main |
flowchart TD
A[qemu_poll_ns] --> B{CONFIG_PPOLL path?}
B -->|Yes| C[Use ppoll; shortcut not involved]
B -->|No| D{Xbox and 0 < timeout < 1.25 ms?}
D -->|No| E[Convert timeout and call normal g_poll]
D -->|Yes| F{CPU-saving control enabled?}
F -->|No| R[Busy-spin to deadline]
F -->|Yes| G{TLS route re-entered, unavailable or in backoff?}
G -->|Yes| R
G -->|No| H[Create or reuse thread-local high-resolution timer]
H -->|Failure| R
H --> I[Copy original poll descriptors and append timer handle]
I --> J[Recompute remaining deadline]
J -->|Expired| K[g_poll original descriptors with timeout 0]
J -->|Remaining| L[Arm relative timer rounded up to 100 ns]
L -->|Failure| R
L --> M[Block on original handles plus timer]
M --> N[Copy original revents/count back]
N --> O{Original event won before timer?}
O -->|Yes| P[Cancel and drain timer]
O -->|No| Q[Timer represents timeout]
P --> S[Return original-handle ready count]
Q --> S
R --> T[Set timeout 0 after deadline]
T --> K
The appended timer is internal. Its revents is never reported as a caller event.
flowchart TD
A[Eligible sub-1.25 ms wait] --> B[Read realtime clock]
B --> C[Spin until deadline]
C --> D[Set timeout to zero]
D --> E[g_poll original handles]
E --> F[Return original readiness]
This is the current short-wait reference—not a generic blocking wait. Longer timeouts use normal g_poll millisecond conversion instead.
| Invariant | Why required | Failure if weakened |
|---|---|---|
| Scope limited to eligible short waits | Policy was designed for sub-ms busy-spin replacement | Unexpected long/infinite wait behavior |
| Original handles copied and waited together | Events must interrupt timeout | Missed I/O/interrupt wake |
| Timer is thread-local | Handle/array/re-entry state is not cross-thread safe | Data race/corruption |
| Re-entry guard | Alertable/re-entrant path can recurse | Reuse in-flight timer/array |
| Deadline recomputed after setup | Setup consumes part of timeout | Late/early wait |
| Relative due time rounds up to 100 ns | Host timer must not expire before requested QEMU deadline | Early timeout/busy loop |
Original revents copied back exactly |
Caller observes GLib poll contract | Wrong event handling |
| Successful early event cancels/drains timer | Timer must not remain signaled/armed for next call | Spurious immediate timeout |
| Unknown cancellation outcome closes handle | Unsafe timer cannot be reused | Stale signal/race |
| Unsupported/failure path backs off/falls back | Avoid repeated expensive failures | Log/CPU thrash |
errno preserved on poll error |
Utility API semantics | Wrong caller behavior |
Each thread lazily owns one timer handle and a growable private poll-descriptor array. The timer persists across eligible waits and is closed at thread exit. Permanent unsupported errors mark the route unavailable; transient failures set a retry deadline.
The control is live because the next eligible wait reads the atomic boolean. An already executing wait is not rewritten.
Fallback sentinel INT_MIN is internal: the caller then executes the established busy-spin path. A real g_poll error remains an error and preserves errno.
A suitable workload should show:
many 0 < timeout < 1.25 ms calls
+ high-resolution wait attempts/successes
+ lower active CPU during waits
+ original event wake counts preserved
+ unchanged guest progress/correctness
Potential tradeoff signature:
- CPU time/use falls;
- p95/p99/max wake or frame intervals worsen;
- more scheduler-ready delay appears;
- guest progress remains unchanged;
- timer failures cause frequent fallback.
Profile scheduled running versus waiting/ready time. A sleeping thread is not automatically faster.
| Counter | Unit | Meaning |
|---|---|---|
host.wait.calls |
calls | All qemu_poll_ns calls |
zero/negative/long_timeout_calls |
calls | Out-of-scope classes |
short_wait_eligible |
calls | 0–1.25 ms scope reached |
high_res_attempts/successes |
calls | Optimized route admission/result |
busy_spin_reference |
calls | Short-wait reference used |
event_wakes |
calls | Original handle ended wait |
timer_timeouts |
calls | Timer ended wait |
create/arm/cancel/poll_failures |
errors | Failure class |
reentry/unavailable/backoff_fallbacks |
calls | Safety fallback reason |
| requested/actual wait duration | ns | Latency/tail evidence |
| active CPU and ready delay | ns | Resource/scheduling outcome |
Counters should be per owning thread and merged during low-frequency evidence export, not one contended atomic on every wait.
- Omitting original handles turns an interruptible wait into a timer-only sleep.
- Rounding down a 100-ns due time expires early.
- Reusing a timer after uncertain cancellation can produce a stale signal.
- Failing to drain an already-signaled timer can make the next wait return immediately.
- Re-entry can corrupt TLS buffer/timer state.
- Recreating the timer every call would replace spin CPU with kernel/object overhead.
- Permanent unsupported hosts must not retry every wait.
- A disconnected automation client does not prove server-side helper/trace processes stopped.
- Lower CPU use can worsen latency tails; the correct decision can be “keep selectable,” not “always faster.”
See issue #98, PR #25, PR #37, Debugging Tools & Commands, and Pull Request Lessons.
- Timeout classes: negative, zero, 1 ns, threshold-1, threshold, threshold+1, long.
- No-handle timeout and one/multiple original handles.
- Each original handle winning before timer; exact
revents/ready count. - Timer timeout with no original readiness.
- Create, arm, cancel, poll error, unsupported, retry/backoff, and re-entry injection.
- Thread-exit cleanup and repeated process lifecycle.
- On/Off same-binary fixed workload with route counters.
- Scheduled running/wait/ready partition, host CPU/power where available, guest progress.
- P50/p95/p99/max wake and frame intervals.
- OpenGL and Vulkan representative titles because host wait is shared.
- Non-Windows and
ppollsource/build controls proving no accidental route change.
The planned #98 enum should preserve the old boolean migration exactly and expose fallbacks rather than pretending the requested lower-CPU route handled every eligible call.
DSP tooling guide and navigation updated November 2, 2026 for experimental PR #384, source 8f51d237. Capture/replay usage and limits.
The earlier wiki-wide review was published November 1, 2026. Emulator source: b7140848. Reviewed wiki content: 1d2b671. Current status · Release availability · Publication process.
- Home
- Current status
- Stable versus main
- Implemented mechanisms
- Controls and defaults
- Measured results and limits
- Complete change register
- System
- CPU and vCPU
- Memory and DMA
- Graphics / NV2A
- APU and audio
- Timing and timers
- Platform and I/O
- Hardware observations
- Fidelity boundaries
- Methodology
- PGR2 routes
- Debugging lag / hot paths
- Tools and commands
- DSP capture, replay and fuzzing
- HLE readiness and public XISO proof
- Concurrent DSP shadow and OpenAL proof
- DSP HLE operation inventory
- DSP HLE fix examples and testing
- Development
- Upstream shortlist
- Wiki maintenance
- Research / retired work
- Optimization catalog
- Hot-path shortcuts
- Researched experiments
- Unproven proposal directory
- PR lessons
- September26 snapshot
Repository · PRs · Issues