Rive Renderer

I asked AI about a native Rive integration with Solar2D. Rive seems good for animated icons, micro-interactions. Is the following answer feasible?

Integrating Rive’s native renderer (built on OpenGL, Metal, or Vulkan) with Solar2D via a custom plugin is technically feasible, but requires careful synchronization between Rive’s renderer and Solar2D’s graphics pipeline. Here’s a detailed breakdown of how this could work:


1. Architecture Overview

  • Rive Renderer: Rive’s renderer outputs frames using low-level GPU APIs (OpenGL, Metal, Vulkan).
  • Solar2D: Uses OpenGL/Vulkan for rendering and provides Lua APIs to manage textures, sprites, and shaders.
  • Goal: Render Rive animations into a GPU texture that Solar2D can display and update dynamically.

2. Key Integration Strategies

A. Texture Sharing via GPU Resources

Rive can render animations into a texture (framebuffer), which Solar2D can then bind and display. This avoids CPU-GPU data copies and is highly efficient.

Steps:
  1. Create a Shared Texture:

    • In the plugin, generate a GPU texture (e.g., GL_TEXTURE_2D for OpenGL).
    • Share this texture handle between Rive’s renderer and Solar2D.
  2. Render to Texture with Rive:

    • Configure Rive to render into the shared texture (e.g., using an offscreen framebuffer).
    • Example for OpenGL:
      // In Rive plugin code
      glGenTextures(1, &sharedTexture);
      glBindTexture(GL_TEXTURE_2D, sharedTexture);
      rive::Renderer::beginFrame(sharedTexture); // Hypothetical API
      
  3. Bind Texture in Solar2D:

    • Use Solar2D’s graphics.newTexture() or a modified Graphics Extension plugin to reference the shared texture.
    • Example Lua usage:
      local riveTexture = graphics.newTexture({ type = "external", name = "rive_shared_texture" })
      local sprite = display.newImageRect(riveTexture, 300, 300)
      

B. Copying Pixel Data (Fallback)

If direct texture sharing is unsupported (e.g., due to API/platform constraints), copy rendered pixels from Rive to Solar2D’s texture.

Steps:
  1. Render to Offscreen Buffer:

    • Use Rive to render into a GPU framebuffer.
    • Read pixels via glReadPixels (OpenGL) or equivalent.
  2. Update Solar2D Texture:

    • Use Solar2D’s graphics.newTexture() with a volatile flag to create a dynamically updatable texture.
    • Update the texture data using graphics.setTexture() (similar to PluginGfxe.cpp).

    Example in C++ (PluginGfxe.cpp-inspired):

    // In the plugin's native code
    void updateTexture(lua_State *L) {
      // 1. Render Rive frame to offscreen buffer
      riveRenderer->drawFrame();
    
      // 2. Read pixel data into a CPU buffer
      std::vector<uint8_t> pixels(width * height * 4);
      glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
    
      // 3. Update Solar2D texture
      CoronaExternalTextureCallbacks textureCallbacks;
      textureCallbacks.size = sizeof(textureCallbacks);
      textureCallbacks.getWidth = getWidthCallback;
      textureCallbacks.getHeight = getHeightCallback;
      textureCallbacks.onRequestBitmap = [pixels](...) { memcpy(bitmap, pixels.data(), size); };
      CoronaExternalPushTexture(L, textureCallbacks);
    }
    

3. Critical Technical Considerations

a. Graphics API Alignment

  • OpenGL: Works best on Android/iOS (Solar2D’s default).
  • Metal/Vulkan: Requires Solar2D’s experimental Vulkan backend or platform-specific tweaks (e.g., iOS Metal ↔ OpenGL interop).

b. Synchronization

  • Ensure Rive’s rendering and Solar2D’s frame loop are synchronized (e.g., use enterFrame listeners to trigger Rive updates).

c. Performance

  • Texture Sharing (Direct): Minimal overhead (GPU-GPU transfer).
  • Pixel Copying (Fallback): Adds latency due to CPU-GPU readback (avoid for 60 FPS animations).

4. Solar2D Graphics Extensions Plugin Analogy

The Graphics Extensions Plugin demonstrates how to:

  1. Decode image data (e.g., JPEG/PNG).
  2. Upload raw pixel data to a Solar2D texture via CoronaExternalTextureCallbacks.

Adapting for Rive:

  • Replace the image decoder with Rive’s renderer.
  • Render frames into a pixel buffer and push it to Solar2D’s texture.

5. Example Workflow

  1. Initialize Rive Renderer in Plugin:

    rive::File* file = rive::File::open("animation.riv");
    rive::Artboard* artboard = file->artboard();
    riveRenderer = artboard->renderer();
    
  2. Render Frame and Update Texture:

    void onFrame(lua_State *L) {
      riveRenderer->draw(); // Render to offscreen buffer
      updateTexture(L); // Copy pixels to Solar2D texture
    }
    
  3. Lua API for Control:

    local rive = require "plugin.rive"
    rive.play("animation.riv", "StateMachine1")
    

6. Challenges

  • Platform-Specific Code: Requires separate implementations for OpenGL (Android), Metal (iOS), and Vulkan (Windows/Linux).
  • Texture Lifetime Management: Ensure shared textures are not destroyed prematurely.
  • Alpha/Blending: Handle transparency correctly between Rive and Solar2D layers.

7. Recommendations

  1. Prioritize Direct Texture Sharing: Use OpenGL texture handles or Metal’s CVPixelBuffer (iOS) for zero-copy workflows.
  2. Leverage Solar2D’s Native Extensions: Use CoronaExternalTextureCallbacks to integrate Rive’s output.
  3. Benchmark Performance: Test both GPU-GPU and CPU-GPU paths to avoid bottlenecks.

