Skip to content

Commit

Permalink
Improve stability of WASM version
Browse files Browse the repository at this point in the history
  • Loading branch information
kikuchan committed Jan 22, 2024
1 parent 4bd8abf commit 33ced36
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 494 deletions.
30 changes: 23 additions & 7 deletions dist/Qrean.d.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
export declare const setText: (mem: Uint8ClampedArray, idx: number, s: string) => void;
export declare const getText: (mem: Uint8ClampedArray, idx: number) => string;
type CreateOptions = {
pageSize?: number;
debug?: boolean;
};
type EncodeOptions = {
code_type?: keyof typeof Qrean.CODE_TYPES;
data_type?: keyof typeof Qrean.DATA_TYPES;
qr_version?: keyof typeof Qrean.QR_VERSIONS;
qr_maskpattern?: keyof typeof Qrean.QR_MASKPATTERNS;
qr_errorlevel?: keyof typeof Qrean.QR_ERRORLEVELS;
scale?: number;
padding?: number;
padding?: number[];
};
type DetectOptions = {
gamma?: number;
digitized?: Uint8ClampedArray;
eci_code?: 'UTF-8' | 'ShiftJIS' | 'Latin1';
outbuf_size?: number;
digitized?: Uint8ClampedArray;
};
type Image = {
width: number;
height: number;
data: Uint8ClampedArray;
};
export declare class Qrean {
wasm: WebAssembly.WebAssemblyInstantiatedSource;
on_found?: (type: string, str: string) => void;
static create(debug?: boolean): Promise<Qrean>;
heap: number;
memory: WebAssembly.Memory;
static create(opts?: CreateOptions): Promise<Qrean>;
private constructor();
memreset(): void;
static CODE_TYPE_QR: "QR";
static CODE_TYPE_MQR: "mQR";
static CODE_TYPE_RMQR: "rMQR";
Expand Down Expand Up @@ -251,7 +262,12 @@ export declare class Qrean {
ShiftJIS: 20;
"UTF-8": 26;
};
encode(text: string, opts?: EncodeOptions | keyof typeof Qrean.CODE_TYPES): ImageData;
detect(imgdata: ImageData, callback: (type: string, str: string) => void, opts?: DetectOptions): any;
encode(text: string, opts?: EncodeOptions | keyof typeof Qrean.CODE_TYPES): Image;
private allocImage;
private readImage;
detect(imgdata: Image, callback: (type: string, str: string) => void, opts?: DetectOptions): {
detected: number;
digitized: Image;
};
}
export {};
126 changes: 83 additions & 43 deletions dist/Qrean.js

Large diffs are not rendered by default.

Binary file modified dist/qrean-detect.exe
Binary file not shown.
Binary file modified dist/qrean.exe
Binary file not shown.
3 changes: 2 additions & 1 deletion src/galois.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ typedef uint8_t gf2_value_t;
#define GF2_POLY_COEFF(x, i) ((x)[1 + (i)])
// #define GF2_POLY_COEFF(x, i) ((x)[1 + GF2_POLY_DEGREE(x) - (i)])
#define CREATE_GF2_POLY(name, degree) \
gf2_value_t name[GF2_POLY_SIZE((degree))] = {}; \
gf2_value_t name[GF2_POLY_SIZE((degree))]; \
memset(name, 0, sizeof(name)); \
GF2_POLY_DEGREE(name) = (degree);

