diff --git a/packages/core/src/image/bmp.ts b/packages/core/src/image/bmp.ts new file mode 100644 index 0000000..81ca274 --- /dev/null +++ b/packages/core/src/image/bmp.ts @@ -0,0 +1,24 @@ +export const BMP_HEADER_LENGTH = 54 +export function writeBMPHeader(buf: Buffer, iconSize: number, iconBytes: number, imagePPM: number): void { + // Uses header format BITMAPINFOHEADER https://en.wikipedia.org/wiki/BMP_file_format + + // Bitmap file header + buf.write('BM') + buf.writeUInt32LE(iconBytes + 54, 2) + buf.writeInt16LE(0, 6) + buf.writeInt16LE(0, 8) + buf.writeUInt32LE(54, 10) // Full header size + + // DIB header (BITMAPINFOHEADER) + buf.writeUInt32LE(40, 14) // DIB header size + buf.writeInt32LE(iconSize, 18) + buf.writeInt32LE(iconSize, 22) + buf.writeInt16LE(1, 26) // Color planes + buf.writeInt16LE(24, 28) // Bit depth + buf.writeInt32LE(0, 30) // Compression + buf.writeInt32LE(iconBytes, 34) // Image size + buf.writeInt32LE(imagePPM, 38) // Horizontal resolution ppm + buf.writeInt32LE(imagePPM, 42) // Vertical resolution ppm + buf.writeInt32LE(0, 46) // Colour pallette size + buf.writeInt32LE(0, 50) // 'Important' Colour count +} diff --git a/packages/core/src/image/index.ts b/packages/core/src/image/index.ts new file mode 100644 index 0000000..6310229 --- /dev/null +++ b/packages/core/src/image/index.ts @@ -0,0 +1,26 @@ +import { jsImageToByteArray } from './nodejs' +import { InternalFillImageOptions } from '../models/base' +import { FillImageTargetOptions } from './options' + +export * from './bmp' +export * from './options' + +// let wasmAppearsOk = true + +export function imageToByteArray( + imageBuffer: Buffer, + sourceOptions: InternalFillImageOptions, + targetOptions: FillImageTargetOptions, + destPadding: number, + imageSize: number +): Buffer { + // try { + // if (wasmAppearsOk) { + // wasmImageToByteArray + // } + // } catch (e) { + // wasmAppearsOk = false + // } + return jsImageToByteArray(imageBuffer, sourceOptions, targetOptions, destPadding, imageSize) + // +} diff --git a/packages/core/src/util.ts b/packages/core/src/image/nodejs.ts similarity index 57% rename from packages/core/src/util.ts rename to packages/core/src/image/nodejs.ts index bfbc467..df4957d 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/image/nodejs.ts @@ -1,13 +1,7 @@ -import { InternalFillImageOptions } from './models/base' +import { InternalFillImageOptions } from '../models/base' +import { FillImageTargetOptions } from '../image/options' -export interface FillImageTargetOptions { - colorMode: 'bgr' | 'rgba' - xFlip?: boolean - yFlip?: boolean - rotate?: boolean -} - -export function imageToByteArray( +export function jsImageToByteArray( imageBuffer: Buffer, sourceOptions: InternalFillImageOptions, targetOptions: FillImageTargetOptions, @@ -56,28 +50,3 @@ export function imageToByteArray( return byteBuffer } - -export const BMP_HEADER_LENGTH = 54 -export function writeBMPHeader(buf: Buffer, iconSize: number, iconBytes: number, imagePPM: number): void { - // Uses header format BITMAPINFOHEADER https://en.wikipedia.org/wiki/BMP_file_format - - // Bitmap file header - buf.write('BM') - buf.writeUInt32LE(iconBytes + 54, 2) - buf.writeInt16LE(0, 6) - buf.writeInt16LE(0, 8) - buf.writeUInt32LE(54, 10) // Full header size - - // DIB header (BITMAPINFOHEADER) - buf.writeUInt32LE(40, 14) // DIB header size - buf.writeInt32LE(iconSize, 18) - buf.writeInt32LE(iconSize, 22) - buf.writeInt16LE(1, 26) // Color planes - buf.writeInt16LE(24, 28) // Bit depth - buf.writeInt32LE(0, 30) // Compression - buf.writeInt32LE(iconBytes, 34) // Image size - buf.writeInt32LE(imagePPM, 38) // Horizontal resolution ppm - buf.writeInt32LE(imagePPM, 42) // Vertical resolution ppm - buf.writeInt32LE(0, 46) // Colour pallette size - buf.writeInt32LE(0, 50) // 'Important' Colour count -} diff --git a/packages/core/src/image/options.ts b/packages/core/src/image/options.ts new file mode 100644 index 0000000..46b205c --- /dev/null +++ b/packages/core/src/image/options.ts @@ -0,0 +1,6 @@ +export interface FillImageTargetOptions { + colorMode: 'bgr' | 'rgba' + xFlip?: boolean + yFlip?: boolean + rotate?: boolean +} diff --git a/packages/core/src/image/wasm.ts b/packages/core/src/image/wasm.ts new file mode 100644 index 0000000..949baf2 --- /dev/null +++ b/packages/core/src/image/wasm.ts @@ -0,0 +1,131 @@ +// Hack to get WebAssembly importable until https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/826 +/// + +import * as path from 'path' +import * as fs from 'fs' + +import { InternalFillImageOptions } from '../models/base' +import { FillImageTargetOptions } from './options' + +const imports = {} +let wasm: any = undefined + +let cachegetUint8Memory0: Uint8Array | null = null +function getUint8Memory0() { + if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer) + } + return cachegetUint8Memory0 +} + +export function wasmImageToByteArray( + imageBuffer: Uint8Array, + sourceOptions: InternalFillImageOptions, + targetOptions: FillImageTargetOptions, + destPadding: number, + imageSize: number +): Uint8Array { + /** + * This is based on what is given by wasm-pack, but has been adapted for better performance. + * A lot of time was being spent on copying Buffers into wasm memory. + * + * We want to support taking subregions of the input Buffer/image. But with the default wrapping that would + * result in copying in the full image every call. Or we could copy into a smaller buffer first, but that would + * result in two copies. We can optimise away this excessive copying and keep it down at one copy. + * + * We know that the input value of dest is not interesting to wasm, so we can skip copying that + * + * Strings have been avoided, as they also incur an overhead + */ + + const destLen = imageSize * imageSize * targetOptions.colorMode.length + const byteBuffer = Buffer.alloc(destPadding + destLen) + + const srcFormatLen = sourceOptions.format.length + const srcLen = imageSize * imageSize * srcFormatLen + + const flipColours = sourceOptions.format.substring(0, 3) !== targetOptions.colorMode.substring(0, 3) + + let destPtr: any + + try { + const srcPtr = wasm.__wbindgen_malloc(srcLen) + + // Copy in the image subregion + const memory = getUint8Memory0() + const row_len = imageSize * srcFormatLen + if (sourceOptions.stride === row_len) { + // stride is a row, so we can take a shortcut + memory.set(imageBuffer.subarray(sourceOptions.offset, sourceOptions.offset + row_len * imageSize), 0) + } else { + // stride is more than a row, so we need to copy line by line + for (let y = 0; y < imageSize; y++) { + const row_start_src = y * sourceOptions.stride + sourceOptions.offset + + memory.set(imageBuffer.subarray(row_start_src, row_start_src + row_len), y * row_len) + } + } + + destPtr = wasm.__wbindgen_malloc(destLen) + + wasmFunction( + srcPtr, + srcLen, + destPtr, + destLen, + srcFormatLen, + targetOptions.colorMode.length, + flipColours, + targetOptions.xFlip ?? false, + targetOptions.yFlip ?? false, + targetOptions.rotate ?? false, + imageSize + ) + + // Copy the result into the buffer + byteBuffer.set(getUint8Memory0().subarray(destPtr, destPtr + destLen), destPadding) + } finally { + // srcPtr is freed by the wasm binary + + wasm.__wbindgen_free(destPtr, destLen) + } + + return byteBuffer +} + +function wasmFunction( + srcPtr: unknown, + srcLen: number, + destPtr: unknown, + destLen: number, + src_format_len: number, + dest_format_len: number, + flip_colours: boolean, + x_flip: boolean, + y_flip: boolean, + rotate: boolean, + image_size: number +): void { + // Dange: the following call needs to match the rust implementation + wasm.image_to_byte_array( + srcPtr, + srcLen, + destPtr, + destLen, + src_format_len, + dest_format_len, + flip_colours, + x_flip, + y_flip, + rotate, + image_size + ) +} + +const wasmPath = path.join(__dirname, '../../../../wasm/pkg/streamdeck_wasm_bg.wasm') +const wasmBytes = fs.readFileSync(wasmPath) + +const wasmModule = new WebAssembly.Module(wasmBytes) +const wasmInstance = new WebAssembly.Instance(wasmModule, imports) +wasm = wasmInstance.exports +module.exports.__wasm = wasm diff --git a/packages/core/src/models/base-gen2.ts b/packages/core/src/models/base-gen2.ts index 2bbec2e..3c62722 100644 --- a/packages/core/src/models/base-gen2.ts +++ b/packages/core/src/models/base-gen2.ts @@ -1,5 +1,5 @@ import { HIDDevice } from '../device' -import { imageToByteArray } from '../util' +import { imageToByteArray } from '../image' import { EncodeJPEGHelper, InternalFillImageOptions, diff --git a/packages/core/src/models/mini.ts b/packages/core/src/models/mini.ts index d9f69b7..aa7b81d 100644 --- a/packages/core/src/models/mini.ts +++ b/packages/core/src/models/mini.ts @@ -1,5 +1,5 @@ import { HIDDevice } from '../device' -import { BMP_HEADER_LENGTH, imageToByteArray, writeBMPHeader } from '../util' +import { BMP_HEADER_LENGTH, imageToByteArray, writeBMPHeader } from '../image' import { InternalFillImageOptions, OpenStreamDeckOptions, StreamDeckProperties } from './base' import { StreamDeckGen1Base } from './base-gen1' import { DeviceModelId } from './id' diff --git a/packages/core/src/models/original.ts b/packages/core/src/models/original.ts index 3822481..b4a3cbc 100644 --- a/packages/core/src/models/original.ts +++ b/packages/core/src/models/original.ts @@ -1,5 +1,5 @@ import { HIDDevice } from '../device' -import { BMP_HEADER_LENGTH, imageToByteArray, writeBMPHeader } from '../util' +import { BMP_HEADER_LENGTH, imageToByteArray, writeBMPHeader } from '../image' import { InternalFillImageOptions, OpenStreamDeckOptions, StreamDeckProperties } from './base' import { StreamDeckGen1Base } from './base-gen1' import { DeviceModelId, KeyIndex } from './id' diff --git a/run.js b/run.js new file mode 100644 index 0000000..8977369 --- /dev/null +++ b/run.js @@ -0,0 +1,57 @@ +/* eslint-disable node/no-unpublished-require */ +const { jsImageToByteArray } = require('./packages/core/dist/image/nodejs') +// const wasm = require('./wasm/pkg/streamdeck_wasm') +// const wasm = require('./wasm/wasm2') +const { wasmImageToByteArray } = require('./packages/core/dist/image/wasm') + +const buf = Buffer.alloc(72 * 72 * 4) + +for (let aaa = 0; aaa < 10; aaa++) { + const count = 1000 + let v + const start = Date.now() + for (let i = 0; i < count; i++) { + v = jsImageToByteArray( + buf, + { + format: 'rgba', + offset: 0, + stride: 72 * 3, + }, + { + colorMode: 'bgr', + rotate: true, + xFlip: true, + yFlip: true, + }, + 0, + 72 + ) + } + const done = Date.now() + console.log(`js took: ${done - start}ms over ${count} samples`) + + const start2 = Date.now() + for (let i = 0; i < count; i++) { + // const b = Buffer.alloc(72 * 72 * 3) + // v = wasm.hello(buf, b, 'rgba', 72 * 3, 0, 'bgr', 0, true, true, true, 72) + v = wasmImageToByteArray( + buf, + { + format: 'rgba', + offset: 0, + stride: 72 * 3, + }, + { + colorMode: 'bgr', + rotate: true, + xFlip: true, + yFlip: true, + }, + 0, + 72 + ) + } + const done2 = Date.now() + console.log(`wasm took: ${done2 - start2}ms over ${count} samples`) +} diff --git a/wasm/.gitignore b/wasm/.gitignore new file mode 100644 index 0000000..453b4c5 --- /dev/null +++ b/wasm/.gitignore @@ -0,0 +1,3 @@ +target +pkg +.idea diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock new file mode 100644 index 0000000..c29b4ac --- /dev/null +++ b/wasm/Cargo.lock @@ -0,0 +1,126 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "streamdeck-wasm" +version = "0.1.0" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 0000000..f09bbe7 --- /dev/null +++ b/wasm/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "streamdeck-wasm" +version = "0.1.0" +edition = "2021" +license = "MIT/Apache-2.0" + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs new file mode 100644 index 0000000..7585687 --- /dev/null +++ b/wasm/src/lib.rs @@ -0,0 +1,64 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn image_to_byte_array( + src: &[u8], + dest: &mut [u8], + src_format_len: usize, + dest_format_len: usize, + flip_colours: bool, + x_flip: bool, + y_flip: bool, + rotate: bool, + image_size: usize, +) { + // TODO - check buffer lengths? + + let source_stride = image_size * src_format_len; + let dest_stride = image_size * dest_format_len; + + for y in 0..image_size { + let row_offset = dest_stride * y; + for x in 0..image_size { + let (x2, y2) = { + let mut x2 = x; + let mut y2 = y; + + if x_flip { + x2 = image_size - x2 - 1; + } + if y_flip { + y2 = image_size - y2 - 1; + } + + if rotate { + (y2, x2) + } else { + (x2, y2) + } + }; + + let src_offset = y2 * source_stride + x2 * src_format_len; + + let red = src[src_offset]; + let green = src[src_offset + 1]; + let blue = src[src_offset + 2]; + + let target_offset = row_offset + x * dest_format_len; + if flip_colours { + dest[target_offset] = blue; + dest[target_offset + 1] = green; + dest[target_offset + 2] = red; + } else { + dest[target_offset] = red; + dest[target_offset + 1] = green; + dest[target_offset + 2] = blue; + } + + if dest_format_len > 3 { + dest[target_offset + 3] = 255; + } + } + } + +}