src/webgpu/api/operation/memory_sync/operation_context_helper.ts (278 lines of code) (raw):
import { assert, unreachable } from '../../../../common/util/util.js';
import { EncodableTextureFormat } from '../../../format_info.js';
import { GPUTest } from '../../../gpu_test.js';
/**
* Boundary between the first operation, and the second operation.
*/
export const kOperationBoundaries = [
'queue-op', // Operations are performed in different queue operations (submit, writeTexture).
'command-buffer', // Operations are in different command buffers.
'pass', // Operations are in different passes.
'execute-bundles', // Operations are in different executeBundles(...) calls
'render-bundle', // Operations are in different render bundles.
'dispatch', // Operations are in different dispatches.
'draw', // Operations are in different draws.
] as const;
export type OperationBoundary = (typeof kOperationBoundaries)[number];
/**
* Context a particular operation is permitted in.
* These contexts should be sorted such that the first is the most top-level
* context, and the last is most nested (inside a render bundle, in a render pass, ...).
*/
export const kOperationContexts = [
'queue', // Operation occurs on the GPUQueue object
'command-encoder', // Operation may be encoded in a GPUCommandEncoder.
'compute-pass-encoder', // Operation may be encoded in a GPUComputePassEncoder.
'render-pass-encoder', // Operation may be encoded in a GPURenderPassEncoder.
'render-bundle-encoder', // Operation may be encoded in a GPURenderBundleEncoder.
] as const;
export type OperationContext = (typeof kOperationContexts)[number];
interface BoundaryInfo {
readonly contexts: [OperationContext, OperationContext][];
// Add fields as needed
}
function combineContexts(
as: readonly OperationContext[],
bs: readonly OperationContext[]
): [OperationContext, OperationContext][] {
const result: [OperationContext, OperationContext][] = [];
for (const a of as) {
for (const b of bs) {
result.push([a, b]);
}
}
return result;
}
const queueContexts = combineContexts(kOperationContexts, kOperationContexts);
const commandBufferContexts = combineContexts(
kOperationContexts.filter(c => c !== 'queue'),
kOperationContexts.filter(c => c !== 'queue')
);
/**
* Mapping of OperationBoundary => to a set of OperationContext pairs.
* The boundary is capable of separating operations in those two contexts.
*/
export const kBoundaryInfo: {
readonly [k in OperationBoundary]: BoundaryInfo;
} = {
'queue-op': {
contexts: queueContexts,
},
'command-buffer': {
contexts: commandBufferContexts,
},
pass: {
contexts: [
['compute-pass-encoder', 'compute-pass-encoder'],
['compute-pass-encoder', 'render-pass-encoder'],
['render-pass-encoder', 'compute-pass-encoder'],
['render-pass-encoder', 'render-pass-encoder'],
['render-bundle-encoder', 'render-pass-encoder'],
['render-pass-encoder', 'render-bundle-encoder'],
['render-bundle-encoder', 'render-bundle-encoder'],
],
},
'execute-bundles': {
contexts: [['render-bundle-encoder', 'render-bundle-encoder']],
},
'render-bundle': {
contexts: [
['render-bundle-encoder', 'render-pass-encoder'],
['render-pass-encoder', 'render-bundle-encoder'],
['render-bundle-encoder', 'render-bundle-encoder'],
],
},
dispatch: {
contexts: [['compute-pass-encoder', 'compute-pass-encoder']],
},
draw: {
contexts: [
['render-pass-encoder', 'render-pass-encoder'],
['render-bundle-encoder', 'render-pass-encoder'],
['render-pass-encoder', 'render-bundle-encoder'],
],
},
};
export class OperationContextHelper {
// We start at the queue context which is top-level.
protected currentContext: OperationContext = 'queue';
// Set based on the current context.
queue: GPUQueue;
commandEncoder?: GPUCommandEncoder;
computePassEncoder?: GPUComputePassEncoder;
renderPassEncoder?: GPURenderPassEncoder;
renderBundleEncoder?: GPURenderBundleEncoder;
protected t: GPUTest;
protected device: GPUDevice;
protected commandBuffers: GPUCommandBuffer[] = [];
protected renderBundles: GPURenderBundle[] = [];
public readonly kTextureSize = [4, 4] as const;
public readonly kTextureFormat: EncodableTextureFormat = 'rgba8unorm';
constructor(t: GPUTest) {
this.t = t;
this.device = t.device;
this.queue = t.device.queue;
}
// Ensure that all encoded commands are finished and submitted.
ensureSubmit() {
this.ensureContext('queue');
this.flushCommandBuffers();
}
private popContext(): GPURenderBundle | GPUCommandBuffer | null {
switch (this.currentContext) {
case 'queue':
unreachable();
break;
case 'command-encoder': {
assert(this.commandEncoder !== undefined);
const commandBuffer = this.commandEncoder.finish();
this.commandEncoder = undefined;
this.currentContext = 'queue';
return commandBuffer;
}
case 'compute-pass-encoder':
assert(this.computePassEncoder !== undefined);
this.computePassEncoder.end();
this.computePassEncoder = undefined;
this.currentContext = 'command-encoder';
break;
case 'render-pass-encoder':
assert(this.renderPassEncoder !== undefined);
this.renderPassEncoder.end();
this.renderPassEncoder = undefined;
this.currentContext = 'command-encoder';
break;
case 'render-bundle-encoder': {
assert(this.renderBundleEncoder !== undefined);
const renderBundle = this.renderBundleEncoder.finish();
this.renderBundleEncoder = undefined;
this.currentContext = 'render-pass-encoder';
return renderBundle;
}
}
return null;
}
private makeDummyAttachment(): GPURenderPassColorAttachment {
const texture = this.t.createTextureTracked({
format: this.kTextureFormat,
size: this.kTextureSize,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
return {
view: texture.createView(),
loadOp: 'load',
storeOp: 'store',
};
}
ensureContext(context: OperationContext) {
// Find the common ancestor. So we can transition from currentContext -> context.
const ancestorContext =
kOperationContexts[
Math.min(
kOperationContexts.indexOf(context),
kOperationContexts.indexOf(this.currentContext)
)
];
// Pop the context until we're at the common ancestor.
while (this.currentContext !== ancestorContext) {
// About to pop the render pass encoder. Execute any outstanding render bundles.
if (this.currentContext === 'render-pass-encoder') {
this.flushRenderBundles();
}
const result = this.popContext();
if (result) {
if (result instanceof GPURenderBundle) {
this.renderBundles.push(result);
} else {
this.commandBuffers.push(result);
}
}
}
if (this.currentContext === context) {
return;
}
switch (context) {
case 'queue':
unreachable();
break;
case 'command-encoder':
assert(this.currentContext === 'queue');
this.commandEncoder = this.device.createCommandEncoder();
break;
case 'compute-pass-encoder':
switch (this.currentContext) {
case 'queue':
this.commandEncoder = this.device.createCommandEncoder();
// fallthrough
case 'command-encoder':
assert(this.commandEncoder !== undefined);
this.computePassEncoder = this.commandEncoder.beginComputePass();
break;
case 'compute-pass-encoder':
case 'render-bundle-encoder':
case 'render-pass-encoder':
unreachable();
}
break;
case 'render-pass-encoder':
switch (this.currentContext) {
case 'queue':
this.commandEncoder = this.device.createCommandEncoder();
// fallthrough
case 'command-encoder':
assert(this.commandEncoder !== undefined);
this.renderPassEncoder = this.commandEncoder.beginRenderPass({
colorAttachments: [this.makeDummyAttachment()],
});
break;
case 'render-pass-encoder':
case 'render-bundle-encoder':
case 'compute-pass-encoder':
unreachable();
}
break;
case 'render-bundle-encoder':
switch (this.currentContext) {
case 'queue':
this.commandEncoder = this.device.createCommandEncoder();
// fallthrough
case 'command-encoder':
assert(this.commandEncoder !== undefined);
this.renderPassEncoder = this.commandEncoder.beginRenderPass({
colorAttachments: [this.makeDummyAttachment()],
});
// fallthrough
case 'render-pass-encoder':
this.renderBundleEncoder = this.device.createRenderBundleEncoder({
colorFormats: [this.kTextureFormat],
});
break;
case 'render-bundle-encoder':
case 'compute-pass-encoder':
unreachable();
}
break;
}
this.currentContext = context;
}
private flushRenderBundles() {
assert(this.renderPassEncoder !== undefined);
if (this.renderBundles.length) {
this.renderPassEncoder.executeBundles(this.renderBundles);
this.renderBundles = [];
}
}
private flushCommandBuffers() {
if (this.commandBuffers.length) {
this.queue.submit(this.commandBuffers);
this.commandBuffers = [];
}
}
ensureBoundary(boundary: OperationBoundary) {
switch (boundary) {
case 'command-buffer':
this.ensureContext('queue');
break;
case 'queue-op':
this.ensureContext('queue');
// Submit any GPUCommandBuffers so the next one is in a separate submit.
this.flushCommandBuffers();
break;
case 'dispatch':
// Nothing to do to separate dispatches.
assert(this.currentContext === 'compute-pass-encoder');
break;
case 'draw':
// Nothing to do to separate draws.
assert(
this.currentContext === 'render-pass-encoder' ||
this.currentContext === 'render-bundle-encoder'
);
break;
case 'pass':
this.ensureContext('command-encoder');
break;
case 'render-bundle':
this.ensureContext('render-pass-encoder');
break;
case 'execute-bundles':
this.ensureContext('render-pass-encoder');
// Execute any GPURenderBundles so the next one is in a separate executeBundles.
this.flushRenderBundles();
break;
}
}
}