Add parallel shader compiling

This commit is contained in:
schmelczerandras 2020-09-10 11:32:05 +02:00
parent a6efbc02b9
commit 39a6ee9a20
17 changed files with 209 additions and 77 deletions

View file

@ -25,14 +25,16 @@ const testSDF = () => {
return -res; return -res;
*/ */
let min = 1000; /*let min = 1000;
for (const t of objects) { for (const t of objects) {
min = Math.min(min, t.distance(position)); min = Math.min(min, t.distance(position));
} }
if (min < 0) { if (min < 0) {
// min = Math.min(min, vec2.distance(position, vec2.fromValues(40, 80 - 31.62)) - 31.62); // min = Math.min(min, vec2.distance(position, vec2.fromValues(40, 80 - 31.62)) - 31.62);
} }
return -min; return -min;*/
return -vec2.distance(position, vec2.fromValues(40, 40)) + 30;
}; };
const width = 80; const width = 80;
@ -55,11 +57,12 @@ const testSDF = () => {
const position = vec2.fromValues(x, y); const position = vec2.fromValues(x, y);
const redIndex = y * (width * 4) + x * 4; const redIndex = y * (width * 4) + x * 4;
const dist = getPixelValue(position); const dist = getPixelValue(position);
if (Math.abs(dist) < 1) { if (Math.abs(dist) < 1.5) {
const blueIndex = y * (width * 4) + x * 4 + 2;
zeroes.push(position); zeroes.push(position);
imageData.data[blueIndex] = 255;
} }
const blueIndex = y * (width * 4) + x * 4 + 2;
//imageData.data[blueIndex] = dist * 10;
imageData.data[redIndex + 3] = 255; imageData.data[redIndex + 3] = 255;
} }
} }
@ -75,25 +78,37 @@ const testSDF = () => {
const position = vec2.fromValues(x, y); const position = vec2.fromValues(x, y);
const dist = getPixelValue(position); const dist = getPixelValue(position);
const nearestDist = const realDistance = zeroes.map((z) => vec2.distance(z, position)).sort()[0];
zeroes.find((z) => Math.abs(vec2.distance(z, position) - dist) < 0.5)?.length > 0;
if (nearestDist) { if (Math.abs(realDistance - dist) < 3) {
const greenIndex = y * (width * 4) + x * 4 + 1; const greenIndex = y * (width * 4) + x * 4 + 1;
imageData.data[greenIndex] = 255; imageData.data[greenIndex] = 255;
} else if (dist >= 0) { } else if (dist >= 0) {
const redIndex = y * (width * 4) + x * 4; const redIndex = y * (width * 4) + x * 4;
errors.push(dist); errors.push(realDistance - dist);
imageData.data[redIndex] = 255; imageData.data[redIndex] = 255;
} }
/*if (zeroes.find((z) => vec2.distance(z, position) < dist) !== undefined) {
const blueIndex = y * (width * 4) + x * 4 + 2;
imageData.data[blueIndex] = 255;
}*/
} }
} }
zeroes.forEach((z) => {
const blueIndex = z.y * (width * 4) + z.x * 4 + 2;
imageData.data[blueIndex] = 255;
imageData.data[blueIndex - 1] = 255;
imageData.data[blueIndex - 2] = 255;
});
console.log(errors); console.log(errors);
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
}; };
// testSDF(); //testSDF();
// extract as new image (data-uri) // extract as new image (data-uri)
/* /*
@ -113,10 +128,14 @@ tree.print();
console.log(tree.findIntersecting(new BoundingBox(960, 1050, 50, 190, 'G'))); console.log(tree.findIntersecting(new BoundingBox(960, 1050, 50, 190, 'G')));
*/ */
try { const main = async () => {
try {
Random.seed = 42; Random.seed = 42;
new Game(); await new Game().start();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
alert(e); alert(e);
} }
};
main();

View file

@ -0,0 +1,13 @@
import { checkShader } from './check-shader';
export const checkProgram = (gl: WebGL2RenderingContext, program: WebGLProgram) => {
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!success) {
gl.getAttachedShaders(program).forEach((s) => {
checkShader(gl, s);
});
throw new Error(gl.getProgramInfoLog(program));
}
};

View file

