| /* | |
| * Copyright 2025 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| @file:JvmName("WebGpuUtils") | |
| package androidx.webgpu.helper | |
| import android.view.Surface | |
| import androidx.webgpu.GPUAdapter | |
| import androidx.webgpu.BackendType | |
| import androidx.webgpu.GPUDevice | |
| import androidx.webgpu.GPUDeviceDescriptor | |
| import androidx.webgpu.DeviceLostCallback | |
| import androidx.webgpu.DeviceLostException | |
| import androidx.webgpu.GPUInstance | |
| import androidx.webgpu.GPUInstanceDescriptor | |
| import androidx.webgpu.GPURequestAdapterOptions | |
| import androidx.webgpu.GPUSurface | |
| import androidx.webgpu.GPUSurfaceDescriptor | |
| import androidx.webgpu.GPUSurfaceSourceAndroidNativeWindow | |
| import androidx.webgpu.UncapturedErrorCallback | |
| import androidx.webgpu.GPU.createInstance | |
| import androidx.webgpu.WebGpuRuntimeException | |
| import androidx.webgpu.helper.Util.windowFromSurface | |
| import java.util.concurrent.Executor | |
| import java.util.concurrent.atomic.AtomicBoolean | |
| import kotlinx.coroutines.CoroutineDispatcher | |
| import kotlinx.coroutines.asCoroutineDispatcher | |
| import kotlinx.coroutines.delay | |
| import kotlinx.coroutines.isActive | |
| import kotlinx.coroutines.runBlocking | |
| import kotlinx.coroutines.withContext | |
| import kotlinx.coroutines.currentCoroutineContext | |
| import java.util.concurrent.Executors | |
| import kotlinx.coroutines.ExecutorCoroutineDispatcher | |
| /** | |
| * Single-threaded [CoroutineDispatcher] dedicated to running all WebGPU/Dawn JNI operations. | |
| * | |
| * The native Dawn engine is not thread-safe for concurrent access. To prevent race conditions | |
| * and segmentation faults, all WebGPU operations (including initialization, processEvents polling, | |
| * queue submission, and teardown/cleanup) must be executed sequentially on this dispatcher thread. | |
| */ | |
| private fun createDefaultWebGpuDispatcher(): CoroutineDispatcher { | |
| return Executors.newSingleThreadExecutor { runnable -> | |
| Thread(runnable, "WebGPU-Thread").apply { isDaemon = true } | |
| }.asCoroutineDispatcher() | |
| } | |
| private const val POLLING_DELAY_MS = 16L // 60 FPS polling | |
| /** | |
| * A helper class representing a WebGPU environment, encapsulating the instance, adapter, device, | |
| * and surface. All operations on these components should be executed on the dedicated dispatcher. | |
| */ | |
| public abstract class WebGpu : AutoCloseable { | |
| public abstract val instance: GPUInstance | |
| public abstract val adapter: GPUAdapter | |
| public abstract val device: GPUDevice | |
| public abstract val webgpuSurface: GPUSurface | |
| public abstract val dispatcher: CoroutineDispatcher | |
| protected val isClosed: AtomicBoolean = AtomicBoolean(false) | |
| /** | |
| * Executes the given suspending [block] on the dedicated WebGPU dispatcher. | |
| * | |
| * Use this method to wrap all operations on native WebGPU objects (like creating buffers, | |
| * pipeline creation, or issuing draw/compute calls) to ensure thread-safety. | |
| * | |
| * @param block The block of code to execute. | |
| * @return The result of executing the block. | |
| */ | |
| public suspend fun <T> execute(block: suspend () -> T): T { | |
| return withContext(dispatcher) { | |
| block() | |
| } | |
| } | |
| /** | |
| * Runs a continuous loop on the WebGPU dispatcher to poll for and process WebGPU events. | |
| * This is required for asynchronous operations (e.g., buffer mapping callbacks) to trigger. | |
| */ | |
| public suspend fun processEventsLoop() { | |
| withContext(dispatcher) { | |
| while (currentCoroutineContext().isActive && !isClosed.get()) { | |
| instance.processEvents() | |
| delay(POLLING_DELAY_MS) | |
| } | |
| } | |
| } | |
| } | |
| private val defaultDeviceDescriptor | |
| get() = GPUDeviceDescriptor( | |
| deviceLostCallback = defaultDeviceLostCallback, | |
| deviceLostCallbackExecutor = Executor(Runnable::run), | |
| uncapturedErrorCallback = defaultUncapturedErrorCallback, | |
| uncapturedErrorCallbackExecutor = Executor(Runnable::run) | |
| ) | |
| /** | |
| * Creates a [WebGpu] environment using a newly created default single-threaded dispatcher. | |
| * | |
| * @param surface An optional Android [Surface] to bind to WebGPU. | |
| * @param instanceDescriptor Options for configuring the native WebGPU instance. | |
| * @param requestAdapterOptions Options for selecting the GPU adapter (e.g., backend type). | |
| * @param deviceDescriptor Options for requesting the GPU device. | |
| * @return A configured [WebGpu] instance. | |
| */ | |
| public suspend fun createWebGpu( | |
| surface: Surface? = null, | |
| instanceDescriptor: GPUInstanceDescriptor = GPUInstanceDescriptor(), | |
| requestAdapterOptions: GPURequestAdapterOptions = GPURequestAdapterOptions(), | |
| deviceDescriptor: GPUDeviceDescriptor = defaultDeviceDescriptor | |
| ): WebGpu = createWebGpuInternal( | |
| dispatcher = createDefaultWebGpuDispatcher(), | |
| isDispatcherOwned = true, | |
| surface = surface, | |
| instanceDescriptor = instanceDescriptor, | |
| requestAdapterOptions = requestAdapterOptions, | |
| deviceDescriptor = deviceDescriptor | |
| ) | |
| /** | |
| * Creates a [WebGpu] environment using the provided [dispatcher]. | |
| * | |
| * @param dispatcher A custom [CoroutineDispatcher] to use for executing WebGPU JNI operations. | |
| * Must be single-threaded. | |
| * @param surface An optional Android [Surface] to bind to WebGPU. | |
| * @param instanceDescriptor Options for configuring the native WebGPU instance. | |
| * @param requestAdapterOptions Options for selecting the GPU adapter (e.g., backend type). | |
| * @param deviceDescriptor Options for requesting the GPU device. | |
| * @return A configured [WebGpu] instance. | |
| */ | |
| public suspend fun createWebGpu( | |
| dispatcher: CoroutineDispatcher, | |
| surface: Surface? = null, | |
| instanceDescriptor: GPUInstanceDescriptor = GPUInstanceDescriptor(), | |
| requestAdapterOptions: GPURequestAdapterOptions = GPURequestAdapterOptions(), | |
| deviceDescriptor: GPUDeviceDescriptor = defaultDeviceDescriptor | |
| ): WebGpu = createWebGpuInternal( | |
| dispatcher = dispatcher, | |
| isDispatcherOwned = false, | |
| surface = surface, | |
| instanceDescriptor = instanceDescriptor, | |
| requestAdapterOptions = requestAdapterOptions, | |
| deviceDescriptor = deviceDescriptor | |
| ) | |
| private suspend fun createWebGpuInternal( | |
| dispatcher: CoroutineDispatcher, | |
| isDispatcherOwned: Boolean, | |
| surface: Surface?, | |
| instanceDescriptor: GPUInstanceDescriptor, | |
| requestAdapterOptions: GPURequestAdapterOptions, | |
| deviceDescriptor: GPUDeviceDescriptor, | |
| ): WebGpu = withContext(dispatcher) { | |
| initLibrary() | |
| val instance = createInstance(instanceDescriptor) | |
| val webgpuSurface = | |
| surface?.let { | |
| instance.createSurface( | |
| GPUSurfaceDescriptor( | |
| surfaceSourceAndroidNativeWindow = | |
| GPUSurfaceSourceAndroidNativeWindow(windowFromSurface(it)) | |
| ) | |
| ) | |
| } | |
| val adapter = requestAdapter(instance, requestAdapterOptions) | |
| val device = requestDevice(adapter, deviceDescriptor) | |
| val dispatcherThread = Thread.currentThread() | |
| object : WebGpu() { | |
| override val instance = instance | |
| override val adapter = adapter | |
| override val webgpuSurface | |
| get() = checkNotNull(webgpuSurface) | |
| override val device = device | |
| override val dispatcher = dispatcher | |
| override fun close() { | |
| if (isClosed.getAndSet(true)) return | |
| //device.close() // TODO(b/428866400): Uncomment when fixed. | |
| val cleanup = { | |
| webgpuSurface?.close() | |
| adapter.close() | |
| instance.close() | |
| if (isDispatcherOwned) { | |
| (dispatcher as? ExecutorCoroutineDispatcher)?.close() | |
| } | |
| } | |
| if (Thread.currentThread() == dispatcherThread) { | |
| cleanup() | |
| } else { | |
| runBlocking(dispatcher) { | |
| cleanup() | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private suspend fun requestAdapter( | |
| instance: GPUInstance, | |
| options: GPURequestAdapterOptions = GPURequestAdapterOptions(backendType = BackendType.Vulkan), | |
| ): GPUAdapter { | |
| return instance.requestAdapter(options) | |
| } | |
| private suspend inline fun requestDevice( | |
| adapter: GPUAdapter, | |
| deviceDescriptor: GPUDeviceDescriptor, | |
| ): GPUDevice { | |
| if (deviceDescriptor.deviceLostCallback == null) { | |
| deviceDescriptor.deviceLostCallback = defaultDeviceLostCallback | |
| } | |
| if (deviceDescriptor.uncapturedErrorCallback == null) { | |
| deviceDescriptor.uncapturedErrorCallback = defaultUncapturedErrorCallback | |
| } | |
| return adapter.requestDevice(deviceDescriptor) | |
| } | |
| private val defaultUncapturedErrorCallback | |
| get(): UncapturedErrorCallback { | |
| return UncapturedErrorCallback { _, type, message -> | |
| throw WebGpuRuntimeException.create(type, message) | |
| } | |
| } | |
| private val defaultDeviceLostCallback get(): DeviceLostCallback { | |
| return DeviceLostCallback { device, reason, message -> | |
| throw DeviceLostException(device, reason, message) | |
| } | |
| } | |
| /** Initializes the native library. This method should be called before making and WebGPU calls. */ | |
| public fun initLibrary() { | |
| System.loadLibrary("webgpu_c_bundled") | |
| } |