pex-context
Version:
Modern WebGL state wrapper for PEX: allocate GPU resources (textures, buffers), setup state pipelines and passes, and combine them into commands.
438 lines (404 loc) • 13.7 kB
JavaScript
// Debug
const NAMESPACE = "pex-context";
const checkProps = (allowedProps, obj) =>
Object.keys(obj).forEach((prop) => {
if (!allowedProps.includes(prop)) throw new Error(`Unknown prop "${prop}"`);
});
const isWebGL2 = (gl) =>
typeof WebGL2RenderingContext !== "undefined" &&
gl instanceof WebGL2RenderingContext;
const isObject = (obj) =>
Object.prototype.toString.call(obj) === "[object Object]";
const is2DArray = (arr) => arr[0]?.length;
// State and gl
function compareFBOAttachments(framebuffer, passOpts) {
const fboDepthAttachment = framebuffer.depth?.texture;
const passDepthAttachment = passOpts.depth?.texture || passOpts.depth;
if (fboDepthAttachment != passDepthAttachment) return false;
if (framebuffer.color.length != passOpts.color?.length) return false;
for (let i = 0; i < framebuffer.color.length; i++) {
const fboColorAttachment = framebuffer.color[i]?.texture;
const passColorAttachment = passOpts.color[i]?.texture || passOpts.color[i];
if (fboColorAttachment != passColorAttachment) return false;
}
return true;
}
const getUniformLocation = (gl, program, name) =>
gl.getUniformLocation(program.handle, name);
function enableVertexData(ctx, vertexLayout, cmd, updateState) {
const gl = ctx.gl;
const { attributes = {}, indices } = cmd;
for (
let i = vertexLayout.length;
i < ctx.capabilities.maxVertexAttribs;
i++
) {
if (ctx.state.activeAttributes[i]) {
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.vertexAttribPointer(i, 4, ctx.DataType.Float32, false, 0, 0);
gl.disableVertexAttribArray(i);
ctx.state.activeAttributes[i] = null;
}
}
for (let i = 0; i < vertexLayout.length; i++) {
const [name, location, size] = vertexLayout[i];
// TODO: is attributes array valid?
const attrib = attributes[i] || attributes[name];
if (!attrib) {
console.debug(
NAMESPACE,
"invalid command",
cmd,
"doesn't satisfy vertex layout",
vertexLayout,
);
throw new Error(
`Command is missing attribute "${name}" at location ${location} with ${attrib}`,
);
}
let buffer = attrib.buffer;
if (!buffer && attrib.class === "vertexBuffer") {
buffer = attrib;
}
if (!buffer || !buffer.target) {
throw new Error(
`Trying to draw arrays with invalid buffer for attribute : ${name}`,
);
}
gl.bindBuffer(buffer.target, buffer.handle);
if (size === 16) {
gl.enableVertexAttribArray(location + 0);
gl.enableVertexAttribArray(location + 1);
gl.enableVertexAttribArray(location + 2);
gl.enableVertexAttribArray(location + 3);
if (updateState) {
ctx.state.activeAttributes[location + 0] = buffer;
ctx.state.activeAttributes[location + 1] = buffer;
ctx.state.activeAttributes[location + 2] = buffer;
ctx.state.activeAttributes[location + 3] = buffer;
}
// TODO: is this still valid?
// we still check for buffer type because while e.g. pex-renderer would copy buffer type to attrib, a raw pex-context example probably would not
gl.vertexAttribPointer(
location,
4,
attrib.type || buffer.type,
attrib.normalized || false,
attrib.stride || 64,
attrib.offset || 0,
);
gl.vertexAttribPointer(
location + 1,
4,
attrib.type || buffer.type,
attrib.normalized || false,
attrib.stride || 64,
attrib.offset || 16,
);
gl.vertexAttribPointer(
location + 2,
4,
attrib.type || buffer.type,
attrib.normalized || false,
attrib.stride || 64,
attrib.offset || 32,
);
gl.vertexAttribPointer(
location + 3,
4,
attrib.type || buffer.type,
attrib.normalized || false,
attrib.stride || 64,
attrib.offset || 48,
);
const divisor = attrib.divisor || 0;
gl.vertexAttribDivisor(location + 0, divisor);
gl.vertexAttribDivisor(location + 1, divisor);
gl.vertexAttribDivisor(location + 2, divisor);
gl.vertexAttribDivisor(location + 3, divisor);
} else {
gl.enableVertexAttribArray(location);
if (updateState) ctx.state.activeAttributes[location] = buffer;
let offset = attrib.offset || 0;
const type = attrib.type || buffer.type;
// Instanced Base fallback
if (
!ctx.capabilities.drawInstancedBase &&
(cmd.baseInstance || cmd.baseVertex)
) {
if (indices) {
offset +=
(attrib.divisor ? cmd.baseInstance : cmd.baseVertex) *
size *
ctx.DataTypeConstructor[type].BYTES_PER_ELEMENT;
} else {
if (attrib.divisor) offset += (cmd.count * cmd.baseInstance) / size;
}
}
gl.vertexAttribPointer(
location,
size,
type,
attrib.normalized || false,
attrib.stride || 0,
offset,
);
gl.vertexAttribDivisor(location, attrib.divisor || 0);
}
// TODO: how to match index with vertexLayout location?
}
if (indices) {
let indexBuffer = indices.buffer;
if (!indexBuffer && indices.class === "indexBuffer") {
indexBuffer = indices;
}
if (!indexBuffer || !indexBuffer.target) {
console.debug(NAMESPACE, "invalid command", cmd, "buffer", indexBuffer);
throw new Error(`Trying to draw arrays with invalid buffer for elements`);
}
if (updateState) ctx.state.indexBuffer = indexBuffer;
gl.bindBuffer(indexBuffer.target, indexBuffer.handle);
}
}
function drawElements(ctx, cmd, instanced, primitive) {
const gl = ctx.gl;
// TODO: is that always correct
const count = cmd.count || ctx.state.indexBuffer.length;
const offset = cmd.indices?.offset || cmd.vertexArray?.indices?.offset || 0;
const type =
cmd.indices?.type ||
cmd.vertexArray?.indices?.offset ||
ctx.state.indexBuffer.type;
// Instanced drawing
if (instanced) {
if (cmd.multiDraw) {
if (ctx.capabilities.multiDraw) {
// Multidraw elements instanced base
if (
ctx.capabilities.multiDrawInstancedBase &&
(cmd.multiDraw.baseVertices || cmd.multiDraw.baseInstances)
) {
const ext = gl.getExtension(
"WEBGL_multi_draw_instanced_base_vertex_base_instance",
);
ext.multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL(
primitive,
cmd.multiDraw.counts,
cmd.multiDraw.countsOffset || 0,
type,
cmd.multiDraw.offsets,
cmd.multiDraw.offsetsOffset || 0,
cmd.multiDraw.instanceCounts,
cmd.multiDraw.instanceCountsOffset || 0,
cmd.multiDraw.baseVertices || [],
cmd.multiDraw.baseVerticesOffset || 0,
cmd.multiDraw.baseInstances || [],
cmd.multiDraw.baseInstancesOffset || 0,
cmd.multiDraw.drawCount || cmd.multiDraw.counts.length,
);
} else {
// Multidraw elements instanced (or multidraw base fallback with offset from enableVertexData)
const ext = gl.getExtension("WEBGL_multi_draw");
// TODO: fallback
if (cmd.multiDraw.baseVertices || cmd.multiDraw.baseInstances) {
console.error("Unsupported multiDrawInstancedBase. No fallback.");
}
ext.multiDrawElementsInstancedWEBGL(
primitive,
cmd.multiDraw.counts,
cmd.multiDraw.countsOffset || 0,
type,
cmd.multiDraw.offsets,
cmd.multiDraw.offsetsOffset || 0,
cmd.multiDraw.instanceCounts,
cmd.multiDraw.instanceCountsOffset || 0,
cmd.multiDraw.drawCount || cmd.multiDraw.counts.length,
);
}
} else {
// Multi draw elements instanced fallback
// TODO: fallback
console.error("Unsupported multidraw. No fallback.");
}
} else {
// Non-multi drawing
if (
ctx.capabilities.drawInstancedBase &&
(Number.isFinite(cmd.baseVertex) || Number.isFinite(cmd.baseInstance))
) {
// Draw elements instanced base
const ext = gl.getExtension(
"WEBGL_draw_instanced_base_vertex_base_instance",
);
ext.drawElementsInstancedBaseVertexBaseInstanceWEBGL(
primitive,
count,
type,
offset,
cmd.instances,
cmd.baseVertex || 0,
cmd.baseInstance || 0,
);
} else {
// Draw elements instanced (or base fallback with offset from enableVertexData)
gl.drawElementsInstanced(primitive, count, type, offset, cmd.instances);
}
}
} else {
// Non instanced drawing
if (cmd.multiDraw) {
if (ctx.capabilities.multiDraw) {
// Multidraw elements
const ext = gl.getExtension("WEBGL_multi_draw");
ext.multiDrawElementsWEBGL(
primitive,
cmd.multiDraw.counts,
cmd.multiDraw.countsOffset || 0,
type,
cmd.multiDraw.offsets,
cmd.multiDraw.offsetsOffset || 0,
cmd.multiDraw.drawCount || cmd.multiDraw.counts.length,
);
} else {
// Multidraw elements fallback
const countsOffset = cmd.multiDraw.countsOffset || 0;
const offsetsOffset = cmd.multiDraw.offsetsOffset || 0;
const drawCount =
cmd.multiDraw.drawCount || cmd.multiDraw.counts.length;
for (let i = 0; i < drawCount; i++) {
gl.drawElements(
primitive,
cmd.multiDraw.counts[i + countsOffset],
type,
cmd.multiDraw.offsets[i + offsetsOffset],
);
}
}
} else {
// Draw elements
gl.drawElements(primitive, count, type, offset);
}
}
}
function drawArrays(ctx, cmd, instanced, primitive) {
const gl = ctx.gl;
if (instanced) {
if (cmd.multiDraw && ctx.capabilities.multiDraw) {
if (
cmd.multiDraw.baseInstances &&
ctx.capabilities.multiDrawInstancedBase
) {
const ext = gl.getExtension(
"WEBGL_multi_draw_instanced_base_vertex_base_instance",
);
ext.multiDrawArraysInstancedBaseInstanceWEBGL(
primitive,
cmd.multiDraw.firsts,
cmd.multiDraw.firstsOffset || 0,
cmd.multiDraw.counts,
cmd.multiDraw.countsOffset || 0,
cmd.multiDraw.instanceCounts,
cmd.multiDraw.instanceCountsOffset || 0,
cmd.multiDraw.baseInstances,
cmd.multiDraw.baseInstancesOffset || 0,
cmd.multiDraw.drawCount || cmd.multiDraw.firsts.length,
);
} else {
const ext = gl.getExtension("WEBGL_multi_draw");
ext.multiDrawArraysInstancedWEBGL(
primitive,
cmd.multiDraw.firsts,
cmd.multiDraw.firstsOffset || 0,
cmd.multiDraw.counts,
cmd.multiDraw.countsOffset || 0,
cmd.multiDraw.instanceCounts,
cmd.multiDraw.instanceCountsOffset || 0,
cmd.multiDraw.drawCount || cmd.multiDraw.firsts.length,
);
}
} else {
// Non-multi drawing
if (
ctx.capabilities.drawInstancedBase &&
Number.isFinite(cmd.baseInstance)
) {
// Draw arrays instanced with base instance
const ext = gl.getExtension(
"WEBGL_draw_instanced_base_vertex_base_instance",
);
ext.drawArraysInstancedBaseInstanceWEBGL(
primitive,
cmd.first || 0,
cmd.count,
cmd.instances,
cmd.baseInstance,
);
} else {
// Draw arrays instanced (or base instance fallback)
gl.drawArraysInstanced(
primitive,
cmd.first || 0,
cmd.count,
cmd.instances,
);
}
}
} else {
// Non instanced drawing
if (cmd.multiDraw) {
// Multidraw arrays
if (ctx.capabilities.multiDraw) {
const ext = gl.getExtension("WEBGL_multi_draw");
ext.multiDrawArraysWEBGL(
primitive,
cmd.multiDraw.firsts,
cmd.multiDraw.firstsOffset || 0,
cmd.multiDraw.counts,
cmd.multiDraw.countsOffset || 0,
cmd.multiDraw.drawCount || cmd.multiDraw.firsts.length,
);
} else {
// Multidraw arrays fallback
const firstsOffset = cmd.multiDraw.firstsOffset || 0;
const countsOffset = cmd.multiDraw.countsOffset || 0;
const drawCount =
cmd.multiDraw.drawCount || cmd.multiDraw.firsts.length;
for (let i = 0; i < drawCount; i++) {
gl.drawArrays(
primitive,
cmd.multiDraw.firsts[i + firstsOffset],
cmd.multiDraw.counts[i + countsOffset],
);
}
}
} else {
// Draw arrays
gl.drawArrays(primitive, cmd.first || 0, cmd.count);
}
}
}
function draw(ctx, cmd) {
const instanced = Object.values(
cmd.attributes || cmd.vertexArray.attributes,
).some((attrib) => attrib.divisor);
const primitive = cmd.pipeline.primitive;
// Draw elements/arrays: instanced, base, multi-draw
if (cmd.indices || cmd.vertexArray?.indices) {
drawElements(ctx, cmd, instanced, primitive);
} else if (cmd.count || cmd.multiDraw?.counts) {
drawArrays(ctx, cmd, instanced, primitive);
} else {
throw new Error("Vertex arrays requires elements, count or multiDraw");
}
}
export {
NAMESPACE,
isWebGL2,
isObject,
is2DArray,
checkProps,
compareFBOAttachments,
getUniformLocation,
enableVertexData,
draw,
};