@ -0,0 +1,7 @@
export const checkShader = (gl: WebGL2RenderingContext, shader: WebGLShader) => {
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!success) {
throw new Error(gl.getShaderInfoLog(shader));
}
};

View file

@ -0,0 +1,44 @@
import { waitWhileFalse } from '../../../helper/wait-for';
import { tryEnableExtension } from '../helper/enable-extension';
import { checkProgram } from './check-program';
import { createShader } from './create-shader';
export const createProgram = async (
gl: WebGL2RenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
substitutions: { [name: string]: string }
): Promise<WebGLProgram> => {
const program = gl.createProgram();
const extension = tryEnableExtension(gl, 'KHR_parallel_shader_compile');
const vertexShader = createShader(
gl,
gl.VERTEX_SHADER,
vertexShaderSource,
substitutions
);
gl.attachShader(program, vertexShader);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource,
substitutions
);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (extension) {
const checkCompileStatus = () =>
gl.getProgramParameter(program, extension.COMPLETION_STATUS_KHR);
await waitWhileFalse(checkCompileStatus);
}
checkProgram(gl, program);
return program;
};

View file

@ -1,26 +1,17 @@
import { settings } from '../../settings';
export const createShader = ( export const createShader = (
gl: WebGL2RenderingContext, gl: WebGL2RenderingContext,
type: GLenum, type: GLenum,
source: string, source: string,
substitutions: { [name: string]: string } substitutions: { [name: string]: string }
): WebGLShader => { ): WebGLShader => {
const allSubstitutions = { ...settings.shaderMacros, ...substitutions };
source = source.replace(/{(.+)}/gm, (_, name: string): string => { source = source.replace(/{(.+)}/gm, (_, name: string): string => {
const value = allSubstitutions[name]; const value = substitutions[name];
return Number.isInteger(value) ? `${value}.0` : value; return Number.isInteger(value) ? `${value}.0` : value;
}); });
const shader = gl.createShader(type); const shader = gl.createShader(type);
gl.shaderSource(shader, source); gl.shaderSource(shader, source);
gl.compileShader(shader); gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!success) {
throw new Error(gl.getShaderInfoLog(shader));
}
return shader; return shader;
}; };

View file

@ -1,7 +1,29 @@
const extensions: Map<string, any> = new Map();
export const tryEnableExtension = (
gl: WebGL2RenderingContext,
name: string
): any | null => {
if (extensions.has(name)) {
return extensions.get(name);
}
let extension = null;
if (gl.getSupportedExtensions().indexOf(name) != -1) {
extension = gl.getExtension(name);
}
extensions.set(name, extension);
return extension;
};
export const enableExtension = (gl: WebGL2RenderingContext, name: string): any => { export const enableExtension = (gl: WebGL2RenderingContext, name: string): any => {
if (gl.getSupportedExtensions().indexOf(name) == -1) { const extension = tryEnableExtension(gl, name);
if (extension === null) {
throw new Error(`Unsupported extension ${name}`); throw new Error(`Unsupported extension ${name}`);
} }
return gl.getExtension(name); return extension;
}; };

View file

