-
-
Notifications
You must be signed in to change notification settings - Fork 671
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
Array passing back and forth between JS and AS #263
Comments
In such a case, I'd most likely allocate a If there's more memory necessary than just those f32 values, there's the It's not strictly necessary to import the memory, actually, as you could simply One thing to take care of, when keeping a view on the memory around in JS, is that if the WASM memory resizes (i.e. through a memory.grow instruction), that the view is invalidated because the original memory becomes detached (and a new view on the updated memory must be created). Pre-allocating a sufficient amount of pages works around this (see also). |
Thank you for your fast answer and the warning about the memory resize concern :-) Do you mean to load and to store float by float in a big loop ? |
Yeah, you'd simply write The pointer in this case is Example: const INPUT_START: usize = 0; // 500000 elements
const OUTPUT_START: usize = 4 * 500000; // 500000 elements
@inline function getInput(index: i32): f32 {
return load<f32>(index << 2, INPUT_START);
}
@inline function setOutput(index: i32, value: f32): void {
store<f32>(index << 2, value, OUTPUT_START);
} The Game of Life example does something like this as well. |
Also: |
@jbousquie I do similar interop for |
@dcodeIO and @MaxGraey Are you fine with me implementing the WebAssembly/design#1231 proposals in lib/loader and PR? |
Possibly related: #136 Note though, that an implementation of getArray etc. would have one level of indirection, whereas the solution proposed here is as direct as it can get. |
@dcode I think much better use code-generation as postprocess for js <-> wasm interop like wasm-bindgen does. |
Well, that would still use the loader, or something very similar, for array interop, because arrays in AS can be at any offset anyway (with a buffer being referenced from anywhere), and there is no concept of a C-like static array in TS (hence the proposed |
@MaxGraey Yeah, I was about to write another comment that code generation would be much better. But then I thought again... maybe not everybody wants to use a generated facade? So maybe having both options would be nice. Having it available in the lib, and code generation probably using this lib code. |
Yeah, and it would be cool if there would be a separation of the API's for the two different behaviors: copy or "pass-by-reference". And having both available in a simple, yet easy to understand way. And to put some docs towards this in the wiki etc. as I see more and more issues here popping up just to ask for help how to do it :) |
I'll write a PoC tonight... and maybe it is good or you decide to throw it ;) |
Also note that these things will become more convenient once reference-types/GC are there. With that, there can be typed array types on the AS side that can flow out to/in from JS naturally. |
Good point. I just feel that it is maybe a good idea not to wait until this lands in all WASM supporting engines as this might take some time. But it basically means that the support for this should probably be flagged as experimental in the loader lib because the internal implementation will surely change once reference-types/GC lands and so will the lib's API as ptr arguments might not be necessary anymore by then (?) But being one of the first projects to support passing TypedArrays between JS/WASM forth and back easily might be a big deal for this project as I guess many dev's are not soo much into "bit dancing" :)) |
thanks guys, I'll make a test these next days with your proposal :-) |
mmh... I guess I need to compile also with the flag |
@MaxGraey I just couldn't find where you used load/store in your port of earcut. Maybe you use another method. |
@jbousquie I used ordinal arrays instead load/store because input and output arrays hasn't fixed length. If you want see how using |
thanks a lot. function getInput(index: i32, byteOffset; usize): f32 {
return load<f32>(index << 2, byteOffset);
}
function setOutput(index: i32, value: f32, byteOffset: usize): void {
store<f32>(index << 2, value, byteOffset);
} This is sad because the big buffer that I share between JS and AS contains many different data types (mesh geometry, mesh normals, transformations, results), although all float32, and I would like to pass the WASM module each byteOffset, meaning each data type or each buffer section. Actually although the size of the buffer is fixed once for all, it depends on the 3D mesh geometry and complexity. So a fixed buffer (and offsets) but only once the mesh is built. Maybe should I get rid of the byteOffset and compute each index directly from zero each time or something like this ? return f32(load<f32>((index + offset) << 2)); I'll have a try... |
So, after working on the code and an PR I realized that if we:
...we can easily go with the API's we have already (more or less). Here is a working example for JS context:
WASM module:
The Experimental Pointer<T> implementation by @dcodeIO saves you the manual pointer arithmetic impl. This example works well for up to Other TypedArray subclasses: You can look them up in the type mapping for the JS context and the type declaration for the AS context. On memory allocation: So, if you go with On memory clearing: So far so good, all my personal needs would be already served if the Also, I remember that it was mentioned (somewhere here) that there are plans for more complex datatype interop between JS and WASM for upcoming WebAssembly standards already. Given this, it may be seen as questionable, if we should add special API's for complex datatype interop now or just go with what we have plus What do you guys think? |
Nice approach. JS side : initialization phase // in a persistent object, call AS array creation functions
this.arrIN1 = module.createFloatArray(size1); // returns the pointer on the array, right ?
this.arrIN2 = module.createIntArray(size2);
this.arrIN3 = ...
this.result = module.createFloatArray(size3); AS side : initialization exported function export function createFloatArray(size: i32): f32[] {
return new Float32Array(size); // should allocate a f32 array in the imported memory and return the pointer
}
// ... same for function createIntArray(size) We don't need to know where the arrays are allocated in the memory (successive locations or not) and we can pass and get back different types of data. JS side, initial array population : Then still JS side, let's call the exported function export function compute(arr1 : f32[], arr2: u32[], arr3: f32[], ..., result: f32[]) {
for(let i = 0; i < arr2.length; i++) {
for(let j = 0; j < arr1.length; j++ {
result[i] = computeStuff(arr1[i], arr3[j]); // internal computations, values stored in result
}
}
} This code and its logic really look like the current TS code so far. JS side : // update the IN arrays with fresh values each frame like the array population example,
// then simply call compute() to update the result array
module.compute(this.arrIN1, this.arrIN2, this.arrIN3, ..., this.result);
// read back updated data from result to pass them to the WebGL buffer Ending, once the render loop is over : module.free(this.arr1);
module.free(this.arr2);
module.free(this.arr3); AS side export free(ptr) {
GC.collect(ptr); // or something else setting the memory free
} Not sure this could work. |
I see it working until the point where the JS side "initialized" Float32Array should write to the same backing memory as the Float32Array in AS context. To make a long story short: I bet, it won't :) Also the Float32Array in AS context has a slightly different memory layout. It stores a But, aside of this nifty details: So yes, in my PR I was working on exactly what you proposed, but using a slightly different implementation and now, as I see that there is a real need for more than only one initial data passing, I will finish my PR as it seems to me to require only a few LoC left to implement all your needs. eta. this evening, if all runs well :) |
thank you |
@jbousquie So, the PR is ready. I guess the code is almost self-explaining:
You can free the memory by calling All https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays subtypes are supported. |
Huge ! I'll test all of this soon |
Finally I used a simpler way, based on the first proposition, to pass floats, ints and get back computed floats. I'll describe this later in this post. |
Ok here's how I did up now, assuming it's compiled using var memory = new WebAssembly.Memory({
initial: 1000
});
this.wasmBuffer = memory.buffer;
// Views on different parts of the wasm buffer
// all types are 4 bytes long here
this.offset1 = this.localPos.length; // don't care the var values, they are just array lengths
this.offset2 = this.offset1 + this.localNor.length;
this.offset3 = this.offset2 + this.shapeLengths.length;
this.offset4 = this.offset3 + this.transformLength;
this.offset5 = this.offset4 + this.localPos.length;
this.byteOffset1 = this.offset1 * 4;
this.byteOffset2 = this.offset2 * 4;
this.byteOffset3 = this.offset3 * 4;
this.byteOffset4 = this.offset4 * 4;
this.byteOffset5 = this.offset5 * 4;
WebAssembly.instantiateStreaming(fetch(wasmURL), imports).then(obj => {
this.wasmModule = obj.instance.exports;
this.compiled = true;
// All this must be AFTER the module instanciation, else something altering the data is written in the memory buffer
// Keep a specific typed view on each part of the global shared buffer
this.wasmPos = new Float32Array(this.wasmBuffer, 0, this.localPos.length);
this.wasmNor = new Float32Array(this.wasmBuffer, this.byteOffset1, this.localNor.length);
this.wasmShp = new Uint32Array(this.wasmBuffer, this.byteOffset2, this.shapeLengths.length);
this.wasmTransforms = new Float32Array(this.wasmBuffer, this.byteOffset3, this.transforms.length);
this.wasmTransformedPos = new Float32Array(this.wasmBuffer, this.byteOffset4, this.localPos.length);
this.wasmTransformedNor = new Float32Array(this.wasmBuffer, this.byteOffset5, this.localNor.length);
// init the buffer with some existing array values
this.wasmPos.set(this.posBuffer);
this.wasmNor.set(this.norBuffer);
this.wasmShp.set(this.shapeLengths);
this.wasmTransforms.set(this.transforms);
this.wasmTransformedPos.set(this.localPos);
this.wasmTransformedNor.set(this.localNor);
});
// .... further in the render loop, each frame
// call the wasm function "transform" that updates the wasm buffer
this.wasmModule.transform(this.particleNb, this.offset1, this.offset2, this.offset3, this.offset4, this.offset5); Now AS side (I won't write all the code, you can find the source here : http://jerome.bousquie.fr/BJS/test/SPSWasm/index.ts ), just :
// function transform(particleNb, offset1, offset2, offset3, offset4, offset5, offset6)
// particleNb : number of particles
// the buffer is populated like this :
// 0..offset1 : float32 localPositions
// offset1..offset2 : float32 localNormals
// offset2..offset3 : uInt32 shapeLengths (one per particle)
// offset3..offset4 : float32 transformations (one set of 9 floats per particle : position/rotation/scaling)
// offset4..offset5 : float32 transformedPositions
// offset5..offset6 : float32 transformedNormals
export function transform(particleNb: u32, offset1: u32, offset2: u32, offset3: u32, offset4: u32, offset5: u32): void {
\\ subpart example ...
for (let p: u32 = 0; p < particleNb; p++) {
// get the current particle transformation
// transformations are stored from offset3
tIdx = p * stride;
offsetTransforms = offset3 + tIdx;
pos_x = load<f32>((offsetTransforms) << 2);
pos_y = load<f32>((offsetTransforms + 1) << 2);
pos_z = load<f32>((offsetTransforms + 2) << 2);
rot_x = load<f32>((offsetTransforms + 3) << 2);
rot_y = load<f32>((offsetTransforms + 4) << 2);
rot_z = load<f32>((offsetTransforms + 5) << 2);
scl_x = load<f32>((offsetTransforms + 6) << 2);
scl_y = load<f32>((offsetTransforms + 7) << 2);
scl_z = load<f32>((offsetTransforms + 8) << 2);
... etc
}
} Back JS side : // each frame
// call the wasm function "transform" that updates the wasm buffer
this.wasmModule.transform(this.particleNb, this.offset1, this.offset2, this.offset3, this.offset4, this.offset5);
// and update the actual mesh computed positions and normals
this.mesh.updateVerticesData(BABYLON.VertexBuffer.PositionKind, this.wasmTransformedPos, false, false);
this.mesh.updateVerticesData(BABYLON.VertexBuffer.NormalKind, this.wasmTransformedNor, false, false); Thank you a lot for your support, answers and suggestion. |
Example code (using current impl. by @dcodeIO): https://github.com/kyr0/assemblyscript-js-wasm-interop-example |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
forgive the resurrection, but ... it seems like either none of this is relevant in the modern assemblyscript, or none of this is exposed in the same way, making it a wee bit difficult to figure out -- i'm here just trying to figure out how to write a simple test to prove this out, from 'asinit', i've got a Assemblyscript module that exports an 'add' function by default, and a Typescript module that imports it, and it runs in node. Trying to figure out how to get to the point where I can access a Uint8Array shared between both sides, and I am completely missing how to do anything here. There is no lib/loader, no export { memory }, no newTypedArray etc etc. Help? Thanks :) |
I am aware that the default passing of arrays is by copy, but I need to understand how to allocate an array on one side or the other and modify it in place from either side. :| |
Currently, assemblyscript generate all necessary glue code automatically. Just add |
That does not supply enough information to get there, given the documentation, examples, and what is prebuilt with 'asinit'. This is incredibly frustrating with the documentation being full of 404s, examples being years out of date, and the prebuilt sample being the absolute bare minimum to be functional -- if it even is. |
Hello,
I'm one the core team member of the BabylonJS 3D framework (https://github.com/BabylonJS/Babylon.js) and I'm evaluating the opportunity to port some parts of BabylonJS to AS.
A 3D engine needs to compute fast many maths between two frames (so ideally in less than 16 milliseconds to get a framerate 60 fps) and to lower the GC impact for performance reasons.
In my test, I would like to :
then each frame :
What I found so far in the Wiki or in the github issues is how to allocate an array AS side and return the pointer to JS, then to read its values with the right typed view or the right offset.
We are really concerned by allocating once only the memory and by sharing it between JS and AS. I guess that I unfortunately copied or reallocated the memory each call either JS side, either AS side as I didn't understand how to access the same initial array both sides in my AS test so far.
Would you please mind to provide a snippet JS and AS side to achieve this ? mainly, the creation of the persistent array among JS and TS and how to access it again and again along the computation calls then.
The text was updated successfully, but these errors were encountered: