WebVR ตอนที่ 3: การปลดล็อกศักยภาพของ WebAssembly และ AssemblyScript

เผยแพร่แล้ว: 2022-03-11

WebAssembly ไม่ได้ แทนที่ JavaScript อย่างแน่นอนในฐานะภาษากลางของเว็บและโลก

WebAssembly (ตัวย่อ Wasm) เป็นรูปแบบคำสั่งไบนารีสำหรับเครื่องเสมือนแบบสแต็ก Wasm ได้รับการออกแบบให้เป็นเป้าหมายแบบพกพาสำหรับการรวบรวมภาษาระดับสูง เช่น C/C++/Rust ทำให้สามารถปรับใช้บนเว็บสำหรับแอปพลิเคชันไคลเอนต์และเซิร์ฟเวอร์” –WebAssembly.org

สิ่งสำคัญคือต้องแยกแยะว่า WebAssembly ไม่ใช่ภาษา WebAssembly เหมือนกับ '.exe' - หรือดีกว่านั้น - ไฟล์ Java '.class' มันถูกรวบรวมโดยนักพัฒนาเว็บจากภาษาอื่น จากนั้นดาวน์โหลดและเรียกใช้บนเบราว์เซอร์ของคุณ

WebAssembly ให้คุณสมบัติทั้งหมดที่เราต้องการยืม JavaScript เป็นครั้งคราว แต่ไม่เคยต้องการเป็นเจ้าของ เลย เหมือนกับการเช่าเรือหรือม้า WebAssembly ช่วยให้เราเดินทางไปยังภาษาอื่นๆ ได้โดยไม่ต้องเลือก "รูปแบบการใช้ชีวิตของภาษา" ที่ฟุ่มเฟือย ซึ่งช่วยให้เว็บมุ่งเน้นไปที่สิ่งที่สำคัญ เช่น การนำเสนอคุณลักษณะและการปรับปรุงประสบการณ์ของผู้ใช้

มากกว่า 20 ภาษาที่คอมไพล์ไปยัง WebAssembly: Rust, C/C++, C#/.Net, Java, Python, Elixir, Go และแน่นอน JavaScript

หากคุณจำไดอะแกรมสถาปัตยกรรมของการจำลองได้ เราได้มอบหมายการจำลองทั้งหมดให้กับ nBodySimulator ดังนั้นมันจึงจัดการ Web Worker

แผนภาพสถาปัตยกรรมของการจำลอง
รูปที่ 1: สถาปัตยกรรมโดยรวม

หากคุณจำได้จากโพสต์แนะนำ nBodySimulator มีฟังก์ชัน step() ที่เรียกทุกๆ 33ms ฟังก์ชัน step() ทำหน้าที่เหล่านี้ - หมายเลขในแผนภาพด้านบน:

  1. calculateForces() ของ nBodySimulator เรียก this.worker.postMessage() เพื่อเริ่มการคำนวณ
  2. workerWasm.js this.onmessage() ได้รับข้อความ
  3. workerWasm.js เรียกใช้ฟังก์ชัน nBodyForces() ของ nBodyForces.wasm พร้อมกัน
  4. workerWasm.js ตอบกลับโดยใช้ this.postMessage() ไปยังเธรดหลักด้วยกองกำลังใหม่
  5. เธรดหลัก this.worker.onMessage() จัดการข้อมูลและการโทรที่ส่งคืน
  6. nBodySimulator's applyForces() เพื่ออัปเดตตำแหน่งของร่างกาย
  7. ในที่สุด Visualizer จะทาสีใหม่

เธรด UI เธรดผู้ปฏิบัติงานเว็บ
รูปที่ 2: ภายในฟังก์ชัน step() ของเครื่องจำลอง

ในโพสต์ที่แล้ว เราได้สร้าง Web Worker ที่รวมการคำนวณ WASM ของเราไว้ วันนี้ เรากำลังสร้างกล่องเล็กๆ ที่ชื่อ “WASM” และย้ายข้อมูลเข้าและออก

เพื่อความง่าย ฉันเลือก AssemblyScript เป็นภาษาซอร์สโค้ดเพื่อเขียนการคำนวณของเรา AssemblyScript เป็นสับเซตของ TypeScript - ซึ่งเป็น JavaScript ที่พิมพ์ - ดังนั้นคุณรู้อยู่แล้ว

