webgpu: external-submission
This is the complete, compile-checked package recipe. The caller owns the device and application state.
ts
/**
* Source: Original ZenFG package recipe.
* Demonstrates: An external renderer submission alongside native graph work.
* Flow: Declare external resource access, enqueue external work and execute native consumers.
* The caller owns the device and native inputs; ZenFG owns graph execution.
* Read next: README.md for inputs and related recipes. The Examples adapter
* and recipeHost.ts provide browser setup and snapshot capture.
*/
import { FrameGraph, TextureAccess, type FrameGraphRecording } from '@zenfg/webgpu';
export type ExternalRenderer = (options: {
readonly device: GPUDevice;
readonly color: GPUTextureView;
}) => undefined;
export type ExternalSubmissionRecordOptions = {
readonly recorder: FrameGraphRecording;
readonly backbufferTexture: GPUTexture;
readonly presentPipeline: GPURenderPipeline;
readonly sampler: GPUSampler;
readonly width: number;
readonly height: number;
readonly renderAndSubmit: ExternalRenderer;
};
export type ExternalSubmissionOptions = {
readonly graph: FrameGraph;
readonly context: GPUCanvasContext;
readonly presentPipeline: GPURenderPipeline;
readonly sampler: GPUSampler;
readonly width: number;
readonly height: number;
readonly frameIndex: number;
readonly renderAndSubmit: ExternalRenderer;
};
/** Declares an opaque submission boundary followed by presentation. */
export function recordExternalSubmission(options: ExternalSubmissionRecordOptions): void {
const externalColor = options.recorder.createTexture({
label: 'external-color',
format: 'rgba8unorm',
size: [options.width, options.height],
});
const backbuffer = options.recorder.importSwapchainTexture(
options.backbufferTexture,
{ label: 'backbuffer' },
);
const externalColorWrite = options.recorder.use(
externalColor,
TextureAccess.ColorAttachmentWrite,
{ contents: 'overwrite' },
);
options.recorder.externalSubmission({
label: 'third-party-renderer',
uses: [externalColorWrite],
submit({ device, unwrap }) {
return options.renderAndSubmit({
device,
color: unwrap(externalColorWrite),
});
},
});
const sampledExternalColor = options.recorder.use(externalColor, TextureAccess.Sampled);
options.recorder.render({
label: 'present-external-color',
uses: [sampledExternalColor],
colorAttachments: [{
target: backbuffer,
loadOp: 'clear',
storeOp: 'store',
clearValue: { r: 0, g: 0, b: 0, a: 1 },
}],
encode({ device, pass, unwrap }) {
const bindGroup = device.createBindGroup({
layout: options.presentPipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: options.sampler },
{ binding: 1, resource: unwrap(sampledExternalColor) },
],
});
pass.setPipeline(options.presentPipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
},
});
options.recorder.markPresent(backbuffer);
}
/** Orders an opaque, caller-submitted renderer before a native present pass. */
export function renderExternalSubmission(options: ExternalSubmissionOptions): void {
const recorder = options.graph.beginFrame();
recordExternalSubmission({
recorder,
backbufferTexture: options.context.getCurrentTexture(),
presentPipeline: options.presentPipeline,
sampler: options.sampler,
width: options.width,
height: options.height,
renderAndSubmit: options.renderAndSubmit,
});
recorder.compile().execute({ frameIndex: options.frameIndex });
}