@ -5,10 +5,14 @@ export class FragmentShaderOnlyProgram extends Program {
constructor( constructor(
gl: WebGL2RenderingContext, gl: WebGL2RenderingContext,
shaderSources: [string, string], sources: [string, string],
substitutions: { [name: string]: string } substitutions: { [name: string]: string }
) { ) {
super(gl, shaderSources, substitutions); super(gl, sources, substitutions);
}
public async initialize(): Promise<void> {
await super.initialize();
this.prepareScreenQuad('vertexPosition'); this.prepareScreenQuad('vertexPosition');
} }

View file

@ -1,6 +1,7 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
export interface IProgram { export interface IProgram {
initialize(): Promise<void>;
setDrawingRectangle(bottomLeft: vec2, size: vec2): void; setDrawingRectangle(bottomLeft: vec2, size: vec2): void;
bindAndSetUniforms(values: { [name: string]: any }): void; bindAndSetUniforms(values: { [name: string]: any }): void;
draw(): void; draw(): void;

View file

@ -1,17 +1,14 @@
import { mat2d, vec2 } from 'gl-matrix'; import { mat2d, vec2 } from 'gl-matrix';
import { createShader } from '../helper/create-shader'; import { settings } from '../../settings';
import { createProgram } from '../compiling/create-program';
import { loadUniform } from '../helper/load-uniform'; import { loadUniform } from '../helper/load-uniform';
import { IProgram } from './i-program'; import { IProgram } from './i-program';
export default abstract class Program implements IProgram { export default abstract class Program implements IProgram {
protected program: WebGLProgram; protected program: WebGLProgram;
private programPromise: Promise<WebGLProgram>;
private shaders: Array<WebGLShader> = [];
private modelTransform = mat2d.identity(mat2d.create()); private modelTransform = mat2d.identity(mat2d.create());
private readonly ndcToUv: mat2d; private readonly ndcToUv: mat2d;
private uniforms: Array<{ private uniforms: Array<{
name: Array<string>; name: Array<string>;
location: WebGLUniformLocation; location: WebGLUniformLocation;
@ -23,13 +20,24 @@ export default abstract class Program implements IProgram {
[vertexShaderSource, fragmentShaderSource]: [string, string], [vertexShaderSource, fragmentShaderSource]: [string, string],
substitutions: { [name: string]: string } substitutions: { [name: string]: string }
) { ) {
this.createProgram(vertexShaderSource, fragmentShaderSource, substitutions); substitutions = { ...settings.shaderMacros, ...substitutions };
this.queryUniforms();
this.programPromise = createProgram(
this.gl,
vertexShaderSource,
fragmentShaderSource,
substitutions
);
this.ndcToUv = mat2d.fromScaling(mat2d.create(), vec2.fromValues(0.5, 0.5)); this.ndcToUv = mat2d.fromScaling(mat2d.create(), vec2.fromValues(0.5, 0.5));
mat2d.translate(this.ndcToUv, this.ndcToUv, vec2.fromValues(1, 1)); mat2d.translate(this.ndcToUv, this.ndcToUv, vec2.fromValues(1, 1));
} }
public async initialize(): Promise<void> {
this.program = await this.programPromise;
this.queryUniforms();
}
public bindAndSetUniforms(values: { [name: string]: any }) { public bindAndSetUniforms(values: { [name: string]: any }) {
this.bind(); this.bind();
this.setUniforms({ modelTransform: this.modelTransform, ...values }); this.setUniforms({ modelTransform: this.modelTransform, ...values });
@ -56,7 +64,7 @@ export default abstract class Program implements IProgram {
} }
public delete() { public delete() {
this.shaders.forEach((s) => this.gl.deleteShader(s)); this.gl.getAttachedShaders(this.program).forEach((s) => this.gl.deleteShader(s));
this.gl.deleteProgram(this.program); this.gl.deleteProgram(this.program);
} }
@ -91,38 +99,4 @@ export default abstract class Program implements IProgram {
return u1; return u1;
}); });
} }
private createProgram(
passthroughVertexShaderSource: string,
fragmentShaderSource: string,
substitutions: { [name: string]: string }
) {
this.program = this.gl.createProgram();
const vertexShader = createShader(
this.gl,
this.gl.VERTEX_SHADER,
passthroughVertexShaderSource,
substitutions
);
this.gl.attachShader(this.program, vertexShader);
this.shaders.push(vertexShader);
const fragmentShader = createShader(
this.gl,
this.gl.FRAGMENT_SHADER,
fragmentShaderSource,
substitutions
);
this.gl.attachShader(this.program, fragmentShader);
this.shaders.push(fragmentShader);
this.gl.linkProgram(this.program);
const success = this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS);
if (!success) {
throw new Error(this.gl.getProgramInfoLog(this.program));
}
}
} }

View file