ตัวอย่างเช่น ฟังก์ชัน AssemblyScript นี้จะคำนวณแรงโน้มถ่วงระหว่างวัตถุสองส่วน :f64 ใน someVar:f64 จะทำเครื่องหมายตัวแปร someVar ว่าเป็นค่าทศนิยมสำหรับคอมไพเลอร์ โปรดจำไว้ว่าโค้ดนี้ได้รับการคอมไพล์และรันในรันไทม์ที่แตกต่างจาก JavaScript อย่างสิ้นเชิง

 // AssemblyScript - a TypeScript-like language that compiles to WebAssembly // src/assembly/nBodyForces.ts /** * Given two bodies, calculate the Force of Gravity, * then return as a 3-force vector (x, y, z) * * Sometimes, the force of gravity is: * * Fg = G * mA * mB / r^2 * * Given: * - Fg = Force of gravity * - r = sqrt ( dx + dy + dz) = straight line distance between 3d objects * - G = gravitational constant * - mA, mB = mass of objects * * Today, we're using better-gravity because better-gravity can calculate * force vectors without polar math (sin, cos, tan) * * Fbg = G * mA * mB * dr / r^3 // using dr as a 3-distance vector lets * // us project Fbg as a 3-force vector * * Given: * - Fbg = Force of better gravity * - dr = (dx, dy, dz) // a 3-distance vector * - dx = bodyB.x - bodyA.x * * Force of Better-Gravity: * * - Fbg = (Fx, Fy, Fz) = the change in force applied by gravity each * body's (x,y,z) over this time period * - Fbg = G * mA * mB * dr / r^3 * - dr = (dx, dy, dz) * - Fx = Gmm * dx / r3 * - Fy = Gmm * dy / r3 * - Fz = Gmm * dz / r3 * * From the parameters, return an array [fx, fy, fz] */ function twoBodyForces(xA: f64, yA: f64, zA: f64, mA: f64, xB: f64, yB: f64, zB: f64, mB: f64): f64[] { // Values used in each x,y,z calculation const Gmm: f64 = G * mA * mB const dx: f64 = xB - xA const dy: f64 = yB - yA const dz: f64 = zB - zA const r: f64 = Math.sqrt(dx * dx + dy * dy + dz * dz) const r3: f64 = r * r * r // Return calculated force vector - initialized to zero const ret: f64[] = new Array<f64>(3) // The best not-a-number number is zero. Two bodies in the same x,y,z if (isNaN(r) || r === 0) return ret // Calculate each part of the vector ret[0] = Gmm * dx / r3 ret[1] = Gmm * dy / r3 ret[2] = Gmm * dz / r3 return ret }

ฟังก์ชัน AssemblyScript นี้ใช้ (x, y, z, มวล) สำหรับเนื้อหาสองส่วนและส่งคืนอาร์เรย์ของสามทุ่นที่อธิบาย (x, y, z) แรงเวกเตอร์ที่เนื้อหาใช้ซึ่งกันและกัน เราไม่สามารถเรียกฟังก์ชันนี้จาก JavaScript ได้เนื่องจาก JavaScript ไม่รู้ว่าจะหาได้จากที่ไหน เราต้อง "ส่งออก" เป็น JavaScript สิ่งนี้นำเราไปสู่ความท้าทายทางเทคนิคครั้งแรกของเรา

WebAssembly นำเข้าและส่งออก

ใน ES6 เรานึกถึงการนำเข้าและส่งออกในโค้ด JavaScript และใช้เครื่องมืออย่างเช่น Rollup หรือ Webpack เพื่อสร้างโค้ดที่ทำงานในเบราว์เซอร์รุ่นเก่าเพื่อจัดการ import และ require() สิ่งนี้จะสร้างแผนผังการพึ่งพาจากบนลงล่างและเปิดใช้งานเทคโนโลยีเจ๋งๆ เช่น "การเขย่าต้นไม้" และการแยกโค้ด

ใน WebAssembly การนำเข้าและส่งออกจะบรรลุภารกิจที่แตกต่างจากการนำเข้า ES6 การนำเข้า/ส่งออก WebAssembly:

  • จัดเตรียมสภาพแวดล้อมรันไทม์สำหรับโมดูล WebAssembly (เช่น ฟังก์ชัน trace() และ abort() )
  • นำเข้าและส่งออกฟังก์ชันและค่าคงที่ระหว่างรันไทม์

