Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .lfsconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[lfs]
fetchexclude = *
3 changes: 3 additions & 0 deletions lib/rive.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ export 'src/file_loader.dart';
export 'src/models/artboard_selector.dart';
export 'src/models/data_bind.dart';
export 'src/models/state_machine_selector.dart';
export 'src/painters/background_widget_controller_stub.dart'
if (dart.library.ffi) 'src/painters/background_widget_controller.dart';
export 'src/painters/widget_controller.dart';
export 'src/rive_extensions.dart';
export 'src/widgets/background_rive_view.dart';
export 'src/widgets/inherited_widgets.dart';
export 'src/widgets/rive_builder.dart';
export 'src/widgets/rive_panel.dart';
Expand Down
244 changes: 244 additions & 0 deletions lib/src/painters/background_widget_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import 'dart:ffi';

// Internal import to access raw native pointers for artboard/SM/VMI handoff.
// ignore: implementation_imports
import 'package:rive_native/src/ffi/rive_ffi_reference.dart'
show RiveFFIReference;
// ignore: implementation_imports
import 'package:rive_native/src/ffi/rive_ffi.dart'
show FFIRiveArtboard, FFIStateMachine;
// ignore: implementation_imports
import 'package:rive_native/src/ffi/rive_threaded_ffi.dart';
import 'package:rive_native/rive_native.dart';