@ -28,6 +28,10 @@ export class UniformArrayAutoScalingProgram implements IProgram {
} }
} }
public async initialize(): Promise<void> {
await Promise.all(this.programs.map((p) => p.program.initialize()));
}
public bindAndSetUniforms(uniforms: { [name: string]: any }): void { public bindAndSetUniforms(uniforms: { [name: string]: any }): void {
const values = this.descriptors.map((d) => const values = this.descriptors.map((d) =>
uniforms[d.uniformName] ? uniforms[d.uniformName].length : 0 uniforms[d.uniformName] ? uniforms[d.uniformName].length : 0

View file

@ -4,6 +4,8 @@ import { IDrawable } from './drawables/i-drawable';
import { ILight } from './drawables/lights/i-light'; import { ILight } from './drawables/lights/i-light';
export interface IRenderer { export interface IRenderer {
initialize(): Promise<void>;
startFrame(deltaTime: DOMHighResTimeStamp): void; startFrame(deltaTime: DOMHighResTimeStamp): void;
finishFrame(): void; finishFrame(): void;

View file

@ -24,6 +24,10 @@ export class RenderingPass {
); );
} }
public async initialize(): Promise<void> {
await this.program.initialize();
}
public addDrawable(drawable: IDrawable) { public addDrawable(drawable: IDrawable) {
this.drawables.push(drawable); this.drawables.push(drawable);
} }

View file

@ -36,6 +36,8 @@ export class WebGl2Renderer implements IRenderer {
private lightingPass: RenderingPass; private lightingPass: RenderingPass;
private autoscaler: FpsAutoscaler; private autoscaler: FpsAutoscaler;
private initializePromise: Promise<[void, void]> = null;
private matrices: { private matrices: {
distanceScreenToWorld?: mat2d; distanceScreenToWorld?: mat2d;
worldToDistanceUV?: mat2d; worldToDistanceUV?: mat2d;
@ -63,6 +65,11 @@ export class WebGl2Renderer implements IRenderer {
this.lightingFrameBuffer this.lightingFrameBuffer
); );
this.initializePromise = Promise.all([
this.distancePass.initialize(),
this.lightingPass.initialize(),
]);
this.autoscaler = new FpsAutoscaler([ this.autoscaler = new FpsAutoscaler([
this.lightingFrameBuffer, this.lightingFrameBuffer,
this.distanceFieldFrameBuffer, this.distanceFieldFrameBuffer,
@ -75,6 +82,10 @@ export class WebGl2Renderer implements IRenderer {
} }
} }
public async initialize(): Promise<void> {
await this.initializePromise;
}
public drawShape(shape: IDrawable) { public drawShape(shape: IDrawable) {
this.distancePass.addDrawable(shape); this.distancePass.addDrawable(shape);
} }

View file

@ -8,11 +8,11 @@ export const settings = {
[0.2, 0.1], [0.2, 0.1],
[0.6, 0.1], [0.6, 0.1],
[1, 1], [1, 1],
[1.25, 0.75], /*[1.25, 0.75],
[1.5, 1], [1.5, 1],
[1.75, 1.25], [1.75, 1.25],
//[1.75, 1.75], //[1.75, 1.75],
//[2, 2], //[2, 2],*/
], ],
startingTargetIndex: 2, startingTargetIndex: 2,
scalingOptions: { scalingOptions: {

View file

@ -29,6 +29,7 @@ export class Game implements IGame {
private infoText = new InfoText(); private infoText = new InfoText();
private character: Character; private character: Character;
private renderer: IRenderer; private renderer: IRenderer;
private initializeRendererPromise: Promise<void>;
constructor() { constructor() {
const canvas: HTMLCanvasElement = document.querySelector('canvas#main'); const canvas: HTMLCanvasElement = document.querySelector('canvas#main');
@ -46,10 +47,13 @@ export class Game implements IGame {
); );
this.renderer = new WebGl2Renderer(canvas, overlay); this.renderer = new WebGl2Renderer(canvas, overlay);
this.initializeRendererPromise = this.renderer.initialize();
this.initializeScene(); this.initializeScene();
this.physics.start(); this.physics.start();
}
public async start(): Promise<void> {
await this.initializeRendererPromise;
requestAnimationFrame(this.gameLoop.bind(this)); requestAnimationFrame(this.gameLoop.bind(this));
} }

View file

@ -0,0 +1,29 @@
export const waitWhileFalse = (body: () => boolean): Promise<void> => {
let resolveOnDone: () => void;
let rejectOnDone: (e: Error) => void;
const onDone = new Promise<void>((resolve, reject) => {
resolveOnDone = resolve;
rejectOnDone = reject;
});
const waiter = () => {
let success: boolean;
try {
success = body();
} catch (e) {
rejectOnDone(e);
return;
}
if (success) {
resolveOnDone();
} else {
requestAnimationFrame(waiter);
}
};
waiter();
return onDone;
};

View file

@ -0,0 +1,3 @@
export const wait = (ms: number): Promise<void> => {
return new Promise<void>((resolve, _) => setTimeout(resolve, ms));
};