From: Chris Duncan Date: Fri, 17 Jan 2025 18:40:12 +0000 (-0800) Subject: Implement busy status for gl. Wrap init in try block. Implement reset and average... X-Git-Tag: v2.0.0~29 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=b1e60149f107c9cf37bc77bd1d98aff03a261cb8;p=nano-pow.git 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. --- 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 }