ในโค้ดด้านล่าง env.abort และ env.trace เป็นส่วนหนึ่งของสภาพแวดล้อมที่เราต้องจัดเตรียมให้กับโมดูล WebAssembly nBodyForces.logI และผองเพื่อน จัดเตรียมข้อความการดีบักไปยังคอนโซล โปรดทราบว่าการส่งสตริงเข้า/ออกจาก WebAssembly นั้นไม่สำคัญ เนื่องจากประเภทเดียวของ WebAssembly คือหมายเลข i32, i64, f32, f64 โดยที่ i32 อ้างอิงถึงหน่วยความจำเชิงเส้นแบบนามธรรม

หมายเหตุ: ตัวอย่างโค้ดเหล่านี้จะสลับไปมาระหว่างโค้ด JavaScript (ผู้ปฏิบัติงานบนเว็บ) และ AssemblyScript (โค้ด WASM)

 // Web Worker JavaScript in workerWasm.js /** * When we instantiate the Wasm module, give it a context to work in: * nBodyForces: {} is a table of functions we can import into AssemblyScript. See top of nBodyForces.ts * env: {} describes the environment sent to the Wasm module as it's instantiated */ const importObj = { nBodyForces: { logI(data) { console.log("Log() - " + data); }, logF(data) { console.log("Log() - " + data); }, }, env: { abort(msg, file, line, column) { // wasm.__getString() is added by assemblyscript's loader: // https://github.com/AssemblyScript/assemblyscript/tree/master/lib/loader console.error("abort: (" + wasm.__getString(msg) + ") at " + wasm.__getString(file) + ":" + line + ":" + column); }, trace(msg, n) { console.log("trace: " + wasm.__getString(msg) + (n ? " " : "") + Array.prototype.slice.call(arguments, 2, 2 + n).join(", ")); } } }

ในโค้ด AssemblyScript ของเรา เราสามารถนำเข้าฟังก์ชันเหล่านี้ได้ดังนี้:

 // nBodyForces.ts declare function logI(data: i32): void declare function logF(data: f64): void

หมายเหตุ : การยกเลิกและการติดตามจะถูกนำเข้าโดยอัตโนมัติ

จาก AssemblyScript เราสามารถส่งออกอินเทอร์เฟซของเรา นี่คือค่าคงที่ที่ส่งออกบางส่วน:

 // src/assembly/nBodyForces.ts // Gravitational constant. Any G could be used in a game. // This value is best for a scientific simulation. export const G: f64 = 6.674e-11; // for sizing and indexing arrays export const bodySize: i32 = 4 export const forceSize: i32 = 3

และนี่คือการส่งออกของ nBodyForces() ซึ่งเราจะเรียกจาก JavaScript เราส่งออกประเภท Float64Array ที่ด้านบนสุดของไฟล์ เพื่อให้เราสามารถใช้ตัวโหลด JavaScript ของ AssemblyScript ใน Web Worker ของเราเพื่อรับข้อมูล (ดูด้านล่าง):

 // src/assembly/nBodyForces.ts export const FLOAT64ARRAY_ID = idof<Float64Array>(); ... /** * Given N bodies with mass, in a 3d space, calculate the forces of gravity to be applied to each body. * * This function is exported to JavaScript, so only takes/returns numbers and arrays. * For N bodies, pass and array of 4N values (x,y,z,mass) and expect a 3N array of forces (x,y,z) * Those forces can be applied to the bodies mass to update its position in the simulation. * Calculate the 3-vector each unique pair of bodies applies to each other. * * 0 1 2 3 4 5 * 0 xxxxx * 1 xxxx * 2 xxx * 3 xx * 4 x * 5 * * Sum those forces together into an array of 3-vector x,y,z forces * * Return 0 on success */ export function nBodyForces(arrBodies: Float64Array): Float64Array { // Check inputs const numBodies: i32 = arrBodies.length / bodySize if (arrBodies.length % bodySize !== 0) trace("INVALID nBodyForces parameter. Chaos ensues...") // Create result array. This should be garbage collected later. let arrForces: Float64Array = new Float64Array(numBodies * forceSize) // For all bodies: for (let i: i32 = 0; i < numBodies; i++) { // Given body i: pair with every body[j] where j > i for (let j: i32 = i + 1; j < numBodies; j++) { // Calculate the force the bodies apply to one another const bI: i32 = i * bodySize const bJ: i32 = j * bodySize const f: f64[] = twoBodyForces( arrBodies[bI], arrBodies[bI + 1], arrBodies[bI + 2], arrBodies[bI + 3], // x,y,z,m arrBodies[bJ], arrBodies[bJ + 1], arrBodies[bJ + 2], arrBodies[bJ + 3], // x,y,z,m ) // Add this pair's force on one another to their total forces applied x,y,z const fI: i32 = i * forceSize const fJ: i32 = j * forceSize // body0 arrForces[fI] = arrForces[fI] + f[0] arrForces[fI + 1] = arrForces[fI + 1] + f[1] arrForces[fI + 2] = arrForces[fI + 2] + f[2] // body1 arrForces[fJ] = arrForces[fJ] - f[0] // apply forces in opposite direction arrForces[fJ + 1] = arrForces[fJ + 1] - f[1] arrForces[fJ + 2] = arrForces[fJ + 2] - f[2] } } // For each body, return the sum of forces all other bodies applied to it. // If you would like to debug wasm, you can use trace or the log functions // described in workerWasm when we initialized // Eg trace("nBodyForces returns (b0x, b0y, b0z, b1z): ", 4, arrForces[0], arrForces[1], arrForces[2], arrForces[3]) // x,y,z return arrForces // success }

