fix(flutter): make RiveWidgetBuilder file loading cancellable (#11190) 9e48626a96

* fix(flutter): make RiveWidgetBuilder file loading cancellable

* docs: update CHANGELOG

Update the versions of the spir-v shaders to all be 460 (#11209) f38d54d8e4
Some tooling changed today-ish that is causing the 310 es shaders to fail to build with:
```
runner/_work/rive/rive/packages/runtime/tests/unit_tests/out/android_arm64_debug/include/generated/shaders/glsl.minified.glsl:450: 'token pasting (##)' : not supported with this profile: es
ERROR: spirv/blit_texture_as_draw_filtered.main:6: 'stringify (#)' : not supported with this profile: es
ERROR: spirv/blit_texture_as_draw_filtered.main:6: '#' : '#' is not followed by a macro parameter.
ERROR: spirv/blit_texture_as_draw_filtered.main:6: '' : missing #endif
ERROR: spirv/blit_texture_as_draw_filtered.main:6: '' : compilation terminated
ERROR: 5 compilation errors.  No code generated.
```

This is only a "310 es" issue, and so since most spir-v shaders are already 460, this moves the rest to be the same.

fix: list updates correctly when items are added or removed (#11206) 255f1ba3d2

fix: ignore non visible clips when removing redundant operations (#11207) 673820da1a

update scripted drawable clipping to new strategy and remove unused function (#11195) 8002b223a3
* update clipping to new strategy

fix(scripting): store data on all cases (#11197) 7fae9fb6c8

fix(EA): list index can be compared to numbers (#11194) e2f7d6cde1

feature: apply clippings as separate drawables (#11183) c3237c0283
* feature: apply clippings as separate drawables

chore(scripting): add clipping support to scripted drawables (#11184) 27085dfb0c

fix(editor): Display runtime errors when Script input VM properties not set (#11177) cd70e30236
Fixes a crash when trying to use pointer events when a ViewModel property input is not setup properly on a scripted drawable instance.

The code was checking for ScriptAsset.vm which will always be non-null when bytecode is compiled in the editor. We should be checking instead for ScriptedObject.state because that will only be non-null if the ScriptedObject has validated (and we early out if that is null so we don't crash).

Also added some errors when VM properties are not setup correctly as Script inputs so users will be aware that there is an issue with their VM setup or bindings

chore: Refactor invalidateEffect for StrokeEffects (#11173) 05630acdb1
Refactors how invalidateEffect in StrokeEffects work. Now there are 2 code paths:

invalidateEffectsFromLocal() - this is called from the StrokeEffect itself. It calls out to ShapePaint::invalidateEffects to let it invalidate any other StrokeEffects that may need to do so. It also calls invalidateEffect on itself.
invalidateEffects() - this can be called from the StrokeEffect (via invalidateEffectsFromLocal) on itself OR by ShapePaint while in the process of determining if any other StrokeEffects should be invalidated via the call above. This adds paint dirt to the StrokeEffect along with any additional work needed when invalidating.
This prevents invalidateEffect from calling ShapePaint::invalidateEffects multiple times.

This also resolves an issue with ScriptedPathEffects in Components

chore: complete scripted converters implementation (#11166) 55853117b2
chore: finish scripted converters implementation

chore: Preprocess transitions conditions on initialization (#11150) bb52cbd4a3
Preprocess transitions conditions on initialization

fix: reset interpolator and initialize it on convert (#11157) 0791ee519d
* fix: reset interpolator and initialize it on convert

feature: goto definition (#11143) 4f005f715e

chore: updating to luau_701 (#11142) d4dfc63c3a

fix(vk): Implement manual MSAA resolves (#11120) 756dc2db91
* fix(vk): Implement manual MSAA resolves

Some Android devices have issues with MSAA resolves when the MSAA color
buffer is also read as an input attachment. In the past we've worked
around this by adding an empty subpass at the end of the render pass.
This PR implements fully manual resolves instead, which we now use when
there are blend modes and partial updates. This is hopefully a more
stable workaround than a mystery subpass, and will ideally get better
performance as well when we don't need to resolve the entire render
target.

* Fix synchronization validation (had a write/write hazard between the image state transition and the load op)

* fix(vk): Implement manual MSAA resolves

Some Android devices have issues with MSAA resolves when the MSAA color
buffer is also read as an input attachment. In the past we've worked
around this by adding an empty subpass at the end of the render pass.
This PR implements fully manual resolves instead, which we now use when
there are blend modes and partial updates. This is hopefully a more
stable workaround than a mystery subpass, and will ideally get better
performance as well when we don't need to resolve the entire render
target.

* Fix synchronization validation (had a write/write hazard between the image state transition and the load op)

feature: add path effects for fills (#11136) 376052977b
feature: add support for path effects for fills

fix(vk): Only rely on implicit PowerVR raster ordering on Vulkan 1.3 (#11132) 4cdb5779cd
After testing a statistically significant number of devices (7,
specifically!) I have concluded that implicit PowerVR raster ordering
only works with Rive on Vulkan 1.3 contexts. Update our renderer
accordingly.

Update path_fiddle.cpp (#11123) 19be344a5a
* Update path_fiddle.cpp

* clang format

fix(Vulkan) Vulkan synchronization fixes (#11091) 810e208837
This gets (non-atomic) Vulkan rendering to pass the Vulkan synchronization validation layer tests.

Nnnnn add support for path effect part 3 (#11107) c27b081319
* WIP

* ui

* fix errors

* remove duplicate code

* remove empty switch case

* make pathData return required

* add web ffi path methods

fix: rename Node to NodeData (#11110) c46192f1fe

add support for path effect (#11095) ab13b4d1a2
* add support for stacked path effects

chore: rename vec2d to vector in scripts (#11097) 4ad14fbe73
* chore: rename vec2d to vector in scripts

* chore: replace comments

* chore: fix formatting

* chore: formatting

* fix: bad definition

* fix: pointer event

chore: Runtime Scripting fixes & tests (#11094) 539bd8c48c
- Call reinit on ScriptedObjects in Artboard::initialize as discussed. We still need to reinit in Artboard::internalDataContext, because things like ScriptInputViewModelProperty depends on the ScriptedObject having a datacontext, which we don't necessarily have at initialize time.
- Always run the setup code in ScriptedObject::scriptInit, but only run the init function if we verified its implementation.
- Added some tests. Still need to add more comprehensive tests once we can get rivs exporting with bytecode.

feature: autocomplete requires! (#11090) 6bd796b5f0
* feature: autocomplete requires!

* chore: cleanup

fix(vk): Never read the resolve attachment (#11081) 2430b66647
When Vulkan expands the renderTarget into the MSAA color buffer for
LoadAction::preserveRenderTarget, we've been reading the resolve texture
as an input attachment. But it's debatable whether a texture can be an
input attachment AND a resolve attachment in the same render pass, and
some early Qualcomm devices have struggled with this even if we
implement the MSAA resolve manually. For now, always copy out the render
target to a separate texture when there's a preserve.

chore(scripting): rename scripting (#11084) 85b425bf93
* chore(scripting): make init and draw optional

* chore(scripting): rename ScriptType to ScriptProtocol and none to utility

* rename Drawing to Node

Peon Worker Script Signing (#11063) 8748f53562
* feature: signing and compiling via ffi

* feature: script signing

* fix ups

* Fix tests

feature: Script signing (#11016) 9295f20b82
* feature: signing and compiling via ffi

* feature: script signing

* fix: missed with rive scripting

* chore: more fixes

* fix: rive_native wasm

* fix: missed dispose call for the workspace

* feature: optional signing

* chore: core collision

* feature: doing actual verification

* fix: signature verification at runtime

* chore: missed files

* fix: switch order

* fix: with rive scripting flag

* fix: core def collisions

* chore: unifying enrich assets

* chore: remove unnecessary type check

* fix: signing context

fix(vk): Use rasterOrdering mode on Imagination GPUs (#11072) 69b2a3c643
We already have a codepath that enables rasterOrdering on ARM, even
if the extension isn't present, because we know that's how these GPUs
work. If we enable this codepath on Imagination, it appears to work as
well. This works around an MSAA crash on Pixel 10.

Co-authored-by: Gordon <pggordonhayes@gmail.com>
This commit is contained in:
HayesGordon
2025-12-03 15:06:17 +00:00
parent 218e71957d
commit ea769d78de
4 changed files with 245 additions and 1 deletions

View File

@@ -1 +1 @@
2aa57b3d8acfb1ede538d3cc0212e6ce51d01c57 9e48626a963ceb704d22308692991b07a5f19497

View File

@@ -3,6 +3,7 @@
### Fixes ### Fixes
- Fixed Android build issues in certain environments. See issue [555](https://github.com/rive-app/rive-flutter/issues/555). Build commands are now executed from Gradle instead of CMakeLists, and the setup process automatically skips `rive_native:setup` if the required libraries are already downloaded. - Fixed Android build issues in certain environments. See issue [555](https://github.com/rive-app/rive-flutter/issues/555). Build commands are now executed from Gradle instead of CMakeLists, and the setup process automatically skips `rive_native:setup` if the required libraries are already downloaded.
- Fixed [570](https://github.com/rive-app/rive-flutter/issues/570) - `setState` called after dispose. Add cancellation checks to prevent stale state updates.
### Build & Platform Updates ### Build & Platform Updates

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rive/rive.dart'; import 'package:rive/rive.dart';
@@ -78,6 +80,7 @@ class RiveWidgetBuilder extends StatefulWidget {
class _RiveWidgetBuilderState extends State<RiveWidgetBuilder> { class _RiveWidgetBuilderState extends State<RiveWidgetBuilder> {
RiveState _state = RiveLoading(); RiveState _state = RiveLoading();
late File _file; late File _file;
Future<void>? _currentSetup;
@override @override
void initState() { void initState() {
@@ -98,10 +101,39 @@ class _RiveWidgetBuilderState extends State<RiveWidgetBuilder> {
} }
Future<void> _setup({required bool withFileLoad}) async { Future<void> _setup({required bool withFileLoad}) async {
final completer = Completer<void>();
_currentSetup = completer.future;
try {
await _setupImpl(completer.future, withFileLoad: withFileLoad);
if (!completer.isCompleted) {
completer.complete();
}
} catch (e, stackTrace) {
if (!completer.isCompleted) {
completer.completeError(e, stackTrace);
}
} finally {
// Only clear if this is still the current setup
if (identical(_currentSetup, completer.future)) {
_currentSetup = null;
}
}
}
Future<void> _setupImpl(
Future<void> thisSetup, {
required bool withFileLoad,
}) async {
try { try {
if (withFileLoad) { if (withFileLoad) {
_file = await widget.fileLoader.file(); _file = await widget.fileLoader.file();
// Check if this operation was cancelled or the widget was disposed
if (!mounted || !identical(_currentSetup, thisSetup)) {
return;
} }
}
final controllerBuilder = widget.controller; final controllerBuilder = widget.controller;
final controller = controllerBuilder != null final controller = controllerBuilder != null
? controllerBuilder(_file) ? controllerBuilder(_file)
@@ -117,6 +149,13 @@ class _RiveWidgetBuilderState extends State<RiveWidgetBuilder> {
vmi = controller.dataBind(dataBind); vmi = controller.dataBind(dataBind);
} }
// Check if this operation was cancelled or the widget was disposed
if (!mounted || !identical(_currentSetup, thisSetup)) {
controller.dispose();
vmi?.dispose();
return;
}
setState(() { setState(() {
_state = RiveLoaded( _state = RiveLoaded(
file: _file, file: _file,
@@ -126,6 +165,10 @@ class _RiveWidgetBuilderState extends State<RiveWidgetBuilder> {
}); });
widget.onLoaded?.call(_state as RiveLoaded); widget.onLoaded?.call(_state as RiveLoaded);
} on Exception catch (e, stackTrace) { } on Exception catch (e, stackTrace) {
// Check if this operation was cancelled or the widget was disposed
if (!mounted || !identical(_currentSetup, thisSetup)) {
return;
}
setState(() { setState(() {
_state = RiveFailed(e, stackTrace); _state = RiveFailed(e, stackTrace);
}); });

View File

@@ -0,0 +1,200 @@
import 'dart:io' as io;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:rive/rive.dart';
void main() {
late File riveFile;
setUp(() async {
final file = io.File('test/assets/rive_file_controller_test.riv');
riveFile = await File.decode(
await file.readAsBytes(),
riveFactory: Factory.flutter,
) as File;
});
testWidgets('RiveWidgetBuilder cancels previous setup when new one starts',
(WidgetTester tester) async {
// Track state changes from each file loader
RiveLoaded? stateFromFirstLoader;
RiveLoaded? stateFromSecondLoader;
// Create a delayed file loader for the first setup
final delayedFileLoader1 = _DelayedFileLoader(
riveFile,
delay: const Duration(milliseconds: 100),
);
// Create a faster file loader for the second setup
final fastFileLoader2 = _DelayedFileLoader(
riveFile,
delay: const Duration(milliseconds: 10),
);
// Track the final loaded state
RiveLoaded? finalLoadedState;
await tester.pumpWidget(
MaterialApp(
home: _TestWidget(
initialFileLoader: delayedFileLoader1,
onStateChanged: (state) {
if (state is RiveLoaded) {
stateFromFirstLoader = state;
finalLoadedState = state;
}
},
),
),
);
// Start the first setup
await tester.pump();
// Wait a bit to ensure first setup has started loading
await tester.pump(const Duration(milliseconds: 20));
// Change to the second file loader - this should cancel the first setup
await tester.pumpWidget(
MaterialApp(
home: _TestWidget(
initialFileLoader: fastFileLoader2,
onStateChanged: (state) {
if (state is RiveLoaded) {
stateFromSecondLoader = state;
finalLoadedState = state;
}
},
),
),
);
// Wait for the second setup to complete
await tester.pumpAndSettle();
// Wait a bit more to ensure first setup would have completed if not cancelled
await tester.pump(const Duration(milliseconds: 150));
// Verify that the second setup completed and is the final state
expect(finalLoadedState, isNotNull);
expect(stateFromSecondLoader, isNotNull);
expect(identical(finalLoadedState, stateFromSecondLoader), isTrue);
// Verify that the first setup was cancelled - it should not have
// updated the state (stateFromFirstLoader should be null or different)
// The first file loader may complete, but the setup should be cancelled
// before calling setState
if (stateFromFirstLoader != null) {
// If the first loader did create a state, it should not be the final one
expect(identical(finalLoadedState, stateFromFirstLoader), isFalse);
}
// Cleanup
delayedFileLoader1.dispose();
fastFileLoader2.dispose();
});
testWidgets(
'RiveWidgetBuilder cancels setup when widget is disposed during loading',
(WidgetTester tester) async {
bool stateUpdatedAfterDisposal = false;
final delayedFileLoader = _DelayedFileLoader(
riveFile,
delay: const Duration(milliseconds: 100),
);
await tester.pumpWidget(
MaterialApp(
home: _TestWidget(
initialFileLoader: delayedFileLoader,
onStateChanged: (state) {
if (state is RiveLoaded) {
stateUpdatedAfterDisposal = true;
}
},
),
),
);
// Start loading
await tester.pump();
// Dispose the widget before loading completes
await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));
// Wait long enough for the delayed file loader to complete
await tester.pump(const Duration(milliseconds: 150));
// The file loader may complete, but the setup should be cancelled
// and not update the state after disposal (mounted check should prevent setState)
// Note: We can't directly verify this without accessing internal state,
// but the fact that the widget was disposed means mounted should be false
expect(stateUpdatedAfterDisposal, isFalse);
delayedFileLoader.dispose();
});
}
/// A file loader that introduces a delay before loading the file.
/// This is used to test cancellation behavior.
class _DelayedFileLoader extends FileLoader {
final File _file;
final Duration delay;
_DelayedFileLoader(
this._file, {
required this.delay,
}) : super.fromFile(_file, riveFactory: Factory.flutter);
@override
Future<File> file() async {
await Future.delayed(delay);
return _file;
}
}
class _TestWidget extends StatefulWidget {
final FileLoader initialFileLoader;
final void Function(RiveState state) onStateChanged;
const _TestWidget({
required this.initialFileLoader,
required this.onStateChanged,
});
@override
State<_TestWidget> createState() => _TestWidgetState();
}
class _TestWidgetState extends State<_TestWidget> {
late FileLoader _fileLoader;
@override
void initState() {
super.initState();
_fileLoader = widget.initialFileLoader;
}
@override
void dispose() {
_fileLoader.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RiveWidgetBuilder(
fileLoader: _fileLoader,
builder: (context, state) {
// Notify about state changes
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onStateChanged(state);
});
return const SizedBox.shrink();
},
);
}
}