export class NanoPowGpu {
// Initialize WebGPU
+ static #busy: boolean = false
static #device: GPUDevice | null = null
static #uboBuffer: GPUBuffer
static #gpuBuffer: GPUBuffer
}
// Initialize WebGPU
- static init () {
+ static async init () {
+ if (this.#busy) return
+ this.#busy = true
// Request device and adapter
if (navigator.gpu == null) {
throw new Error('WebGPU is not supported in this browser.')
}
- navigator.gpu.requestAdapter()
- .then(adapter => {
- if (adapter == null) {
- throw new Error('WebGPU adapter refused by browser.')
- }
- adapter.requestDevice()
- .then(device => {
- this.#device = device
- this.#device.lost.then(loss => {
- console.dir(loss)
- console.warn(loss.reason, loss.message)
- console.warn(`Device lost. Reinitializing...`)
- this.init()
- })
-
- // Create buffers for writing GPU calculations and reading from Javascript
- this.#uboBuffer = this.#device.createBuffer({
- size: 48,
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
- })
- this.#gpuBuffer = this.#device.createBuffer({
- size: 16,
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
- })
- this.#cpuBuffer = this.#device.createBuffer({
- size: 16,
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
- })
-
- // Create binding group data structure and use it later once UBO is known
- this.#bindGroupLayout = this.#device.createBindGroupLayout({
- entries: [
- {
- binding: 0,
- visibility: GPUShaderStage.COMPUTE,
- buffer: {
- type: 'uniform'
- },
- },
- {
- binding: 1,
- visibility: GPUShaderStage.COMPUTE,
- buffer: {
- type: 'storage'
- },
- }
- ]
- })
-
- // Create pipeline to connect compute shader to binding layout
- this.#pipeline = this.#device.createComputePipeline({
- layout: this.#device.createPipelineLayout({
- bindGroupLayouts: [this.#bindGroupLayout]
- }),
- compute: {
- entryPoint: 'main',
- module: this.#device.createShaderModule({
- code: NanoPowGpuComputeShader
- })
- }
- })
- })
+ try {
+ const adapter = await navigator.gpu.requestAdapter()
+ if (adapter == null) {
+ throw new Error('WebGPU adapter refused by browser.')
+ }
+ const device = await adapter.requestDevice()
+ if (!(device instanceof GPUDevice)) {
+ throw new Error('WebGPU device failed to load.')
+ }
+ device.lost.then(loss => {
+ console.dir(loss)
+ console.warn(`Device lost. Reinitializing...`)
+ this.#cpuBuffer?.destroy()
+ this.#gpuBuffer?.destroy()
+ this.#uboBuffer?.destroy()
+ this.init()
})
- .catch(err => { throw new Error(err.message) })
+ this.#device = device
+ this.setup()
+ } catch (err) {
+ throw new Error(`WebGPU initialization failed. ${err}`)
+ } finally {
+ this.#busy = false
+ }
+ }
+
+ static setup () {
+ if (this.#device == null) throw new Error(`WebGPU device failed to load.`)
+ // Create buffers for writing GPU calculations and reading from Javascript
+ this.#uboBuffer = this.#device.createBuffer({
+ size: 48,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
+ })
+ this.#gpuBuffer = this.#device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
+ })
+ this.#cpuBuffer = this.#device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
+ })
+ // Create binding group data structure and use it later once UBO is known
+ this.#bindGroupLayout = this.#device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'uniform'
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: 'storage'
+ },
+ }
+ ]
+ })
+ // Create pipeline to connect compute shader to binding layout
+ this.#pipeline = this.#device.createComputePipeline({
+ layout: this.#device.createPipelineLayout({
+ bindGroupLayouts: [this.#bindGroupLayout]
+ }),
+ compute: {
+ entryPoint: 'main',
+ module: this.#device.createShaderModule({
+ code: NanoPowGpuComputeShader
+ })
+ }
+ })
}
/**
* @param {number} [threshold=0xfffffff8] - Difficulty of proof-of-work calculation
*/
static async search (hash: string, threshold: number = 0xfffffff8): Promise<string> {
+ if (this.#busy) {
+ return new Promise(resolve => {
+ setTimeout(async () => {
+ const result = this.search(hash, threshold)
+ resolve(result)
+ }, 100)
+ })
+ }
+ this.#busy = true
if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`)
- if (typeof threshold !== 'number') throw new TypeError(`Invalid threshold ${threshold}`)
+ if (typeof threshold !== 'number') throw new TypeError(`Invalid threshold ${threshold}`)
- // Ensure WebGPU is initialized before calculating, up to a max time frame
- while (this.#device == null && performance.now() < 8000) {
+ // Ensure WebGPU is initialized before calculating
+ let loads = 0
+ while (this.#device == null && loads < 20) {
await new Promise(resolve => {
setTimeout(resolve, 500)
})
}
if (this.#device == null) throw new Error(`WebGPU device failed to load.`)
- // Set up uniform buffer object
- // Note: u32 size is 4, but total alignment must be multiple of 16
- const uboView = new DataView(new ArrayBuffer(48))
- for (let i = 0; i < 64; i += 8) {
- const uint32 = hash.slice(i, i + 8)
- uboView.setUint32(i / 2, parseInt(uint32, 16))
- }
- const random = Math.floor((Math.random() * 0xffffffff))
- uboView.setUint32(32, random, true)
- uboView.setUint32(36, threshold, true)
- this.#device.queue.writeBuffer(this.#uboBuffer, 0, uboView)
-
- // Reset `found` flag to 0u in WORK before each calculation
- this.#device.queue.writeBuffer(this.#gpuBuffer, 8, new Uint32Array([0]))
-
- // Bind UBO read and GPU write buffers
- const bindGroup = this.#device.createBindGroup({
- layout: this.#bindGroupLayout,
- entries: [
- {
- binding: 0,
- resource: {
- buffer: this.#uboBuffer
- },
- },
- {
- binding: 1,
- resource: {
- buffer: this.#gpuBuffer
- },
- },
- ],
- })
-
- // Create command encoder to issue commands to GPU and initiate computation
- const commandEncoder = this.#device.createCommandEncoder()
- const passEncoder = commandEncoder.beginComputePass()
-
- // Issue commands and end compute pass structure
- passEncoder.setPipeline(this.#pipeline)
- passEncoder.setBindGroup(0, bindGroup)
- passEncoder.dispatchWorkgroups(256, 256, 256)
- passEncoder.end()
-
- // Copy 8-byte nonce and 4-byte found flag from GPU to CPU for reading
- commandEncoder.copyBufferToBuffer(
- this.#gpuBuffer,
- 0,
- this.#cpuBuffer,
- 0,
- 12
- )
-
- // End computation by passing array of command buffers to command queue for execution
- this.#device.queue.submit([commandEncoder.finish()])
-
- // Read results back to Javascript and then unmap buffer after reading
- await this.#cpuBuffer.mapAsync(GPUMapMode.READ)
- await this.#device.queue.onSubmittedWorkDone()
+ let nonce = 0n
+ let found = false
try {
- const data = new DataView(this.#cpuBuffer.getMappedRange())
- const nonce = data.getBigUint64(0, true)
- const found = !!data.getUint32(8)
- this.#cpuBuffer.unmap()
-
- if (found) {
- const hex = nonce.toString(16).padStart(16, '0')
- return hex
- } else {
- return await this.search(hash, threshold)
- }
+ do {
+ // Set up uniform buffer object
+ // Note: u32 size is 4, but total alignment must be multiple of 16
+ const uboView = new DataView(new ArrayBuffer(48))
+ for (let i = 0; i < 64; i += 8) {
+ const uint32 = hash.slice(i, i + 8)
+ uboView.setUint32(i / 2, parseInt(uint32, 16))
+ }
+ const random = Math.floor((Math.random() * 0xffffffff))
+ uboView.setUint32(32, random, true)
+ uboView.setUint32(36, threshold, true)
+ this.#device.queue.writeBuffer(this.#uboBuffer, 0, uboView)
+
+ // Reset `found` flag to 0u in WORK before each calculation
+ this.#device.queue.writeBuffer(this.#gpuBuffer, 8, new Uint32Array([0]))
+
+ // Bind UBO read and GPU write buffers
+ const bindGroup = this.#device.createBindGroup({
+ layout: this.#bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: this.#uboBuffer
+ },
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: this.#gpuBuffer
+ },
+ },
+ ],
+ })
+
+ // Create command encoder to issue commands to GPU and initiate computation
+ const commandEncoder = this.#device.createCommandEncoder()
+ const passEncoder = commandEncoder.beginComputePass()
+
+ // Issue commands and end compute pass structure
+ passEncoder.setPipeline(this.#pipeline)
+ passEncoder.setBindGroup(0, bindGroup)
+ passEncoder.dispatchWorkgroups(256, 256, 256)
+ passEncoder.end()
+
+ // Copy 8-byte nonce and 4-byte found flag from GPU to CPU for reading
+ commandEncoder.copyBufferToBuffer(
+ this.#gpuBuffer,
+ 0,
+ this.#cpuBuffer,
+ 0,
+ 12
+ )
+
+ // End computation by passing array of command buffers to command queue for execution
+ this.#device.queue.submit([commandEncoder.finish()])
+
+ // Read results back to Javascript and then unmap buffer after reading
+ await this.#cpuBuffer.mapAsync(GPUMapMode.READ)
+ await this.#device.queue.onSubmittedWorkDone()
+ const dataBuffer = this.#cpuBuffer.getMappedRange().slice(0)
+ this.#cpuBuffer.unmap()
+
+ if (dataBuffer == null) throw new Error(`Failed to get data from buffer.`)
+ const dataView = new DataView(dataBuffer)
+ nonce = dataView.getBigUint64(0, true)
+ found = !!dataView.getUint32(8)
+ } while (!found)
} catch (err) {
- console.warn(`Error getting data from GPU, retrying. ${err}`)
- return await this.search(hash, threshold)
+ console.warn(`Error getting data from GPU. ${err}`)
+ } finally {
+ this.#busy = false
+ return nonce.toString(16).padStart(16, '0')
}
}
}