<!-- markdownlint-disable MD013 -->
# Multiple components cannot effectively share a GPUDevice because it is stateful
Issue: <https://github.com/gpuweb/gpuweb/issues/3250>
Proposal written and proposed by Kai, based on in-depth discussion with editors (Brandon/Myles/Kai) and more input from Brandon.
[**Previous drafts**](https://hackmd.io/@webgpu/SkRjVIcjc)
## Premises
- It should be possible for multiple "components" using the same device on the same thread to have different error scope state.
- It's okay if each component can't device.destroy() independently.
- The per-component device should be a lightweight object that can be handled by GC.
- This avoids difficult-to-understand tracking of which resources would get destroyed upon destroying one component's device.
- Error scope state should not be shared across threads by default, as it would be a footgun.
- For implementing native semantics over JS, it is useful for there to be a way for a device's error state to be shared across multiple threads.
## Proposal
(Names are all placeholders.)
- All objects have an `[[errorTarget]]`, which (roughly speaking) has type `GPUDevice?`.
- This means `createView()`, for example, does not have to move.
- The only pre-V1 change to today's API is the addition of `GPURootDevice`. (I have long expected we would want this - I just didn't realize it was a breaking change.)
- There is only one `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.
- When an object is sent to another thread, the error target does not come with it; `[[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.
- When you send it to another thread you have to explicitly choose whether it's going to share its error state tracker or get a new one.
### Pre-V1
Changes:
- ~~Add a `GPURootDevice` subclass of `GPUDevice`. Move `EventTarget` to `GPURootDevice`. Return `GPURootDevice` from `requestDevice()`.~~
- ~~This is done now only because the addition of a new object in the prototype chain is observable. This was planned for multi-threading anyway.~~
- **This is impossible because it requires multiple inheritance (`GPURootDevice : GPUDevice, EventTarget`).**
Editorial changes:
- `GPUObjectBase` gets an internal slot `[[errorTarget]]`, pointing to a "device client" (a thing which can send operations to a device).
- A new object created from any object `this` inherits `this.[[errorTarget]]`.
- All operations send their errors to `(GPUObjectBase)this`, which passes them through `[[errorTarget]]` and forwards uncaptured errors to `uncapturederror`.
Notes:
- There is exactly one error target per device, and not yet any way to override `[[errorTarget]]`. No significant implementation work is needed at this stage.
### Post-V1: Multi-Client
Changes:
- Most objects gain a method `object.withErrorTarget(GPUDevice errorTarget)`, which gives you a new JS object with the same underlying object and the provided `[[errorTarget]]`.
- `GPUDevice` and `GPURootDevice` don't have these.
- Object labels are client-shared state.
- Add `GPUDevice.withNewErrorTracker()` which returns a new `GPUDevice` with a new "device client" with an empty error state tracker.
### Post-V1: Multi-Threading
Changes:
- Make `[[errorTarget]]` nullable.
- Objects without an `[[errorTarget]]` send errors directly to `uncapturederror`.
- Most objects are thread-shareable. When deserialized, they have a null `[[errorTarget]]`. To associate them with an error target, the receiver must call `withErrorTarget()`.
- Object labels are thread-shared state.
- When `GPUDevice` or `GPURootDevice` 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.)
- You can't get a `GPURootDevice` from it (that object is tied to the original thread).
- This affects whether the internal `[[errorTarget]]` points to the same device "client" or a new one.
Note:
- Native-style semantics can be implemented with `withSharedErrorTracker()`. This also eliminates a need in the actual native API to use thread-local storage to track which error scope state to target.
### Post-V1: Multi-Queue
New independent queue objects behave the same way as other objects.