diff --git a/.lfsconfig b/.lfsconfig new file mode 100644 index 00000000..2cf1bce1 --- /dev/null +++ b/.lfsconfig @@ -0,0 +1,2 @@ +[lfs] + fetchexclude = * diff --git a/lib/src/painters/widget_controller.dart b/lib/src/painters/widget_controller.dart index e03542fc..c088557a 100644 --- a/lib/src/painters/widget_controller.dart +++ b/lib/src/painters/widget_controller.dart @@ -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:` 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. @@ -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. @@ -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. @@ -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; } diff --git a/lib/src/widgets/inherited_widgets.dart b/lib/src/widgets/inherited_widgets.dart index 58f982fb..8b54d6c5 100644 --- a/lib/src/widgets/inherited_widgets.dart +++ b/lib/src/widgets/inherited_widgets.dart @@ -19,6 +19,19 @@ class SharedRenderTexture { final List 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, @@ -26,19 +39,29 @@ class SharedRenderTexture { 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) { @@ -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(); } } diff --git a/lib/src/widgets/shared_texture_view.dart b/lib/src/widgets/shared_texture_view.dart index 964e75ab..f6f1902b 100644 --- a/lib/src/widgets/shared_texture_view.dart +++ b/lib/src/widgets/shared_texture_view.dart @@ -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) { @@ -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(); @@ -195,7 +205,7 @@ class SharedTextureViewRenderObject extends RiveNativeRenderBox texture, devicePixelRatio, size, - elapsedSeconds, + effectiveElapsed, ) ?? false; if (_shouldAdvance) { @@ -220,6 +230,8 @@ class SharedTextureViewRenderObject extends RiveNativeRenderBox @override void frameCallback(Duration duration) { super.frameCallback(duration); + _accumulatedElapsed += elapsedSeconds; + _shared.onFrameTick?.call(elapsedSeconds); _shared.schedulePaint(); }