typegpu
Version:
A thin layer between JS and WebGPU/WGSL that improves development experience and allows for faster iteration.
226 lines (225 loc) • 9.61 kB
JavaScript
import { MissingBindGroupsError, MissingVertexBuffersError } from "../../errors.js";
import { $internal } from "../../shared/symbols.js";
import { logger } from "../../tgpuLogger.js";
import { isBindGroup, } from "../../tgpuBindGroupLayout.js";
import { logDataFromGPU } from "../../tgsl/consoleLog/deserializers.js";
import { isBuffer } from "../../types.js";
import { queueTimestampResolve } from "./timeable.js";
export class RenderDrawState {
bindGroups = new Map();
vertexBuffers = new Map();
currentPipeline;
indexBuffer;
stencilReference;
/** What the raw pass holds, starting at the WebGPU default; survives executeBundles */
appliedStencilReference = 0;
version = 0;
/** Raw access via `root.unwrap(pass)` can mutate state invisibly, disabling deduplication */
rawAccessed = false;
}
export class ComputeDrawState {
bindGroups = new Map();
currentPipeline;
version = 0;
rawAccessed = false;
}
export function recordBindGroup(state, first, bindGroup) {
if (isBindGroup(first)) {
state.bindGroups.set(first.layout, first);
}
else {
state.bindGroups.set(first, bindGroup);
}
state.version++;
}
/** Writes the pipeline and its bound resources into pass state; later set* calls overwrite them */
export function stampRenderPipeline(state, pipeline) {
const { priors } = pipeline[$internal];
state.currentPipeline = pipeline;
if (priors.bindGroupLayoutMap) {
for (const [layout, group] of priors.bindGroupLayoutMap) {
state.bindGroups.set(layout, group);
}
}
if (priors.vertexLayoutMap) {
for (const [layout, buffer] of priors.vertexLayoutMap) {
state.vertexBuffers.set(layout, { buffer, offset: undefined, size: undefined });
}
}
if (priors.indexBuffer) {
state.indexBuffer = priors.indexBuffer;
}
if (priors.stencilReference !== undefined) {
state.stencilReference = priors.stencilReference;
}
state.version++;
}
/** The compute counterpart of {@link stampRenderPipeline} */
export function stampComputePipeline(state, pipeline) {
const { priors } = pipeline[$internal];
state.currentPipeline = pipeline;
if (priors.bindGroupLayoutMap) {
for (const [layout, group] of priors.bindGroupLayoutMap) {
state.bindGroups.set(layout, group);
}
}
state.version++;
}
function applyIndexBuffer(encoder, root, entry) {
const { buffer, indexFormat, offsetBytes, sizeBytes } = entry;
if (isBuffer(buffer)) {
encoder.setIndexBuffer(root.unwrap(buffer), indexFormat, offsetBytes, sizeBytes);
}
else {
encoder.setIndexBuffer(buffer, indexFormat, offsetBytes, sizeBytes);
}
}
function applyBindGroups(encoder, root, usedBindGroupLayouts, catchall, resolveBindGroup) {
const missingBindGroups = new Set(usedBindGroupLayouts);
usedBindGroupLayouts.forEach((layout, idx) => {
if (catchall && idx === catchall[0]) {
encoder.setBindGroup(idx, root.unwrap(catchall[1]));
missingBindGroups.delete(layout);
}
else {
const bindGroup = resolveBindGroup(layout);
if (bindGroup !== undefined) {
missingBindGroups.delete(layout);
if (isBindGroup(bindGroup)) {
encoder.setBindGroup(idx, root.unwrap(bindGroup));
}
else {
encoder.setBindGroup(idx, bindGroup);
}
}
}
});
if (missingBindGroups.size > 0) {
throw new MissingBindGroupsError(missingBindGroups);
}
}
function applyVertexBuffers(encoder, root, usedVertexLayouts, resolveVertexBuffer) {
const missingVertexLayouts = new Set();
usedVertexLayouts.forEach((vertexLayout, idx) => {
const entry = resolveVertexBuffer(vertexLayout);
if (!entry || !entry.buffer) {
missingVertexLayouts.add(vertexLayout);
}
else if (isBuffer(entry.buffer)) {
encoder.setVertexBuffer(idx, root.unwrap(entry.buffer), entry.offset, entry.size);
}
else {
encoder.setVertexBuffer(idx, entry.buffer, entry.offset, entry.size);
}
});
if (missingVertexLayouts.size > 0) {
throw new MissingVertexBuffersError(missingVertexLayouts);
}
}
function applyRenderPipelineState(encoder, root, pipeline, passState) {
const memo = pipeline[$internal].core.unwrap();
encoder.setPipeline(memo.pipeline);
applyBindGroups(encoder, root, memo.usedBindGroupLayouts, memo.catchall, (layout) => passState.bindGroups.get(layout));
applyVertexBuffers(encoder, root, memo.usedVertexLayouts, (vertexLayout) => passState.vertexBuffers.get(vertexLayout));
if (passState.indexBuffer !== undefined) {
applyIndexBuffer(encoder, root, passState.indexBuffer);
}
if (typeof encoder.setStencilReference === 'function' &&
passState.stencilReference !== undefined) {
if (passState.rawAccessed || passState.stencilReference !== passState.appliedStencilReference) {
encoder.setStencilReference(passState.stencilReference);
passState.appliedStencilReference = passState.stencilReference;
}
}
}
function applyComputePipelineState(encoder, root, pipeline, passState) {
const memo = pipeline[$internal].core.unwrap();
encoder.setPipeline(memo.pipeline);
applyBindGroups(encoder, root, memo.usedBindGroupLayouts, memo.catchall, (layout) => passState.bindGroups.get(layout));
}
export function requireIndexBuffer(indexBuffer) {
if (!indexBuffer) {
throw new Error('No index buffer is set. Call pipeline.withIndexBuffer or pass.setIndexBuffer before drawing indexed geometry.');
}
}
function warnAboutUnreachableSubmission(core, what) {
logger.warnOnce('suspicious', core, what, `${what} is ignored when recording into a raw GPUCommandEncoder, since there is no submission to report after. Use root['~unstable'].createCommandEncoder() instead.`);
}
/** Returns false when there is no encoder to defer the read to, meaning the output is lost */
function queueLogDrain(encoder, logResources) {
if (!encoder || encoder[$internal].adopted) {
return false;
}
encoder[$internal].afterSubmit.set(logResources, () => logDataFromGPU(logResources));
return true;
}
export function finalizeOwnEncoder(encoder, core, logResources, priors) {
if (logResources && !queueLogDrain(encoder, logResources)) {
warnAboutUnreachableSubmission(core, 'Shader console.log output');
}
if (priors.performanceCallback && !queueTimestampResolve(encoder, priors)) {
warnAboutUnreachableSubmission(core, 'The performance callback');
}
if (priors.encoder === undefined) {
encoder.submit();
}
}
const PassKindWording = {
render: {
into: 'drawing into a render pass',
intoRaw: 'drawing into a raw render pass',
begin: 'beginRenderPass',
},
compute: {
into: 'dispatching into a compute pass',
intoRaw: 'dispatching into a raw compute pass',
begin: 'beginComputePass',
},
};
function reportIgnoredPriors(core, owner, hasTimestampWrites, logResources, passKind, hasAttachments = false) {
const wording = PassKindWording[passKind];
if (hasAttachments) {
logger.warnOnce('suspicious', core, 'attachments', `Pipeline-level attachments are ignored when ${wording.into}. Pass \`colorAttachments\` and \`depthStencilAttachment\` to encoder.${wording.begin} instead.`);
}
if (hasTimestampWrites) {
logger.warnOnce('suspicious', core, 'timestampWrites', `Pipeline-level timestamp writes are ignored when ${wording.into}. Pass \`timestampWrites\` to encoder.${wording.begin} instead.`);
}
if (logResources && !queueLogDrain(owner, logResources)) {
logger.warnOnce('suspicious', core, 'logs', `Shader console.log output is ignored when ${wording.intoRaw} encoder, since there is no submission to read it back after.`);
}
}
export function emitRenderDraw(root, passInternals, pipeline, usesIndexBuffer, emit, ownsPass = false) {
const { state, rawPass } = passInternals;
const { core, priors } = pipeline[$internal];
if (state.currentPipeline !== pipeline) {
stampRenderPipeline(state, pipeline);
}
if (usesIndexBuffer) {
requireIndexBuffer(state.indexBuffer);
}
const memo = core.unwrap();
if (!ownsPass) {
reportIgnoredPriors(core, passInternals.owner, !!priors.timestampWrites, memo.logResources, 'render', !!priors.colorAttachment || !!priors.depthStencilAttachment);
}
if (state.rawAccessed || passInternals.appliedVersion !== state.version) {
applyRenderPipelineState(rawPass, root, pipeline, state);
passInternals.appliedVersion = state.version;
}
emit(rawPass);
}
export function emitComputeDispatch(root, passInternals, pipeline, emit, ownsPass = false) {
const { state, rawPass } = passInternals;
const { core, priors } = pipeline[$internal];
if (state.currentPipeline !== pipeline) {
stampComputePipeline(state, pipeline);
}
const memo = core.unwrap();
if (!ownsPass) {
reportIgnoredPriors(core, passInternals.owner, !!priors.timestampWrites, memo.logResources, 'compute');
}
if (state.rawAccessed || passInternals.appliedVersion !== state.version) {
applyComputePipelineState(rawPass, root, pipeline, state);
passInternals.appliedVersion = state.version;
}
emit(rawPass);
}