Skip to main content

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
Native validation is mandatory

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.

Protocol changes are ABI changes

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.

Recovery stays below the JVM boundary

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.

Source​