Repository navigation
Android: Luau offscreen Canvas content is blank because AndroidRenderTextureFrameSink does not restore GL state before canvas replay (0.2.0-dev.3) #662
Description
Submission checklist
- I have confirmed the issue is present in the latest version of the
riveFlutter package (0.15.0-dev.3, withrive_native 0.2.0-dev.3) - I have searched the documentation and forums and could not find an answer
- I have searched existing issues and this is not a duplicate. Android: widgets stop painting on 0.15.0-dev.1 (works on 0.14.11) #651 and Android: deferred texture disposal blocks the main thread while GL pipelines link (0.2.0-dev.3) #661 are different Android problems on the same version line.
Description
On Android, anything a Luau script draws into an offscreen Canvas (context:canvas(...)) comes out empty. Compositing that canvas, directly or through GPUCanvas passes, gives a blank or black stage. The same artboard drawn straight to the main renderer shows up. iOS renders both paths correctly.
Expected: the offscreen canvas holds the same 2D content as direct drawing, and its image composites normally on every frame.
We hit this independently in two scripted assets:
- A battle stage that draws a child artboard into a
Canvas, then runs blur, glow and composite passes onGPUCanvas. The stage is fully black on Android and correct on iOS. - A second asset with the same canvas-then-
drawImagepattern, including a branch with no GPU passes at all. Android shows nothing at any effect level, while iOS shows the full scene.
Drawing the artboard straight onto the main renderer, with no Canvas, works on Android in both assets. We ship that as our workout.
Root cause
We traced it in the published rive_native 0.2.0-dev.3 source, native/platform/android/rive_native_android.cpp:
AndroidRenderTexture::submitFrame()flushes the screen frame, then callsRenderContextGLImpl::unbindGLInternalResources(). That unbinds Rive's internal GL textures, including the tessellation, gradient, Gaussian-integral and feather-atlas resources.- On the next deferred replay, offscreen canvas content renders before the screen frame.
AndroidRenderTextureFrameSinkoverridesbeginScreen()but notbeginCanvasContent(). The inheritedHostFrameSink::beginCanvasContentopens the canvas frame without callinginvalidateGLState(), so canvas draws run with those resources unbound.- The screen path restores state later, which is too late for canvas pixels already drawn.
HostFrameSink::afterOreFrame() does not cover this, because it runs after Ore work, not before the first canvas replay.
Patch
We rebuilt the Android native libraries from the published dev.3 package source, once without and once with this change, using identical build options:
- Without the change, the stage stayed black.
- With it, the unmodified
.rivrendered the stage correctly.
--- a/native/platform/android/rive_native_android.cpp
+++ b/native/platform/android/rive_native_android.cpp
@@ -1410,6 +1410,17 @@
return m_renderTexture->renderer();
}
+ // submitFrame() unbinds Rive's GL textures after every screen flush, and
+ // canvases replay before the screen, so rebind them first.
+ rive::Renderer* beginCanvasContent(rive::gpu::RenderCanvas* canvas,
+ uint32_t clearColor) override
+ {
+ renderContext()
+ ->static_impl_cast<rive::gpu::RenderContextGLImpl>()
+ ->invalidateGLState();
+ return HostFrameSink::beginCanvasContent(canvas, clearColor);
+ }
+
private:
AndroidRenderTexture* m_renderTexture;
};Reproduction steps / code
The failing sequence inside a scripted layout's draw(self, renderer):
-- blank on Android, correct on iOS
local r = self.canvas:beginFrame({ clearColor = 0x00000000 })
self.child:draw(r) -- any visible child artboard
self.canvas:endFrame()
renderer:drawImage(self.canvas.image, ImageSampler("clamp", "clamp", "nearest"), "srcOver", 1)
-- visible on Android
self.child:draw(renderer)- Use one
RiveWidgetwithFactory.riveand a scripted layout that instances a small child artboard, such as a filled rectangle. - Create the canvas during
draw, withcontext:canvas({ width = w, height = h }). - Run the canvas branch above for several frames. On Android the rectangle never appears.
- Switch to the direct branch. The rectangle appears.
No images, shaders or GPUCanvas are needed to trigger it, though our production assets also use them. This is a reduced description of our assets, not a separately built sample project. Our .riv files are private, but we can share them at support@rive.app if that helps.
Rive Flutter package version
0.15.0-dev.3, with transitive rive_native 0.2.0-dev.3 (native version.txt: 0.2.0-dev.3+2)
Flutter version
Flutter 3.47.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 5fc346839b • 2026-09-30 15:02:49 -0700
Engine • hash b8c8d3d8d5d0095127057f8a29ca8cc53da2167c (revision 692136cb65)
Tools • Dart 3.13.5 • DevTools 2.60.0
Device
Android Emulator: Pixel 7 profile, arm64-v8a, Google APIs Play Store image. Seen with both the swangle and host GPU modes. The control is the iOS Simulator (iPhone 16 Pro), which renders correctly.
OS version
Android 16 (API 36) emulator. Control: iOS 18.3 simulator.
Additional context
- Renderer:
Factory.rive, deferred rendering, Android OpenGL ES. - We haven't confirmed this on a physical Android device yet. The root cause is in host code shared by all Android GL devices, so we expect it there too.
- Our current workaround: on Android, write an effects input of 0 and draw the scene straight onto the main renderer. That loses every offscreen-canvas effect on Android.
Activity
HayesGordon commentedon Oct 20, 2026
Thanks for for the report, we ended up with the same conclusion. The canvas path opens its frame without re-validating GL state after the screen flush unbinds Rive's internal textures, so canvas draws land with those resources unbound. The fix is up as rive-app/rive#14842, awaiting review, and it should land in the next release.