Skip to content

reimplement imageToByteArray in wasm #50

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/core/src/image/bmp.ts
Original file line number Diff line number Diff line change
@@ -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
}
26 changes: 26 additions & 0 deletions packages/core/src/image/index.ts
Original file line number Diff line number Diff line change
@@ -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)
//
}
37 changes: 3 additions & 34 deletions packages/core/src/util.ts → packages/core/src/image/nodejs.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
}
6 changes: 6 additions & 0 deletions packages/core/src/image/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface FillImageTargetOptions {
colorMode: 'bgr' | 'rgba'
xFlip?: boolean
yFlip?: boolean
rotate?: boolean
}
131 changes: 131 additions & 0 deletions packages/core/src/image/wasm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Hack to get WebAssembly importable until https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/826
/// <reference lib="dom" />

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
2 changes: 1 addition & 1 deletion packages/core/src/models/base-gen2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HIDDevice } from '../device'
import { imageToByteArray } from '../util'
import { imageToByteArray } from '../image'
import {
EncodeJPEGHelper,
InternalFillImageOptions,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/models/mini.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/models/original.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
57 changes: 57 additions & 0 deletions run.js
Original file line number Diff line number Diff line change
@@ -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`)
}
3 changes: 3 additions & 0 deletions wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target
pkg
.idea
Loading