From 9351d57627ff468420ad96f692d2e67410dff4f2 Mon Sep 17 00:00:00 2001 From: Lukas Gail Date: Sun, 1 Feb 2026 22:27:17 +0100 Subject: [PATCH 1/3] Improve transferImageData performance - Transfers are batched horizontally to make combining them faster. - Use fast library for base64 decoding - Simplify conversion from bgra to rgba --- package.json | 283 ++++++++++++++++++++++---------------------- src/base/wrapper.ts | 94 ++++++++++----- 2 files changed, 209 insertions(+), 168 deletions(-) diff --git a/package.json b/package.json index 041d6b2..5929669 100644 --- a/package.json +++ b/package.json @@ -1,143 +1,144 @@ { - "name": "alt1", - "version": "0.1.2", - "private": false, - "author": { - "name": "Skillbert", - "url": "https://runeapps.org" - }, - "repository": { - "type": "git", - "url": "https://github.com/skillbert/alt1.git" - }, - "scripts": { - "clean": "rimraf dist/", - "build": "webpack --config scripts/main.config.mjs", - "watch": "webpack --config scripts/main.config.mjs --watch", - "build-tools": "webpack --config ./scripts/tools.config.mjs", - "build-full": "npm run clean && npm run build-tools && npm run build", - "tests": "webpack serve --config ./scripts/tests.config.mjs" - }, - "exports": { - ".": { - "alt1-source": "./src/base/index.ts", - "default": "./dist/base/index.js" - }, - "./ability": { - "alt1-source": "./src/ability/index.ts", - "default": "./dist/ability/index.js" - }, - "./abilitytooltip": { - "alt1-source": "./src/abilitytooltip/index.ts", - "default": "./dist/abilitytooltip/index.js" - }, - "./animal": { - "alt1-source": "./src/animal/index.ts", - "default": "./dist/animal/index.js" - }, - "./base": { - "alt1-source": "./src/base/index.ts", - "default": "./dist/base/index.js" - }, - "./bosstimer": { - "alt1-source": "./src/bosstimer/index.ts", - "default": "./dist/bosstimer/index.js" - }, - "./buffs": { - "alt1-source": "./src/buffs/index.ts", - "default": "./dist/buffs/index.js" - }, - "./chatbox": { - "alt1-source": "./src/chatbox/index.ts", - "default": "./dist/chatbox/index.js" - }, - "./datapng-loader": { - "alt1-source": "./src/datapng-loader/index.ts", - "default": "./dist/datapng-loader/index.js" - }, - "./dialog": { - "alt1-source": "./src/dialog/index.ts", - "default": "./dist/dialog/index.js" - }, - "./dropsmenu": { - "alt1-source": "./src/dropsmenu/index.ts", - "default": "./dist/dropsmenu/index.js" - }, - "./font-loader": { - "alt1-source": "./src/font-loader/index.ts", - "default": "./dist/font-loader/index.js" - }, - "./imagedata-loader": { - "alt1-source": "./src/imagedata-loader/index.ts", - "default": "./dist/imagedata-loader/index.js" - }, - "./ocr": { - "alt1-source": "./src/ocr/index.ts", - "default": "./dist/ocr/index.js" - }, - "./targetmob": { - "alt1-source": "./src/targetmob/index.ts", - "default": "./dist/targetmob/index.js" - }, - "./tooltip": { - "alt1-source": "./src/tooltip/index.ts", - "default": "./dist/tooltip/index.js" - }, - "./xpcounter": { - "alt1-source": "./src/xpcounter/index.ts", - "default": "./dist/xpcounter/index.js" - }, - "./fonts/*": { - "alt1-source": "./src/fonts/*", - "default": "./dist/fonts/*" - }, - "./*": { - "alt1-source": [ - "./src/*/index.ts", - "./src/*.ts" - ], - "default": [ - "./dist/*/index.js", - "./dist/*.js" - ] - } - }, - "type": "commonjs", - "devDependencies": { - "@types/glob": "^5.0.35", - "@types/node": "^20.3.1", - "@types/react": "^16.9.2", - "@types/react-dom": "^16.9.0", - "@types/sharp": ">=0.27.1", - "@types/webpack": "^5.28.0", - "@types/webpack-env": "^1.16.2", - "canvas": ">=2.11.2", - "css-loader": "^6.2.0", - "glob": "^7.1.2", - "rimraf": "^5.0.1", - "sharp": ">=0.31.3", - "style-loader": "^3.2.1", - "ts-loader": "^9.2.5", - "typescript": "^5.1.3", - "webpack": "^5.86.0", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.2.3" - }, - "sideEffects": [ - "./src/base/imagedata-extensions.ts" - ], - "peerDependencies": { - "canvas": ">=2.11.2", - "sharp": ">=0.31.3" - }, - "peerDependenciesMeta": { - "canvas": { - "//reason": "Polyfill for canvas actions when running outside the browser", - "optional": true - }, - "sharp": { - "//reason": "Needed for image compression/decompression when running outside the browser and during webpack compilation", - "optional": true - } - } + "name": "alt1", + "version": "0.1.2", + "private": false, + "author": { + "name": "Skillbert", + "url": "https://runeapps.org" + }, + "repository": { + "type": "git", + "url": "https://github.com/skillbert/alt1.git" + }, + "scripts": { + "clean": "rimraf dist/", + "build": "webpack --config scripts/main.config.mjs", + "watch": "webpack --config scripts/main.config.mjs --watch", + "build-tools": "webpack --config ./scripts/tools.config.mjs", + "build-full": "npm run clean && npm run build-tools && npm run build", + "tests": "webpack serve --config ./scripts/tests.config.mjs" + }, + "exports": { + ".": { + "alt1-source": "./src/base/index.ts", + "default": "./dist/base/index.js" + }, + "./ability": { + "alt1-source": "./src/ability/index.ts", + "default": "./dist/ability/index.js" + }, + "./abilitytooltip": { + "alt1-source": "./src/abilitytooltip/index.ts", + "default": "./dist/abilitytooltip/index.js" + }, + "./animal": { + "alt1-source": "./src/animal/index.ts", + "default": "./dist/animal/index.js" + }, + "./base": { + "alt1-source": "./src/base/index.ts", + "default": "./dist/base/index.js" + }, + "./bosstimer": { + "alt1-source": "./src/bosstimer/index.ts", + "default": "./dist/bosstimer/index.js" + }, + "./buffs": { + "alt1-source": "./src/buffs/index.ts", + "default": "./dist/buffs/index.js" + }, + "./chatbox": { + "alt1-source": "./src/chatbox/index.ts", + "default": "./dist/chatbox/index.js" + }, + "./datapng-loader": { + "alt1-source": "./src/datapng-loader/index.ts", + "default": "./dist/datapng-loader/index.js" + }, + "./dialog": { + "alt1-source": "./src/dialog/index.ts", + "default": "./dist/dialog/index.js" + }, + "./dropsmenu": { + "alt1-source": "./src/dropsmenu/index.ts", + "default": "./dist/dropsmenu/index.js" + }, + "./font-loader": { + "alt1-source": "./src/font-loader/index.ts", + "default": "./dist/font-loader/index.js" + }, + "./imagedata-loader": { + "alt1-source": "./src/imagedata-loader/index.ts", + "default": "./dist/imagedata-loader/index.js" + }, + "./ocr": { + "alt1-source": "./src/ocr/index.ts", + "default": "./dist/ocr/index.js" + }, + "./targetmob": { + "alt1-source": "./src/targetmob/index.ts", + "default": "./dist/targetmob/index.js" + }, + "./tooltip": { + "alt1-source": "./src/tooltip/index.ts", + "default": "./dist/tooltip/index.js" + }, + "./xpcounter": { + "alt1-source": "./src/xpcounter/index.ts", + "default": "./dist/xpcounter/index.js" + }, + "./fonts/*": { + "alt1-source": "./src/fonts/*", + "default": "./dist/fonts/*" + }, + "./*": { + "alt1-source": [ + "./src/*/index.ts", + "./src/*.ts" + ], + "default": [ + "./dist/*/index.js", + "./dist/*.js" + ] + } + }, + "type": "commonjs", + "devDependencies": { + "@types/glob": "^5.0.35", + "@types/node": "^20.3.1", + "@types/react": "^16.9.2", + "@types/react-dom": "^16.9.0", + "@types/sharp": ">=0.27.1", + "@types/webpack": "^5.28.0", + "@types/webpack-env": "^1.16.2", + "canvas": ">=2.11.2", + "css-loader": "^6.2.0", + "glob": "^7.1.2", + "rimraf": "^5.0.1", + "sharp": ">=0.31.3", + "style-loader": "^3.2.1", + "ts-loader": "^9.2.5", + "typescript": "^5.1.3", + "webpack": "^5.86.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.2.3" + }, + "sideEffects": [ + "./src/base/imagedata-extensions.ts" + ], + "peerDependencies": { + "canvas": ">=2.11.2", + "sharp": ">=0.31.3", + "byte-base64": ">=1.2.7" + }, + "peerDependenciesMeta": { + "canvas": { + "//reason": "Polyfill for canvas actions when running outside the browser", + "optional": true + }, + "sharp": { + "//reason": "Needed for image compression/decompression when running outside the browser and during webpack compilation", + "optional": true + } + } } diff --git a/src/base/wrapper.ts b/src/base/wrapper.ts index a004682..2c76284 100644 --- a/src/base/wrapper.ts +++ b/src/base/wrapper.ts @@ -3,6 +3,7 @@ import Rect, { RectLike } from "./rect"; import { ImgRefBind, ImgRefCtx, ImgRefData, ImgRef } from "./imgref"; import { ImageData } from "./imagedata-extensions"; import "./alt1api"; +import {base64ToBytes} from "byte-base64"; declare global { namespace alt1 { @@ -151,42 +152,81 @@ export function captureHoldFullRs() { * @deprecated This should be handled internall by the imgrefbind.toData method */ export function transferImageData(handle: number, x: number, y: number, w: number, h: number) { - x = Math.round(x); y = Math.round(y); w = Math.round(w); h = Math.round(h); - requireAlt1(); + x = Math.round(x); + y = Math.round(y); + w = Math.round(w); + h = Math.round(h); + requireAlt1(); - if (alt1.bindGetRegionBuffer) { - return new ImageData(alt1.bindGetRegionBuffer(handle, x, y, w, h), w, h); - } - var r = new ImageData(w, h); - - var x1 = x; - while (true) {//split up the request to to exceed the single transfer limit (for now) - var x2 = Math.min(x + w, Math.floor(x1 + (maxtransfer / 4 / h))); - var a = alt1.bindGetRegion(handle, x1, y, x2 - x1, h); - if (!a) { throw new Alt1Error(); } - decodeImageString(a, r, x1 - x, 0, x2 - x1, h); - x1 = x2; - if (x1 == x + w) { break; }; - } - return r; + console.log("transferImageData"); + + if (alt1.bindGetRegionBuffer) { + return new ImageData(alt1.bindGetRegionBuffer(handle, x, y, w, h), w, h); + } + + const data_per_row = w * 4; + const rows_per_transfer = Math.floor(maxtransfer / data_per_row); + + const transfers = Math.ceil(h / rows_per_transfer); + + const buffers: Uint8Array[] = []; + + for (let i = 0; i < transfers; i++) { + const remaining_rows = h - i * rows_per_transfer; + + const y2 = i * rows_per_transfer + y; + + const section = alt1.bindGetRegion(handle, x, y2, w, Math.min(remaining_rows, rows_per_transfer)); + if (!section) { throw new Alt1Error(); } + + // base64ToBytes is around 40% faster than Buffer.from(section, "base64") + buffers.push(base64ToBytes(section)); + } + + if (buffers.length == 1) { + return bgraToRgbaInPlace(new ImageData(new Uint8ClampedArray(buffers[0]), w, h)) + } else { + const r = new ImageData(w, h); + + //Copy buffers from the individual reads into the full image + for (let i = 0; i < transfers; i++) { + const row = buffers[i]; + r.data.set(row, i * rows_per_transfer * data_per_row); + } + return bgraToRgbaInPlace(r); + } +} + +/** + * Fixes pixel data from gbra format to rgba format. + * Alt1 supplies images as gbra, but typescript needs it in rgba + * @param data The image to fix. + */ +function bgraToRgbaInPlace(data: ImageData): ImageData { + for (let i = 0; i < data.data.length; i += 4) { + const tmp = data.data[i]; + data.data[i] = data.data[i + 2]; + data.data[i + 2] = tmp; + } + return data } /** * decodes a returned string from alt1 to an imagebuffer. You generally never have to do this yourself */ export function decodeImageString(imagestring: string, target: ImageData, x: number, y: number, w: number, h: number) { - var bin = atob(imagestring); + const bin = btoa(imagestring) - var bytes = target.data; + const bytes = target.data; w |= 0; h |= 0; - var offset = 4 * x + 4 * y * target.width; - var target_width = target.width | 0; - for (var a = 0; a < w; a++) { - for (var b = 0; b < h; b++) { - var i1 = (offset + (a * 4 | 0) + (b * target_width * 4 | 0)) | 0; - var i2 = ((a * 4 | 0) + (b * 4 * w | 0)) | 0; + const offset = 4 * x + 4 * y * target.width; + const target_width = target.width | 0; + for (let a = 0; a < w; a++) { + for (let b = 0; b < h; b++) { + const i1 = (offset + (a * 4 | 0) + (b * target_width * 4 | 0)) | 0; + const i2 = ((a * 4 | 0) + (b * 4 * w | 0)) | 0; bytes[i1 + 0 | 0] = bin.charCodeAt(i2 + 2 | 0);//fix weird red/blue swap in c# bytes[i1 + 1 | 0] = bin.charCodeAt(i2 + 1 | 0); bytes[i1 + 2 | 0] = bin.charCodeAt(i2 + 0 | 0); @@ -347,7 +387,7 @@ export class ImageStreamReader { } /** - * + * */ setFrameBuffer(buffer: ImageData) { if (this.reading) { throw new Error("can't change framebuffer while reading"); } @@ -487,7 +527,7 @@ export async function captureAsync(...args: [rect: RectLike] | [x: number, y: nu /** * Asynchronously captures multple area's. This method captures the images in the same render frame if possible - * @param areas + * @param areas */ export async function captureMultiAsync(areas: T) { requireAlt1(); From f5ca0c2abcda69770ba8b52b746e8c39dec6e7fe Mon Sep 17 00:00:00 2001 From: Lukas Gail Date: Sun, 1 Feb 2026 23:15:02 +0100 Subject: [PATCH 2/3] Remove debug print --- src/base/wrapper.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/base/wrapper.ts b/src/base/wrapper.ts index 2c76284..ddc1959 100644 --- a/src/base/wrapper.ts +++ b/src/base/wrapper.ts @@ -149,7 +149,7 @@ export function captureHoldFullRs() { /** * returns a subregion from a bound image * used internally in imgreftobuf if imgref is a bound image - * @deprecated This should be handled internall by the imgrefbind.toData method + * @deprecated This should be handled internally by the imgrefbind.toData method */ export function transferImageData(handle: number, x: number, y: number, w: number, h: number) { x = Math.round(x); @@ -158,8 +158,6 @@ export function transferImageData(handle: number, x: number, y: number, w: numbe h = Math.round(h); requireAlt1(); - console.log("transferImageData"); - if (alt1.bindGetRegionBuffer) { return new ImageData(alt1.bindGetRegionBuffer(handle, x, y, w, h), w, h); } From dd3367e49019bd34232841f76a5b11005527693f Mon Sep 17 00:00:00 2001 From: Lukas Gail Date: Tue, 3 Feb 2026 08:36:36 +0100 Subject: [PATCH 3/3] Change to byte-base64 version that exists --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5929669..43fb314 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "peerDependencies": { "canvas": ">=2.11.2", "sharp": ">=0.31.3", - "byte-base64": ">=1.2.7" + "byte-base64": "^1.1.0" }, "peerDependenciesMeta": { "canvas": {