Skip to content

Android: Luau offscreen Canvas content is blank because AndroidRenderTextureFrameSink does not restore GL state before canvas replay (0.2.0-dev.3) #662

Open

Description

@reynard93

Submission checklist

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:

  1. A battle stage that draws a child artboard into a Canvas, then runs blur, glow and composite passes on GPUCanvas. The stage is fully black on Android and correct on iOS.
  2. A second asset with the same canvas-then-drawImage pattern, 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 calls RenderContextGLImpl::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.
  • AndroidRenderTextureFrameSink overrides beginScreen() but not beginCanvasContent(). The inherited HostFrameSink::beginCanvasContent opens the canvas frame without calling invalidateGLState(), 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 .riv rendered 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)
  1. Use one RiveWidget with Factory.rive and a scripted layout that instances a small child artboard, such as a filled rectangle.
  2. Create the canvas during draw, with context:canvas({ width = w, height = h }).
  3. Run the canvas branch above for several frames. On Android the rectangle never appears.
  4. 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

  1. added
    knownTriaged: understood and tracked on our side
    on Oct 20, 2026
  2. HayesGordon commented on Oct 20, 2026

    @HayesGordon
    Contributor

    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.

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

    knownTriaged: understood and tracked on our side

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions