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 ) ```