WebAssembly Artifacts: .wasm และ .wat

เมื่อ AssemblyScript nBodyForces.ts ของเราถูกคอมไพล์เป็นไบนารี nBodyForces.wasm จะมีตัวเลือกในการสร้างเวอร์ชัน "ข้อความ" ที่อธิบายคำแนะนำในไบนารี

WebAssembly Artifacts
รูปที่ 3: จำไว้ว่า AssemblyScript เป็นภาษา WebAssembly เป็นคอมไพเลอร์และรันไทม์

ภายในไฟล์ nBodyForces.wat เราจะเห็นการนำเข้าและส่งออกเหล่านี้:

 ;; This is a comment in nBodyForces.wat (module ;; compiler defined types (type $FUNCSIG$iii (func (param i32 i32) (result i32))) … ;; Expected imports from JavaScript (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64))) ;; Memory section defining data constants like strings (memory $0 1) (data (i32.const 8) "\1e\00\00\00\01\00\00\00\01\00\00\00\1e\00\00\00~\00l\00i\00b\00/\00r\00t\00/\00t\00l\00s\00f\00.\00t\00s\00") ... ;; Our global constants (not yet exported) (global $nBodyForces/FLOAT64ARRAY_ID i32 (i32.const 3)) (global $nBodyForces/G f64 (f64.const 6.674e-11)) (global $nBodyForces/bodySize i32 (i32.const 4)) (global $nBodyForces/forceSize i32 (i32.const 3)) ... ;; Memory management functions we'll use in a minute (export "memory" (memory $0)) (export "__alloc" (func $~lib/rt/tlsf/__alloc)) (export "__retain" (func $~lib/rt/pure/__retain)) (export "__release" (func $~lib/rt/pure/__release)) (export "__collect" (func $~lib/rt/pure/__collect)) (export "__rtti_base" (global $~lib/rt/__rtti_base)) ;; Finally our exported constants and function (export "FLOAT64ARRAY_ID" (global $nBodyForces/FLOAT64ARRAY_ID)) (export "G" (global $nBodyForces/G)) (export "bodySize" (global $nBodyForces/bodySize)) (export "forceSize" (global $nBodyForces/forceSize)) (export "nBodyForces" (func $nBodyForces/nBodyForces)) ;; Implementation details ...

ตอนนี้เรามีไบนารี nBodyForces.wasm และพนักงานเว็บเพื่อใช้งาน เตรียมตัวให้พร้อม! และการจัดการหน่วยความจำบางอย่าง!

เพื่อให้การผสานรวมเสร็จสมบูรณ์ เราต้องส่งอาร์เรย์ตัวแปรของ floats ไปยัง WebAssembly และส่งคืนอาร์เรย์ตัวแปรของ floats ไปยัง JavaScript

ด้วยชนชั้นนายทุน JavaScript ที่ไร้เดียงสา ฉันตั้งใจที่จะส่งอาร์เรย์ขนาดตัวแปรฉูดฉาดเหล่านี้เข้าและออกจากรันไทม์ประสิทธิภาพสูงข้ามแพลตฟอร์มเท่านั้น การส่งข้อมูลไปยัง/จาก WebAssembly ถือเป็นปัญหาที่คาดไม่ถึงมากที่สุดในโปรเจ็กต์นี้

