Skip to content
Draft
2 changes: 0 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
test/assets/** filter=lfs diff=lfs merge=lfs -text
test/goldens/**/images/*.png filter=lfs diff=lfs merge=lfs -text
5 changes: 5 additions & 0 deletions .lfsconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[lfs]
fetchexclude = *
[filter "lfs"]
smudge = git-lfs smudge --skip -- %f
process = git-lfs filter-process --skip
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
33 changes: 29 additions & 4 deletions lib/src/widgets/inherited_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,49 @@ class SharedRenderTexture {
final List<SharedTexturePainter> painters = [];
final GlobalKey panelKey;

bool _dirty = true;
bool _scheduled = false;

/// When true, [_paintShared] skips the clear→paint→flush cycle if no
/// painter called [markDirty] since the last flush. When false (default),
/// every scheduled paint runs the full cycle — identical to upstream.
bool dirtyTrackingEnabled = false;

/// Called every frame by the render object's ticker with the frame's
/// elapsed seconds. Listeners can accumulate time and call [markDirty]
/// when a state-machine advance is needed.
void Function(double elapsedSeconds)? onFrameTick;

SharedRenderTexture({
required this.texture,
required this.devicePixelRatio,
required this.backgroundColor,
required this.panelKey,
});

/// Mark the texture as needing a repaint on the next scheduled frame.
void markDirty() {
_dirty = true;
}

/// Paint the shared render texture.
///
/// When [dirtyTrackingEnabled] is true and the texture is clean, the entire
/// clear→paint→flush cycle is skipped. The render-object ticker stays alive
/// independently and invokes [onFrameTick] each frame so external code can
/// call [markDirty] when a state-machine advance is needed.
void _paintShared(_) {
_scheduled = false;
if (dirtyTrackingEnabled && !_dirty) return;

texture.clear(backgroundColor);
for (final painter in painters) {
painter.paintIntoSharedTexture(texture);
}
texture.flush(devicePixelRatio);

_scheduled = false;
_dirty = false;
}

bool _scheduled = false;

/// Schedule a paint of the shared render texture.
void schedulePaint() {
if (_scheduled) {
Expand All @@ -52,11 +75,13 @@ class SharedRenderTexture {
void addPainter(SharedTexturePainter painter) {
painters.add(painter);
painters.sort((a, b) => a.sharedDrawOrder.compareTo(b.sharedDrawOrder));
markDirty();
}

/// Remove a painter from the shared render texture.
void removePainter(SharedTexturePainter painter) {
painters.remove(painter);
markDirty();
}
}

Expand Down
14 changes: 13 additions & 1 deletion lib/src/widgets/shared_texture_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ class SharedTextureViewRenderObject extends RiveNativeRenderBox

int drawOrder = 1;

/// Accumulated elapsed seconds across frames while dirty tracking skips
/// the paint cycle. Reset to 0 after each [paintIntoSharedTexture] call.
double _accumulatedElapsed = 0;

SharedRenderTexture get shared => _shared;
set shared(SharedRenderTexture value) {
if (_shared == value) {
Expand Down Expand Up @@ -182,6 +186,12 @@ class SharedTextureViewRenderObject extends RiveNativeRenderBox
Offset panelPosition = renderBox.localToGlobal(Offset.zero);
Offset globalPosition = localToGlobal(Offset.zero) - panelPosition;

// When dirty tracking is enabled, use accumulated elapsed time so the
// controller receives the full wall-clock delta since the last advance.
final effectiveElapsed =
_shared.dirtyTrackingEnabled ? _accumulatedElapsed : elapsedSeconds;
_accumulatedElapsed = 0;

final renderer = texture.renderer;

renderer.save();
Expand All @@ -195,7 +205,7 @@ class SharedTextureViewRenderObject extends RiveNativeRenderBox
texture,
devicePixelRatio,
size,
elapsedSeconds,
effectiveElapsed,
) ??
false;
if (_shouldAdvance) {
Expand All @@ -220,6 +230,8 @@ class SharedTextureViewRenderObject extends RiveNativeRenderBox
@override
void frameCallback(Duration duration) {
super.frameCallback(duration);
_accumulatedElapsed += elapsedSeconds;
_shared.onFrameTick?.call(elapsedSeconds);
_shared.schedulePaint();
}

Expand Down
3 changes: 0 additions & 3 deletions test/assets/rive_file_controller_test.riv

This file was deleted.