From b1e60149f107c9cf37bc77bd1d98aff03a261cb8 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Fri, 17 Jan 2025 10:40:12 -0800 Subject: [PATCH] Implement busy status for gl. Wrap init in try block. Implement reset and average logging methods. Refactor checkqueryresult promises. Reset canvas size if user has changed effort. Add debug logging. --- src/classes/gl.ts | 221 +++++++++++++++++++++++++++++++--------------- 1 file changed, 151 insertions(+), 70 deletions(-) diff --git a/src/classes/gl.ts b/src/classes/gl.ts index 17a3c1c..260de13 100644 --- a/src/classes/gl.ts +++ b/src/classes/gl.ts @@ -6,6 +6,7 @@ import { NanoPowGlFragmentShader, NanoPowGlVertexShader } from '../shaders' import type { NanoPowOptions } from '../../types.d.ts' export class NanoPowGl { + static #busy: boolean = false /** Used to set canvas size. Must be a multiple of 256. */ static #WORKLOAD: number = 256 * Math.max(1, Math.floor(navigator.hardwareConcurrency)) @@ -38,67 +39,115 @@ export class NanoPowGl { ]) /** Compile */ - static async init () { - this.#gl = new OffscreenCanvas(this.#WORKLOAD, this.#WORKLOAD).getContext('webgl2') - if (this.#gl == null) throw new Error('WebGL 2 is required') - this.#gl.clearColor(0, 0, 0, 1) - - this.#program = this.#gl.createProgram() - if (this.#program == null) throw new Error('Failed to create shader program') - - this.#vertexShader = this.#gl.createShader(this.#gl.VERTEX_SHADER) - if (this.#vertexShader == null) throw new Error('Failed to create vertex shader') - this.#gl.shaderSource(this.#vertexShader, NanoPowGlVertexShader) - this.#gl.compileShader(this.#vertexShader) - if (!this.#gl.getShaderParameter(this.#vertexShader, this.#gl.COMPILE_STATUS)) - throw new Error(this.#gl.getShaderInfoLog(this.#vertexShader) ?? `Failed to compile vertex shader`) - - this.#fragmentShader = this.#gl.createShader(this.#gl.FRAGMENT_SHADER) - if (this.#fragmentShader == null) throw new Error('Failed to create fragment shader') - this.#gl.shaderSource(this.#fragmentShader, NanoPowGlFragmentShader) - this.#gl.compileShader(this.#fragmentShader) - if (!this.#gl.getShaderParameter(this.#fragmentShader, this.#gl.COMPILE_STATUS)) - throw new Error(this.#gl.getShaderInfoLog(this.#fragmentShader) ?? `Failed to compile fragment shader`) - - this.#gl.attachShader(this.#program, this.#vertexShader) - this.#gl.attachShader(this.#program, this.#fragmentShader) - this.#gl.linkProgram(this.#program) - if (!this.#gl.getProgramParameter(this.#program, this.#gl.LINK_STATUS)) - throw new Error(this.#gl.getProgramInfoLog(this.#program) ?? `Failed to link program`) - - /** Construct simple 2D geometry */ - this.#gl.useProgram(this.#program) - const triangleArray = this.#gl.createVertexArray() - this.#gl.bindVertexArray(triangleArray) - - this.#positionBuffer = this.#gl.createBuffer() - this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, this.#positionBuffer) - this.#gl.bufferData(this.#gl.ARRAY_BUFFER, this.#positions, this.#gl.STATIC_DRAW) - this.#gl.vertexAttribPointer(0, 3, this.#gl.FLOAT, false, 0, 0) - this.#gl.enableVertexAttribArray(0) - - this.#uvBuffer = this.#gl.createBuffer() - this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, this.#uvBuffer) - this.#gl.bufferData(this.#gl.ARRAY_BUFFER, this.#uvPosArray, this.#gl.STATIC_DRAW) - this.#gl.vertexAttribPointer(1, 2, this.#gl.FLOAT, false, 0, 0) - this.#gl.enableVertexAttribArray(1) - - this.#uboBuffer = this.#gl.createBuffer() - this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#uboBuffer) - this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 144, this.#gl.DYNAMIC_DRAW) - this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null) - this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 0, this.#uboBuffer) - this.#gl.uniformBlockBinding(this.#program, this.#gl.getUniformBlockIndex(this.#program, 'UBO'), 0) + static async init (): Promise { + if (this.#busy) return + this.#busy = true - this.#workBuffer = this.#gl.createBuffer() - this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#workBuffer) - this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 32, this.#gl.STREAM_DRAW) - this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null) - this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 1, this.#workBuffer) - this.#gl.uniformBlockBinding(this.#program, this.#gl.getUniformBlockIndex(this.#program, 'WORK'), 1) + try { + this.#gl = new OffscreenCanvas(this.#WORKLOAD, this.#WORKLOAD).getContext('webgl2') + if (this.#gl == null) throw new Error('WebGL 2 is required') + this.#gl.clearColor(0, 0, 0, 1) + + this.#program = this.#gl.createProgram() + if (this.#program == null) throw new Error('Failed to create shader program') + + this.#vertexShader = this.#gl.createShader(this.#gl.VERTEX_SHADER) + if (this.#vertexShader == null) throw new Error('Failed to create vertex shader') + this.#gl.shaderSource(this.#vertexShader, NanoPowGlVertexShader) + this.#gl.compileShader(this.#vertexShader) + if (!this.#gl.getShaderParameter(this.#vertexShader, this.#gl.COMPILE_STATUS)) + throw new Error(this.#gl.getShaderInfoLog(this.#vertexShader) ?? `Failed to compile vertex shader`) + + this.#fragmentShader = this.#gl.createShader(this.#gl.FRAGMENT_SHADER) + if (this.#fragmentShader == null) throw new Error('Failed to create fragment shader') + this.#gl.shaderSource(this.#fragmentShader, NanoPowGlFragmentShader) + this.#gl.compileShader(this.#fragmentShader) + if (!this.#gl.getShaderParameter(this.#fragmentShader, this.#gl.COMPILE_STATUS)) + throw new Error(this.#gl.getShaderInfoLog(this.#fragmentShader) ?? `Failed to compile fragment shader`) + + this.#gl.attachShader(this.#program, this.#vertexShader) + this.#gl.attachShader(this.#program, this.#fragmentShader) + this.#gl.linkProgram(this.#program) + if (!this.#gl.getProgramParameter(this.#program, this.#gl.LINK_STATUS)) + throw new Error(this.#gl.getProgramInfoLog(this.#program) ?? `Failed to link program`) + + /** Construct simple 2D geometry */ + this.#gl.useProgram(this.#program) + const triangleArray = this.#gl.createVertexArray() + this.#gl.bindVertexArray(triangleArray) + + this.#positionBuffer = this.#gl.createBuffer() + this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, this.#positionBuffer) + this.#gl.bufferData(this.#gl.ARRAY_BUFFER, this.#positions, this.#gl.STATIC_DRAW) + this.#gl.vertexAttribPointer(0, 3, this.#gl.FLOAT, false, 0, 0) + this.#gl.enableVertexAttribArray(0) + + this.#uvBuffer = this.#gl.createBuffer() + this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, this.#uvBuffer) + this.#gl.bufferData(this.#gl.ARRAY_BUFFER, this.#uvPosArray, this.#gl.STATIC_DRAW) + this.#gl.vertexAttribPointer(1, 2, this.#gl.FLOAT, false, 0, 0) + this.#gl.enableVertexAttribArray(1) + + this.#uboBuffer = this.#gl.createBuffer() + this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#uboBuffer) + this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 144, this.#gl.DYNAMIC_DRAW) + this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null) + this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 0, this.#uboBuffer) + this.#gl.uniformBlockBinding(this.#program, this.#gl.getUniformBlockIndex(this.#program, 'UBO'), 0) + + this.#workBuffer = this.#gl.createBuffer() + this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, this.#workBuffer) + this.#gl.bufferData(this.#gl.UNIFORM_BUFFER, 32, this.#gl.STREAM_DRAW) + this.#gl.bindBuffer(this.#gl.UNIFORM_BUFFER, null) + this.#gl.bindBufferBase(this.#gl.UNIFORM_BUFFER, 1, this.#workBuffer) + this.#gl.uniformBlockBinding(this.#program, this.#gl.getUniformBlockIndex(this.#program, 'WORK'), 1) + + this.#pixels = new Uint8Array(this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight * 4) + this.#query = this.#gl.createQuery() + } catch (err) { + throw new Error(`WebGL initialization failed. ${err}`) + } finally { + this.#busy = false + } + } + + static reset (): void { + NanoPowGl.#query = null + NanoPowGl.#workBuffer = null + NanoPowGl.#uboBuffer = null + NanoPowGl.#uvBuffer = null + NanoPowGl.#positionBuffer = null + NanoPowGl.#fragmentShader = null + NanoPowGl.#vertexShader = null + NanoPowGl.#program = null + NanoPowGl.#gl = null + NanoPowGl.#busy = false + NanoPowGl.init() + } - this.#pixels = new Uint8Array(this.#gl.drawingBufferWidth * this.#gl.drawingBufferHeight * 4) - this.#query = this.#gl.createQuery() + static #logAverages (times: number[]): void { + let count = times.length, sum = 0, reciprocals = 0, logarithms = 0, truncated = 0, min = 0xffff, max = 0, rate = 0 + times.sort() + for (let i = 0; i < count; i++) { + sum += times[i] + reciprocals += 1 / times[i] + logarithms += Math.log(times[i]) + min = Math.min(min, times[i]) + max = Math.max(max, times[i]) + if (count > 2 && i > (count * 0.1) && i < (count * 0.9)) truncated += times[i] + } + const averages = { + "Count (frames)": count, + "Total (ms)": sum, + "Rate (f/s)": 1000 * count / (truncated || sum), + "Minimum (ms)": min, + "Maximum (ms)": max, + "Arithmetic Mean (ms)": sum / count, + "Truncated Mean (ms)": truncated / count, + "Harmonic Mean (ms)": count / reciprocals, + "Geometric Mean (ms)": Math.exp(logarithms / count) + } + console.table(averages) } static #draw (work: Uint8Array): void { @@ -117,17 +166,18 @@ export class NanoPowGl { } static async #checkQueryResult (): Promise { - if (this.#gl == null || this.#query == null) throw new Error('WebGL 2 is required to check query results') - if (this.#gl.getQueryParameter(this.#query, this.#gl.QUERY_RESULT_AVAILABLE)) { - return !!(this.#gl.getQueryParameter(this.#query, this.#gl.QUERY_RESULT)) - } - /** Query result not yet available, check again in the next frame */ - return new Promise((resolve, reject): void => { + return new Promise((resolve, reject) => { try { - requestAnimationFrame(async (): Promise => { - const result = await NanoPowGl.#checkQueryResult() - resolve(result) - }) + if (this.#gl == null || this.#query == null) throw new Error('WebGL 2 is required to check query results') + if (this.#gl.getQueryParameter(this.#query, this.#gl.QUERY_RESULT_AVAILABLE)) { + resolve(!!(this.#gl.getQueryParameter(this.#query, this.#gl.QUERY_RESULT))) + } else { + /** Query result not yet available, check again in the next frame */ + requestAnimationFrame(async (): Promise => { + const result = await NanoPowGl.#checkQueryResult() + resolve(result) + }) + } } catch (err) { reject(err) } @@ -168,6 +218,15 @@ export class NanoPowGl { * @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation */ static async search (hash: string, options?: NanoPowOptions): Promise { + if (this.#busy) { + return new Promise(resolve => { + setTimeout(async (): Promise => { + const result = this.search(hash, options) + resolve(result) + }, 100) + }) + } + this.#busy = true if (NanoPowGl.#gl == null) throw new Error('WebGL 2 is required') if (this.#gl == null) throw new Error('WebGL 2 is required') if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`) @@ -179,6 +238,12 @@ export class NanoPowGl { : options.effort const debug = !!(options?.debug) + /** Reset if user specified new level of effort */ + if (this.#WORKLOAD !== 256 * effort) { + this.#WORKLOAD = 256 * effort + this.reset() + } + /** Set up uniform buffer object */ const uboView = new DataView(new ArrayBuffer(144)) for (let i = 0; i < 64; i += 8) { @@ -192,16 +257,22 @@ export class NanoPowGl { NanoPowGl.#gl.bindBuffer(NanoPowGl.#gl.UNIFORM_BUFFER, null) /** Start drawing to calculate one nonce per pixel */ + let times = [] + let start = performance.now() let nonce = null const seed = new Uint8Array(8) while (nonce == null) { + start = performance.now() crypto.getRandomValues(seed) this.#draw(seed) const found = await this.#checkQueryResult() + times.push(performance.now() - start) if (found) { nonce = this.#readResult(seed) } } + this.#busy = false + if (debug) this.#logAverages(times) return nonce } @@ -213,14 +284,23 @@ export class NanoPowGl { * @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation */ static async validate (work: string, hash: string, options?: NanoPowOptions): Promise { + if (this.#busy) { + return new Promise(resolve => { + setTimeout(async (): Promise => { + const result = this.validate(work, hash, options) + resolve(result) + }, 100) + }) + } + this.#busy = true if (NanoPowGl.#gl == null) throw new Error('WebGL 2 is required') if (this.#gl == null) throw new Error('WebGL 2 is required') if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new Error(`Invalid work ${work}`) if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`) - const debug = !!(options?.debug) const threshold = (typeof options?.threshold !== 'number' || options.threshold < 0x0 || options.threshold > 0xffffffff) ? 0xfffffff8 : options.threshold + const debug = !!(options?.debug) /** Set up uniform buffer object */ const uboView = new DataView(new ArrayBuffer(144)) @@ -248,6 +328,7 @@ export class NanoPowGl { found = false } } + this.#busy = false if (found && nonce !== work) throw new Error(`Nonce found but does not match work`) return found } -- 2.34.1