อย่างไรก็ตาม ด้วยความขอบคุณอย่างมากสำหรับการทำงานหนักโดยทีมงาน AssemblyScript เราสามารถใช้ "ตัวโหลด" เพื่อช่วย:

 // workerWasm.js - our web worker /** * AssemblyScript loader adds helpers for moving data to/from AssemblyScript. * Highly recommended */ const loader = require("assemblyscript/lib/loader")

require() หมายความว่าเราจำเป็นต้องใช้ชุดรวมโมดูลเช่น Rollup หรือ Webpack สำหรับโปรเจ็กต์นี้ ฉันเลือก Rollup เนื่องจากมีความเรียบง่ายและยืดหยุ่น และไม่เคยมองย้อนกลับไป

โปรดจำไว้ว่าผู้ปฏิบัติงานเว็บของเราทำงานในเธรดแยกต่างหากและโดยพื้นฐานแล้วเป็น onmessage() ที่มีคำสั่ง switch()

loader สร้างโมดูล wasm ของเราด้วยฟังก์ชันการจัดการหน่วยความจำที่มีประโยชน์เป็นพิเศษ __retain() และ __release() จัดการการอ้างอิงการรวบรวมขยะในรันไทม์ของผู้ปฏิบัติงาน __allocArray() คัดลอกอาร์เรย์พารามิเตอร์ของเราลงในหน่วยความจำของโมดูล __getFloat64Array() คัดลอกอาร์เรย์ผลลัพธ์จากโมดูล wasm ไปยังรันไทม์ของผู้ปฏิบัติงาน

ตอนนี้เราสามารถจัดการอาร์เรย์ float เข้าและออกจาก nBodyForces() และทำการจำลองให้เสร็จสมบูรณ์ได้:

 // workerWasm.js /** * Web workers listen for messages from the main thread. */ this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. wasm = loader.instantiate(msg.wasmModule, importObj) // Throws // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return // Message: Given array of floats describing a system of bodies (x,y,x,mass), // calculate the Grav forces to be applied to each body case 'nBodyForces': if (!wasm) throw new Error('wasm not initialized') // Copy msg.arrBodies array into the wasm instance, increase GC count const dataRef = wasm.__retain(wasm.__allocArray(wasm.FLOAT64ARRAY_ID, msg.arrBodies)); // Do the calculations in this thread synchronously const resultRef = wasm.nBodyForces(dataRef); // Copy result array from the wasm instance to our javascript runtime const arrForces = wasm.__getFloat64Array(resultRef); // Decrease the GC count on dataRef from __retain() here, // and GC count from new Float64Array in wasm module wasm.__release(dataRef); wasm.__release(resultRef); // Message results back to main thread. // see nBodySimulation.js this.worker.onmessage return this.postMessage({ purpose: 'nBodyForces', arrForces }) } }

จากทั้งหมดที่เราได้เรียนรู้มา มาทบทวน Web Worker และเส้นทาง WebAssembly กัน ยินดีต้อนรับสู่แบ็กเอนด์เบราว์เซอร์ใหม่ของเว็บ นี่คือลิงค์ไปยังรหัสบน GitHub:

  1. รับ Index.html
  2. main.js
  3. nBodySimulator.js - ส่งข้อความไปยังผู้ดูแลเว็บ
  4. workerWasm.js - เรียกฟังก์ชัน WebAssembly
  5. nBodyForces.ts - คำนวณและส่งกลับอาร์เรย์ของกองกำลัง
  6. workerWasm.js - ส่งผลลัพธ์กลับไปที่เธรดหลัก
  7. nBodySimulator.js - แก้ไขคำมั่นสัญญาสำหรับกองกำลัง
  8. nBodySimulator.js - จากนั้นใช้แรงกับร่างกายและบอกให้นักสร้างภาพระบายสี

จากนี้ไป มาเริ่มการแสดงโดยสร้าง nBodyVisualizer.js ! โพสต์ถัดไปของเราสร้างวิชวลไลเซอร์โดยใช้ Canvas API และโพสต์สุดท้ายปิดท้ายด้วย WebVR และ Aframe

ที่เกี่ยวข้อง: บทช่วยสอน WebAssembly / Rust: การประมวลผลเสียงที่สมบูรณ์แบบ