Draw Command Protocol
The JVM/native renderer boundary is intentionally small.
Instead of sending arbitrary Kotlin/Java objects into native code, Kanvas converts a frame into a bounded command batch plus explicit texture resources.
The native presentation entry point is implemented in jni_display_bridge.c.
Frame batch layout
The JNI present call receives four parallel arrays:
kinds[] → command type
geometry[] → x, y, width, height
colors[] → r, g, b, a
uvs[] → u0, v0, u1, v1
For N commands, each non-kind array must contain exactly N × 4 elements.
The bridge rejects the frame when:
- any array is missing
- command count is zero
- command count exceeds
KD_DRAW_MAX_COMMANDS - array lengths do not match the expected shape
JNI is a trust boundary. Once data is in C, a mismatched array length can become native memory corruption instead of a normal JVM exception.
The bridge validates dimensions before obtaining raw array elements.
Native conversion
After validation, JNI allocates a native kd_draw_command[] and lowers each array entry into one struct:
commands[i].kind
commands[i].x
commands[i].y
commands[i].width
commands[i].height
commands[i].color.*
commands[i].u0
commands[i].v0
commands[i].u1
commands[i].v1
The JNI arrays are released before backend presentation continues.
This means backend code receives a native command buffer rather than JVM-owned memory.
Why parallel primitive arrays?
The protocol favors:
- predictable memory layout
- a single JNI call per frame batch
- no reflection/native traversal of object graphs
- explicit validation
- backend-neutral data
The trade-off is that adding new command fields requires evolving the protocol intentionally.
The Gradle plugin, agent, JNI bridge, and native runtime are released as one compatibility unit. Changing command layout or resource semantics without coordinating both sides can produce hard native failures.
Texture protocol
Textures are separate resources identified by integer IDs.
Upload validates:
- renderer handle
- texture ID range
- width/height
- pixel count
- input array/address shape
The native runtime stores:
texture ID
width
height
pixel copy
revision
next
A revision increments on each re-upload.
Backends can use that revision to know when their GPU-side copy is stale.
Address-based uploads
Kanvas also has an upload path that copies rows from a native address with an explicit row_bytes stride.
The bridge requires:
row_bytes >= width × sizeof(uint32_t)
and copies each row into tightly packed native storage.
This avoids assuming that the source buffer is tightly packed.
Explicit release
Texture release removes the resource from the native linked list and asks the active backend to release its corresponding GPU resource.
That matters because JVM garbage collection cannot safely decide when a graphics API resource is no longer referenced by in-flight rendering work.
Presentation recovery
Presentation can return statuses such as:
KD_OK
KD_SWAPCHAIN_OUT_OF_DATE
KD_GRAPHICS_ERROR
For Vulkan and D3D9 swapchain/device-style failures, Kanvas first destroys and recreates the current backend, then retries the same frame.
When the requested backend is AUTO, a graphics failure can then fall through to another GPU backend.
Backend recreation requires native window/device/swapchain state. Keeping recovery in C prevents the Java/Kotlin side from having to understand backend-specific object lifetimes.