Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
render_loop.hpp
Go to the documentation of this file.
1#pragma once
2
3// RenderLoop abstraction for frame-paced GPU rendering.
4//
5// Platform-specific implementations drive the render callback at the
6// display's refresh rate (vblank). The canonical pattern is *"set a dirty
7// flag, repaint on the next frame"* rather than periodic polling — call
8// request_frame() to mark dirty and the loop fires the callback exactly
9// once on the next vblank, coalescing any number of intervening requests.
10//
11// macOS : CVDisplayLink (real vblank)
12// iOS : CADisplayLink (real vblank)
13// Android : AChoreographer (real vblank)
14// Windows : DwmFlush (DWM-composed vblank wait) (real vblank)
15// Browser : requestAnimationFrame (real vblank)
16// Linux : timer fallback (conscious fallback —
17// a real X11/Wayland present-sync source drops in behind the
18// same factory seam when available; see render_loop.cpp)
19// Other : timer fallback (~60 Hz)
20//
21// The VBlank-locked safe-repaint pattern keeps dirty-frame coalescing in
22// RenderLoopState (render_loop_state.hpp), which is platform-agnostic and
23// unit-tested; each platform subclass only owns the native vsync source.
24//
25// Wiring status (important — do not mistake these backends for dead code):
26// only the WEB host (window_host_web.cpp, emscripten) currently drives its
27// frames through RenderLoop::create() in production. The macOS / Windows /
28// iOS / Android / timer backends are fully implemented and unit-tested but are
29// NOT instantiated by their native hosts today — those hosts hand-roll their
30// vsync source directly (macOS drives CVDisplayLink inside window_host_mac.mm;
31// iOS drives CADisplayLink inside window_host_ios.mm; Android via
32// AChoreographer). This class is therefore a *reusable vsync abstraction for
33// host implementors* plus the one live web consumer, not an unused facade.
34// Consolidating the native hosts onto these backends (dedup, one vsync
35// abstraction) is a deliberate future slice, not a bug — it is intentionally
36// deferred because the hand-rolled paths carry host-specific lifecycle
37// (display-link start/stop on show/hide, secondary-window display links) that
38// a migration must preserve.
39
40#include <functional>
41#include <memory>
42
43namespace pulp::render {
44
45using FrameCallback = std::function<void()>;
46
58
61
65 return backend != RenderLoopBackend::timer;
66}
67
68// Decides when a display-linked loop asks for the panel's maximum refresh rate.
69//
70// A ProMotion display runs a CADisplayLink at 60 Hz unless the link requests
71// more, so an animation is capped at 60 fps on a 120 Hz panel. Holding the
72// maximum while idle costs battery for frames nobody draws. The policy raises
73// the request on the first vsync that draws and lowers it only after
74// `kIdleVsyncsBeforeLower` consecutive vsyncs without a frame, so a gesture
75// that requests frames with short gaps keeps one steady rate instead of
76// toggling it every gap. Main-thread only; pure, so it is tested on any host.
77//
78// Every iOS host that owns a CADisplayLink (IOSRenderLoop and the hand-rolled
79// window and plug-in view hosts) feeds this policy each tick and hands the
80// result to apply_display_rate_change().
82 public:
83 enum class Change { none, raise, lower };
84
85 static constexpr int kIdleVsyncsBeforeLower = 30;
86
87 // Record one vsync and whether it produced a frame.
88 Change on_vsync(bool drew) {
89 if (drew) {
90 idle_vsyncs_ = 0;
91 if (boosted_)
92 return Change::none;
93 boosted_ = true;
94 return Change::raise;
95 }
96 if (!boosted_)
97 return Change::none;
98 if (++idle_vsyncs_ < kIdleVsyncsBeforeLower)
99 return Change::none;
100 boosted_ = false;
101 idle_vsyncs_ = 0;
102 return Change::lower;
103 }
104
105 // The loop stopped: drop any request it still holds.
107 idle_vsyncs_ = 0;
108 if (!boosted_)
109 return Change::none;
110 boosted_ = false;
111 return Change::lower;
112 }
113
114 bool boosted() const {
115 return boosted_;
116 }
117
118 private:
119 bool boosted_ = false;
120 int idle_vsyncs_ = 0;
121};
122
128
130public:
131 // Create a platform-appropriate render loop.
132 // native_handle: optional window/view handle (NSView*, HWND, SDL_Window*).
133 // Defining PULP_RENDER_LOOP_FORCE_TIMER forces the timer fallback on
134 // every platform — used by headless unit tests.
135 static std::unique_ptr<RenderLoop> create(void* native_handle = nullptr);
136
137 // Create the conscious ~60 Hz timer-backed render loop explicitly, on
138 // any platform. Unlike create(), the result drives its callback from a
139 // worker thread and never depends on a native run loop / compositor, so
140 // it is deterministic in headless contexts. Intended for unit tests and
141 // for callers that deliberately want the no-vsync fallback.
142 //
143 // Exception — Emscripten: the timer loop is thread-backed and a wasm build
144 // without pthreads cannot construct a std::thread at all, so this returns
145 // the requestAnimationFrame loop there. Callers must not assume
146 // backend() == timer; check it if the distinction matters.
147 static std::unique_ptr<RenderLoop> create_timer_loop();
148
149 virtual ~RenderLoop() = default;
150
151 // Start the render loop with a frame callback. The callback is invoked
152 // at (approximately) display refresh rate, but only on frames where a
153 // request_frame() has been posted since the last callback — the loop is
154 // demand-driven, not free-running, so an idle UI costs nothing.
155 virtual void start(FrameCallback on_frame) = 0;
156
157 // Stop the render loop. Idempotent; safe to call before start().
158 virtual void stop() = 0;
159
160 // Request a frame be rendered. Marks the loop dirty; the callback fires
161 // once on the next vsync. Any number of calls between two vsyncs
162 // coalesce into a single callback invocation. RT-safe (lock-free).
163 virtual void request_frame() = 0;
164
165 // Is the loop currently running?
166 virtual bool is_running() const = 0;
167
168 // Which backend this instance is. Lets callers detect a timer fallback.
169 virtual RenderLoopBackend backend() const = 0;
170};
171
172} // namespace pulp::render
Definition render_loop.hpp:81
static constexpr int kIdleVsyncsBeforeLower
Definition render_loop.hpp:85
bool boosted() const
Definition render_loop.hpp:114
Change on_stop()
Definition render_loop.hpp:106
Change
Definition render_loop.hpp:83
@ raise
Definition render_loop.hpp:83
@ none
Definition render_loop.hpp:83
@ lower
Definition render_loop.hpp:83
Change on_vsync(bool drew)
Definition render_loop.hpp:88
Definition render_loop.hpp:129
virtual void request_frame()=0
static std::unique_ptr< RenderLoop > create_timer_loop()
virtual void stop()=0
virtual void start(FrameCallback on_frame)=0
static std::unique_ptr< RenderLoop > create(void *native_handle=nullptr)
virtual RenderLoopBackend backend() const =0
virtual ~RenderLoop()=default
virtual bool is_running() const =0
Definition gpu_fwd.hpp:31
std::function< void()> FrameCallback
Definition render_loop.hpp:45
void apply_display_rate_change(void *ca_display_link, DisplayRateBoost::Change change)
iOS only: applies change to a CADisplayLink (passed as void* so C++ callers need no Objective-C).
RenderLoopBackend
Which concrete backend RenderLoop::create() selected.
Definition render_loop.hpp:50
@ raf
Browser — requestAnimationFrame (compositor vblank).
Definition render_loop.hpp:55
@ timer
Conscious ~60 Hz timer fallback (Linux/other).
Definition render_loop.hpp:56
@ ca_display_link
iOS — real vblank.
Definition render_loop.hpp:52
@ choreographer
Android — real vblank.
Definition render_loop.hpp:53
@ dwm_flush
Windows — DWM-composed vblank wait.
Definition render_loop.hpp:54
@ cv_display_link
macOS — real vblank.
Definition render_loop.hpp:51
const char * render_loop_backend_name(RenderLoopBackend backend)
Human-readable name for a backend (for logs / diagnostics).
constexpr bool render_loop_backend_is_vsync(RenderLoopBackend backend)
True if the backend is locked to a real hardware/compositor vblank signal (as opposed to the consciou...
Definition render_loop.hpp:64