🐛 serialize Skia codec/bitmap close with in-flight readPixels#4438
Merged
Conversation
Decoding frames on ioDispatcher (#4422) made codec.readPixels a synchronous JNI call that ignores coroutine cancellation, while DisposableEffect.onDispose closed bitmaps and produceState.awaitDispose closed the codec — both could free native pixel buffers under an active decode and crash the process. Fold codec + bitmap ownership into a single LaunchedEffect whose finally runs only after the last withContext returns, so close happens after readPixels regardless of when composition leaves.
- Nest try/finally so codec.close() still runs if Bitmap.allocPixels
throws (e.g. OOM) before the inner try is entered.
- Replace the 20ms !playing polling delay with snapshotFlow { playing }
.first { it } so a hidden preview suspends instead of waking ~50x/s.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4437
Summary
produceCodecState+AnimatedFramesinto a singleLaunchedEffect(path)insideSkiaAnimatedImageViewthat owns the entire codec + bitmap lifecycle. Cleanup lives in onetry { ... } finally { bitmaps.forEach { it.close() }; codec.close() }, which guaranteesfinallyruns only after any in-flightwithContext(ioDispatcher) { readPixels }returns naturally. No cross-composable handoff, noMutexneeded (andMutexwould not work anyway becauseonDisposecannot suspend).isPlayingis now read viarememberUpdatedState, so toggling play/pause no longer restarts the effect (previously safe only via aframeTick == 0skip-prime trick; now structurally correct).LoadingIndicatorstays up until frame 0 is primed, eliminating the brief empty-bitmap flash on first frame.SkiaAnimatedImageViewsignature unchanged; preserved helpers (isAnimatedImage,frameDurationMs,MIN_FRAME_DURATION_MS) keep all 13 existing tests passing.Test plan
./gradlew app:desktopTest --tests "com.crosspaste.ui.paste.side.preview.SkiaAnimatedImageViewTest"— all 13 tests pass./gradlew :app:compileKotlinDesktopcompiles clean