typegpu
Version:
A thin layer between JS and WebGPU/WGSL that improves development experience and allows for faster iteration.
137 lines (136 loc) • 5.72 kB
JavaScript
import { alignmentOf, customAlignmentOf } from "../../data/alignmentOf.js";
import { getCustomLocation, isLooseDecorated, isUnstruct } from "../../data/dataTypes.js";
import { sizeOf } from "../../data/sizeOf.js";
import { isDecorated, isWgslStruct } from "../../data/wgslTypes.js";
import { roundUp } from "../../mathUtils.js";
import { setName } from "../../shared/meta.js";
import { $internal, $soul } from "../../shared/symbols.js";
import { kindToDefaultFormatMap, vertexFormats, } from "../../shared/vertexFormat.js";
export function vertexLayout(schemaForCount, stepMode = 'vertex') {
return new TgpuVertexLayoutImpl(schemaForCount, stepMode);
}
export function isVertexLayout(value) {
return value?.resourceType === 'vertex-layout';
}
// --------------
// Implementation
// --------------
const defaultAttribEntry = Symbol('defaultAttribEntry');
function dataToContainedAttribs(layout, data, offset, customLocationMap, key) {
if (isDecorated(data) || isLooseDecorated(data)) {
const customLocation = getCustomLocation(data);
if (customLocation !== undefined) {
customLocationMap[key ?? defaultAttribEntry] = customLocation;
}
return dataToContainedAttribs(layout, data.inner, roundUp(offset, customAlignmentOf(data)), customLocationMap);
}
if (isWgslStruct(data)) {
let memberOffset = offset;
const propTypes = data.propTypes;
return Object.fromEntries(Object.entries(propTypes).map(([key, value]) => {
memberOffset = roundUp(memberOffset, alignmentOf(value));
const attrib = [
key,
dataToContainedAttribs(layout, value, memberOffset, customLocationMap, key),
];
memberOffset += sizeOf(value);
return attrib;
}));
}
if (isUnstruct(data)) {
let memberOffset = offset;
const propTypes = data.propTypes;
return Object.fromEntries(Object.entries(propTypes).map(([key, value]) => {
memberOffset = roundUp(memberOffset, customAlignmentOf(value));
const attrib = [
key,
dataToContainedAttribs(layout, value, memberOffset, customLocationMap, key),
];
memberOffset += sizeOf(value);
return attrib;
}));
}
if ('type' in data && typeof data.type === 'string') {
if (vertexFormats.includes(data.type)) {
return {
_layout: layout, // hidden property, used to determine which buffers to apply when executing the pipeline
format: data.type,
offset,
// oxlint-disable-next-line typescript/no-explicit-any -- too many type shenanigans
};
}
const format = kindToDefaultFormatMap[data.type];
if (format) {
return {
_layout: layout, // hidden property, used to determine which buffers to apply when executing the pipeline
format,
offset,
// oxlint-disable-next-line typescript/no-explicit-any -- too many type shenanigans
};
}
}
throw new Error(`Unsupported data used in vertex layout: ${String(data)}`);
}
class TgpuVertexLayoutImpl {
[$internal] = true;
[$soul];
resourceType = 'vertex-layout';
stride;
attrib;
schemaForCount;
#customLocationMap = {};
constructor(schemaForCount, stepMode) {
this.schemaForCount = schemaForCount;
// `0` signals that the data-type is runtime-sized, and should not be used to create buffers.
const arraySchema = schemaForCount(0);
this[$soul] = {
type: 'vertex-layout',
schema: arraySchema,
stepMode,
label: undefined,
};
this.stride = roundUp(sizeOf(arraySchema.elementType), alignmentOf(arraySchema));
this.attrib = dataToContainedAttribs(this, arraySchema.elementType, 0, this.#customLocationMap);
}
get stepMode() {
return this[$soul].stepMode;
}
get vertexLayout() {
// If defaultAttribEntry is in the custom location map,
// it means that the vertex layout is based on a single attribute
if (this.#customLocationMap[defaultAttribEntry] !== undefined) {
if (typeof this.attrib.format !== 'string' || typeof this.attrib.offset !== 'number') {
throw new Error('Single attribute vertex layouts must have a format and offset.');
}
return {
arrayStride: this.stride,
stepMode: this.stepMode,
attributes: [
{
format: this.attrib.format,
offset: this.attrib.offset,
shaderLocation: this.#customLocationMap[defaultAttribEntry],
},
],
};
}
// check if all attributes have custom locations
const allAttributesHaveCustomLocations = Object.keys(this.attrib).every((key) => this.#customLocationMap[key] !== undefined);
if (!allAttributesHaveCustomLocations) {
throw new Error('All attributes must have custom locations in order to unwrap a vertex layout.');
}
return {
arrayStride: this.stride,
stepMode: this.stepMode,
attributes: Object.entries(this.attrib).map(([key, attrib]) => ({
format: attrib.format,
offset: attrib.offset,
shaderLocation: this.#customLocationMap[key],
})),
};
}
$name(label) {
setName(this, label);
return this;
}
}