gpu-curtains
Version:
gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.
294 lines (293 loc) • 10.9 kB
JavaScript
import { throwWarning } from "../../../utils/utils.mjs";
import { getBufferLayout } from "../utils.mjs";
/**
* Used to handle each {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | buffer binding array} view and data layout alignment.
* Compute the exact alignment offsets needed to fill an {@link ArrayBuffer} that will be sent to a {@link GPUBuffer}, based on an input type and value.
* Also update the view array at the correct offset.
*
* So all our struct need to be packed into our arrayBuffer using a precise layout.
* They will be stored in rows, each row made of 4 slots and each slots made of 4 bytes. Depending on the binding element type, its row and slot may vary and we may have to insert empty padded values.
* All in all it looks like that:<br>
* <pre>
* slot 0 slot 1 slot 2 slot 3
* row 0 | _ _ _ _ | _ _ _ _ | _ _ _ _ | _ _ _ _ |
* row 1 | _ _ _ _ | _ _ _ _ | _ _ _ _ | _ _ _ _ |
* row 2 | _ _ _ _ | _ _ _ _ | _ _ _ _ | _ _ _ _ |
* </pre>
* see https://webgpufundamentals.org/webgpu/lessons/resources/wgsl-offset-computer.html
*/
var BufferElement = class BufferElement {
/**
* BufferElement constructor
* @param parameters - {@link BufferElementParams | parameters} used to create our {@link BufferElement}.
*/
constructor({ name, key, type = "f32" }) {
this.name = name;
this.key = key;
this.type = type;
this.baseType = BufferElement.getBaseType(this.type);
this.bufferLayout = getBufferLayout(this.baseType);
this.alignment = {
start: {
row: 0,
byte: 0
},
end: {
row: 0,
byte: 0
}
};
this.setValue = null;
}
/**
* Get the {@link BufferElement} {@link WGSLVariableType | WGSL type}.
* @param type - Original type passed.
* @returns - The {@link BufferElement} {@link WGSLVariableType | WGSL type}.
*/
static getType(type) {
return type.replace("array", "").replace("<", "").replace(">", "");
}
/**
* Get the {@link BufferElement} {@link WGSLBaseVariableType | WGSL base type}.
* @param type - Original type passed.
* @returns - The {@link BufferElement} {@link WGSLBaseVariableType | WGSL base type}.
*/
static getBaseType(type) {
return BufferElement.getType(type.replace("atomic", "").replace("array", "").replaceAll("<", "").replaceAll(">", ""));
}
/**
* Get the total number of rows used by this {@link BufferElement}.
* @readonly
*/
get rowCount() {
return this.alignment.end.row - this.alignment.start.row + 1;
}
/**
* Get the total number of bytes used by this {@link BufferElement} based on {@link BufferElementAlignment | alignment} start and end offsets.
* @readonly
*/
get byteCount() {
return Math.abs(this.endOffset - this.startOffset) + 1;
}
/**
* Get the total number of bytes used by this {@link BufferElement}, including final padding.
* @readonly
*/
get paddedByteCount() {
return (this.alignment.end.row + 1) * 16;
}
/**
* Get the offset (i.e. byte index) at which our {@link BufferElement} starts.
* @readonly
*/
get startOffset() {
return this.getByteCountAtPosition(this.alignment.start);
}
/**
* Get the array offset (i.e. array index) at which our {@link BufferElement} starts.
* @readonly
*/
get startOffsetToIndex() {
return this.startOffset / 4;
}
/**
* Get the offset (i.e. byte index) at which our {@link BufferElement} ends.
* @readonly
*/
get endOffset() {
return this.getByteCountAtPosition(this.alignment.end);
}
/**
* Get the array offset (i.e. array index) at which our {@link BufferElement} ends.
* @readonly
*/
get endOffsetToIndex() {
return Math.floor(this.endOffset / 4);
}
/**
* Get the position at given offset (i.e. byte index).
* @param offset - Byte index to use.
*/
getPositionAtOffset(offset = 0) {
return {
row: Math.floor(offset / 16),
byte: offset % 16
};
}
/**
* Get the number of bytes at a given {@link BufferElementAlignmentPosition | position}.
* @param position - {@link BufferElementAlignmentPosition | Position} from which to count.
* @returns - Byte count at the given {@link BufferElementAlignmentPosition | position}.
*/
getByteCountAtPosition(position = {
row: 0,
byte: 0
}) {
return position.row * 16 + position.byte;
}
/**
* Check that a {@link BufferElementAlignmentPosition#byte | byte position} does not overflow its max value (16).
* @param position - {@link BufferElementAlignmentPosition | Position}.
* @returns - Updated {@link BufferElementAlignmentPosition | position}.
*/
applyOverflowToPosition(position = {
row: 0,
byte: 0
}) {
if (position.byte > 15) {
const overflow = position.byte % 16;
position.row += Math.floor(position.byte / 16);
position.byte = overflow;
}
return position;
}
/**
* Get the number of bytes between two {@link BufferElementAlignmentPosition | positions}.
* @param p1 - First {@link BufferElementAlignmentPosition | position}.
* @param p2 - Second {@link BufferElementAlignmentPosition | position}.
* @returns - Number of bytes.
*/
getByteCountBetweenPositions(p1 = {
row: 0,
byte: 0
}, p2 = {
row: 0,
byte: 0
}) {
return Math.abs(this.getByteCountAtPosition(p2) - this.getByteCountAtPosition(p1));
}
/**
* Compute the right alignment (i.e. start and end rows and bytes) given the size and align properties and the next available {@link BufferElementAlignmentPosition | position}.
* @param nextPositionAvailable - next {@link BufferElementAlignmentPosition | position} at which we should insert this element.
* @returns - Computed {@link BufferElementAlignment | alignment}.
*/
getElementAlignment(nextPositionAvailable = {
row: 0,
byte: 0
}) {
const alignment = {
start: nextPositionAvailable,
end: nextPositionAvailable
};
const { size, align } = this.bufferLayout;
if (nextPositionAvailable.byte % align !== 0) nextPositionAvailable.byte += nextPositionAvailable.byte % align;
if (size <= 16 && nextPositionAvailable.byte + size > 16) {
nextPositionAvailable.row += 1;
nextPositionAvailable.byte = 0;
} else if (size > 16 && (nextPositionAvailable.byte > 16 || nextPositionAvailable.byte > 0)) {
nextPositionAvailable.row += 1;
nextPositionAvailable.byte = 0;
}
alignment.end = {
row: nextPositionAvailable.row + Math.ceil(size / 16) - 1,
byte: nextPositionAvailable.byte + (size % 16 === 0 ? 15 : size % 16 - 1)
};
alignment.end = this.applyOverflowToPosition(alignment.end);
return alignment;
}
/**
* Set the {@link BufferElementAlignment | alignment} from a {@link BufferElementAlignmentPosition | position}.
* @param position - {@link BufferElementAlignmentPosition | position} at which to start inserting the values in the {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}.
*/
setAlignmentFromPosition(position = {
row: 0,
byte: 0
}) {
this.alignment = this.getElementAlignment(position);
}
/**
* Set the {@link BufferElementAlignment | alignment} from an offset (byte count).
* @param startOffset - Offset at which to start inserting the values in the parent {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}.
* @param minStride - Minimum stride to use for the values in the parent {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}.
*/
setAlignment(startOffset = 0, minStride = 0) {
this.setAlignmentFromPosition(this.getPositionAtOffset(startOffset));
}
/**
* Set this {@link BufferElement} {@link view} into a parent {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}.
* @param arrayBuffer - The parent {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}.
* @param arrayView - The parent {@link core/bindings/BufferBinding.BufferBinding#arrayView | BufferBinding arrayView}.
*/
setView(arrayBuffer, arrayView) {
this.view = new this.bufferLayout.View(arrayBuffer, this.startOffset, this.byteCount / this.bufferLayout.View.BYTES_PER_ELEMENT);
}
/**
* Set the {@link view} value from a float or an int.
* @param value - Float or int to use.
*/
setValueFromNumber(value) {
this.view[0] = value;
}
/**
* Set the {@link view} value from a {@link Vec2} or an array.
* @param value - {@link Vec2} or array to use.
*/
setValueFromVec2(value) {
this.view[0] = value.x ?? value[0] ?? 0;
this.view[1] = value.y ?? value[1] ?? 0;
}
/**
* Set the {@link view} value from a {@link Vec3} or an array.
* @param value - {@link Vec3} or array to use.
*/
setValueFromVec3(value) {
this.view[0] = value.x ?? value[0] ?? 0;
this.view[1] = value.y ?? value[1] ?? 0;
this.view[2] = value.z ?? value[2] ?? 0;
}
/**
* Set the {@link view} value from a {@link Mat4} or {@link Quat}.
* @param value - {@link Mat4} or {@link Quat} to use.
*/
setValueFromMat4OrQuat(value) {
this.view.set(value.elements);
}
/**
* Set the {@link view} value from a {@link Mat3}.
* @param value - {@link Mat3} to use.
*/
setValueFromMat3(value) {
this.setValueFromArrayWithPad(value.elements);
}
/**
* Set the {@link view} value from an array.
* @param value - Array to use.
*/
setValueFromArray(value) {
this.view.set(value);
}
/**
* Set the {@link view} value from an array with pad applied.
* @param value - Array to use.
*/
setValueFromArrayWithPad(value) {
for (let i = 0, offset = 0; i < this.view.length; i += this.bufferLayout.pad[0] + this.bufferLayout.pad[1], offset++) for (let j = 0; j < this.bufferLayout.pad[0]; j++) this.view[i + j] = value[i + j - offset];
}
/**
* Update the {@link view} based on the new value.
* @param value - New value to use.
*/
update(value) {
if (!this.setValue) this.setValue = ((value) => {
if (typeof value === "number") return this.setValueFromNumber;
else if (this.type === "vec2f") return this.setValueFromVec2;
else if (this.type === "vec3f") return this.setValueFromVec3;
else if (this.type === "mat3x3f") return value.elements ? this.setValueFromMat3 : this.setValueFromArrayWithPad;
else if (value.elements) return this.setValueFromMat4OrQuat;
else if (ArrayBuffer.isView(value) || Array.isArray(value)) if (!this.bufferLayout.pad) return this.setValueFromArray;
else return this.setValueFromArrayWithPad;
else throwWarning(`${this.constructor.name}: value passed to ${this.name} cannot be used: ${value}`);
})(value);
this.setValue(value);
}
/**
* Extract the data corresponding to this specific {@link BufferElement} from a {@link Float32Array} holding the {@link GPUBuffer} data of the parentMesh {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}.
* @param result - {@link Float32Array} holding {@link GPUBuffer} data.
* @returns - Extracted data from the {@link Float32Array}.
*/
extractDataFromBufferResult(result) {
return result.slice(this.startOffsetToIndex, this.endOffsetToIndex);
}
};
//#endregion
export { BufferElement };