feat: Backport the gooey morph to API 29-32 (OpenGL → hardware Bitmap)#1
Conversation
Renders the blur + alpha-threshold morph in OpenGL ES 3.0 and presents it as a hardware Bitmap (no SurfaceView), so API 29-32 shows the real effect instead of a crossfade. The API 33+ RenderEffect path and public API are unchanged.
There was a problem hiding this comment.
Really appreciate the contribution, and I love how clean the structure is! The MorphRenderer split and the GL port both read really nicely.
Most of my inline comments come down to one theme: release() is treated as a one-shot teardown, but renderer is a long-lived val that needs to survive detach/re-attach and resize, so some state doesn't reset cleanly across that. Thanks again! 🙏
Addresses the PR review on the GL morph backend. The unifying theme is that the renderer is a long-lived object but release() was written as one-shot teardown. - release() now resets to a restartable state: it drains the GL thread (quitSafely + join) before returning, so a re-attach can't race teardown for the pipeline/leases, and it resets lastSubmitted/pending/renderScheduled so a re-attach at the same size/progress re-renders instead of stalling on the crossfade - EglCore validates every EGL return and throws on failure, so a one-off init failure isn't cached and the next frame retries; the GL path is also gated behind a device GL ES 3.0 check - Lease.close() recycles the bitmap to drop the HardwareBuffer ref deterministically - decompose MorphGlPipeline into SwapChain / RenderTarget / SourceTexture and per-pass program objects
|
@blazejkustra Thanks for the careful review. What changed in this update:
Point by point: 1. Fixed. 2. Fixed, and same root cause as 1. 3. The 2-deep lease trim is count-based, not fence-based Fair concern. It matters more here because the GL thread is async and can render ahead of what's on screen. Reasons I kept count-based for now:
The proper fix is to fence on the buffer's release, wait until HWUI is done reading before the buffer is reused. That release fence isn't exposed by the Java 4. Resize closes the Here I'd push back, and I'd welcome your read. I didn't add an explicit drain; I added The reason I think the crash doesn't occur: a If you've actually reproduced the 5. Fixed on two levels:
The decomposition in the list above also came out of your "release is one-shot" point, splitting the pipeline into owned units made the restartable Happy to adjust #3 and #4 if you'd like them handled here. |
Hi! 👋
I'm not really a React Native developer, so apologies in advance if I've missed any RN conventions here, and I'm happy to fix anything. I work mostly with Android and graphics. I came across this project, really liked the effect, and noticed it only runs the real morph on API 33+ (
RenderEffect+RuntimeShader) and quietly falls back to a plain crossfade below that. I'd explored these exact GPU/EGL paths while building imla, so making the effect run on more devices felt like low-hanging fruit, and I figured I'd give it a go.So this PR backports the blur + alpha-threshold morph down to API 29-32.
How it works
Bitmap.wrapHardwareBuffer: renders into anImageReader, wraps itsHardwareBufferas a hardwareBitmap, and lets it composite through the normal HWUI path. NoSurfaceView, no new dependency.RenderEffect.createBlurEffect1:1 (same Skiasigma = 0.57735 * radius + 0.5), so it looks the same as the platform path.The whole pipeline is GPU-accelerated, so performance is on par with the native RenderEffect path.
Scope
I tried to keep the change as small and isolated as possible. It all lives in one internal package, and the existing paths are left alone:
minSdkunchanged.RenderEffectpath is untouched (just moved behind a small interface); below API 29 it still falls back to crossfade as before.Tested
morph_showcase.mp4
Totally understand if you'd prefer a different direction or want changes. Just thought I'd offer it.
Thanks for the lovely library! 🙂