]> zoso.dev Git - nano-pow.git/commitdiff
Implement busy status for gl. Wrap init in try block. Implement reset and average...
authorChris Duncan <chris@zoso.dev>
Fri, 17 Jan 2025 18:40:12 +0000 (10:40 -0800)
committerChris Duncan <chris@zoso.dev>
Fri, 17 Jan 2025 18:40:12 +0000 (10:40 -0800)
src/classes/gl.ts

index 17a3c1cd77b08b0a6a7d5f44e485a785ef2701a3..260de13773947bd4ebfff54edb2823144dfe502b 100644 (file)
@@ -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<void> {
+               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<boolean> {
-               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<void> => {
-                                       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<void> => {
+                                               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<string> {
+               if (this.#busy) {
+                       return new Promise(resolve => {
+                               setTimeout(async (): Promise<void> => {
+                                       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<boolean> {
+               if (this.#busy) {
+                       return new Promise(resolve => {
+                               setTimeout(async (): Promise<void> => {
+                                       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
        }