Skip to content

rive_native: standalone Factory.decodeImage() entries block unrelated File.decode() calls (hangs after a test ends) #658

Open

Description

@josh-burton

Summary

With the Flutter software renderer (Factory.flutter), a standalone Factory.decodeImage() call makes every later
File.decode() wait for that image, even though the two are unrelated. If that image decode never completes, which
happens whenever it was started in a Flutter widget test's fake-async zone that has since ended, every later Rive file
decode in the isolate hangs forever.

We hit this in a large widget-test suite where many test files run in one isolate: one test that decoded images for
data binding and then ended made unrelated Rive file loads in later tests time out.

Cause

In lib/src/ffi/flutter_renderer_ffi.dart:

  • FFIFlutterFactory.decodeImage() adds its FFIFlutterFactoryImage to the static _loadingImages list.
  • completedDecodingFile() snapshots _loadingImages, clears it, and awaits every entry.

_loadingImages exists so a file decode can wait for the images embedded in that file. Because standalone
decodeImage() calls use the same list, a file decode also awaits images that belong to unrelated callers, possibly
from long before. Awaiting a future resumes in the zone that created it, so an entry created in a zone that no longer
runs (a finished fake-async test zone) blocks the file decode permanently.

The same code is present in rive_native 0.1.11 and 0.2.0-dev.2.

Reproduction

rive 0.14.11, rive_native 0.1.11, Flutter 3.47.5, macOS (flutter test). Any small .riv file works; we used a
121-byte placeholder.

import 'dart:io' as io;
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:rive/rive.dart';

Future<Uint8List> _png() async {
  final recorder = ui.PictureRecorder();
  ui.Canvas(recorder).drawRect(const Rect.fromLTWH(0, 0, 4, 4), ui.Paint()..color = const Color(0xFFFF0000));
  final image = await recorder.endRecording().toImage(4, 4);
  return (await image.toByteData(format: ui.ImageByteFormat.png))!.buffer.asUint8List();
}

void main() {
  late Uint8List png;
  setUpAll(() async {
    await RiveNative.init();
    png = await _png();
  });

  testWidgets('1: a standalone image decode is started and the test ends', (tester) async {
    // Not awaited: its completion needs real time, which this fake-async test never gives it.
    Factory.flutter.decodeImage(png);
  });

  testWidgets('2: a later, unrelated file decode never completes', (tester) async {
    final bytes = io.File('path/to/any.riv').readAsBytesSync();
    final file = await tester.runAsync(
      () => File.decode(bytes, riveFactory: Factory.flutter).timeout(const Duration(seconds: 10)),
    );
    expect(file, isNotNull);
  });
}
  • Test 2 run on its own passes immediately.
  • Run after test 1, test 2 fails with TimeoutException after 0:00:10.000000: Future not completed, even though it
    decodes inside runAsync in the real zone.

Expected

A file decode waits only for the images embedded in that file. Standalone decodeImage() calls already return their
own future to their caller, so they should not join the list that file decodes await.

Possible fix

Do not add standalone decodeImage() results to _loadingImages, or have each file decode track only the images
created by _decodeImageFromNative while it runs.

Workaround

In tests, we decode a tiny placeholder .riv right after each decodeImage() call in the same zone. Because
completedDecodingFile() empties the list at the start of every file decode, this hands the entry to a decode owned by
the same test.

Activity

  1. HayesGordon commented on Oct 19, 2026

    @HayesGordon
    Contributor

    Thanks for the report. Confirmed and a fix is in flight, a File.decode() now waits only for images decoded during its own load, so standalone decodeImage() calls no longer join it.

  2. added
    knownTriaged: understood and tracked on our side
    on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    knownTriaged: understood and tracked on our side

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions