Implementation of Base58 encoding algorithm in a WebGPU compute shader
💻 See the demo here (your browser should support WebGPU)! The shader code itself is available at src/base58.wgsl.
This general algorithm was designed to work with arbitrary small input sizes (< 1000 bytes) and because of that it is highly inefficient due to the reasons detailed in the Limitations section below. Therefore, it is strongly recommended to modify the shader in the following ways for production:
- Use a small fixed input size appropriate for your use case
- Define
b58_byteslocally instead of using a slowvar<storage>variable - Use
uniformbuffers instead ofstoragebuffers for input
-
The
inputbuffer accepts an array of unsigned 32-bit integers. Each integer should be treated as 4 bytes in big-endian byte order. See thestringToU32Arraymethod in src/main.ts for JS implementation details. -
The
input_size_arrbuffer accepts an array with a single unsigned 32-bit integer, which indicates the size of the actual data ininputin bytes. For example, if there are 3 bytes of data, you still have to pad theinputbuffer size to 4 bytes in WebGPU. In this caseinput_size_arr[0]should be 3. -
The
b58_bytesbuffer is initialized with zeros and should have a size ofceil(input_size * 4 * 1.37)bytes (times 4 because each byte in input corresponds to 1u32of size 4 bytes inb58_bytes). This sizing accounts for the Base58 encoding expansion: since Base58 has a smaller radix than Base256, approximately 1.37 times more space is needed in a worst case scenario (derived from log(256) / log(58)). -
The result of the computation is composed of the
b58_bytesarray and 2 unsigned 32-bit integers inresult_info. Theresult_info[0]contains the count of leading zeros ('1' characters in Base58 alphabet), andresult_info[1]indicates the starting index of encoded digits inb58_bytes. The final string is obtained by translating numbers inb58_bytesto the Base58 alphabet starting from the index specified inresult_info[1]and prepending the string withresult_info[0]leading '1's.
-
This algorithm can theoretically process an input up to 747 MB in size. Due to a nested cycle, though, large inputs will most certainly take enormous amount of time to compute, which in turn will trigger a GPU watchdog timeout on most systems. In general, the shader was designed to only work with relatively small input sizes.
-
Another drawback is the use of
var<storage> b58_bytes, which is needed to work with arbitrary input sizes. However, accessing astoragevariable is way slower than accessing aprivateorfunctionvariable, and because we are constantly updating values inb58_bytes, this slows down the computation significantly.