Compare commits
3 commits
d97004a6dd
...
883ef7357c
Author | SHA1 | Date | |
---|---|---|---|
883ef7357c | |||
02a26109c0 | |||
14642b74d7 |
7 changed files with 93 additions and 51 deletions
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@msagent.js/core",
|
"name": "@msagent.js/core",
|
||||||
|
"description": "msagent.js core. Provides structures, parsing, and bitmap utilties for higher level usage",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"packageManager": "yarn@4.2.2",
|
"packageManager": "yarn@4.2.2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
// Please note that the "meaningless" shifts of 0 are to force
|
import { WasmModule, WebassemblyOwnMemoryExports } from './wasm_module';
|
||||||
// the value to be a 32-bit integer. Do not remove them.
|
|
||||||
|
|
||||||
let compressWasm: WebAssembly.WebAssemblyInstantiatedSource;
|
// WASM exports for the decompression module.
|
||||||
|
interface CompressWasmExports extends WebassemblyOwnMemoryExports {
|
||||||
interface CompressWasmExports {
|
agentDecompressWASM: (pSource: number, sourceLen: number, pDest: number, destLen: number) => number;
|
||||||
memory: WebAssembly.Memory;
|
|
||||||
agentDecompressWASM: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let compressWasm = new WasmModule<CompressWasmExports>(new URL('decompress.wasm', import.meta.url));
|
||||||
|
|
||||||
// Initalize the decompression module
|
// Initalize the decompression module
|
||||||
export async function compressInit() {
|
export async function compressInit() {
|
||||||
let url = new URL('decompress.wasm', import.meta.url);
|
await compressWasm.Initalize();
|
||||||
compressWasm = await WebAssembly.instantiateStreaming(fetch(url));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function compressWasmGetExports() {
|
function compressWasmGetExports() {
|
||||||
return compressWasm.instance.exports as any as CompressWasmExports;
|
return compressWasm.Exports;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compressWASMGetMemory(): WebAssembly.Memory {
|
function compressWASMGetMemory(): WebAssembly.Memory {
|
||||||
|
@ -32,14 +30,9 @@ function compressWASMGetMemory(): WebAssembly.Memory {
|
||||||
export function compressDecompress(src: Uint8Array, dest: Uint8Array) {
|
export function compressDecompress(src: Uint8Array, dest: Uint8Array) {
|
||||||
// Grow the WASM heap if needed. Funnily enough, this code is never hit in most
|
// Grow the WASM heap if needed. Funnily enough, this code is never hit in most
|
||||||
// ACSes, so IDK if it's even needed
|
// ACSes, so IDK if it's even needed
|
||||||
let memory = compressWASMGetMemory();
|
compressWasm.growHeapTo(src.length + dest.length);
|
||||||
if (memory.buffer.byteLength < src.length + dest.length) {
|
|
||||||
// A WebAssembly page is 64kb, so we need to grow at least that much
|
|
||||||
let npages = Math.floor((src.length + dest.length) / 65535) + 1;
|
|
||||||
console.log('Need to grow WASM heap', npages, 'pages', '(current byteLength is', memory.buffer.byteLength, ', we need', src.length + dest.length, ')');
|
|
||||||
memory.grow(npages);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let memory = compressWASMGetMemory();
|
||||||
let copyBuffer = new Uint8Array(memory.buffer);
|
let copyBuffer = new Uint8Array(memory.buffer);
|
||||||
|
|
||||||
// Copy source to memory[0]. This will make things a bit simpler
|
// Copy source to memory[0]. This will make things a bit simpler
|
||||||
|
@ -50,6 +43,7 @@ export function compressDecompress(src: Uint8Array, dest: Uint8Array) {
|
||||||
|
|
||||||
if (nrBytesDecompressed != dest.length) throw new Error(`decompression failed: ${nrBytesDecompressed} != ${dest.length}`);
|
if (nrBytesDecompressed != dest.length) throw new Error(`decompression failed: ${nrBytesDecompressed} != ${dest.length}`);
|
||||||
|
|
||||||
// Dest will be memory[src.length..dest.length]
|
// The uncompressed data is located at memory[src.length..dest.length].
|
||||||
|
// Copy it into the destination buffer.
|
||||||
dest.set(copyBuffer.slice(src.length, src.length + dest.length), 0);
|
dest.set(copyBuffer.slice(src.length, src.length + dest.length), 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { AcsImageEntry } from './structs/image';
|
import { AcsImageEntry } from './structs/image';
|
||||||
|
import { RGBAColor } from './structs/core';
|
||||||
|
|
||||||
// probably should be in a utility module
|
// probably should be in a utility module
|
||||||
function dwAlign(off: number): number {
|
function dwAlign(off: number): number {
|
||||||
|
@ -10,9 +11,6 @@ function dwAlign(off: number): number {
|
||||||
return ul;
|
return ul;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { BufferStream, SeekDir } from './buffer';
|
|
||||||
import { RGBAColor } from './structs/core';
|
|
||||||
|
|
||||||
/// Draws an ACS image to a newly allocated buffer.
|
/// Draws an ACS image to a newly allocated buffer.
|
||||||
/// This function normalizes the agent 8bpp DIB format to a saner RGBA format,
|
/// This function normalizes the agent 8bpp DIB format to a saner RGBA format,
|
||||||
/// that can be directly converted to an ImageData for drawing to a web canvas.
|
/// that can be directly converted to an ImageData for drawing to a web canvas.
|
||||||
|
@ -20,29 +18,17 @@ import { RGBAColor } from './structs/core';
|
||||||
/// However, that should be done (and is done) by a higher level web layer.
|
/// However, that should be done (and is done) by a higher level web layer.
|
||||||
export function imageDrawToBuffer(imageEntry: AcsImageEntry, palette: RGBAColor[]) {
|
export function imageDrawToBuffer(imageEntry: AcsImageEntry, palette: RGBAColor[]) {
|
||||||
let rgbaBuffer = new Uint32Array(imageEntry.image.width * imageEntry.image.height);
|
let rgbaBuffer = new Uint32Array(imageEntry.image.width * imageEntry.image.height);
|
||||||
|
|
||||||
let buffer = imageEntry.image.data;
|
let buffer = imageEntry.image.data;
|
||||||
let bufStream = new BufferStream(buffer);
|
|
||||||
|
|
||||||
let rows = new Array<Uint8Array>(imageEntry.image.height - 1);
|
// Next, draw the rows converted to RGBA, top down (so it's drawn correctly,
|
||||||
|
// and in the RGBA format we want to return)
|
||||||
// Read all the rows bottom-up first. This idiosyncracy is due to the fact
|
|
||||||
// that the bitmap data is actually formatted to be used as a GDI DIB
|
|
||||||
// (device-independent bitmap), so it inherits all the strange baggage from that.
|
|
||||||
for (let y = imageEntry.image.height - 1; y >= 0; --y) {
|
|
||||||
let row = bufStream.subBuffer(imageEntry.image.width).raw();
|
|
||||||
rows[y] = row.slice(0, imageEntry.image.width);
|
|
||||||
|
|
||||||
// Seek to the next DWORD aligned spot to get to the next row.
|
|
||||||
// For most images this may mean not seeking at all.
|
|
||||||
bufStream.seek(dwAlign(bufStream.tell()), SeekDir.BEG);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, draw the rows converted to RGBA, top down (so it's drawn as you'd expect)
|
|
||||||
for (let y = 0; y < imageEntry.image.height - 1; ++y) {
|
for (let y = 0; y < imageEntry.image.height - 1; ++y) {
|
||||||
let row = rows[y];
|
// flip y so it's all top down properly
|
||||||
|
let yy = imageEntry.image.height - 1 - y;
|
||||||
|
let rowStartOffset = yy * dwAlign(imageEntry.image.width);
|
||||||
|
|
||||||
for (let x = 0; x < imageEntry.image.width; ++x) {
|
for (let x = 0; x < imageEntry.image.width; ++x) {
|
||||||
rgbaBuffer[y * imageEntry.image.width + x] = palette[row[x]].to_rgba();
|
rgbaBuffer[y * imageEntry.image.width + x] = palette[buffer[rowStartOffset + x]].to_rgba();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rgbaBuffer;
|
return rgbaBuffer;
|
||||||
|
|
44
msagent.js/core/src/wasm_module.ts
Normal file
44
msagent.js/core/src/wasm_module.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
export interface WebassemblyOwnMemoryExports extends WebAssembly.Exports {
|
||||||
|
memory: WebAssembly.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper for WASM modules.
|
||||||
|
// This currently only works for modules that export their own memory,
|
||||||
|
// and do not import it.
|
||||||
|
//
|
||||||
|
// This covers all of the modules in the core so far, so
|
||||||
|
// this is perfectly fine, but it's still worth noting.
|
||||||
|
export class WasmModule<TExports extends WebassemblyOwnMemoryExports> {
|
||||||
|
private module: WebAssembly.WebAssemblyInstantiatedSource | null = null;
|
||||||
|
private url: URL;
|
||||||
|
|
||||||
|
constructor(url: URL) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Initalize(): Promise<void> {
|
||||||
|
this.module = await WebAssembly.instantiateStreaming(fetch(this.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
get Module() {
|
||||||
|
if (this.module == null) throw new Error('WasmModule<T> has not been initalized');
|
||||||
|
return this.module;
|
||||||
|
}
|
||||||
|
|
||||||
|
get Exports() {
|
||||||
|
let module = this.Module;
|
||||||
|
return module.instance.exports as TExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grows the WebAssembly heap if required.
|
||||||
|
growHeapTo(newSize: number) {
|
||||||
|
let exports = this.Exports;
|
||||||
|
let memory: WebAssembly.Memory = exports.memory;
|
||||||
|
if (memory.buffer.byteLength < newSize) {
|
||||||
|
// A WebAssembly page is 64kb, so we need to grow at least a single page,
|
||||||
|
// even if it would be relatively wasteful to do so.
|
||||||
|
let npages = Math.floor(newSize / 65535) + 1;
|
||||||
|
memory.grow(npages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,11 @@ import { Agent } from './agent.js';
|
||||||
let acsDataCache = new Map<string, AcsData>();
|
let acsDataCache = new Map<string, AcsData>();
|
||||||
|
|
||||||
// Purges the ACS cache.
|
// Purges the ACS cache.
|
||||||
|
//
|
||||||
|
// FIXME: A smarter way to do this would probably be instead reference count
|
||||||
|
// AcsData instances, then when an agent is disposed decrement reference count
|
||||||
|
// (or leave it at 0). Once it's 0 then a globally running interval can remove
|
||||||
|
// all keys that have no refcount.
|
||||||
export function agentPurgeACSCache() {
|
export function agentPurgeACSCache() {
|
||||||
acsDataCache.clear();
|
acsDataCache.clear();
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ input.addEventListener('change', async () => {
|
||||||
agent.addToDom(mount);
|
agent.addToDom(mount);
|
||||||
|
|
||||||
agent.show();
|
agent.show();
|
||||||
|
await agent.playAnimationByNamePromise("Show");
|
||||||
console.log('Agent created');
|
console.log('Agent created');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -31,13 +32,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
let form = document.getElementById('acsUrlForm') as HTMLFormElement;
|
let form = document.getElementById('acsUrlForm') as HTMLFormElement;
|
||||||
form.addEventListener('submit', (e) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let url = (document.getElementById('acsUrl') as HTMLInputElement).value;
|
let url = (document.getElementById('acsUrl') as HTMLInputElement).value;
|
||||||
msagent.agentCreateCharacterFromUrl(url).then((agent) => {
|
let agent = await msagent.agentCreateCharacterFromUrl(url)
|
||||||
w.agents.push(agent);
|
w.agents.push(agent);
|
||||||
agent.addToDom(document.body);
|
agent.addToDom(document.body);
|
||||||
|
|
||||||
agent.show();
|
agent.show();
|
||||||
|
await agent.playAnimationByNamePromise("Show");
|
||||||
|
|
||||||
console.log(`Loaded agent from ${url}`);
|
console.log(`Loaded agent from ${url}`);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -3619,11 +3619,11 @@ __metadata:
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"node-abi@npm:^3.3.0":
|
"node-abi@npm:^3.3.0":
|
||||||
version: 3.65.0
|
version: 3.67.0
|
||||||
resolution: "node-abi@npm:3.65.0"
|
resolution: "node-abi@npm:3.67.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: "npm:^7.3.5"
|
semver: "npm:^7.3.5"
|
||||||
checksum: 10c0/112672015d8f27d6be2f18d64569f28f5d6a15a94cc510da513c69c3e3ab5df6dac196ef13ff115a8fadb69b554974c47ef89b4f6350a2b02de2bca5c23db1e5
|
checksum: 10c0/72ce2edbdfb84745bc201a4e48aa7146fd88a0d2c80046b6b17f28439c9a7683eab846f40f1e819349c31f7d9331ed5c50d1e741208d938dd5f38b29cab2275e
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -4246,7 +4246,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.4, semver@npm:^7.6.0":
|
"semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.4, semver@npm:^7.6.0":
|
||||||
version: 7.6.2
|
version: 7.6.2
|
||||||
resolution: "semver@npm:7.6.2"
|
resolution: "semver@npm:7.6.2"
|
||||||
bin:
|
bin:
|
||||||
|
@ -4255,6 +4255,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"semver@npm:^7.3.8":
|
||||||
|
version: 7.6.3
|
||||||
|
resolution: "semver@npm:7.6.3"
|
||||||
|
bin:
|
||||||
|
semver: bin/semver.js
|
||||||
|
checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"seq-queue@npm:^0.0.5":
|
"seq-queue@npm:^0.0.5":
|
||||||
version: 0.0.5
|
version: 0.0.5
|
||||||
resolution: "seq-queue@npm:0.0.5"
|
resolution: "seq-queue@npm:0.0.5"
|
||||||
|
|
Loading…
Reference in a new issue