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.
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.
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
automay fall through to another GPU backend
Keeping that policy native avoids trying to reconstruct platform graphics state from the Java side.