Skip to main content

JNI & Native Bridge

Kanvas crosses from the JVM into a native C runtime through a narrow bridge rather than exposing platform graphics APIs directly to application Kotlin code.

Key implementation files:

Boundary shape​

Conceptually:

captured frame
│
▼
Kotlin / Java representation
│
▼
primitive JNI arrays + texture handles
│
▼
C draw-command batch
│
▼
selected graphics backend

The JNI present call validates that command arrays agree in size before allocating a native command batch.

Why flatten commands before JNI?​

JNI crossings are relatively expensive and difficult to reason about when object graphs are deep.

Kanvas batches frame data into parallel primitive arrays so that one presentation call can transfer a complete command batch with predictable memory layout.

Validation belongs at the boundary

The C bridge checks command counts and array lengths before reading them. That prevents malformed JVM-side data from turning directly into out-of-bounds native memory access.

Native lifetime is explicit

The JVM can own references to renderer objects, but native GPU resources have backend-specific lifetimes. Kanvas therefore uses explicit create/upload/release operations instead of relying on garbage collection.

Textures are explicit resources​

Textures receive native IDs and have explicit upload/release paths. The native bridge keeps track of those resources and forwards releases to the active backend.

This is necessary because a JVM object becoming unreachable is not the same event as a Vulkan/Metal/OpenGL/D3D resource being safe to destroy.

Why the native layer owns backend recovery​

The native runtime knows whether a failure means:

  • the swapchain is out of date
  • the current backend should be recreated
  • auto may fall through to another GPU backend

Keeping that policy native avoids trying to reconstruct platform graphics state from the Java side.