/// A controller for Rive content that advances and renders on a dedicated C++
/// background thread.
///
/// Unlike [RiveWidgetController], this class does NOT advance the state machine
/// on the Flutter UI thread. Instead it:
///
/// 1. Creates its own [RenderTexture] to obtain a `MetalTextureRenderer*`.
/// 2. Passes that renderer — plus the artboard, state machine, and optional
/// ViewModel instance — to [RiveThreadedBindings.create] which hands off
/// native ownership to a [ThreadedScene] running on a background C++ thread.
/// 3. Exposes [advance], [setEnumProperty], [acquireSnapshot], etc. as thin
/// wrappers around [RiveThreadedBindings].
///
/// ## Ownership
///
/// After [initialize] succeeds:
/// - The native [ThreadedScene] owns the artboard and state machine via
/// `unique_ptr`. [releaseNativeOwnership] is called on the Dart wrappers so
/// their [NativeFinalizer]s do not attempt a double-free.
/// - The ViewModel instance is ref-counted; both Dart and C++ hold a ref.
/// - The [RenderTexture] is owned by this controller and disposed in [dispose].
///
/// ## Lifecycle
///
/// 1. Construct with already-created [artboard], [stateMachine], optional
/// [viewModelInstance].
/// 2. Call [initialize] once the layout size is known (async — awaits
/// [RenderTexture.makeRenderTexture]).
/// 3. [BackgroundRiveView] drives the ticker and calls [advance] each frame.
/// 4. Call [dispose] when done.
class BackgroundRiveWidgetController {
BackgroundRiveWidgetController({
required this.artboard,
required this.stateMachine,
this.viewModelInstance,
});

final Artboard artboard;
final StateMachine stateMachine;

/// Optional ViewModel instance. The C++ side adds its own ref, so the Dart
/// side retains ownership and must call [viewModelInstance.dispose] normally.
final ViewModelInstance? viewModelInstance;

RenderTexture? _renderTexture;
RiveThreadedBindings? _bindings;

/// Properties queued via [watchProperty] before [initialize] completes.
final List<String> _pendingWatchProperties = [];

bool get isInitialized => _bindings != null;

/// The [RenderTexture] whose [textureId] [BackgroundRiveView] composites.
///
/// Valid only after [initialize] returns `true`.
RenderTexture get renderTexture {
assert(isInitialized, 'renderTexture accessed before initialize()');
return _renderTexture!;
}

// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------

/// Initialises the [RenderTexture] and the native [ThreadedScene] binding.
///
/// [width] / [height] are physical pixel dimensions (logical × device pixel
/// ratio). [devicePixelRatio] is used for Metal's display scaling.
///
/// Returns `true` on success (Metal / iOS / macOS with Phase 2 symbols).
/// Returns `false` on unsupported platforms or if object pointers are invalid.
Future<bool> initialize({
required int width,
required int height,
required double devicePixelRatio,
}) async {
assert(!isInitialized, 'initialize() called more than once');

final rt = RiveNative.instance.makeRenderTexture();
await rt.makeRenderTexture(width, height);

if (!rt.isReady) {
rt.dispose();
return false;
}

final rendererPtr = rt.nativeRendererPtr;
if (rendererPtr is! Pointer<Void> || rendererPtr == nullptr) {
rt.dispose();
return false;
}

final abPtr = _nativePtrOf(artboard);
final smPtr = _nativePtrOf(stateMachine);
final vmiPtr = _nativePtrOf(viewModelInstance);

if (abPtr == nullptr || smPtr == nullptr) {
rt.dispose();
return false;
}

final bindings = RiveThreadedBindings.create(
metalTextureRenderer: rendererPtr,
artboard: abPtr,
stateMachine: smPtr,
viewModelInstance: vmiPtr,
width: width,
height: height,
devicePixelRatio: devicePixelRatio,
);

if (bindings == null) {
rt.dispose();
return false;
}

// Transfer native ownership so Dart finalizers don't double-free.
if (artboard is FFIRiveArtboard) {
(artboard as FFIRiveArtboard).releaseNativeOwnership();
}
if (stateMachine is FFIStateMachine) {
(stateMachine as FFIStateMachine).releaseNativeOwnership();
}

_renderTexture = rt;
_bindings = bindings;

// Flush any watchProperty calls made before initialization completed.
for (final name in _pendingWatchProperties) {
_bindings!.watchProperty(name);
}
_pendingWatchProperties.clear();

return true;
}

/// Stops the background thread, unregisters the Flutter texture, and
/// disposes the [RenderTexture].
void dispose() {
_bindings?.dispose();
_bindings = null;
_renderTexture?.dispose();
_renderTexture = null;
// Artboard/SM are owned by C++ (already released above).
// ViewModelInstance retains normal Dart ownership — caller disposes it.
}

// ---------------------------------------------------------------------------
// Per-frame
// ---------------------------------------------------------------------------

/// Posts [elapsedSeconds] to the background thread. Non-blocking.
void advance(double elapsedSeconds) =>
_bindings?.postElapsedTime(elapsedSeconds);

// ---------------------------------------------------------------------------
// ViewModel inputs
// ---------------------------------------------------------------------------

void setEnumProperty(String name, String value) =>
_bindings?.setEnumProperty(name, value);

void setNumberProperty(String name, double value) =>
_bindings?.setNumberProperty(name, value);

void setBoolProperty(String name, bool value) =>
_bindings?.setBoolProperty(name, value);

void setStringProperty(String name, String value) =>
_bindings?.setStringProperty(name, value);

void fireTriggerProperty(String name) => _bindings?.fireTrigger(name);

// ---------------------------------------------------------------------------
// Snapshot / watch
// ---------------------------------------------------------------------------

void watchProperty(String name) {
if (_bindings != null) {
_bindings!.watchProperty(name);
} else {
_pendingWatchProperties.add(name);
}
}

void unwatchProperty(String name) {
_pendingWatchProperties.remove(name);
_bindings?.unwatchProperty(name);
}

/// Atomically acquires the latest ViewModel property snapshot.
///
/// Returns an empty list if the controller is not initialised or no snapshot
/// has been produced yet.
List<SnapshotEntry> acquireSnapshot({int maxProperties = 32}) =>
_bindings?.acquireSnapshot(maxProperties: maxProperties) ?? const [];

// ---------------------------------------------------------------------------
// Events
// ---------------------------------------------------------------------------

/// Drains pending Rive state-machine reported events.
List<RiveThreadedEvent> pollEvents({int maxEvents = 32}) =>
_bindings?.pollEvents(maxEvents: maxEvents) ?? const [];

// ---------------------------------------------------------------------------
// Pointer events
// ---------------------------------------------------------------------------

void pointerDown(double x, double y, {int pointerId = 0}) =>
_bindings?.pointerDown(x, y, pointerId: pointerId);

void pointerMove(double x, double y, {int pointerId = 0}) =>
_bindings?.pointerMove(x, y, pointerId: pointerId);

void pointerUp(double x, double y, {int pointerId = 0}) =>
_bindings?.pointerUp(x, y, pointerId: pointerId);

void pointerExit(double x, double y, {int pointerId = 0}) =>
_bindings?.pointerExit(x, y, pointerId: pointerId);
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

Pointer<Void> _nativePtrOf(Object? obj) {
if (obj == null) return nullptr;
if (obj is RiveFFIReference) return obj.pointer;
return nullptr;
}
60 changes: 60 additions & 0 deletions lib/src/painters/background_widget_controller_stub.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/// Web stub for [BackgroundRiveWidgetController].
///
/// Background rendering requires native Metal/Vulkan GPU access and is not
/// supported on web. This stub provides the same public API surface so that
/// the types are importable on web, but [initialize] always returns `false`.

import 'package:rive_native/rive_native.dart';

class SnapshotEntry {
final String name;
final int type;
final String rawValue;
const SnapshotEntry({
required this.name,
required this.type,
required this.rawValue,
});
}

class RiveThreadedEvent {
final String name;
final double secondsDelay;
const RiveThreadedEvent(this.name, this.secondsDelay);
}

class BackgroundRiveWidgetController {
BackgroundRiveWidgetController({
required Artboard artboard,
required StateMachine stateMachine,
ViewModelInstance? viewModelInstance,
});

bool get isInitialized => false;

RenderTexture get renderTexture =>
throw UnsupportedError('Background rendering is not supported on web');

Future<bool> initialize({
required int width,
required int height,
required double devicePixelRatio,
}) async =>
false;

void dispose() {}
void advance(double elapsedSeconds) {}
void setEnumProperty(String name, String value) {}
void setNumberProperty(String name, double value) {}
void setBoolProperty(String name, bool value) {}
void setStringProperty(String name, String value) {}
void fireTriggerProperty(String name) {}
void watchProperty(String name) {}
void unwatchProperty(String name) {}
List<SnapshotEntry> acquireSnapshot({int maxProperties = 32}) => const [];
List<RiveThreadedEvent> pollEvents({int maxEvents = 32}) => const [];
void pointerDown(double x, double y, {int pointerId = 0}) {}
void pointerMove(double x, double y, {int pointerId = 0}) {}
void pointerUp(double x, double y, {int pointerId = 0}) {}
void pointerExit(double x, double y, {int pointerId = 0}) {}
}
37 changes: 37 additions & 0 deletions lib/src/painters/widget_controller.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import 'dart:developer' as developer;

import 'package:flutter/gestures.dart';
import 'package:rive/rive.dart';

/// Global toggle for per-artboard advance profiling.
///
/// When enabled, each [RiveWidgetController.advance] call emits a
/// `dart:developer` [developer.Timeline] sync event named
/// `Rive.advance:<artboard>` with the advance duration in microseconds
/// and whether the state machine reported a change.
///
/// Set to `true` from app code (e.g. behind a feature flag) to activate.
bool riveAdvanceProfilingEnabled = false;

/// {@template rive_controller}
/// This controller builds on top of the concept of a Rive painter, but
/// provides a more convenient API for building Rive widgets.
Expand All @@ -19,6 +31,9 @@ base class RiveWidgetController extends BasicArtboardPainter
/// The state machine that the [RiveWidgetController] is using.
late final StateMachine stateMachine;

/// Cached label for profiling (avoids string allocation per frame).
late final String _profilingLabel;

/// {@macro rive_controller}
/// - The [file] parameter is the Rive file to paint.
/// - The [artboardSelector] parameter specifies which artboard to use.
Expand All @@ -30,6 +45,7 @@ base class RiveWidgetController extends BasicArtboardPainter
}) {
artboard = _createArtboard(file, artboardSelector);
stateMachine = _createStateMachine(artboard, stateMachineSelector);
_profilingLabel = 'Rive.advance:${artboard.name}';
}

/// Whether the state machine has been scheduled for repaint.
Expand Down Expand Up @@ -198,9 +214,30 @@ base class RiveWidgetController extends BasicArtboardPainter
}
}

static final Stopwatch _profilingStopwatch = Stopwatch();

@override
bool advance(double elapsedSeconds) {
_repaintScheduled = false;

if (riveAdvanceProfilingEnabled) {
_profilingStopwatch.reset();
_profilingStopwatch.start();
final didAdvance = stateMachine.advanceAndApply(elapsedSeconds);
_profilingStopwatch.stop();

developer.Timeline.instantSync(
_profilingLabel,
arguments: {
'us': _profilingStopwatch.elapsedMicroseconds.toString(),
'didAdvance': didAdvance.toString(),
'elapsed': elapsedSeconds.toString(),
},
);

return didAdvance && active;
}

final didAdvance = stateMachine.advanceAndApply(elapsedSeconds);
return didAdvance && active;
}
Expand Down
Loading