Skip to content

Android 17: TextureView presents only a fraction of rendered frames (updateTexImage drain from #1980 no longer works); SurfaceView opt-in proposal #4058

Description

@jslok

Description

On Android 17 the default SkiaTextureView path presents only a fraction of the frames Skia renders, so Reanimated-driven canvases look like a low-fps stepping animation. The same build is smooth on iOS and was smooth on the same device on Android 16.

Cause: the per-frame SurfaceTexture.updateTexImage() drain added in #1980 (the fix for #1960) no longer drains on Android 17. On Android ≤16 it fails after acquiring/releasing the pending buffer (updateAndRelease: EGLConsumer is not attached, #2054), so it still kept the TextureView in step by accident. On 17 it returns early:

E <app>: [SurfaceTexture-0-<pid>-5] updateTexImage: LegacySurfaceTexture is not attached!
I RNSkia  : updateAndRelease() failed. The exception above can safely be ignored

Evidence (Pixel 10, Android 17 build CP2A.260805.005, app render rate 60 Hz):

  • RNSkia log above fires every ~16.6 ms → Skia renders 60 fps.
  • dumpsys gfxinfo <pkg> framestats: HWUI presents the TextureView content in RenderThread-only frames (flag 2) at ~19–23/s, interleaved with main-thread frames every 2 vsyncs; ~44 HWUI frames/s total, intervals alternating 16.6/33.3 ms. GPU time 1 ms p99, so it is not GPU load.
  • Switching the same canvas to SkiaSurfaceView presents every frame and the animation is smooth (verified on the device).

React Native Skia Version

2.11.2

React Native Version

0.87.1 (react-native-reanimated 4.6.0, react-native-worklets 0.12.1)

Using New Architecture

  • Enabled

Steps to Reproduce

  1. Pixel 10 on Android 17.
  2. <Canvas> with a <Path> whose path is a useDerivedValue driven by withSpring shared values (any continuously animated Skia prop works; ours is a 4-corner polygon overlaying a camera preview).
  3. Retarget the springs continuously (or just let one settle) and watch the motion; enable Developer options → "Show refresh rate" to confirm the display is not the limit.
  4. adb logcat | grep -E "RNSkia|SurfaceTexture" and adb shell dumpsys gfxinfo <pkg> framestats while it animates.

Snack, Code Example, Screenshot, or Link to Repository

const x = useSharedValue(0)
useEffect(() => { x.value = withRepeat(withSpring(300), -1, true) }, [])
const path = useDerivedValue(() => {
  const p = Skia.Path.Make()
  p.addRect({ x: x.value, y: 100, width: 120, height: 160 })
  return p
})
<Canvas style={StyleSheet.absoluteFill}>
  <Path path={path} color="lime" style="stroke" strokeWidth={4} />
</Canvas>

Fix we are shipping (patched locally, tested working on a real Pixel 10): use SkiaSurfaceView, made usable as an overlay:

// SkiaSurfaceView constructor
getHolder().setFormat(PixelFormat.TRANSLUCENT); // EGL config already has alpha 8; renderer clears to transparent
setZOrderMediaOverlay(true);                    // above sibling SurfaceViews (camera preview), below the window's views

Today the only way to reach SkiaSurfaceView is opaque, and it is opaque black. Proposal: an opt-in androidSurfaceView prop on Canvas that selects the SurfaceView and applies the translucent format + media-overlay z-order when opaque is false, leaving opaque unchanged. Trade-offs to document: the canvas composites below the window's own views, and usual SurfaceView limitations during view transitions.

Should we submit a PR with this implementation?

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions