As I've been experementing with [androidx.webgpu](https://developer.android.com/jetpack/androidx/releases/webgpu) I found several moments that can improve an experience.
The example repo can be found [here](https://github.com/ShashlikMap/webgpu-kt-template)
### Leverage Android Parcelable as a shader struct
androidx.webgpu requires us to use ByteBuffer to pass some data to GPU. So we need something that can help us to do that.
I've tried to utilize [kotlinx.serialization(+ protobuf)](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-protobuf/kotlinx.serialization.protobuf/-proto-buf/) and Java [ObjectOutputStream](https://docs.oracle.com/javase/8/docs/api/java/io/ObjectOutputStream.html) but all these methods give incorrect size and add some additional metadata to the final byte array.
What we can use instead is Android Parcelable.
Let's imagine we have the following typical struct in our shader:
```wgsl
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) color: vec4<f32>,
}
```
By using Parcelable + kotlinx.parcelize plugin we can add several helpful classes in Kotlin:
```kotlin
@Parcelize
data class Vec3(val a: Float, val b: Float, val c: Float) : Parcelable
@Parcelize
data class Vec4(val a: Float, val b: Float, val c: Float, val w: Float) : Parcelable
```
Then the list of Parcelables can be easily marshalled to the ByteBuffer:
```kotlin
fun Iterable<Parcelable>.toByteBuffer(): ByteBuffer {
val parcel = Parcel.obtain()
forEach { data ->
data.writeToParcel(parcel, 0)
}
val bytes = parcel.marshall()
parcel.recycle()
return ByteBuffer.wrap(bytes)
}
```
And now we can create our geometry and pass it to the GPU:
```kotlin
val GEOMETRY =
listOf(
Vertex(
position = Vec3(-1.0f, -1.0f, 0.0f),
color = Vec4(1.0f, 1.0f, 1.0f, 1.0f)
),
Vertex(
position = Vec3(-1.0f, 1.0f, 0.0f),
color = Vec4(1.0f, 1.0f, 1.0f, 1.0f)
),
Vertex(
position = Vec3(1.0f, -1.0f, 0.0f),
color = Vec4(1.0f, 1.0f, 1.0f, 1.0f)
)
)
...
gpuDevice.createBufferInit(
GPUBufferDescriptorInit(
usage = BufferUsage.Vertex,
content = GEOMETRY.toByteBuffer(),
)
)
```
See [more](https://github.com/ShashlikMap/webgpu-kt-template/blob/main/app/src/main/java/com/shashlikmap/webgpukt/webgpu/utils/Structs.kt) for details.
### Creating a mapped buffer and generating of vertex attributes
If we need to create a buffer with data that not going to be changed we can use a simple GpuDevice extension.
_This was inspired byt Rust WGPU [device extension](https://github.com/gfx-rs/wgpu/blob/trunk/wgpu/src/util/device.rs)_
```kotlin
fun GPUDevice.createBufferInit(descriptor: GPUBufferDescriptorInit): GPUBuffer {
val buffer = createBuffer(
GPUBufferDescriptor(
size = descriptor.content.paddedSize,
label = descriptor.label,
usage = descriptor.usage,
mappedAtCreation = true
)
)
val mapped = buffer.getMappedRange()
mapped.put(descriptor.content)
buffer.unmap()
return buffer
}
```
with an additional class for the descriptor:
```kotlin
class GPUBufferDescriptorInit
@JvmOverloads constructor(
/** The allowed usages for the buffer (e.g., vertex, uniform, copy_dst). */
@BufferUsage var usage: Int,
/** Contents of a buffer on creation. */
var content: ByteBuffer,
/** The label for the buffer. */
var label: String? = null,
)
```
_Note:_ the size should be properly padded:
```kotlin
val ByteBuffer.paddedSize: Long
get() = padded((remaining()).toLong())
private fun padded(dataSize: Long) = (dataSize + 3) and -ALIGNMENT
```
---
The `GPUVertexBufferLayout` requires us to provide `arrayStride` in bytes and `Array<GPUVertexAttribute>`.
Unfortunatly, androidx.webgpu doesn't have any convenient methods to do that as well as `VertexFormat` doesn't expose its size.
Inspired by [vertex_attr_array](https://github.com/gfx-rs/wgpu/blob/1d520a098d46f8298b7d6eb6c7066983dd29932a/wgpu/src/macros/mod.rs#L45) in Rust WGPU:
First, let's add all sizes:
```kotlin
val @VertexFormat Int.byteSize: Long
get() {
return when (this) {
Uint8, Sint8, Unorm8, Snorm8 -> 1L
Uint8x2, Sint8x2, Unorm8x2, Snorm8x2, Uint16, Sint16, Unorm16, Snorm16, Float16 -> 2L
Uint8x4, Sint8x4, Unorm8x4, Snorm8x4, Uint16x2, Sint16x2, Unorm16x2, Snorm16x2, Float16x2, Float32, Uint32, Sint32, Unorm10_10_10_2, Unorm8x4BGRA -> 4L
Uint16x4, Sint16x4, Unorm16x4, Snorm16x4, Float16x4, Float32x2, Uint32x2, Sint32x2 -> 8L
Float32x3, Uint32x3, Sint32x3 -> 12L
Float32x4, Uint32x4, Sint32x4 -> 16L
else -> throw Exception("Unknown VertexFormat: $this")
}
}
```
Now we can calculate the total byte size and generate attributes for any list of `VertexFormat`:
```kotlin
val Array<@VertexFormat Int>.byteSize: Long
get() = sumOf { it.byteSize }
/**
* Generates [GPUVertexAttribute] array for the list of [VertexFormat]
*/
val Array<@VertexFormat Int>.gpuVertexAttributes: Array<GPUVertexAttribute>
get() = foldIndexed(arrayListOf<GPUVertexAttribute>()) { index, acc, type ->
val offset = acc.lastOrNull()?.let { attr ->
attr.offset + attr.format.byteSize
} ?: 0L
acc += GPUVertexAttribute(
format = type,
offset = offset,
shaderLocation = index
)
acc
}.toTypedArray()
```
Usage:
```kotlin
val SHADER_STRUCT = arrayOf(Float32x3, Float32x4)
...
val vertexBufferLayout = GPUVertexBufferLayout(
arrayStride = SHADER_STRUCT.byteSize,
stepMode = VertexStepMode.Vertex,
attributes = SHADER_STRUCT.gpuVertexAttributes
)
```