By aligning Rive’s renderer with Solar2D’s graphics pipeline, you can achieve smooth, real-time animations. Let me know if you’d like a sample plugin skeleton!

1 Like

Another AI answer. The native spine plugin would be a good reference, thank you @ depilz for the great work. Next I will look into setting up building the rive-runtime for Solar2D plugin

Yes, the texture handling in the Spine plugin for Solar2D is conceptually similar to what I described for the Graphics Extensions (Gfxe) plugin and a hypothetical Rive plugin. Let’s break down the parallels and differences:


1. Texture Workflow Similarities

a. Texture Creation & Sharing

  • Spine Plugin:

    • Uses OpenGL to render Spine skeletal animations into textures.
    • Shares GPU textures with Solar2D via native plugin code (e.g., CoronaExternalTextureCallbacks).
    • Example code from spinePlugin:
      // Create OpenGL texture for Spine atlas
      glGenTextures(1, &textureID);
      glBindTexture(GL_TEXTURE_2D, textureID);
      glTexImage2D(..., atlas->getPixels());
      
    • Solar2D Lua binds to this texture via graphics.newTexture().
  • Gfxe/Rive Analog:

    • Both would use similar OpenGL/Vulkan texture workflows.
    • For Rive, replace atlas->getPixels() with Rive’s renderer output (e.g., rive::Renderer::draw()).

b. Dynamic Texture Updates

  • Spine Plugin:

    • Updates textures on every frame by rendering Spine animations into OpenGL textures.
    • Syncs with Solar2D’s frame loop via enterFrame listeners.
  • Gfxe/Rive:

    • Gfxe updates textures by pushing decoded image data (e.g., PNG/JPEG) via CoronaExternalPushTexture.
    • For Rive, you’d render frames into a GPU texture (shared or copied) and update it dynamically.

2. Key Differences

a. Rendering Backend

  • Spine: Uses its own OpenGL renderer for skeletal animations.
  • Rive: Uses Rive’s renderer (OpenGL/Metal/Vulkan) for vector animations.
  • Gfxe: Focuses on static image decoding (no runtime animation).

b. Data Flow

  • Spine:

    • Requires a .atlas file and texture atlas (prebaked).
    • Animations are driven by Spine’s runtime (C++/Java).
  • Rive:

    • Uses .riv files with vector paths and state machines.
    • Requires runtime interpolation of vector data (more dynamic than Spine’s skeletal system).

3. Example Workflow Comparison

Spine Plugin (Reference)

  1. Load Assets:
    // spinePlugin.cpp
    spine::Atlas* atlas = new spine::Atlas("skeleton.atlas", new MyTextureLoader());
    spine::SkeletonJson json(atlas);
    spine::SkeletonData* skeletonData = json.readSkeletonDataFile("skeleton.json");
    
  2. Render Frame:
    void update(lua_State* L) {
      spineRenderer->draw(); // Renders to OpenGL texture
      CoronaExternalPushTexture(L, textureCallbacks); // Update Solar2D texture
    }
    

Rive Plugin (Hypothetical)

  1. Load Rive File:
    // rivePlugin.cpp
    rive::File* file = rive::File::open("animation.riv");
    rive::Artboard* artboard = file->artboard();
    rive::Renderer* renderer = artboard->renderer();
    
  2. Render Frame:
    void update(lua_State* L) {
      renderer->draw(); // Renders to OpenGL/Metal/Vulkan texture
      CoronaExternalPushTexture(L, textureCallbacks); // Share with Solar2D
    }
    

4. Critical Code Snippets

Texture Callbacks (Shared Between Plugins)

// Similar in Spine, Gfxe, and Rive plugins
CoronaExternalTextureCallbacks textureCallbacks;
textureCallbacks.size = sizeof(textureCallbacks);
textureCallbacks.getWidth = [](void* context) { return width; };
textureCallbacks.getHeight = [](void* context) { return height; };
textureCallbacks.onRequestBitmap = [](void* context, CoronaBitmap* bitmap) {
  // Copy pixel data (Gfxe) or bind GPU texture (Spine/Rive)
};

Solar2D Lua Integration

-- Spine, Gfxe, or Rive would expose similar APIs
local spine = require "plugin.spine"
local rive = require "plugin.rive"

local spineActor = spine.newActor("skeleton.json", "atlas.atlas")
local riveAnimation = rive.newAnimation("animation.riv")

5. Takeaways

  • Shared Pattern: All plugins (Spine, Gfxe, Rive) use native code to generate/update textures and expose them to Solar2D via CoronaExternalTextureCallbacks.
  • Rendering Differences:
    • Spine: Prebaked atlas + skeletal deformation.
    • Rive: Vector paths + runtime interpolation.
  • Performance: Direct GPU texture sharing (Spine/Rive) is more efficient than CPU pixel copying (Gfxe).

For a Rive plugin, you’d adapt the Spine plugin’s OpenGL texture workflow but replace Spine’s renderer with Rive’s. The Graphics Extensions Plugin is also a useful reference for handling dynamic pixel data.

As far as I know Rive requires Opengl 4.x api (maybe)

This is hard to implement in Solar2D currently.

rive c++ backends supports PC(Vulkan), iOS/macOS, Android (OpenGL ES 3, Vulkan 1.0+), WebGL 2.0. So maybe I could try PC(Vulkan) or iOS/macOS(metal)?

rive-runtime/tree/main/renderer/src seems openGL3.1

  1. Fragment shader interlock via ARB_fragment_shader_interlock and INTEL_fragment_shader_ordering extensions

  2. Shader image access and memory barriers (GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)

  3. Atomic operations in fragment shaders