These are drafts. The working proposal can be found here.
Multiple components, for example multiple graph/plot widgets on a page, cannot effectively share a GPUDevice
because it is stateful:
GPUDevice.destroy()
to do so. An application can somewhat work around this by tracking its own resource allocations, which is also more flexible for more complex ownership models (like refcounting), though that only allows freeing Buffer/Texture/QuerySet.This can be useful for some other things as well:
GPUDevice
to a user can separate the user's state from its own.Relevant past work:
*Handle
objects.GPUErrorSink
object.GPUTexture
and possibly everything else also be a client so createView()
doesn't have to move.
GPUErrorSink
which holds the error state and is an (immutable) member of both GPUDevice
and GPUQueue
(and command/bundle encoders).GPUDevice
s and out out of having it destroyed and unmapped by GPUDevice.destroy()
. Keep the per-device per-realm thing.[[errorTarget]]
, which roughly speaking has type GPUDevice?
.
createView()
, for example, does not have to move.GPURootDevice
. (I have long expected we would want this - I just didn't realize it was a breaking change.)
uncapturederror
EventTarget
object for the device (GPURootDevice
) and it's never possible to duplicate or transfer it. This approach is chosen so [[errorTarget]]
can be nullable.[[errorTarget]]
is null
. Errors that happen on this object get sent straight to uncapturederror
. To get them to go to an error target, you have to "clone" the object and specify an errorTarget
to use.GPUDevice
is the error target and can never have a null error target.
Changes:
GPURootDevice
subclass of GPUDevice
. Move EventTarget
to GPURootDevice
. Return GPURootDevice
from requestDevice()
.Editorial changes:
GPUObjectBase
gets an internal slot [[errorTarget]]
, pointing to a device "client".this
inherits this.[[errorTarget]]
.(GPUObjectBase)this
, which passes them through [[errorTarget]]
and forwards uncaptured errors to uncapturederror
.Notes:
[[errorTarget]]
. No significant implementation work is needed at this stage.Changes:
object.withErrorTarget(GPUDevice errorTarget)
, which gives you an instance of the object with the provided [[errorTarget]]
.
GPUDevice
and GPURootDevice
don't have these, as (roughly speaking) they are the error target.GPUDevice.withNewErrorTracker()
which returns a new GPUDevice
with an empty error state tracker.Changes:
[[errorTarget]]
nullable.
[[errorTarget]]
send errors directly to uncapturederror
.[[errorTarget]]
. To associate them with an error target, the receiver must call withErrorTarget()
.
GPURootDevice
is not shareable or transferrable. If you try to send it we send the base GPUDevice
. (TBD how to spec this.)GPUDevice
is sent, you receive a GPUReceivedDevice
. To get a GPUDevice
from it, you must choose either .withNewErrorTracker()
or .withSharedErrorTracker()
. (These can be called multiple times.)
[[errorTarget]]
points to the same device "client" or a new one.Note:
withSharedErrorTracker()
. Also eliminates a need in the actual native API to use thread-local storage to track which error scope state to target.New independent queue objects behave the same way as other objects.
Changes:
GPURootDevice
subclass of GPUDevice
. Move EventTarget
to GPURootDevice
. Return GPURootDevice
from requestDevice()
.Editorial changes:
GPUObjectBase
gets an internal slot [[errorTarget]]
, pointing to a DeviceClient.this
inherits this.[[errorTarget]]
.(GPUObjectBase)this
which passes them through [[errorTarget]]
and forwards uncaptured errors to uncapturederror
.Notes:
[[errorTarget]]
. No significant implementation work is needed at this stage.Changes:
*Handle
version, accessible by object.handle
.
GPUDeviceHandle
but not a GPURootDeviceHandle
.handle.instantiate(GPUDevice errorTarget)
gives you an instance of the object, with the provided [[errorTarget]]
.GPUDeviceHandle.instantiate()
does not take an errorTarget
, as it's creating one.Notes:
requestDevice
can be described as creating a GPUDeviceHandle
and returning GPUDeviceHandle.instantiate()
.Changes:
[[errorTarget]]
nullable.
[[errorTarget]]
send errors directly to uncapturederror
.*Handle
versions are shareable. The receiver must call handle.instantiate()
and may pass a GPUDevice
or null
.GPUDeviceHandle.instantiateWithSharedErrorTracker()
.
[[errorTarget]]
point to the same device "client" instead of a new one.Note:
instantiateWithSharedErrorTracker()
. Also eliminates a need in the actual native API to use thread-local storage to track which error scope state to target.Nothing special compared to other objects.
Changes:
GPUErrorSink
object and move pushErrorScope()
/popErrorScope()
to it.GPUDevice
gets a readonly attribute GPUErrorSink errorSink
.Editorial changes:
GPUObjectBase
gets an internal slot [[errorSink]]
, nullable, pointing to an error sink (handle).requestDevice
creates a new GPUErrorSink
and uses it for the GPUDevice
and its defaultQueue
.this
(a device/texture/pipeline).(GPUObjectBase)this
which passes them through [[errorSink]]
(if any) and forwards uncaptured errors to uncapturederror
.Notes:
GPUErrorSink
to new objects. No significant implementation work is needed at this stage.Changes:
*Handle
version, accessible by object.handle
.handle.instantiate(GPUErrorSink? errorSink)
gives you an instance of the object, with the provided GPUErrorSink
(if any).
uncapturederror
.GPUDeviceHandle.instantiate()
is different from the others:
errorSink
argument is optional instead of nullable.GPUErrorSink
was provided, it creates a new one for the device and its defaultQueue
.GPUDeviceClone
instead, which does not receive uncapturederror
events.Notes:
GPUErrorSink
can now be attached to new objects.GPUErrorSink
is not constructible and new ones can only be gotten from GPUDevice
.requestDevice
can be described as creating a GPUDeviceHandle
and returning GPUDeviceHandle.instantiate()
.Nothing special compared to other objects.
GPUErrorSink
is thread-shareable (and internally synchronized). This never happens implicitly.*Handle
versions are shareable. The receiver must call handle.instantiate()
and may pass a GPUErrorSink
.Note:
GPUErrorSink
object across threads. This eliminates a need in the actual native API to use thread-local storage to track which error scope state to target.In this proposal, we keep things as is except you can have multiple GPUDeviceClient
s on one thread, with their own error scope states, instead of having all GPUDevice
s on one thread magically point to the same one. For multi-threading, one client (and its error scope state) is shared across threads and internally synchronized.
GPUDeviceClient.destroy()
still unmaps all buffers in the same thread, as GPUBuffer
s are not associated with a specific client.
(Native API note: eliminates thread local storage for error scope state.)
Changes:
GPUTexture.createView()
back out to GPUDevice.createTextureView()
so there's a place to send its errors.GPUPipelineBase.getBindGroupLayout()
to GPUDevice
.
GPUBindGroupLayout
that doesn't actually generate any errors upon its creation. This would kind of make sense because it's not "creating" an object (but it is).Notes:
GPUDevice
or GPUQueue
:
finish()
calls can stay on command/bundle encoders. It can be said that an encoder is permanently associated with the GPUDeviceClient
it's created on.mapAsync()
, getMappedRange()
, unmap()
, compilationInfo()
, and buffer/texture/queryset destroy()
would not be able to generate WebGPU errors (they do not, currently).GPUDevice
becomes an "interface" through which you send commands to an underlying device/queue (referred to by a "handle"). For now call this GPUDeviceClient
.
GPUQueue
becomes permanently associated with the specific GPUDeviceClient
that it was created from. For now call this GPUQueueClient
.
(In practice, the names wouldn't change.)
Changes: none
Notes:
GPUQueueClient
, GPUCommandEncoder
, and GPURenderBundleEncoder
are permanently associated with a specific GPUDeviceClient
.label
remains associated with GPUDeviceClient
/GPUQueueClient
, not the respective handles.Allow creating new GPUDeviceClient
s for existing devices.
Changes:
GPUDeviceClient.createNewClient()
to create new clients from a handle.
defaultQueue
of this new GPUDeviceClient
is a new GPUQueueClient
(of the old queue handle) associated with the new GPUDeviceClient
.Notes:
GPUDeviceClient.destroy()
destroys the whole device, not just the client. Just like today, it still unmaps all buffers for that device on the thread where it's called, but not on other threads.
GPUDeviceClient
without destroying the underlying device. It should be a lightweight object that can be handled by GC.Changes: none
Notes:
GPUQueueClient
pointing back to the GPUDeviceClient
it was created from. If you want a queue with its own error state (for some reason) you can just create it off of a fresh GPUDeviceClient
.Changes:
GPUBuffer
, etc.)GPUDeviceClient
shareable across threads (but not transferrable).
GPUDeviceClient
's error state would be shared and internally synchronized.GPUQueueClient
serializable (but not transferrable).
GPUDeviceClient
.uncapturederror
exists for the telemetry use case. Should there be a separate uncapturederror sink for each GPUDeviceClient
, or one common one shared by all? (If there's a common one, an uncaptured error on any client would probably trigger the event on all clients.)
uncapturederror
coming in on multiple threads. However, this is also true of the unhandledrejection
event for unhandled promise rejections, so this is probably fine.In this proposal, we keep things as is except we extract the error state into a separate object.
For multi-threading, we keep the "per-device per-realm" semantics, but only for GPUDeviceClient.destroy()
unmapping buffers, not for error scope state.
Changes:
GPUTexture.createView()
back out to GPUDevice.createTextureView()
so there's a place to send its errors.GPUPipelineBase.getBindGroupLayout()
to GPUDevice
.
GPUBindGroupLayout
that doesn't actually generate any errors upon its creation. This would kind of make sense because it's not "creating" an object (but it is).Notes:
GPUDevice
or GPUQueue
:
finish()
calls can stay on command/bundle encoders. It can be said that an encoder inherits the GPUErrorSink
from the GPUDeviceClient
it's created on.mapAsync()
, getMappedRange()
, unmap()
, compilationInfo()
, and buffer/texture/queryset destroy()
would not be able to generate WebGPU errors (they do not, currently).GPUDevice
and GPUQueue
become "interfaces" through which you send commands to an underlying device/queue. For now call these GPUDeviceClient
and GPUQueueClient
. (In practice, the names wouldn't change.)
Error stuff moves to a new object. For now call this GPUErrorSink
.
Changes:
pushErrorScope
/popErrorScope
, and EventTarget
/onuncapturederror
onto GPUErrorSink
(as pushScope
/popScope
).[[error_sink]]
internal slots to GPUDeviceClient
and GPUQueueClient
. Not yet configurable, and by default a device and its default queue get the same GPUErrorSink
, so semantically nothing changes.GPUDeviceClient.errorSink
readonly attribute.Notes:
label
remains associated with GPUDeviceClient
, not GPUDeviceHandle
.uncapturederror
exists for the telemetry use case. If it's on GPUErrorSink
, applications must register it on every GPUErrorSink
they create. It could go on GPUDeviceHandle
instead, which makes it easier to capture everything for telemetry, but it's also less flexible (can't specifically ignore errors from one user of the device).
uncapturederror
coming in on multiple threads. However, this is also true of the unhandledrejection
event for unhandled promise rejections, so this is probably fine.Allow creating new GPUDeviceClient
s for existing devices.
Add an object to represent the underlying device. For now call this GPUDeviceHandle
.
Changes:
GPUDeviceHandle
as GPUDeviceClient.handle
.GPUErrorSink
.GPUDeviceHandle.createClient()
to create new clients from a handle.errorSink
in requestDevice()
and both createClient()
methods.
GPUErrorSink
is not provided, both create a new one.Notes:
GPUErrorSink
can now be shared by multiple completely unrelated devices.GPUQueueClient
objects are the default queue of some GPUDeviceClient
and therefore inherit their error sinks.GPUDeviceClient.destroy()
destroys the whole device, not just the client. Just like today, it still unmaps all buffers for that device on the thread where it's called, but not on other threads.
GPUDeviceClient
without destroying the underlying device. It should be a lightweight object that can be handled by GC.Changes:
GPUDeviceHandle
sharable. GPUDeviceClient
and GPUErrorSink
are not sharable or transferrable.Notes:
GPUDeviceHandle
can have events registered from any thread. If there's an uncaptured error, fire all of them. (This isn't really any different from registering multiple handlers on one event except that they can't cancel each other.)Now, not all queues are default queues.
Changes:
GPUQueueHandle
, GPUQueueClient.handle
, GPUQueueHandle.createClient()
.Separate the error scope state from the "global" object so that there can be multiple states maintained separately (for each component).
For multithreading, take advantage of this separation as well, so there's no "per-device per-realm" state like we had planned.
(In this proposal, bad names are used intentionally for disambiguation from current concepts, and so we have to replace them later.)
Centralize state, state checks, and ordering on one object. For now call this GPUStatefulDevice
.
Each error-generating call must be clearly associated with a single GPUStatefulDevice
so there is a place for errors to go.
Changes:
GPUTexture.createView()
back out to GPUStatefulDevice.createTextureView()
.Notes:
finish()
calls stay on the encoders. An encoder is permanently associated with one GPUStatefulDevice
.defaultQueue
becomes associated with one GPUStatefulDevice
in particular. For now call this type GPUSharedQueueInterface
.Create a new concept of a group of GPUStatefulDevice
objects which can all share their objects freely. For now call this GPUAdapterConnection
.
Changes:
GPUStatefulDevice.connection
, which is the GPUAdapterConnection
for the GPUStatefulDevice
.Notes:
GPUStatefulDevice.destroy()
destroys the whole group, because it's not really meaningful to destroy just one GPUStatefulDevice
(it doesn't own any resources so there's nothing to clean up).destroy()
, and possibly limits
, features
, could move to GPUAdapterConnection
.GPUStatefulDevice
s (post-V1)Make it possible to get more GPUStatefulDevice
s from the GPUAdapterConnection
.
Required for multi-threading.
Changes:
GPUAdapterConnection.createStatefulDevice()
which gives you a new device in the same group, with the same limits/features, and an empty error scope state. Without multi-queue, it gives you a GPUSharedQueueInterface defaultQueue
which has the same underlying GPUSharedQueue
as the other GPUStatefulDevice
s.GPUAdapterConnection
is serializable (sharable).Notes:
GPUStatefulDevice
is not sharable, because it is stateful. It is also not transferrable, because that would require it to be closeable.Changes
GPUSharedQueue
which represents the actual queue (without an associated GPUStatefulDevice
). You can't do anything with this except create GPUSharedQueueInterface
s.GPUSharedQueueInterface.sharedQueue
pointing to the GPUSharedQueue
(maybe).GPUAdapterConnection.defaultQueue
which is the default GPUSharedQueue
(maybe).GPUAdapterConnection.createStatefulDevice()
, allow configuring the defaultQueue
. Depending on the solution to multi-queue, this may take a GPUQueueDescriptor
, or it may only take a GPUSharedQueue
that the defaultQueue
should point to.GPUAdapterConnection
.GPUStatefulDevice
and GPUQueue
. Multi-queue becomes multi-GPUStatefulDeviceAndQueue
. It would not be possible to submit work from multiple threads to the same queue (probably a dealbreaker).~GPUAdapterConnection
, instead "clone" GPUStatefulDevice
directly. Find a name that makes it clear it won't clone the error scope state, like makeNewStatefulDeviceForAdapterConnection
.GPUStatefulDevice
/GPUAdapterConnection
, have a device and sub-devices. The sub-devices wouldn't have destroy()
. (I think I had a reason not to do this but I forgot it.)GPUStatefulDevice.destroy()
meaningful (destroy only the stuff on that device and not the whole group)? Would require some form of ownership.GPUDevice.destroy()
can unmap everything on the thread. This becomes impossible. Can we preserve that?