Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions examples/flutter/quickstart/integration_test/framerate_test.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Integration test that verifies setTargetFramerate changes the rate at which
// render work is accepted — not just an internal interval value.
//
// It counts frames admitted past the FrameScheduler's native throttle and
// in-flight guard (via scheduledFrameCount) over wall-clock windows before and
// It counts handlers dispatched past the FrameScheduler's native throttle and
// in-flight guard (via dispatchedFrameCount) over wall-clock windows before and
// after a setTargetFramerate call. The native
// scheduler (CVDisplayLink / CADisplayLink / AChoreographer / DXGI) drives
// the loop, so the numbers reflect work the device actually accepts.
Expand All @@ -29,20 +29,20 @@ void main() {
/// Frames-per-second accepted over a [window] of real wall-clock time,
/// measured with a [Stopwatch] (system clock, unaffected by any test
/// clock). Lets the native vsync-driven scheduler tick at its own cadence.
Future<double> measureScheduledFps(Duration window) async {
Future<double> measureDispatchedFps(Duration window) async {
final sw = Stopwatch()..start();
final start = FrameScheduler.instance.scheduledFrameCount;
final start = FrameScheduler.instance.dispatchedFrameCount;
// Yield to the event loop in small increments so the native scheduler
// callbacks are delivered while real time elapses.
while (sw.elapsed < window) {
await Future<void>.delayed(const Duration(milliseconds: 20));
}
final frames = FrameScheduler.instance.scheduledFrameCount - start;
final frames = FrameScheduler.instance.dispatchedFrameCount - start;
final realSeconds = sw.elapsed.inMicroseconds / 1e6;
return frames / realSeconds;
}

testWidgets('setTargetFramerate lowers scheduled frames-per-second',
testWidgets('setTargetFramerate lowers dispatched frames-per-second',
(tester) async {
ThermionViewer? viewer;
final sun = DirectLight.sun(direction: Vector3(0.7, -1, -0.8).normalized());
Expand Down Expand Up @@ -94,9 +94,9 @@ void main() {

// Let the loop settle, then measure the 60 FPS rate.
await Future<void>.delayed(const Duration(seconds: 1));
final defaultFps = await measureScheduledFps(const Duration(seconds: 2));
final defaultFps = await measureDispatchedFps(const Duration(seconds: 2));
debugPrint(
'Thermion scheduled FPS: default=${defaultFps.toStringAsFixed(1)}',
'Thermion dispatched FPS: default=${defaultFps.toStringAsFixed(1)}',
);

// Sanity: the loop is rendering continuously (not stalled). The absolute
Expand All @@ -105,13 +105,13 @@ void main() {
expect(defaultFps, greaterThanOrEqualTo(15),
reason: 'loop should render continuously; got $defaultFps');

// Cap well below the render-limited default and confirm the scheduled rate
// Cap well below the render-limited default and confirm the dispatched rate
// drops to the cap. 5 FPS is unambiguously below any reasonable default.
FilamentApp.instance!.setTargetFramerate(5);
// Give the new interval a moment to take effect.
await Future<void>.delayed(const Duration(milliseconds: 500));
final lowFps = await measureScheduledFps(const Duration(seconds: 2));
debugPrint('Thermion scheduled FPS: limited=${lowFps.toStringAsFixed(1)}');
final lowFps = await measureDispatchedFps(const Duration(seconds: 2));
debugPrint('Thermion dispatched FPS: limited=${lowFps.toStringAsFixed(1)}');

expect(lowFps, lessThan(defaultFps / 2),
reason: '5 FPS cap should schedule far fewer frames than the default '
Expand All @@ -126,9 +126,9 @@ void main() {
await FrameScheduler.instance.start();
await Future<void>.delayed(const Duration(milliseconds: 500));
final afterRestartFps =
await measureScheduledFps(const Duration(seconds: 2));
await measureDispatchedFps(const Duration(seconds: 2));
debugPrint(
'Thermion scheduled FPS: after restart='
'Thermion dispatched FPS: after restart='
'${afterRestartFps.toStringAsFixed(1)}',
);
expect(afterRestartFps, closeTo(5, 3),
Expand All @@ -139,9 +139,9 @@ void main() {
// Restore and confirm the rate climbs back near the default.
FilamentApp.instance!.setTargetFramerate(60);
await Future<void>.delayed(const Duration(milliseconds: 500));
final restoredFps = await measureScheduledFps(const Duration(seconds: 2));
final restoredFps = await measureDispatchedFps(const Duration(seconds: 2));
debugPrint(
'Thermion scheduled FPS: restored=${restoredFps.toStringAsFixed(1)}',
'Thermion dispatched FPS: restored=${restoredFps.toStringAsFixed(1)}',
);
expect(restoredFps, greaterThan(lowFps * 2),
reason: 'restoring 60 FPS should raise the rate well above the 5 FPS '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ void main() {
() => !scheduler.isRendering,
'the current frame to finish',
);
scheduler.setOnFrame(() async {
scheduler.setFrameHandler(() async {
await renderGate.future;
await FilamentApp.instance?.render();
});
Expand Down
8 changes: 5 additions & 3 deletions thermion_dart/ffigen/native.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ headers:
include-directives:
- '../native/include/c_api/*.h'
- '../native/include/c_api/**/*.h'
compiler-opts:
- '-DTHERMION_FFIGEN'
ffi-native:
asset-id: package:thermion_dart/thermion_dart.dart
ignore-source-errors: true
Expand All @@ -14,9 +16,9 @@ functions:
include:
- '.*'
exclude:
# FrameScheduler_stop joins the scheduler thread and waits for the final
# render task to drain. Blocking functions must not be Dart FFI leaf
# calls. This excludes it only from isLeaf:true, not from the bindings.
# FrameScheduler_stop joins the scheduler callback thread. Blocking
# functions must not be Dart FFI leaf calls. This excludes it only from
# isLeaf:true, not from the bindings.
- 'FrameScheduler_stop'
enums:
as-int:
Expand Down
3 changes: 2 additions & 1 deletion thermion_dart/ffigen/web.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ headers:
- '../native/include/c_api/**/*.h'
compiler-opts:
- '-D__EMSCRIPTEN__'
- '-DTHERMION_FFIGEN'
structs:
dependency-only: opaque
exclude:
Expand All @@ -22,4 +23,4 @@ ignore-source-errors: true
enums:
as-int:
include:
- .*
- .*
28 changes: 5 additions & 23 deletions thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4605,27 +4605,12 @@ external void Scene_setIndirectLight(ffi.Pointer<TScene> tScene, ffi.Pointer<TIn
@ffi.Native<ffi.Void Function(ffi.Pointer<TScene>, ffi.Pointer<TFilamentAsset>)>(isLeaf: true)
external void Scene_addFilamentAsset(ffi.Pointer<TScene> tScene, ffi.Pointer<TFilamentAsset> asset);

@ffi.Native<ffi.Void Function(FrameCallback, ffi.Int)>(isLeaf: true)
external void FrameScheduler_start(FrameCallback callback, int targetFps);
@ffi.Native<ffi.Void Function(FrameTickCallback, ffi.Int)>(isLeaf: true)
external void FrameScheduler_startWithCallback(FrameTickCallback tickCallback, int targetFps);

@ffi.Native<ffi.Void Function()>()
external void FrameScheduler_stop();

@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>(isLeaf: true)
external void FrameScheduler_setRenderThread(ffi.Pointer<ffi.Void> renderThread);

@ffi.Native<ffi.Void Function(ffi.Pointer<TRenderManager>)>(isLeaf: true)
external void FrameScheduler_setRenderManager(ffi.Pointer<TRenderManager> rm);

@ffi.Native<ffi.Void Function(PostRenderCallback, ffi.Pointer<ffi.Void>)>(isLeaf: true)
external void FrameScheduler_setPostRenderCallback(PostRenderCallback callback, ffi.Pointer<ffi.Void> userData);

@ffi.Native<ffi.Bool Function(ffi.Uint64)>(isLeaf: true)
external bool FrameScheduler_requestRender(int frameTimeNanos);

@ffi.Native<ffi.Void Function(ffi.Int)>(isLeaf: true)
external void FrameScheduler_startNativeRenderLoop(int targetFps);

@ffi.Native<ffi.Int Function(ffi.Pointer<ffi.Void>)>(isLeaf: true)
external int FrameScheduler_initDartApi(ffi.Pointer<ffi.Void> data);

Expand Down Expand Up @@ -5734,12 +5719,9 @@ final class TShadowOptions extends ffi.Struct {
typedef FilamentRenderCallbackFunction = ffi.Void Function(ffi.Pointer<ffi.Void> owner);
typedef DartFilamentRenderCallbackFunction = void Function(ffi.Pointer<ffi.Void> owner);
typedef FilamentRenderCallback = ffi.Pointer<ffi.NativeFunction<FilamentRenderCallbackFunction>>;
typedef FrameCallbackFunction = ffi.Void Function(ffi.Uint64 frameTimeNanos);
typedef DartFrameCallbackFunction = void Function(int frameTimeNanos);
typedef FrameCallback = ffi.Pointer<ffi.NativeFunction<FrameCallbackFunction>>;
typedef PostRenderCallbackFunction = ffi.Void Function(ffi.Pointer<ffi.Void> userData);
typedef DartPostRenderCallbackFunction = void Function(ffi.Pointer<ffi.Void> userData);
typedef PostRenderCallback = ffi.Pointer<ffi.NativeFunction<PostRenderCallbackFunction>>;
typedef FrameTickCallbackFunction = ffi.Void Function(ffi.Uint64 frameTimeNanos);
typedef DartFrameTickCallbackFunction = void Function(int frameTimeNanos);
typedef FrameTickCallback = ffi.Pointer<ffi.NativeFunction<FrameTickCallbackFunction>>;

final class TMovementIntentCalculator extends ffi.Opaque {}

Expand Down
53 changes: 8 additions & 45 deletions thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2376,13 +2376,8 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject {
external Pointer<TSkybox> _Scene_getSkybox(Pointer<TScene> tScene);
external void _Scene_setIndirectLight(Pointer<TScene> tScene, Pointer<TIndirectLight> tIndirectLight);
external void _Scene_addFilamentAsset(Pointer<TScene> tScene, Pointer<TFilamentAsset> asset);
external void _FrameScheduler_start(FrameCallback callback, int targetFps);
external void _FrameScheduler_startWithCallback(FrameTickCallback tickCallback, int targetFps);
external void _FrameScheduler_stop();
external void _FrameScheduler_setRenderThread(Pointer<Void> renderThread);
external void _FrameScheduler_setRenderManager(Pointer<TRenderManager> rm);
external void _FrameScheduler_setPostRenderCallback(PostRenderCallback callback, Pointer<Void> userData);
external int _FrameScheduler_requestRender(JSBigInt frameTimeNanos);
external void _FrameScheduler_startNativeRenderLoop(int targetFps);
external int _FrameScheduler_initDartApi(Pointer<Void> data);
external void _FrameScheduler_startWithPort(JSBigInt port, int targetFps);
external void _FrameScheduler_setTargetFps(int fps);
Expand Down Expand Up @@ -8764,9 +8759,9 @@ void Scene_addFilamentAsset(Pointer<TScene> tScene, Pointer<TFilamentAsset> asse
return result;
}

void FrameScheduler_start(DartFrameCallback callback, int targetFps) {
final result = GeneratedBindings.instance._FrameScheduler_start(
callback as Pointer<NativeFunction<FrameCallbackFunction>>,
void FrameScheduler_startWithCallback(DartFrameTickCallback tickCallback, int targetFps) {
final result = GeneratedBindings.instance._FrameScheduler_startWithCallback(
tickCallback as Pointer<NativeFunction<FrameTickCallbackFunction>>,
targetFps,
);
return result;
Expand All @@ -8777,34 +8772,6 @@ void FrameScheduler_stop() {
return result;
}

void FrameScheduler_setRenderThread(Pointer<Void> renderThread) {
final result = GeneratedBindings.instance._FrameScheduler_setRenderThread(renderThread);
return result;
}

void FrameScheduler_setRenderManager(Pointer<TRenderManager> rm) {
final result = GeneratedBindings.instance._FrameScheduler_setRenderManager(rm.cast());
return result;
}

void FrameScheduler_setPostRenderCallback(DartPostRenderCallback callback, Pointer<Void> userData) {
final result = GeneratedBindings.instance._FrameScheduler_setPostRenderCallback(
callback as Pointer<NativeFunction<PostRenderCallbackFunction>>,
userData,
);
return result;
}

bool FrameScheduler_requestRender(BigInt frameTimeNanos) {
final result = GeneratedBindings.instance._FrameScheduler_requestRender(frameTimeNanos.toJSBigInt);
return result == 1;
}

void FrameScheduler_startNativeRenderLoop(int targetFps) {
final result = GeneratedBindings.instance._FrameScheduler_startNativeRenderLoop(targetFps);
return result;
}

int FrameScheduler_initDartApi(Pointer<Void> data) {
final result = GeneratedBindings.instance._FrameScheduler_initDartApi(data);
return result;
Expand Down Expand Up @@ -11292,14 +11259,10 @@ final class TSurfaceOrientation extends Struct {
}
}

typedef FrameCallback = Pointer<NativeFunction<FrameCallbackFunction>>;
typedef DartFrameCallback = Pointer<NativeFunction<FrameCallbackFunction>>;
typedef FrameCallbackFunction = void Function(JSBigInt frameTimeNanos);
typedef DartFrameCallbackFunction = void Function(BigInt frameTimeNanos);
typedef PostRenderCallback = Pointer<NativeFunction<PostRenderCallbackFunction>>;
typedef DartPostRenderCallback = Pointer<NativeFunction<PostRenderCallbackFunction>>;
typedef PostRenderCallbackFunction = void Function(Pointer<Void> userData);
typedef DartPostRenderCallbackFunction = void Function(Pointer<Void> userData);
typedef FrameTickCallback = Pointer<NativeFunction<FrameTickCallbackFunction>>;
typedef DartFrameTickCallback = Pointer<NativeFunction<FrameTickCallbackFunction>>;
typedef FrameTickCallbackFunction = void Function(JSBigInt frameTimeNanos);
typedef DartFrameTickCallbackFunction = void Function(BigInt frameTimeNanos);

extension TMovementIntentExecutorExt on Pointer<TMovementIntentExecutor> {
TMovementIntentExecutor toDart() {
Expand Down
16 changes: 7 additions & 9 deletions thermion_dart/native/include/c_api/FrameSchedulerApi.h
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
#pragma once

#include "APIBoundaryTypes.h"

// Preserve the historical generated-binding declaration order without making
// the production timing API depend on rendering types.
#ifdef THERMION_FFIGEN
#include "TRenderManager.h"
#endif

#ifdef __cplusplus
namespace thermion
{
extern "C"
{
#endif
typedef void (*FrameCallback)(uint64_t frameTimeNanos);
typedef void (*PostRenderCallback)(void* userData);
typedef void (*FrameTickCallback)(uint64_t frameTimeNanos);

EMSCRIPTEN_KEEPALIVE void FrameScheduler_start(FrameCallback callback, int targetFps);
EMSCRIPTEN_KEEPALIVE void FrameScheduler_startWithCallback(FrameTickCallback tickCallback, int targetFps);
EMSCRIPTEN_KEEPALIVE void FrameScheduler_stop();

EMSCRIPTEN_KEEPALIVE void FrameScheduler_setRenderThread(void* renderThread);
EMSCRIPTEN_KEEPALIVE void FrameScheduler_setRenderManager(TRenderManager* rm);
EMSCRIPTEN_KEEPALIVE void FrameScheduler_setPostRenderCallback(PostRenderCallback callback, void* userData);
EMSCRIPTEN_KEEPALIVE bool FrameScheduler_requestRender(uint64_t frameTimeNanos);
EMSCRIPTEN_KEEPALIVE void FrameScheduler_startNativeRenderLoop(int targetFps);

EMSCRIPTEN_KEEPALIVE int FrameScheduler_initDartApi(void* data);
EMSCRIPTEN_KEEPALIVE void FrameScheduler_startWithPort(int64_t port, int targetFps);

Expand Down
25 changes: 13 additions & 12 deletions thermion_dart/native/include/rendering/FrameScheduler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,17 @@ namespace thermion {
class FrameScheduler {
public:
/// Receives a monotonic frame timestamp and the pointer supplied to start().
using Callback = void (*)(uint64_t frameTimeNanos, void* userData);
using TickCallback = void (*)(uint64_t frameTimeNanos, void* userData);

virtual ~FrameScheduler() = default;

/// Starts the frame source. Each source tick that passes rate gating invokes
/// `callback` with its timestamp and `userData`.
/// `tickCallback` with its timestamp and `userData`.
///
/// The scheduler does not own `userData`. The callback and `userData` must
/// remain valid until stop() returns. Call stop() before a second start().
virtual void start(Callback callback, void* userData = nullptr) = 0;
/// The scheduler does not own `userData`. The tick callback and `userData`
/// must remain valid until stop() returns. Call stop() before a second
/// start().
virtual void start(TickCallback tickCallback, void* userData = nullptr) = 0;

/// Stops the frame source and waits for its callback context to stop.
/// Work that the callback already posted can continue after this returns.
Expand All @@ -84,8 +85,8 @@ class FrameScheduler {
void setTargetFps(int fps);

protected:
Callback _callback = nullptr;
void* _callbackUserData = nullptr;
TickCallback _tickCallback = nullptr;
void* _tickUserData = nullptr;

// setTargetFps() can change _fpsLimit from another thread. The source
// callback owns the other timing fields while the scheduler runs.
Expand All @@ -109,7 +110,7 @@ class TimerFrameScheduler : public FrameScheduler {
public:
explicit TimerFrameScheduler(int targetFps) : _targetFps(targetFps) {}
~TimerFrameScheduler() override { stop(); }
void start(Callback callback, void* userData = nullptr) override;
void start(TickCallback tickCallback, void* userData = nullptr) override;
void stop() override;
private:
void run();
Expand All @@ -122,7 +123,7 @@ class CADisplayLinkScheduler : public FrameScheduler {
void* _wrapper = nullptr;
public:
~CADisplayLinkScheduler() override { stop(); }
void start(Callback callback, void* userData = nullptr) override;
void start(TickCallback tickCallback, void* userData = nullptr) override;
void stop() override;
private:
static void displayLinkCallback(uint64_t frameTimeNanos, void* context);
Expand All @@ -137,7 +138,7 @@ class CVDisplayLinkScheduler : public FrameScheduler {
mach_timebase_info_data_t _timebase{};
public:
~CVDisplayLinkScheduler() override { stop(); }
void start(Callback callback, void* userData = nullptr) override;
void start(TickCallback tickCallback, void* userData = nullptr) override;
void stop() override;
private:
static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink,
Expand All @@ -155,7 +156,7 @@ class DXGIFrameScheduler : public FrameScheduler {
public:
explicit DXGIFrameScheduler(int targetFps) : _targetFps(targetFps) {}
~DXGIFrameScheduler() override { stop(); }
void start(Callback callback, void* userData = nullptr) override;
void start(TickCallback tickCallback, void* userData = nullptr) override;
void stop() override;
};
#endif
Expand All @@ -172,7 +173,7 @@ class AChoreographerFrameScheduler : public FrameScheduler {
uint64_t _nextSourceFrameNs = 0;
public:
~AChoreographerFrameScheduler() override { stop(); }
void start(Callback callback, void* userData = nullptr) override;
void start(TickCallback tickCallback, void* userData = nullptr) override;
void stop() override;
private:
void scheduleNextFrame(uint64_t lastFrameTimeNanos = 0);
Expand Down
Loading
Loading