Module wgpu_core::track

source ·
Expand description

Resource State and Lifetime Trackers

These structures are responsible for keeping track of resource state, generating barriers where needednd making sure resources are kept alive until the trackers die.

§General Architecture

Tracking is some of the hottest code in the entire codebase, so the trackers are designed to be as cache efficient as possible. They store resource state in flat vectors, storing metadata SOA style, one vector per type of metadata.

A lot of the tracker code is deeply unsafe, using unchecked accesses all over to make performance as good as possible. However, for all unsafe accesses, there is a corresponding debug assert the checks if that access is valid. This helps get bugs caught fast, while still letting users not need to pay for the bounds checks.

In wgpu, each resource ID includes a bitfield holding an index. Indices are allocated and re-used, so they will always be as low as reasonably possible. This allows us to use IDs to index into an array of tracking information.

§Statefulness

There are two main types of trackers, stateful and stateless.

Stateful trackers are for buffers and textures. They both have resource state attached to them which needs to be used to generate automatic synchronization. Because of the different requirements of buffers and textures, they have two separate tracking structures.

Stateless trackers only store metadata and own the given resource.

§Use Case

Within each type of tracker, the trackers are further split into 3 different use cases, Bind Group, Usage Scopend a full Tracker.

Bind Group trackers are just a list of different resources, their refcount, and how they are used. Textures are used via a selector and a usage type. Buffers by just a usage type. Stateless resources don’t have a usage type.

Usage Scope trackers are only for stateful resources. These trackers represent a single UsageScope in the spec. When a use is added to a usage scope, it is merged with all other uses of that resource in that scope. If there is a usage conflict, merging will fail and an error will be reported.

Full trackers represent a before and after state of a resource. These are used for tracking on the device and on command buffers. The before state represents the state the resource is first used as in the command buffer, the after state is the state the command buffer leaves the resource in. These double ended buffers can then be used to generate the needed transitions between command buffers.

§Dense Datastructure with Sparse Data

This tracking system is based on having completely dense data, but trackers do not always contain every resource. Some resources (or even most resources) go unused in any given command buffer. So to help speed up the process of iterating through possibly thousands of resources, we use a bit vector to represent if a resource is in the buffer or not. This allows us extremely efficient memory utilizations well as being able to bail out of whole blocks of 32-64 resources with a single usize comparison with zero. In practice this means that merging partially resident buffers is extremely quick.

The main advantage of this dense datastructure is that we can do merging of trackers in an extremely efficient fashion that results in us doing linear scans down a couple of buffers. CPUs and their caches absolutely eat this up.

§Stateful Resource Operations

All operations on stateful trackers boil down to one of four operations:

  • insert(tracker, new_state) adds a resource with a given state to the tracker for the first time.
  • merge(tracker, new_state) merges this new state with the previous state, checking for usage conflicts.
  • barrier(tracker, new_state) compares the given state to the existing state and generates the needed barriers.
  • update(tracker, new_state) takes the given new state and overrides the old state.

This allows us to compose the operations to form the various kinds of tracker merges that need to happen in the codebase. For each resource in the given merger, the following operation applies:

UsageScope <- Resource = insert(scope, usage) OR merge(scope, usage)
UsageScope <- UsageScope = insert(scope, scope) OR merge(scope, scope)
CommandBuffer <- UsageScope = insert(buffer.start, buffer.end, scope)
                              OR barrier(buffer.end, scope) + update(buffer.end, scope)
Device <- CommandBuffer = insert(device.start, device.end, buffer.start, buffer.end)
                          OR barrier(device.end, buffer.start) + update(device.end, buffer.end)

Modules§

Structs§

  • All the usages that a bind group contains. The uses are not deduplicated in any way and may include conflicting uses. This is fully compliant by the WebGPU spec.
  • A tracker used by Device.
  • Pretty print helper that shows helpful descriptions of a conflicting usage.
  • A structure containing all the information about a particular resource transition. User code should be able to generate a pipeline barrier based on the contents.
  • This is a render bundle specific usage scope. It includes stateless resources that are not normally included in a usage scope, but are used by render bundles and need to be owned by the render bundles.
  • See TrackerIndexAllocator.
  • Tracker 🔒
    A full double sided tracker used by CommandBuffers.
  • wgpu-core internally use some array-like storage for tracking resources. To that end, there needs to be a uniquely assigned index for each live resource of a certain type. This index is separate from the resource ID for various reasons:
  • UsageScope 🔒
    A usage scope tracker. Only needs to store stateful resources as stateless resources cannot possibly have a usage conflict.

Enums§

Traits§

  • The uses that a resource or subresource can be in.

Functions§

  • Returns true if the given states violates the usage scope rule of any(inclusive) XOR one(exclusive)
  • Returns true if the transition from one state to another does not require a barrier.

Type Aliases§

  • A pool for storing the memory used by UsageScopes. We take and store this memory when the scope is dropped to avoid reallocating. The memory required only grows and allocation cost is significant when a large number of resources have been used.