Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
host_frame_pump.hpp File Reference

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.

Detailed Description

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:

  1. FrameClock is never clamped. FrameClock::time() is the shader clock (iTime-style uniforms read it) and the animation clock. A global clamp silently desynchronises it from wall time — after a 500 ms hitch, a clamp to 64 ms leaves the clock 436 ms behind, forever. So a dropped or slow frame while we were actively rendering passes its raw measured dt through: a 1 s fade still finishes ~1 s after it started even if a frame hitched. Integrators catch up, which is what you want.
  2. 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.