typedef gf2_value_t gf2_poly_t;
Expand Down
3 changes: 2 additions & 1 deletion src/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ void image_init(image_t *img, size_t width, size_t height, void *buffer);
#ifdef NO_MALLOC
// stack version
#define CREATE_IMAGE(name, width, height) \
image_pixel_t _##name##__buffer[(width) * (height)] = {}; \
image_pixel_t _##name##__buffer[(width) * (height)]; \
memset(_##name##__buffer, 0, sizeof(_##name##__buffer)); \
image_t name = create_image((width), (height), _##name##__buffer);

#define DESTROY_IMAGE(name) (void)(name);
Expand Down
15 changes: 9 additions & 6 deletions wasm/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ CFLAGS += --target=wasm32
CFLAGS += -O3
CFLAGS += -nostdlib
CFLAGS += -DUSE_JS_MATH
CFLAGS += -DNO_MALLOC
#CFLAGS += -DNO_MALLOC
#CFLAGS += -DNO_DEBUG
CFLAGS += -DNO_PRINTF
CFLAGS += -I../src
Expand All @@ -26,18 +26,21 @@ CFLAGS += -I$(abspath ./tinymm)
LDFLAGS += -Wl,--no-entry
LDFLAGS += -Wl,--export-all
LDFLAGS += -Wl,--allow-undefined
LDFLAGS += -Wl,--initial-memory=6553600
#LDFLAGS += -Wl,--initial-memory=6553600
#LDFLAGS += -Wl,--max-memory=6553600
LDFLAGS += -Wl,-z,stack-size=655360
#LDFLAGS += -Wl,-z,stack-size=3276800
#LDFLAGS += -Wl,--import-memory

all: $(BUILDDIR)/qrean.wasm.js
all: $(BUILDDIR)/Qrean.js

$(BUILDDIR)/libqrean.a:
@CC=$(CC) AR=$(AR) CFLAGS="$(CFLAGS)" BUILDDIR=$(BUILDDIR) $(MAKE) -C ../src
@CC=$(CC) AR=$(AR) NO_SHLIB=1 CFLAGS="$(CFLAGS)" BUILDDIR=$(BUILDDIR) $(MAKE) -C ../src

$(BUILDDIR)/qrean.wasm.js: $(BUILDDIR)/libqrean.a
$(BUILDDIR)/qrean.wasm.js: $(SRCS) $(BUILDDIR)/libqrean.a Makefile
$(CC) $(CFLAGS) -nostartfiles $(SRCS) $(BUILDDIR)/libqrean.a -o $(BUILDDIR)/qrean.wasm $(LDFLAGS)
deno run -A https://code4fukui.github.io/bin2js/bin2js.js $(BUILDDIR)/qrean.wasm

$(BUILDDIR)/Qrean.js: $(BUILDDIR)/qrean.wasm.js Qrean.ts Makefile
deno bundle Qrean.ts $(BUILDDIR)/Qrean.js
npx tsc --declaration --emitDeclarationOnly --declarationDir $(BUILDDIR) Qrean.ts
cp $(BUILDDIR)/Qrean.js ../dist/
Expand Down
163 changes: 116 additions & 47 deletions wasm/Qrean.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import wasmbin from "../build/wasm/qrean.wasm.js";

export const setText = (mem: Uint8ClampedArray, idx: number, s: string) => {
const sbin = new TextEncoder().encode(s + "\0");
mem.set(sbin, idx);
const toCString = (s: string) => {
return new TextEncoder().encode(s + "\0");
};

export const getText = (mem: Uint8ClampedArray, idx: number) => {
const fromCString = (memory: WebAssembly.Memory, idx: number) => {
const mem = new Uint8ClampedArray(memory.buffer);
for (let i = 0;; i++) {
if (mem[idx + i] == 0) {
const s = new TextDecoder().decode(mem.slice(idx, idx + i));
Expand All @@ -14,29 +14,45 @@ export const getText = (mem: Uint8ClampedArray, idx: number) => {
}
};

type CreateOptions = {
pageSize?: number;
debug?: boolean;
};

type EncodeOptions = {
code_type?: keyof typeof Qrean.CODE_TYPES;
data_type?: keyof typeof Qrean.DATA_TYPES;
qr_version?: keyof typeof Qrean.QR_VERSIONS;
qr_maskpattern?: keyof typeof Qrean.QR_MASKPATTERNS;
qr_errorlevel?: keyof typeof Qrean.QR_ERRORLEVELS;
scale?: number;
padding?: number;
padding?: number[];
};

type DetectOptions = {
gamma?: number;
digitized?: Uint8ClampedArray;
eci_code?: 'UTF-8' | 'ShiftJIS' | 'Latin1';
outbuf_size?: number;
digitized?: Uint8ClampedArray;
};

type Image = {
width: number;
height: number;
data: Uint8ClampedArray;
}

export class Qrean {
wasm: WebAssembly.WebAssemblyInstantiatedSource;
on_found?: (type: string, str: string) => void;
heap: number;

memory: WebAssembly.Memory;

static async create(debug?: boolean) {
let memory: WebAssembly.ExportValue;
static async create(opts: CreateOptions = {}) {
let memory: WebAssembly.Memory;
let obj: Qrean;

const importObject = {
env: {
atan2: (y: number, x: number) => Math.atan2(y, x),
Expand All @@ -48,29 +64,41 @@ export class Qrean {
roundf: (f: number) => Math.round(f),

on_found: function (type: number, ptr: number) {
const result = getText(new Uint8ClampedArray((memory as WebAssembly.Memory).buffer), ptr);
const result = fromCString(memory, ptr);
const typestr = Object.entries(Qrean.CODE_TYPES).find(([_, v]) => v == type)?.[0] ?? 'Unknown';
if (obj.on_found) obj.on_found.call(obj, typestr, result);
},

debug: function (ptr: number) {
const result = getText(new Uint8ClampedArray((memory as WebAssembly.Memory).buffer), ptr);
const result = fromCString(memory, ptr);
console.log("DEBUG:", result);
},
},
};

const wasm = await WebAssembly.instantiate(wasmbin, importObject);
memory = wasm.instance.exports.memory;

if (debug) {
memory = wasm.instance.exports.memory as WebAssembly.Memory;

if (opts.debug) {
(wasm.instance.exports as any).enable_debug();
}

return obj = new Qrean(wasm);
return (obj = new Qrean(wasm, memory, opts.pageSize ?? 100));
}

private constructor(wasm: WebAssembly.WebAssemblyInstantiatedSource) {
private constructor(wasm: WebAssembly.WebAssemblyInstantiatedSource, memory: WebAssembly.Memory, pageSize: number) {
this.wasm = wasm;
this.memory = memory;
this.heap = (wasm.instance.exports.__heap_base as WebAssembly.Global).value;
this.memory.grow(pageSize);
}

memreset() {
const exp: any = this.wasm.instance.exports;

new Uint8ClampedArray(this.memory.buffer).fill(0, this.heap);
exp.tinymm_init(this.heap, this.memory.buffer.byteLength - this.heap, 100);
}

// type
Expand Down Expand Up @@ -317,77 +345,118 @@ export class Qrean {
};

encode(text: string, opts: EncodeOptions | keyof typeof Qrean.CODE_TYPES = {}) {
this.memreset();

if (typeof opts == 'string') {
opts = { code_type: opts };
}

const exp: any = this.wasm.instance.exports;
exp.memreset();
const view = new DataView((exp.memory as WebAssembly.Memory).buffer);
const mem = new Uint8ClampedArray((exp.memory as WebAssembly.Memory).buffer);
const view = new DataView(this.memory.buffer);
const mem = new Uint8ClampedArray(this.memory.buffer);

setText(mem, exp.inputbuf.value, text);
const cstr = toCString(text);
const text_ptr = exp.malloc(cstr.byteLength);
mem.set(cstr, text_ptr);

const pimage = exp.encode(
const opts_ptr = exp.malloc(16 * 4);

const optsbuf = [
Qrean.CODE_TYPES[opts.code_type ?? Qrean.CODE_TYPE_QR],
Qrean.DATA_TYPES[opts.data_type ?? Qrean.DATA_TYPE_AUTO],
Qrean.QR_ERRORLEVELS[opts.qr_errorlevel ?? Qrean.QR_ERRORLEVEL_M],
Qrean.QR_VERSIONS[opts.qr_version ?? Qrean.QR_VERSION_AUTO],
Qrean.QR_MASKPATTERNS[opts.qr_maskpattern ?? Qrean.QR_MASKPATTERN_AUTO],
opts.padding || 0,
opts.padding?.[0] ?? -1,
opts.padding?.[1] ?? -1,
opts.padding?.[2] ?? -1,
opts.padding?.[3] ?? -1,
opts.scale || 4,
];
for (let i = 0; i < optsbuf.length; i++) {
view.setUint32(opts_ptr + i * 4, optsbuf[i], true);
}

const result = exp.encode(
text_ptr,
opts_ptr,
);

if (!pimage) {
if (!result) {
return null;
}

const pbuf = view.getUint32(pimage + 0, true);
const width = view.getUint32(pimage + 4, true);
const height = view.getUint32(pimage + 8, true);
const plen = width * height * 4;
const imgdata = this.readImage(result);

exp.free(result);

return imgdata;
}

private allocImage(img: Image): number {
const exp: any = this.wasm.instance.exports;
const view = new DataView(this.memory.buffer);
const mem = new Uint8Array(this.memory.buffer);

const ptr = exp.malloc(12 + img.width * img.height * 4);
const imgbuf = ptr + 12;
view.setUint32(ptr + 0, img.width, true);
view.setUint32(ptr + 4, img.height, true);
view.setUint32(ptr + 8, imgbuf, true);
mem.set(img.data, imgbuf);

const imgdata: ImageData = {
return ptr;
}

private readImage(ptr: number): Image {
const view = new DataView(this.memory.buffer);
const mem = new Uint8ClampedArray(this.memory.buffer);

const width = view.getUint32(ptr + 0, true);
const height = view.getUint32(ptr + 4, true);
const imgbuf = view.getUint32(ptr + 8, true);

return {
width,
height,
colorSpace: 'srgb',
data: mem.slice(pbuf, pbuf + plen),
data: mem.slice(imgbuf, imgbuf + width * height * 4),
};
return imgdata;
}

detect(imgdata: ImageData, callback: (type: string, str: string) => void, opts: DetectOptions = {}) {
detect(imgdata: Image, callback: (type: string, str: string) => void, opts: DetectOptions = {}) {
this.memreset();

const gamma_value = opts.gamma ?? 1.0;
const exp: any = this.wasm.instance.exports;
exp.memreset();
const view = new DataView((exp.memory as WebAssembly.Memory).buffer);
const mem = new Uint8Array((exp.memory as WebAssembly.Memory).buffer);

const pimage = exp.imagebuf.value as number;
const pbuf = pimage + 12;
const width = imgdata.width;
const height = imgdata.height;
const outbuf_size = opts.outbuf_size ?? 1024;
const outbuf_ptr = exp.malloc(outbuf_size);

view.setUint32(pimage + 0, pbuf, true);
view.setUint32(pimage + 4, width, true);
view.setUint32(pimage + 8, height, true);
mem.set(imgdata.data, pbuf);
const image_ptr = this.allocImage(imgdata);

this.on_found = callback;
const r = exp.detect(
const detected: number = exp.detect(
outbuf_ptr,
outbuf_size,
image_ptr,
gamma_value,
Qrean.QR_ECI_CODES[opts.eci_code ?? Qrean.QR_ECI_CODE_LATIN1],
);
this.on_found = undefined;

// write back
const digitized = this.readImage(image_ptr);
if (opts.digitized) {
const mono = mem.slice(pbuf, pbuf + width * height * 4);
for (let i = 0; i < width * height * 4; i++) {
opts.digitized[i] = i % 4 == 3 ? 0xff : mono[i];
for (let i = 0; i < digitized.width * digitized.height * 4; i++) {
opts.digitized[i] = i % 4 == 3 ? 0xff : digitized.data[i];
}
}

return r;
exp.free(image_ptr);
exp.free(outbuf_ptr);

return {
detected,
digitized,
}
}
}
Loading

0 comments on commit 33ced36

Please sign in to comment.