The one frame-timing contract every host render loop must follow. More...
#include <atomic>#include <cstdint>#include <pulp/view/continuous_frames.hpp>#include <pulp/view/frame_clock.hpp>Go to the source code of this file.
Classes | |
| class | pulp::view::HostFramePump |
| Converts a host's monotonic presentation timestamps into per-frame deltas. More... | |
| struct | pulp::view::HostFrameTick |
| Result of begin_host_frame. More... | |
Namespaces | |
| namespace | pulp |
| namespace | pulp::view |
| Hover-cursor tracking for platform window hosts. | |
Functions | |
| bool | pulp::view::should_dispatch_host_frame (HostFramePump &pump, bool needs_repaint, bool continuous, bool has_idle_callback) |
| Step 0 of a host tick, on the VSYNC SOURCE'S OWN THREAD: does this vsync need to reach the UI thread at all? | |
| HostFrameTick | pulp::view::begin_host_frame (View *root, FrameClock &clock, HostFramePump &pump, double now_seconds, bool needs_repaint) |
| Step 1 of a host tick: measure dt, pump the FrameClock's activity probes with it, then decide whether this frame needs compositing. | |
| void | pulp::view::advance_host_frame (View *root, FrameClock &clock, float dt) |
| Step 2 of a host tick, run only when HostFrameTick::should_render: advance every dt-integrating consumer in the tree by the SAME dt — the render clock (and its subscribers), widget animations, and CSS animation timelines — and pump the gesture recognizers. | |
| bool | pulp::view::host_frame_composites (const HostFrameTick &tick, bool damaged) noexcept |
| Step 3 of a host tick, after advance_host_frame: does this frame composite? | |
| void | pulp::view::advance_widget_animations (View *view, float dt) |
| Advance the built-in widget animations and the CSS animation timeline for view and every descendant. | |
The one frame-timing contract every host render loop must follow.
A host (native window, plugin-view host, foreign-host embed tick) drives the view system from a vsync source — CVDisplayLink, CADisplayLink, an NSTimer, or a test harness. Everything time-integrating downstream (FrameClock render subscribers, CSS animation timelines, widget hover/thumb/scroll animations, time-driven gesture recognizers, wake-from-idle activity probes) advances by a dt the host hands it. Historically every Apple host handed them a hardcoded 1.0f / 60.0f, so on a 120 Hz ProMotion display animation time ran at 2x wall clock, and under load (coalesced or dropped callbacks) it ran slow. Motion duration became a function of the display and the machine.
HostFramePump turns the host's own monotonic presentation timestamp into a measured dt; begin_host_frame / advance_host_frame then deliver that ONE dt to every consumer, in the right order, identically on every platform.
Two deliberate design decisions:
Wake-from-idle is not a slow frame, and is the one place we do clamp. Pulp idles a static editor at 0 fps: the host stops pumping entirely. When it resumes seconds (or minutes) later, the wall-clock gap is not elapsed animation time — nothing was animating. Feeding it through would teleport any animation that starts on wake straight to its end state on its first frame. So a gap larger than wake_threshold() (default 250 ms ≈ 15 dropped vsyncs at 60 Hz — far beyond any plausible frame hitch) is reported as a resume: dt = nominal_dt() (one frame), and the pump re-bases on the new timestamp. suspend() marks a resume explicitly when the host knows it stopped (display link stopped, view detached).
The honest cost: a genuine >250 ms stall while animating is indistinguishable from a wake at this seam and is treated as a resume, so the clock falls behind by the stall. That is the conservative failure — never teleport — and a stall that long has already destroyed the animation visually.
The wake threshold alone is NOT enough, because a host can idle for far less than it: the macOS display links keep firing while the tree is static, and the host declines to dispatch those vsyncs to the UI thread at all (should_dispatch_host_frame). The pump therefore stops measuring while the tree is idle, and a 100 ms mouse-outside-the-window gap followed by a hover animation would otherwise hand that animation a 100 ms first frame — completing an 80 ms hover on frame one. So a host that SKIPS a vsync must say so (note_frame_skipped), and the next measured frame is a resume no matter how short the gap was. Idle time is never animation time, at any scale.
Thread affinity: begin_host_frame / advance_host_frame are UI thread only — they walk the view tree and fire callbacks without locking. should_dispatch_host_frame / HostFramePump::note_frame_skipped are the one exception: they are called from the vsync source's own thread (the CVDisplayLink thread) and touch only an atomic flag.