Skip to content
Open
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
32 changes: 28 additions & 4 deletions packages/flame/lib/src/components/text_box_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -416,15 +416,36 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
// See issue #1618 for details.
Future.delayed(const Duration(milliseconds: 100), () {
Comment thread
Barba2k2 marked this conversation as resolved.
cachedToRemove.remove(cachedImage);
if (isMounted) {
cachedImage.dispose();
}
_safeDispose(cachedImage);
});
}
cache = await _fullRenderAsImage(newSize);
size = newSize;
}

/// Dispose of [image] best-effort, swallowing the two errors that mean
/// "the resource is already gone, there is nothing left to free".
///
/// In **release** mode the production crash from issue #3724 is a
/// `StateError` ("Bad state: A Dart object attempted to access a native
/// peer, but the native peer has been collected (nullptr)") thrown when
/// `Image.dispose` accesses the native peer after the engine has dropped
/// it under memory pressure.
///
/// In **debug** mode `Image.dispose` is guarded by `assert(!_disposed)`,
/// so a redundant dispose throws an `AssertionError`. Test environments
/// are debug-mode, hence we also catch that flavour to keep the safety
/// net consistent across modes.
static void _safeDispose(Image image) {
try {
image.dispose();
} on StateError {
// Release-mode: native peer already collected; nothing left to free.
} on AssertionError {
// Debug-mode: image already disposed by a parallel path.
}
}

@override
void update(double dt) {
_lifeTime += dt;
Expand All @@ -448,7 +469,10 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
@mustCallSuper
void onRemove() {
super.onRemove();
cache?.dispose();
final cachedImage = cache;
if (cachedImage != null) {
_safeDispose(cachedImage);
}
cache = null;
}

Expand Down
25 changes: 25 additions & 0 deletions packages/flame/test/components/text_box_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,31 @@ void main() {
},
);

testWithFlameGame(
'does not throw if the cached image was already disposed '
'(regression #3724)',
(game) async {
final c = TextBoxComponent(text: 'foo bar');

await game.ensureAdd(c);
final imageCache = c.cache;
expect(imageCache, isNotNull);

// Simulate the native peer being collected before our delayed
// dispose / onRemove runs by disposing the cached image first.
imageCache!.dispose();
expect(imageCache.debugDisposed, isTrue);

// Removing the component must not crash even though dispose() on the
// already-disposed image would normally throw a StateError.
expect(() {
game.remove(c);
game.update(0);
}, returnsNormally);
expect(c.cache, null);
},
);

testWithFlameGame(
'internal image is redrawn when component is re-added',
(game) async {
Expand Down
Loading