Skip to content

Commit

Permalink
fix(webgl): Use VertexState shader location for WebGL attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
jespertheend committed Aug 21, 2024
1 parent 92bf0ea commit 4ccd817
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 47 deletions.
2 changes: 1 addition & 1 deletion src/rendering/VertexState.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class VertexState {
* @param {object} options
* @param {PreferredShaderLocation[]} [options.preferredShaderLocations] If the vertex state
* has "auto", null or -1 for an attribute, this will be used to determine the shader location.
* If this an attribute with automatic shader location is not present in this list,
* If an attribute with automatic shader location is not present in this list,
* a location will be assigned that hasn't been used yet.
* If this list contains the same attribute type multiple times, an error will be thrown.
* If the list contains a shader location that has already been taken by the vertex state, an error will be thrown.
Expand Down
13 changes: 12 additions & 1 deletion src/rendering/renderers/webGl/CachedMeshBufferData.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Mesh } from "../../../core/Mesh.js";

export class CachedMeshBufferData {
#meshBuffer;
#vertexStateBuffer;
#cachedMeshData;

#bufferDirty = true;
Expand All @@ -10,10 +11,12 @@ export class CachedMeshBufferData {

/**
* @param {import("../../../core/MeshAttributeBuffer.js").MeshAttributeBuffer} meshBuffer
* @param {import("../../VertexStateBuffer.js").VertexStateBuffer} vertexStateBuffer
* @param {import("./CachedMeshData.js").CachedMeshData} meshData
*/
constructor(meshBuffer, meshData) {
constructor(meshBuffer, vertexStateBuffer, meshData) {
this.#meshBuffer = meshBuffer;
this.#vertexStateBuffer = vertexStateBuffer;
this.#cachedMeshData = meshData;

meshBuffer.onBufferChanged(this.#onBufferChanged);
Expand Down Expand Up @@ -54,6 +57,7 @@ export class CachedMeshBufferData {
}

const attributes = [];
let i = 0;
for (const attributeSettings of this.#meshBuffer.attributeSettings) {
let type;
const normalized = false;
Expand All @@ -66,12 +70,19 @@ export class CachedMeshBufferData {
} else {
throw new Error("Mesh has an unsupported attribute format");
}
const vertexStateAttribute = this.#vertexStateBuffer.attributes[i];
const shaderLocation = vertexStateAttribute.shaderLocation;
if (shaderLocation == null || shaderLocation == "auto" || shaderLocation < 0) {
throw new Error("Automatic shader locations are not supported in the webgl renderer.");
}
attributes.push({
componentCount: attributeSettings.componentCount,
type,
normalized,
offset: 0, // TODO
shaderLocation,
});
i++;
}

return {
Expand Down
9 changes: 8 additions & 1 deletion src/rendering/renderers/webGl/CachedMeshData.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,19 @@ export class CachedMeshData {
constructor(mesh, renderer) {
this.#mesh = mesh;
this.#renderer = renderer;
const vertexState = mesh.vertexState;
if (!vertexState) {
throw new Error("Assertion failed, mesh has no vertex state");
}

// todo: remove old bufferdata when the list of buffers changes
this.#buffers = [];
let i = 0;
for (const meshBuffer of mesh.getAttributeBuffers(false)) {
const bufferData = new CachedMeshBufferData(meshBuffer, this);
const vertexStateBuffer = vertexState.buffers[i];
const bufferData = new CachedMeshBufferData(meshBuffer, vertexStateBuffer, this);
this.#buffers.push(bufferData);
i++;
}

this.createIndexGpuBuffer();
Expand Down
54 changes: 52 additions & 2 deletions src/rendering/renderers/webGl/CachedProgramData.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
* @property {WebGLUniformLocation?} mvpMatrix
*/

import { parseAttributeLocations as parseTaggedAttributeLocations } from "./glslParsing.js";

export class CachedProgramData {
#program;
get program() {
return this.#program;
}

/** @type {ViewUniformLocations?} */
#viewUniformLocations = null;
Expand All @@ -18,10 +23,35 @@ export class CachedProgramData {
/** @type {Map<string, WebGLUniformLocation?>} */
#materialUniformLocations = new Map();

/** @type {Map<number, string>} */
#taggedAttributeLocations = new Map();

/**
* @param {WebGLProgram} program
* @param {WebGLRenderingContext} gl
* @param {import("../../ShaderSource.js").ShaderSource} vertexShaderSource
* @param {import("../../ShaderSource.js").ShaderSource} fragmentShaderSource
* @param {WebGLShader} vertexShader
* @param {WebGLShader} fragmentShader
*/
constructor(program) {
constructor(gl, vertexShaderSource, fragmentShaderSource, vertexShader, fragmentShader) {
const program = gl.createProgram();
if (!program) throw new Error("Failed to create program");

const taggedAttributeLocations = parseTaggedAttributeLocations(vertexShaderSource.source);
for (const {identifier, location} of taggedAttributeLocations) {
if (this.#taggedAttributeLocations.has(location)) {
throw new Error(`Shader contains multiple attributes tagged with @location(${location})`);
}
this.#taggedAttributeLocations.set(location, identifier);
}

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(`Failed to link shader program: ${gl.getProgramInfoLog(program)}`);
}

this.#program = program;
}

Expand Down Expand Up @@ -62,4 +92,24 @@ export class CachedProgramData {
this.#materialUniformLocations.set(name, location);
return location;
}

/**
* @param {WebGLRenderingContext} gl
* @param {number} taggedShaderLocation The id that the attribute was tagged
* with in the shader using a `@location` comment.
*/
getAttribLocation(gl, taggedShaderLocation) {
const identifier = this.#taggedAttributeLocations.get(taggedShaderLocation);
if (!identifier) {
// If no identifier with this shader location was found in the vertex shader, this could either be because:
// - the user forgot to tag it with a @location comment
// - or because the attribute is not used at all.
// In the first case we should ideally throw an error, in the second case we should do nothing.
// However, there's no easy way for us to detect if an attribute is unused, so we'll just
// return -1, this will cause the renderer to not bind the attribute buffer.
return -1;
}

return gl.getAttribLocation(this.#program, identifier);
}
}
58 changes: 18 additions & 40 deletions src/rendering/renderers/webGl/WebGlRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CachedMeshData } from "./CachedMeshData.js";
import { MultiKeyWeakMap } from "../../../util/MultiKeyWeakMap.js";
import { Mesh } from "../../../core/Mesh.js";
import { CachedProgramData } from "./CachedProgramData.js";
import { parseAttributeLocations } from "./glslParsing.js";

/**
* @extends {Renderer<WebGlRendererDomTarget>}
Expand Down Expand Up @@ -42,16 +43,13 @@ export class WebGlRenderer extends Renderer {
/** @type {WeakMap<import("../../Material.js").Material, CachedMaterialData>} */
#cachedMaterialData = new WeakMap();

/** @type {WeakMap<WebGLProgram, CachedProgramData>} */
#cachedProgramData = new WeakMap();

/** @type {WeakMap<import("../../../core/Mesh.js").Mesh, CachedMeshData>} */
#cachedMeshDatas = new WeakMap();

/** @type {MultiKeyWeakMap<[number, import("../../ShaderSource.js").ShaderSource], WebGLShader>} */
#cachedShaders = new MultiKeyWeakMap([], { allowNonObjects: true });

/** @type {MultiKeyWeakMap<[import("../../ShaderSource.js").ShaderSource, import("../../ShaderSource.js").ShaderSource], WebGLProgram>} */
/** @type {MultiKeyWeakMap<[import("../../ShaderSource.js").ShaderSource, import("../../ShaderSource.js").ShaderSource], CachedProgramData>} */
#cachedPrograms = new MultiKeyWeakMap();

/** @type {OES_element_index_uint?} */
Expand Down Expand Up @@ -197,7 +195,7 @@ export class WebGlRenderer extends Renderer {

/**
* @typedef MaterialConfigRenderData
* @property {Map<WebGLProgram, MaterialRenderData>} materialRenderDatas
* @property {Map<CachedProgramData, MaterialRenderData>} materialRenderDatas
*/

// Group all meshes by material config
Expand All @@ -212,7 +210,7 @@ export class WebGlRenderer extends Renderer {
const materialConfig = materialData.getMaterialConfig();
if (!materialConfig || !materialConfig.vertexShader || !materialConfig.fragmentShader) continue;

const program = this.#getProgram(materialConfig.vertexShader, materialConfig.fragmentShader);
const program = this.#getCachedProgramData(materialConfig.vertexShader, materialConfig.fragmentShader);

let programRenderData = materialConfigRenderDatas.get(materialConfig);
if (!programRenderData) {
Expand Down Expand Up @@ -247,9 +245,8 @@ export class WebGlRenderer extends Renderer {
});

for (const [materialConfig, programRenderData] of sortedProgramRenderDatas) {
for (const [program, materialRenderData] of programRenderData.materialRenderDatas) {
gl.useProgram(program);
const programData = this.#getCachedProgramData(program);
for (const [programData, materialRenderData] of programRenderData.materialRenderDatas) {
gl.useProgram(programData.program);
const viewUniformLocations = programData.getViewUniformLocations(gl);
const modelUniformLocations = programData.getModelUniformLocations(gl);

Expand Down Expand Up @@ -295,6 +292,7 @@ Material.setProperty("${mappedData.mappedName}", customData)`;
for (const { component: meshComponent, worldMatrix } of meshRenderDatas) {
const mesh = meshComponent.mesh;
if (!mesh) continue;
if (!mesh.vertexState) continue;

if (modelUniformLocations.mvpMatrix) {
const mvpMatrix = Mat4.multiplyMatrices(worldMatrix, viewProjectionMatrix);
Expand All @@ -317,19 +315,20 @@ Material.setProperty("${mappedData.mappedName}", customData)`;
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferData.buffer);

let i = 0;
for (const { buffer, attributes, stride } of meshData.getAttributeBufferData()) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
for (const { componentCount, type, normalized, offset } of attributes) {
gl.vertexAttribPointer(i, componentCount, type, normalized, stride, offset);
gl.enableVertexAttribArray(i);
i++;
for (const { shaderLocation, componentCount, type, normalized, offset } of attributes) {
const index = programData.getAttribLocation(gl, shaderLocation);
if (index >= 0) {
gl.vertexAttribPointer(index, componentCount, type, normalized, stride, offset);
gl.enableVertexAttribArray(index);
}
}
}

gl.drawElements(gl.TRIANGLES, indexBufferData.count, indexFormat, 0);
} else {
// TODO
// TODO
}
}
}
Expand All @@ -354,18 +353,6 @@ Material.setProperty("${mappedData.mappedName}", customData)`;
return data;
}

/**
* @param {WebGLProgram} program
*/
#getCachedProgramData(program) {
let data = this.#cachedProgramData.get(program);
if (!data) {
data = new CachedProgramData(program);
this.#cachedProgramData.set(program, data);
}
return data;
}

/**
* @param {import("../../../core/Mesh.js").Mesh} mesh
*/
Expand Down Expand Up @@ -405,7 +392,7 @@ Material.setProperty("${mappedData.mappedName}", customData)`;
* @param {import("../../ShaderSource.js").ShaderSource} vertexShaderSource
* @param {import("../../ShaderSource.js").ShaderSource} fragmentShaderSource
*/
#getProgram(vertexShaderSource, fragmentShaderSource) {
#getCachedProgramData(vertexShaderSource, fragmentShaderSource) {
const existing = this.#cachedPrograms.get([vertexShaderSource, fragmentShaderSource]);
if (existing) return existing;

Expand All @@ -415,18 +402,9 @@ Material.setProperty("${mappedData.mappedName}", customData)`;
const vertexShader = this.#getShader(vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = this.#getShader(fragmentShaderSource, gl.FRAGMENT_SHADER);

const program = gl.createProgram();
if (!program) throw new Error("Failed to create program");

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(`Failed to link shader program: ${gl.getProgramInfoLog(program)}`);
}

this.#cachedPrograms.set([vertexShaderSource, fragmentShaderSource], program);
return program;
const cachedProgramData = new CachedProgramData(gl, vertexShaderSource, fragmentShaderSource, vertexShader, fragmentShader);
this.#cachedPrograms.set([vertexShaderSource, fragmentShaderSource], cachedProgramData);
return cachedProgramData;
}

/**
Expand Down
61 changes: 61 additions & 0 deletions src/rendering/renderers/webGl/glslParsing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Regex string for matching glsl identifiers according to the glsl spec:
* https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#identifiers
* @param {string} group
*/
export const identifierRegex = "(?:[a-zA-Z_][0-9a-zA-Z_]*)";

/**
* @typedef ParsedAttributeLocation
* @property {string} identifier The name of the attribute as it appears in the shader.
* @property {number} location The shader location that the identifier was tagged with.
*/

/**
* Finds all attributes in a shader and the value of the `@location` comment they are tagged with.
* @param {string} shaderSource
* @returns {ParsedAttributeLocation[]}
*/
export function parseAttributeLocations(shaderSource) {
// This loosely follows
// https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#shading-language-grammar:~:text=conditional_expression-,declaration%20%3A,-function_prototype%20SEMICOLON%0Ainit_declarator_list
let attributesRegex = "";
// Capture the location tag
attributesRegex += "@location\\s*\\(\\s*(?<location>\\d+)\\s*\\)";
// Allow whitespace or any other tags after the line that contains the location tag
attributesRegex += ".*";
// Only one new line allowed
attributesRegex += "\\n";
// Allow whitespace before the attribute keyword
attributesRegex += "\\s*";
// Attribute storage qualifier
attributesRegex += "attribute";
// any additional `type_qualifier`s
attributesRegex += ".*";
// at least one whitespace
attributesRegex += "\\s";
// Capture the IDENTIFIER
attributesRegex += `(?<identifier>${identifierRegex})`;
// whitespace
attributesRegex += "\\s*";
// SEMICOLON
attributesRegex += ";"


/** @type {ParsedAttributeLocation[]} */
const parsedLocations = [];

for (const match of shaderSource.matchAll(new RegExp(attributesRegex, "g"))) {
if (!match.groups) continue;
const identifier = match.groups.identifier;
if (!identifier) continue;
const location = match.groups.location;
if (!location) continue;
parsedLocations.push({
identifier,
location: parseInt(location, 10),
});
}

return parsedLocations;
}
3 changes: 1 addition & 2 deletions src/util/wgslParsing.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,7 @@ export function parseBindings(shaderSource) {
/**
* @typedef ParsedVertexInputProperty
* @property {string} identifier The name of the binding as it appears in the shader.
* @property {number} location The shader location that should be used when the vertex state
* has a shader location set to 'auto'.
* @property {number} location The shader location that the identifier was tagged with.
*/

/**
Expand Down
Loading

0 comments on commit 4ccd817

Please sign in to comment.