Try   HackMD

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

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.