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))
])
/** 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 {
}
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)
}
* @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}`)
: 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) {
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
}
* @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))
found = false
}
}
+ this.#busy = false
if (found && nonce !== work) throw new Error(`Nonce found but does not match work`)
return found
}