diff --git a/Cargo.lock b/Cargo.lock index f2bc838..0744971 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,18 +457,18 @@ dependencies = [ [[package]] name = "pvm-assets-export" -version = "0.1.1" +version = "0.1.2" dependencies = [ "pvm-runtime-assets", ] [[package]] name = "pvm-gpu-wire" -version = "0.1.1" +version = "0.1.2" [[package]] name = "pvm-runtime" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "log", @@ -486,7 +486,7 @@ dependencies = [ [[package]] name = "pvm-runtime-assets" -version = "0.1.1" +version = "0.1.2" dependencies = [ "sha2", ] diff --git a/Cargo.toml b/Cargo.toml index 228ae59..e5bc053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.1.1" +version = "0.1.2" edition = "2021" license = "MPL-2.0" repository = "https://github.com/paritytech/pvm-host-runtime" diff --git a/js/packages/pvm-browser-runtime/SOURCE b/js/packages/pvm-browser-runtime/SOURCE index 5fc098a..4f057d2 100644 --- a/js/packages/pvm-browser-runtime/SOURCE +++ b/js/packages/pvm-browser-runtime/SOURCE @@ -1,6 +1,6 @@ PolkaVM App v2 browser runtime Source repository: https://github.com/paritytech/pvm-host-runtime -Release tag: v0.1.1 +Release tag: v0.1.2 Rust crate: rust/crates/pvm-runtime GPU wire crate: rust/crates/pvm-gpu-wire PolkaVM native revision: e06cce9da5c6d3f72458168876ea304e27992094 diff --git a/js/packages/pvm-browser-runtime/package.json b/js/packages/pvm-browser-runtime/package.json index 50f3a74..3a76ac8 100644 --- a/js/packages/pvm-browser-runtime/package.json +++ b/js/packages/pvm-browser-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@parity/pvm-browser-runtime", - "version": "0.1.1", + "version": "0.1.2", "description": "Host-neutral PolkaVM App v2 browser runtime artifacts", "license": "MPL-2.0", "type": "module", diff --git a/js/packages/pvm-browser-runtime/src/pvm-runtime-core.js b/js/packages/pvm-browser-runtime/src/pvm-runtime-core.js index e5a40ed..87d89da 100644 --- a/js/packages/pvm-browser-runtime/src/pvm-runtime-core.js +++ b/js/packages/pvm-browser-runtime/src/pvm-runtime-core.js @@ -140,6 +140,18 @@ globalThis.createPvmRuntime = endpoint => { } } + function drainTruapiRequests() { + while (pvm.pvm_browser_take_truapi_request?.()) { + const length = pvm.pvm_browser_truapi_request_length(); + const bytes = new Uint8Array( + pvm.memory.buffer, + pvm.pvm_browser_truapi_request_pointer(), + length + ).slice(); + postMessage({ type: "truapi-request", bytes }, [bytes.buffer]); + } + } + function drainAudio() { while (pvm.pvm_browser_take_audio()) { const sampleRate = pvm.pvm_browser_audio_sample_rate(); @@ -199,6 +211,7 @@ globalThis.createPvmRuntime = endpoint => { drainFrame(); drainTri2d(); drainGpuBatches(); + drainTruapiRequests(); drainAudio(); drainSave(); drainLogs(); @@ -472,6 +485,7 @@ globalThis.createPvmRuntime = endpoint => { postMessage({ type: "startup", stage: "interpreter-initialized" }); drainTri2d(); drainGpuBatches(); + drainTruapiRequests(); drainLogs(); } startedAt = performance.now(); @@ -523,6 +537,21 @@ globalThis.createPvmRuntime = endpoint => { check(pvm.pvm_browser_send_gpu_event(), "send PolkaVM browser GPU event"); } + function sendTruapiResponse(bytes) { + if (!running || !pvm || !bytes.byteLength) { + return; + } + if (translated) { + translated.sendTruapiResponse(bytes); + return; + } + stage(bytes); + check( + pvm.pvm_browser_send_truapi_response(), + "send PolkaVM browser TrUAPI response" + ); + } + endpoint.onmessage = event => { const message = event.data; if (message?.type === "start") { @@ -547,6 +576,14 @@ globalThis.createPvmRuntime = endpoint => { postMessage({ type: "error", message: error.message }); postMessage({ type: "terminated" }); } + } else if (message?.type === "truapi-response") { + try { + sendTruapiResponse(new Uint8Array(message.bytes)); + } catch (error) { + stopRuntime(); + postMessage({ type: "error", message: error.message }); + postMessage({ type: "terminated" }); + } } else if (message?.type === "stop") { stopRuntime(); postMessage({ type: "terminated" }); diff --git a/js/packages/pvm-browser-runtime/src/pvm-wasm-translated.js b/js/packages/pvm-browser-runtime/src/pvm-wasm-translated.js index a4bf246..a224ea7 100644 --- a/js/packages/pvm-browser-runtime/src/pvm-wasm-translated.js +++ b/js/packages/pvm-browser-runtime/src/pvm-wasm-translated.js @@ -23,6 +23,9 @@ const MAX_GPU_EVENT_BYTES = 64 * 1024; const MAX_GPU_EVENTS = 256; const MAX_GPU_SUBMITS_PER_UPDATE = 8; + const MAX_TRUAPI_FRAME_BYTES = 1024 * 1024; + const MAX_TRUAPI_FRAMES = 32; + const MAX_TRUAPI_QUEUE_BYTES = 4 * 1024 * 1024; const MAX_GPU_COMMANDS = 16_384; const GPU_ERROR_MALFORMED_BATCH = -2; const GPU_ERROR_QUOTA_EXCEEDED = -3; @@ -258,6 +261,10 @@ this.gpuEvents = []; this.gpuSubmits = 0; this.gpuLastSequence = 0n; + this.truapiRequests = 0; + this.truapiRequestBytes = 0; + this.truapiResponses = []; + this.truapiResponseBytes = 0; this.tri2dSubmitted = false; this.maxGas = BigInt(maxGas); this.input = []; @@ -310,6 +317,8 @@ } this.timeMs = timeMs; this.gpuSubmits = 0; + this.truapiRequests = 0; + this.truapiRequestBytes = 0; this.#resetBudget( this.coreVm && !this.coreVmStarted ? MAX_HOSTCALLS_PER_INIT @@ -400,11 +409,34 @@ this.gpuEvents.push(bytes.slice()); } + sendTruapiResponse(bytes) { + if ( + this.stopped || + !(bytes instanceof Uint8Array) || + !bytes.byteLength || + bytes.byteLength > MAX_TRUAPI_FRAME_BYTES + ) { + throw new Error("invalid translated TrUAPI response"); + } + if ( + this.truapiResponses.length === MAX_TRUAPI_FRAMES || + this.truapiResponseBytes + bytes.byteLength > MAX_TRUAPI_QUEUE_BYTES + ) { + throw new Error("translated TrUAPI response queue overflow"); + } + this.truapiResponses.push(bytes.slice()); + this.truapiResponseBytes += bytes.byteLength; + } + stop() { this.stopped = true; this.input.length = 0; this.coreInput.length = 0; + this.truapiRequests = 0; + this.truapiRequestBytes = 0; this.gpuEvents.length = 0; + this.truapiResponses.length = 0; + this.truapiResponseBytes = 0; } #resetBudget(hostcalls) { @@ -764,6 +796,43 @@ this.#setReg(7, 0n); return false; } + case "host_truapi_send": { + const length = this.#u32(a1); + if (!length || length > MAX_TRUAPI_FRAME_BYTES) { + this.#setReg(7, 1n); + return false; + } + if ( + this.truapiRequests === MAX_TRUAPI_FRAMES || + this.truapiRequestBytes + length > MAX_TRUAPI_QUEUE_BYTES + ) { + this.#setReg(7, 2n); + return false; + } + const bytes = this.#read(this.#u32(a0), length); + this.emit({ type: "truapi-request", bytes }, [bytes.buffer]); + this.truapiRequests++; + this.truapiRequestBytes += length; + this.#setReg(7, 0n); + return false; + } + case "host_truapi_poll": { + const response = this.truapiResponses[0]; + if (response === undefined) { + this.#setReg(7, 0n); + return false; + } + const capacity = this.#u32(a1); + if (capacity < response.byteLength) { + this.#setReg(7, BigInt(-response.byteLength)); + return false; + } + this.#write(this.#u32(a0), response); + this.truapiResponses.shift(); + this.truapiResponseBytes -= response.byteLength; + this.#setReg(7, BigInt(response.byteLength)); + return false; + } case "host_asset_read": { const nameLength = this.#u32(a1); const offset = this.#u32(a2); @@ -918,6 +987,9 @@ // eslint-disable-next-line complexity -- Flat hostcall dispatch mirrors the guest ABI. #handleCoreVmCall(name) { switch (name) { + case "host_truapi_send": + case "host_truapi_poll": + return this.#handleCooperativeCall(name); case "pvm_set_palette": { const palette = this.#read(this.#u32(this.#reg(7)), 256 * 3); for (let index = 0; index < 256; index++) { diff --git a/package-lock.json b/package-lock.json index 7d4ae35..f9276a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pvm-host-runtime-workspace", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pvm-host-runtime-workspace", - "version": "0.1.1", + "version": "0.1.2", "license": "MPL-2.0", "workspaces": [ "js/packages/*" @@ -17,7 +17,7 @@ }, "js/packages/pvm-browser-runtime": { "name": "@parity/pvm-browser-runtime", - "version": "0.1.1", + "version": "0.1.2", "license": "MPL-2.0", "engines": { "node": ">=22" diff --git a/package.json b/package.json index 41c55cb..6394360 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pvm-host-runtime-workspace", "private": true, - "version": "0.1.1", + "version": "0.1.2", "description": "Host-neutral PolkaVM application runtime workspace", "license": "MPL-2.0", "type": "module", diff --git a/rust/crates/pvm-assets-export/Cargo.toml b/rust/crates/pvm-assets-export/Cargo.toml index d0d7759..6665bc9 100644 --- a/rust/crates/pvm-assets-export/Cargo.toml +++ b/rust/crates/pvm-assets-export/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pvm-assets-export" -version = "0.1.1" +version = "0.1.2" edition = "2021" description = "Exports host-neutral PolkaVM browser runtime assets" license = "MPL-2.0" @@ -8,4 +8,4 @@ repository = "https://github.com/paritytech/pvm-host-runtime" publish = false [dependencies] -pvm-runtime-assets = { version = "0.1.1", path = "../pvm-runtime-assets" } +pvm-runtime-assets = { version = "0.1.2", path = "../pvm-runtime-assets" } diff --git a/rust/crates/pvm-gpu-wire/Cargo.toml b/rust/crates/pvm-gpu-wire/Cargo.toml index f6263fb..5a8b77a 100644 --- a/rust/crates/pvm-gpu-wire/Cargo.toml +++ b/rust/crates/pvm-gpu-wire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pvm-gpu-wire" -version = "0.1.1" +version = "0.1.2" edition = "2021" description = "Bounded host-neutral WebGPU Raster wire protocol for PolkaVM applications" license = "MPL-2.0" diff --git a/rust/crates/pvm-runtime-assets/Cargo.toml b/rust/crates/pvm-runtime-assets/Cargo.toml index d8f74ad..0198489 100644 --- a/rust/crates/pvm-runtime-assets/Cargo.toml +++ b/rust/crates/pvm-runtime-assets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pvm-runtime-assets" -version = "0.1.1" +version = "0.1.2" edition = "2021" description = "Source-built browser assets for the host-neutral PolkaVM runtime" license = "MPL-2.0" diff --git a/rust/crates/pvm-runtime-assets/assets/SHA256SUMS b/rust/crates/pvm-runtime-assets/assets/SHA256SUMS index ac58358..ab9e78c 100644 --- a/rust/crates/pvm-runtime-assets/assets/SHA256SUMS +++ b/rust/crates/pvm-runtime-assets/assets/SHA256SUMS @@ -1,6 +1,6 @@ -182c471bd62e3279abe996f33d8e9ccfded4de7e6b4a18470cad1e67f1e074f3 pvm-browser-runtime.wasm -c25dbaef94f91dcdfe33185fc58485fdc128c5f9edde11daa23f94fdb6464d18 pvm-worker.js +df0d64ed4a028693c680380990c6f2b84dae096373d15f716d7c6ad96fe45041 pvm-browser-runtime.wasm +921d6c3b06be34ad39a57ae9ae612e37840338e58b7e83a2f81bbea9a12c6ff1 pvm-worker.js 33418e2c81c117539569eb4cf91af4d058cdfae7dd4556b13f3687f6d6b3bae4 pvm-gpu-worker.js -865bfe48c8e01c18a93d40f868afd8b21e2be2ec84e0c993faa54470480abd3c pvm-wasm-translated.js -51891fa4bd0a513dd7ad14b82c4a9f6cbb880755f93612fa4d4ebecd1bcc8dcb pvm-runtime-core.js +17e52fff13b7f5e6732c6290c11e133869cda46b64d4fd09c6089972b9f36904 pvm-wasm-translated.js +f527c1f530ce326e4d40840a5c0456dc9dad5798818038c8f0fe865040742c50 pvm-runtime-core.js 9c929f5d5c64a1b75e7e48485d7c3944ed6838112177ea778827a2c407c2d820 pvm-wasm-worker-entry.js diff --git a/rust/crates/pvm-runtime-assets/assets/pvm-browser-runtime.wasm b/rust/crates/pvm-runtime-assets/assets/pvm-browser-runtime.wasm index d3efad0..03fd724 100755 Binary files a/rust/crates/pvm-runtime-assets/assets/pvm-browser-runtime.wasm and b/rust/crates/pvm-runtime-assets/assets/pvm-browser-runtime.wasm differ diff --git a/rust/crates/pvm-runtime-assets/assets/pvm-runtime-core.js b/rust/crates/pvm-runtime-assets/assets/pvm-runtime-core.js index e5a40ed..87d89da 100644 --- a/rust/crates/pvm-runtime-assets/assets/pvm-runtime-core.js +++ b/rust/crates/pvm-runtime-assets/assets/pvm-runtime-core.js @@ -140,6 +140,18 @@ globalThis.createPvmRuntime = endpoint => { } } + function drainTruapiRequests() { + while (pvm.pvm_browser_take_truapi_request?.()) { + const length = pvm.pvm_browser_truapi_request_length(); + const bytes = new Uint8Array( + pvm.memory.buffer, + pvm.pvm_browser_truapi_request_pointer(), + length + ).slice(); + postMessage({ type: "truapi-request", bytes }, [bytes.buffer]); + } + } + function drainAudio() { while (pvm.pvm_browser_take_audio()) { const sampleRate = pvm.pvm_browser_audio_sample_rate(); @@ -199,6 +211,7 @@ globalThis.createPvmRuntime = endpoint => { drainFrame(); drainTri2d(); drainGpuBatches(); + drainTruapiRequests(); drainAudio(); drainSave(); drainLogs(); @@ -472,6 +485,7 @@ globalThis.createPvmRuntime = endpoint => { postMessage({ type: "startup", stage: "interpreter-initialized" }); drainTri2d(); drainGpuBatches(); + drainTruapiRequests(); drainLogs(); } startedAt = performance.now(); @@ -523,6 +537,21 @@ globalThis.createPvmRuntime = endpoint => { check(pvm.pvm_browser_send_gpu_event(), "send PolkaVM browser GPU event"); } + function sendTruapiResponse(bytes) { + if (!running || !pvm || !bytes.byteLength) { + return; + } + if (translated) { + translated.sendTruapiResponse(bytes); + return; + } + stage(bytes); + check( + pvm.pvm_browser_send_truapi_response(), + "send PolkaVM browser TrUAPI response" + ); + } + endpoint.onmessage = event => { const message = event.data; if (message?.type === "start") { @@ -547,6 +576,14 @@ globalThis.createPvmRuntime = endpoint => { postMessage({ type: "error", message: error.message }); postMessage({ type: "terminated" }); } + } else if (message?.type === "truapi-response") { + try { + sendTruapiResponse(new Uint8Array(message.bytes)); + } catch (error) { + stopRuntime(); + postMessage({ type: "error", message: error.message }); + postMessage({ type: "terminated" }); + } } else if (message?.type === "stop") { stopRuntime(); postMessage({ type: "terminated" }); diff --git a/rust/crates/pvm-runtime-assets/assets/pvm-wasm-translated.js b/rust/crates/pvm-runtime-assets/assets/pvm-wasm-translated.js index a4bf246..a224ea7 100644 --- a/rust/crates/pvm-runtime-assets/assets/pvm-wasm-translated.js +++ b/rust/crates/pvm-runtime-assets/assets/pvm-wasm-translated.js @@ -23,6 +23,9 @@ const MAX_GPU_EVENT_BYTES = 64 * 1024; const MAX_GPU_EVENTS = 256; const MAX_GPU_SUBMITS_PER_UPDATE = 8; + const MAX_TRUAPI_FRAME_BYTES = 1024 * 1024; + const MAX_TRUAPI_FRAMES = 32; + const MAX_TRUAPI_QUEUE_BYTES = 4 * 1024 * 1024; const MAX_GPU_COMMANDS = 16_384; const GPU_ERROR_MALFORMED_BATCH = -2; const GPU_ERROR_QUOTA_EXCEEDED = -3; @@ -258,6 +261,10 @@ this.gpuEvents = []; this.gpuSubmits = 0; this.gpuLastSequence = 0n; + this.truapiRequests = 0; + this.truapiRequestBytes = 0; + this.truapiResponses = []; + this.truapiResponseBytes = 0; this.tri2dSubmitted = false; this.maxGas = BigInt(maxGas); this.input = []; @@ -310,6 +317,8 @@ } this.timeMs = timeMs; this.gpuSubmits = 0; + this.truapiRequests = 0; + this.truapiRequestBytes = 0; this.#resetBudget( this.coreVm && !this.coreVmStarted ? MAX_HOSTCALLS_PER_INIT @@ -400,11 +409,34 @@ this.gpuEvents.push(bytes.slice()); } + sendTruapiResponse(bytes) { + if ( + this.stopped || + !(bytes instanceof Uint8Array) || + !bytes.byteLength || + bytes.byteLength > MAX_TRUAPI_FRAME_BYTES + ) { + throw new Error("invalid translated TrUAPI response"); + } + if ( + this.truapiResponses.length === MAX_TRUAPI_FRAMES || + this.truapiResponseBytes + bytes.byteLength > MAX_TRUAPI_QUEUE_BYTES + ) { + throw new Error("translated TrUAPI response queue overflow"); + } + this.truapiResponses.push(bytes.slice()); + this.truapiResponseBytes += bytes.byteLength; + } + stop() { this.stopped = true; this.input.length = 0; this.coreInput.length = 0; + this.truapiRequests = 0; + this.truapiRequestBytes = 0; this.gpuEvents.length = 0; + this.truapiResponses.length = 0; + this.truapiResponseBytes = 0; } #resetBudget(hostcalls) { @@ -764,6 +796,43 @@ this.#setReg(7, 0n); return false; } + case "host_truapi_send": { + const length = this.#u32(a1); + if (!length || length > MAX_TRUAPI_FRAME_BYTES) { + this.#setReg(7, 1n); + return false; + } + if ( + this.truapiRequests === MAX_TRUAPI_FRAMES || + this.truapiRequestBytes + length > MAX_TRUAPI_QUEUE_BYTES + ) { + this.#setReg(7, 2n); + return false; + } + const bytes = this.#read(this.#u32(a0), length); + this.emit({ type: "truapi-request", bytes }, [bytes.buffer]); + this.truapiRequests++; + this.truapiRequestBytes += length; + this.#setReg(7, 0n); + return false; + } + case "host_truapi_poll": { + const response = this.truapiResponses[0]; + if (response === undefined) { + this.#setReg(7, 0n); + return false; + } + const capacity = this.#u32(a1); + if (capacity < response.byteLength) { + this.#setReg(7, BigInt(-response.byteLength)); + return false; + } + this.#write(this.#u32(a0), response); + this.truapiResponses.shift(); + this.truapiResponseBytes -= response.byteLength; + this.#setReg(7, BigInt(response.byteLength)); + return false; + } case "host_asset_read": { const nameLength = this.#u32(a1); const offset = this.#u32(a2); @@ -918,6 +987,9 @@ // eslint-disable-next-line complexity -- Flat hostcall dispatch mirrors the guest ABI. #handleCoreVmCall(name) { switch (name) { + case "host_truapi_send": + case "host_truapi_poll": + return this.#handleCooperativeCall(name); case "pvm_set_palette": { const palette = this.#read(this.#u32(this.#reg(7)), 256 * 3); for (let index = 0; index < 256; index++) { diff --git a/rust/crates/pvm-runtime-assets/assets/pvm-worker.js b/rust/crates/pvm-runtime-assets/assets/pvm-worker.js index a16fcdd..a9866d5 100644 --- a/rust/crates/pvm-runtime-assets/assets/pvm-worker.js +++ b/rust/crates/pvm-runtime-assets/assets/pvm-worker.js @@ -23,6 +23,9 @@ const MAX_GPU_EVENT_BYTES = 64 * 1024; const MAX_GPU_EVENTS = 256; const MAX_GPU_SUBMITS_PER_UPDATE = 8; + const MAX_TRUAPI_FRAME_BYTES = 1024 * 1024; + const MAX_TRUAPI_FRAMES = 32; + const MAX_TRUAPI_QUEUE_BYTES = 4 * 1024 * 1024; const MAX_GPU_COMMANDS = 16_384; const GPU_ERROR_MALFORMED_BATCH = -2; const GPU_ERROR_QUOTA_EXCEEDED = -3; @@ -258,6 +261,10 @@ this.gpuEvents = []; this.gpuSubmits = 0; this.gpuLastSequence = 0n; + this.truapiRequests = 0; + this.truapiRequestBytes = 0; + this.truapiResponses = []; + this.truapiResponseBytes = 0; this.tri2dSubmitted = false; this.maxGas = BigInt(maxGas); this.input = []; @@ -310,6 +317,8 @@ } this.timeMs = timeMs; this.gpuSubmits = 0; + this.truapiRequests = 0; + this.truapiRequestBytes = 0; this.#resetBudget( this.coreVm && !this.coreVmStarted ? MAX_HOSTCALLS_PER_INIT @@ -400,11 +409,34 @@ this.gpuEvents.push(bytes.slice()); } + sendTruapiResponse(bytes) { + if ( + this.stopped || + !(bytes instanceof Uint8Array) || + !bytes.byteLength || + bytes.byteLength > MAX_TRUAPI_FRAME_BYTES + ) { + throw new Error("invalid translated TrUAPI response"); + } + if ( + this.truapiResponses.length === MAX_TRUAPI_FRAMES || + this.truapiResponseBytes + bytes.byteLength > MAX_TRUAPI_QUEUE_BYTES + ) { + throw new Error("translated TrUAPI response queue overflow"); + } + this.truapiResponses.push(bytes.slice()); + this.truapiResponseBytes += bytes.byteLength; + } + stop() { this.stopped = true; this.input.length = 0; this.coreInput.length = 0; + this.truapiRequests = 0; + this.truapiRequestBytes = 0; this.gpuEvents.length = 0; + this.truapiResponses.length = 0; + this.truapiResponseBytes = 0; } #resetBudget(hostcalls) { @@ -764,6 +796,43 @@ this.#setReg(7, 0n); return false; } + case "host_truapi_send": { + const length = this.#u32(a1); + if (!length || length > MAX_TRUAPI_FRAME_BYTES) { + this.#setReg(7, 1n); + return false; + } + if ( + this.truapiRequests === MAX_TRUAPI_FRAMES || + this.truapiRequestBytes + length > MAX_TRUAPI_QUEUE_BYTES + ) { + this.#setReg(7, 2n); + return false; + } + const bytes = this.#read(this.#u32(a0), length); + this.emit({ type: "truapi-request", bytes }, [bytes.buffer]); + this.truapiRequests++; + this.truapiRequestBytes += length; + this.#setReg(7, 0n); + return false; + } + case "host_truapi_poll": { + const response = this.truapiResponses[0]; + if (response === undefined) { + this.#setReg(7, 0n); + return false; + } + const capacity = this.#u32(a1); + if (capacity < response.byteLength) { + this.#setReg(7, BigInt(-response.byteLength)); + return false; + } + this.#write(this.#u32(a0), response); + this.truapiResponses.shift(); + this.truapiResponseBytes -= response.byteLength; + this.#setReg(7, BigInt(response.byteLength)); + return false; + } case "host_asset_read": { const nameLength = this.#u32(a1); const offset = this.#u32(a2); @@ -918,6 +987,9 @@ // eslint-disable-next-line complexity -- Flat hostcall dispatch mirrors the guest ABI. #handleCoreVmCall(name) { switch (name) { + case "host_truapi_send": + case "host_truapi_poll": + return this.#handleCooperativeCall(name); case "pvm_set_palette": { const palette = this.#read(this.#u32(this.#reg(7)), 256 * 3); for (let index = 0; index < 256; index++) { @@ -1398,6 +1470,18 @@ globalThis.createPvmRuntime = endpoint => { } } + function drainTruapiRequests() { + while (pvm.pvm_browser_take_truapi_request?.()) { + const length = pvm.pvm_browser_truapi_request_length(); + const bytes = new Uint8Array( + pvm.memory.buffer, + pvm.pvm_browser_truapi_request_pointer(), + length + ).slice(); + postMessage({ type: "truapi-request", bytes }, [bytes.buffer]); + } + } + function drainAudio() { while (pvm.pvm_browser_take_audio()) { const sampleRate = pvm.pvm_browser_audio_sample_rate(); @@ -1457,6 +1541,7 @@ globalThis.createPvmRuntime = endpoint => { drainFrame(); drainTri2d(); drainGpuBatches(); + drainTruapiRequests(); drainAudio(); drainSave(); drainLogs(); @@ -1730,6 +1815,7 @@ globalThis.createPvmRuntime = endpoint => { postMessage({ type: "startup", stage: "interpreter-initialized" }); drainTri2d(); drainGpuBatches(); + drainTruapiRequests(); drainLogs(); } startedAt = performance.now(); @@ -1781,6 +1867,21 @@ globalThis.createPvmRuntime = endpoint => { check(pvm.pvm_browser_send_gpu_event(), "send PolkaVM browser GPU event"); } + function sendTruapiResponse(bytes) { + if (!running || !pvm || !bytes.byteLength) { + return; + } + if (translated) { + translated.sendTruapiResponse(bytes); + return; + } + stage(bytes); + check( + pvm.pvm_browser_send_truapi_response(), + "send PolkaVM browser TrUAPI response" + ); + } + endpoint.onmessage = event => { const message = event.data; if (message?.type === "start") { @@ -1805,6 +1906,14 @@ globalThis.createPvmRuntime = endpoint => { postMessage({ type: "error", message: error.message }); postMessage({ type: "terminated" }); } + } else if (message?.type === "truapi-response") { + try { + sendTruapiResponse(new Uint8Array(message.bytes)); + } catch (error) { + stopRuntime(); + postMessage({ type: "error", message: error.message }); + postMessage({ type: "terminated" }); + } } else if (message?.type === "stop") { stopRuntime(); postMessage({ type: "terminated" }); diff --git a/rust/crates/pvm-runtime-assets/src/lib.rs b/rust/crates/pvm-runtime-assets/src/lib.rs index 3e474f6..78a6fcb 100644 --- a/rust/crates/pvm-runtime-assets/src/lib.rs +++ b/rust/crates/pvm-runtime-assets/src/lib.rs @@ -26,13 +26,13 @@ const ASSETS: [BrowserAsset; 7] = [ path: "pvm-browser-runtime.wasm", content_type: "application/wasm", bytes: include_bytes!("../assets/pvm-browser-runtime.wasm"), - sha256: "182c471bd62e3279abe996f33d8e9ccfded4de7e6b4a18470cad1e67f1e074f3", + sha256: "df0d64ed4a028693c680380990c6f2b84dae096373d15f716d7c6ad96fe45041", }, BrowserAsset { path: "pvm-worker.js", content_type: "text/javascript", bytes: include_bytes!("../assets/pvm-worker.js"), - sha256: "c25dbaef94f91dcdfe33185fc58485fdc128c5f9edde11daa23f94fdb6464d18", + sha256: "921d6c3b06be34ad39a57ae9ae612e37840338e58b7e83a2f81bbea9a12c6ff1", }, BrowserAsset { path: "pvm-gpu-worker.js", @@ -44,13 +44,13 @@ const ASSETS: [BrowserAsset; 7] = [ path: "pvm-wasm-translated.js", content_type: "text/javascript", bytes: include_bytes!("../assets/pvm-wasm-translated.js"), - sha256: "865bfe48c8e01c18a93d40f868afd8b21e2be2ec84e0c993faa54470480abd3c", + sha256: "17e52fff13b7f5e6732c6290c11e133869cda46b64d4fd09c6089972b9f36904", }, BrowserAsset { path: "pvm-runtime-core.js", content_type: "text/javascript", bytes: include_bytes!("../assets/pvm-runtime-core.js"), - sha256: "51891fa4bd0a513dd7ad14b82c4a9f6cbb880755f93612fa4d4ebecd1bcc8dcb", + sha256: "f527c1f530ce326e4d40840a5c0456dc9dad5798818038c8f0fe865040742c50", }, BrowserAsset { path: "pvm-wasm-worker-entry.js", @@ -62,7 +62,7 @@ const ASSETS: [BrowserAsset; 7] = [ path: "SHA256SUMS", content_type: "text/plain", bytes: include_bytes!("../assets/SHA256SUMS"), - sha256: "2d73c779a18c2d07eb3b276021d495f0f37b7ed559738fd6660643969989f0a2", + sha256: "2019fae0b6e9e45336d6a1da8c53a8ce9756c1c4689a6df0b873c507aefbd851", }, ]; diff --git a/rust/crates/pvm-runtime/Cargo.toml b/rust/crates/pvm-runtime/Cargo.toml index 55fd9e4..bd79280 100644 --- a/rust/crates/pvm-runtime/Cargo.toml +++ b/rust/crates/pvm-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pvm-runtime" -version = "0.1.1" +version = "0.1.2" edition = "2021" description = "Host-neutral App manifest v2 PolkaVM runtime and browser execution wrapper" license = "MPL-2.0" @@ -16,7 +16,7 @@ ffi = ["dep:thiserror", "dep:uniffi"] [dependencies] anyhow = "1" log = "0.4" -pvm-gpu-wire = { version = "0.1.1", path = "../pvm-gpu-wire" } +pvm-gpu-wire = { version = "0.1.2", path = "../pvm-gpu-wire" } serde = { version = "1", features = ["derive"] } serde_json = "1" wasm-encoder = "0.240" diff --git a/rust/crates/pvm-runtime/src/application.rs b/rust/crates/pvm-runtime/src/application.rs index 713c397..4a28f9b 100644 --- a/rust/crates/pvm-runtime/src/application.rs +++ b/rust/crates/pvm-runtime/src/application.rs @@ -165,10 +165,28 @@ impl ApplicationRuntime { } } + pub fn take_truapi_request(&mut self) -> Option> { + match self { + Self::Cooperative(runtime) => runtime.take_truapi_request(), + Self::CoreVm(runtime) => runtime.vm.take_truapi_request(), + } + } + + pub fn send_truapi_response(&mut self, bytes: Vec) -> Result<()> { + match self { + Self::Cooperative(runtime) => runtime.send_truapi_response(bytes), + Self::CoreVm(runtime) => runtime + .vm + .send_truapi_response(bytes) + .map_err(anyhow::Error::msg), + } + } + #[cfg(target_arch = "wasm32")] pub fn set_time_ms(&mut self, time_ms: u64) { - if let Self::Cooperative(runtime) = self { - runtime.set_time_ms(time_ms); + match self { + Self::Cooperative(runtime) => runtime.set_time_ms(time_ms), + Self::CoreVm(runtime) => runtime.vm.set_time_ms(time_ms), } } @@ -300,6 +318,14 @@ impl CoreVmRuntime { } fn send_input(&mut self, event: InputEvent) { + let pointer_delta = if event.event_type == InputEventType::PointerDelta { + match (corevm_pointer_delta(event.x), corevm_pointer_delta(event.y)) { + (Some(delta_x), Some(delta_y)) => Some((delta_x, delta_y)), + _ => return, + } + } else { + None + }; if self.vm.uses_epoca_inputs() { self.vm.send_epoca_input(event); return; @@ -326,10 +352,11 @@ impl CoreVmRuntime { } self.pointer = Some((event.x, event.y)); } - InputEventType::PointerDelta => self.vm.send_mouse_move( - (event.x as i16).clamp(i8::MIN as i16, i8::MAX as i16) as i8, - (event.y as i16).clamp(i8::MIN as i16, i8::MAX as i16) as i8, - ), + InputEventType::PointerDelta => { + if let Some((delta_x, delta_y)) = pointer_delta { + self.vm.send_mouse_move(delta_x, delta_y); + } + } InputEventType::SurfaceMetrics => {} } } @@ -338,3 +365,25 @@ impl CoreVmRuntime { fn signed_delta(current: u16, previous: u16) -> i8 { (i32::from(current) - i32::from(previous)).clamp(i8::MIN as i32, i8::MAX as i32) as i8 } + +fn corevm_pointer_delta(value: u16) -> Option { + i8::try_from(value as i16).ok() +} + +#[cfg(test)] +mod tests { + use super::corevm_pointer_delta; + + #[test] + fn corevm_pointer_delta_preserves_representable_signed_values() { + assert_eq!(corevm_pointer_delta(127), Some(127)); + assert_eq!(corevm_pointer_delta((-128_i16) as u16), Some(-128)); + } + + #[test] + fn corevm_pointer_delta_drops_safari_pointer_lock_discontinuities() { + assert_eq!(corevm_pointer_delta(128), None); + assert_eq!(corevm_pointer_delta((-129_i16) as u16), None); + assert_eq!(corevm_pointer_delta(430), None); + } +} diff --git a/rust/crates/pvm-runtime/src/corevm.rs b/rust/crates/pvm-runtime/src/corevm.rs index 492ba85..177196f 100644 --- a/rust/crates/pvm-runtime/src/corevm.rs +++ b/rust/crates/pvm-runtime/src/corevm.rs @@ -12,6 +12,7 @@ use polkavm::{ use std::collections::{BTreeMap, VecDeque}; use std::mem::MaybeUninit; use std::sync::Arc; +#[cfg(not(target_arch = "wasm32"))] use std::time::Instant; struct File { @@ -64,7 +65,14 @@ pub struct Vm { input_events: VecDeque, audio_channels: u32, epoca_input_events: VecDeque<[u8; crate::INPUT_EVENT_BYTES]>, + #[cfg(not(target_arch = "wasm32"))] started: Instant, + #[cfg(target_arch = "wasm32")] + now_ms: u64, + truapi_requests: VecDeque>, + truapi_request_bytes: usize, + truapi_responses: VecDeque>, + truapi_response_bytes: usize, import_syscall: Option, import_set_palette: Option, @@ -78,6 +86,8 @@ pub struct Vm { import_time_ms: Option, import_log: Option, import_yield: Option, + import_truapi_send: Option, + import_truapi_poll: Option, } #[derive(Copy, Clone)] @@ -218,6 +228,8 @@ impl Vm { let mut import_time_ms = None; let mut import_log = None; let mut import_yield = None; + let mut import_truapi_send = None; + let mut import_truapi_poll = None; for (import_index, import) in module.imports().into_iter().enumerate() { let Some(import) = import else { @@ -238,6 +250,8 @@ impl Vm { b"pvm_time_ms" => import_time_ms = Some(import_index), b"host_log" => import_log = Some(import_index), b"pvm_yield" => import_yield = Some(import_index), + b"host_truapi_send" => import_truapi_send = Some(import_index), + b"host_truapi_poll" => import_truapi_poll = Some(import_index), _ => return Err(format!("unsupported import: {}", import).into()), } } @@ -253,7 +267,14 @@ impl Vm { input_events: VecDeque::with_capacity(MAX_QUEUED_INPUT_EVENTS), audio_channels: 0, epoca_input_events: VecDeque::with_capacity(MAX_QUEUED_INPUT_EVENTS), + #[cfg(not(target_arch = "wasm32"))] started: Instant::now(), + #[cfg(target_arch = "wasm32")] + now_ms: 0, + truapi_requests: VecDeque::new(), + truapi_request_bytes: 0, + truapi_responses: VecDeque::new(), + truapi_response_bytes: 0, import_syscall, import_set_palette, import_display, @@ -266,13 +287,52 @@ impl Vm { import_time_ms, import_log, import_yield, + import_truapi_send, + import_truapi_poll, }) } + #[cfg(target_arch = "wasm32")] + pub fn set_time_ms(&mut self, time_ms: u64) { + self.now_ms = self.now_ms.max(time_ms); + } + + fn time_ms(&self) -> u64 { + #[cfg(target_arch = "wasm32")] + { + self.now_ms + } + #[cfg(not(target_arch = "wasm32"))] + { + self.started.elapsed().as_millis() as u64 + } + } + pub fn backend(&self) -> polkavm::BackendKind { self.backend } + pub fn take_truapi_request(&mut self) -> Option> { + let frame = self.truapi_requests.pop_front()?; + self.truapi_request_bytes -= frame.len(); + Some(frame) + } + + pub fn send_truapi_response(&mut self, bytes: Vec) -> Result<(), String> { + if bytes.is_empty() || bytes.len() > crate::MAX_TRUAPI_FRAME_BYTES { + return Err("invalid TrUAPI response frame".into()); + } + if self.truapi_responses.len() == crate::MAX_QUEUED_TRUAPI_FRAMES + || self.truapi_response_bytes.saturating_add(bytes.len()) + > crate::MAX_QUEUED_TRUAPI_BYTES + { + return Err("TrUAPI response queue overflow".into()); + } + self.truapi_response_bytes += bytes.len(); + self.truapi_responses.push_back(bytes); + Ok(()) + } + pub fn set_gas(&mut self, gas: u64) { self.instance.set_gas(gas.min(i64::MAX as u64) as i64); } @@ -572,6 +632,49 @@ impl Vm { self.instance.set_reg(Reg::A0, written as u64); continue; } + InterruptKind::Ecalli(hostcall) if Some(hostcall) == self.import_truapi_send => { + let address = u32::try_from(self.instance.reg(Reg::A0)) + .map_err(|_| "TrUAPI request address is out of range".to_owned())?; + let length = usize::try_from(self.instance.reg(Reg::A1)).unwrap_or(usize::MAX); + if length == 0 || length > crate::MAX_TRUAPI_FRAME_BYTES { + self.instance.set_reg(Reg::A0, 1); + continue; + } + if self.truapi_requests.len() == crate::MAX_QUEUED_TRUAPI_FRAMES + || self.truapi_request_bytes.saturating_add(length) + > crate::MAX_QUEUED_TRUAPI_BYTES + { + self.instance.set_reg(Reg::A0, 2); + continue; + } + let bytes = self.instance.read_memory(address, length as u32)?; + self.truapi_request_bytes += bytes.len(); + self.truapi_requests.push_back(bytes); + self.instance.set_reg(Reg::A0, 0); + continue; + } + InterruptKind::Ecalli(hostcall) if Some(hostcall) == self.import_truapi_poll => { + let address = u32::try_from(self.instance.reg(Reg::A0)) + .map_err(|_| "TrUAPI response address is out of range".to_owned())?; + let capacity = + usize::try_from(self.instance.reg(Reg::A1)).unwrap_or(usize::MAX); + let Some(required) = self.truapi_responses.front().map(Vec::len) else { + self.instance.set_reg(Reg::A0, 0); + continue; + }; + if capacity < required { + let required = i32::try_from(required) + .map_err(|_| "TrUAPI response length overflow".to_owned())?; + self.instance.set_reg(Reg::A0, i64::from(-required) as u64); + continue; + } + let response = self.truapi_responses.front().unwrap(); + self.instance.write_memory(address, response)?; + self.truapi_responses.pop_front(); + self.truapi_response_bytes -= required; + self.instance.set_reg(Reg::A0, required as u64); + continue; + } InterruptKind::Ecalli(hostcall) if Some(hostcall) == self.import_asset_read => { let name_address = u32::try_from(self.instance.reg(Reg::A0)) .map_err(|_| "asset name address is out of range".to_owned())?; @@ -602,8 +705,7 @@ impl Vm { continue; } InterruptKind::Ecalli(hostcall) if Some(hostcall) == self.import_time_ms => { - self.instance - .set_reg(Reg::A0, self.started.elapsed().as_millis() as u64); + self.instance.set_reg(Reg::A0, self.time_ms()); continue; } InterruptKind::Ecalli(hostcall) if Some(hostcall) == self.import_log => { diff --git a/rust/crates/pvm-runtime/src/lib.rs b/rust/crates/pvm-runtime/src/lib.rs index 092726e..09aa1f9 100644 --- a/rust/crates/pvm-runtime/src/lib.rs +++ b/rust/crates/pvm-runtime/src/lib.rs @@ -97,6 +97,9 @@ const MAX_QUEUED_GPU_BATCHES: usize = 4; const MAX_QUEUED_GPU_EVENTS: usize = 256; const MAX_GPU_SUBMITS_PER_TICK: u32 = 8; const MAX_GPU_UPLOAD_BYTES_PER_TICK: usize = 16 * 1024 * 1024; +pub const MAX_TRUAPI_FRAME_BYTES: usize = 1024 * 1024; +const MAX_QUEUED_TRUAPI_FRAMES: usize = 32; +const MAX_QUEUED_TRUAPI_BYTES: usize = 4 * 1024 * 1024; pub(crate) fn validate_program_configuration( program_len: usize, @@ -298,6 +301,10 @@ struct HostState { gpu_capabilities: Option>, gpu_batches: VecDeque, gpu_events: VecDeque>, + truapi_requests: VecDeque>, + truapi_request_bytes: usize, + truapi_responses: VecDeque>, + truapi_response_bytes: usize, gpu_last_sequence: u64, gpu_submits_remaining: u32, gpu_upload_bytes_remaining: usize, @@ -329,6 +336,10 @@ impl HostState { gpu_capabilities: None, gpu_batches: VecDeque::new(), gpu_events: VecDeque::new(), + truapi_requests: VecDeque::new(), + truapi_request_bytes: 0, + truapi_responses: VecDeque::new(), + truapi_response_bytes: 0, gpu_last_sequence: 0, gpu_submits_remaining: 0, gpu_upload_bytes_remaining: 0, @@ -395,6 +406,26 @@ impl HostState { } self.input.push_back(event); } + + fn take_truapi_request(&mut self) -> Option> { + let frame = self.truapi_requests.pop_front()?; + self.truapi_request_bytes -= frame.len(); + Some(frame) + } + + fn queue_truapi_response(&mut self, bytes: Vec) -> Result<()> { + if bytes.is_empty() || bytes.len() > MAX_TRUAPI_FRAME_BYTES { + return Err(anyhow!("invalid TrUAPI response frame")); + } + if self.truapi_responses.len() == MAX_QUEUED_TRUAPI_FRAMES + || self.truapi_response_bytes.saturating_add(bytes.len()) > MAX_QUEUED_TRUAPI_BYTES + { + return Err(anyhow!("TrUAPI response queue overflow")); + } + self.truapi_response_bytes += bytes.len(); + self.truapi_responses.push_back(bytes); + Ok(()) + } } pub struct Runtime { @@ -663,6 +694,63 @@ impl Runtime { ) .context("define host_gpu_receive")?; + linker + .define_typed( + "host_truapi_send", + |caller: polkavm::Caller<'_, HostState>, + pointer: u32, + length: u32| + -> Result { + let length = length as usize; + if length == 0 || length > MAX_TRUAPI_FRAME_BYTES { + return Ok(1); + } + caller.user_data.charge_hostcall(length)?; + if caller.user_data.truapi_requests.len() == MAX_QUEUED_TRUAPI_FRAMES + || caller.user_data.truapi_request_bytes.saturating_add(length) + > MAX_QUEUED_TRUAPI_BYTES + { + return Ok(2); + } + let bytes = read_guest_memory(caller.instance, pointer, length)?; + caller.user_data.truapi_request_bytes += bytes.len(); + caller.user_data.truapi_requests.push_back(bytes); + Ok(0) + }, + ) + .context("define host_truapi_send")?; + + linker + .define_typed( + "host_truapi_poll", + |caller: polkavm::Caller<'_, HostState>, + pointer: u32, + capacity: u32| + -> Result { + caller.user_data.charge_hostcall(0)?; + let Some(required_len) = + caller.user_data.truapi_responses.front().map(Vec::len) + else { + return Ok(0); + }; + let required = i32::try_from(required_len) + .map_err(|_| anyhow!("TrUAPI response length overflow"))?; + if (capacity as usize) < required_len { + return Ok(-required); + } + caller.user_data.charge_hostcall_bytes(required_len)?; + let response = caller.user_data.truapi_responses.front().unwrap(); + caller + .instance + .write_memory(pointer, response) + .map_err(|error| anyhow!("write TrUAPI response: {error:?}"))?; + caller.user_data.truapi_responses.pop_front(); + caller.user_data.truapi_response_bytes -= required_len; + Ok(required) + }, + ) + .context("define host_truapi_poll")?; + linker .define_typed( "host_poll_input", @@ -946,6 +1034,14 @@ impl Runtime { self.state.gpu_batches.pop_front() } + pub fn take_truapi_request(&mut self) -> Option> { + self.state.take_truapi_request() + } + + pub fn send_truapi_response(&mut self, bytes: Vec) -> Result<()> { + self.state.queue_truapi_response(bytes) + } + #[cfg(target_arch = "wasm32")] pub fn set_time_ms(&mut self, time_ms: u64) { self.state.clock.set_time_ms(time_ms); @@ -1236,6 +1332,41 @@ mod tests { ); } + #[test] + fn truapi_queues_enforce_frame_count_and_byte_limits() { + let mut state = HostState::new(HashMap::new(), PresentationProfile::Framebuffer, false); + assert!(state.queue_truapi_response(Vec::new()).is_err()); + assert!(state + .queue_truapi_response(vec![0; MAX_TRUAPI_FRAME_BYTES + 1]) + .is_err()); + + for _ in 0..MAX_QUEUED_TRUAPI_FRAMES { + state + .queue_truapi_response(vec![0; 1]) + .expect("bounded response should queue"); + } + assert!( + state.queue_truapi_response(vec![0; 1]).is_err(), + "frame-count overflow must fail closed" + ); + + state.truapi_responses.clear(); + state.truapi_response_bytes = 0; + for _ in 0..(MAX_QUEUED_TRUAPI_BYTES / MAX_TRUAPI_FRAME_BYTES) { + state + .queue_truapi_response(vec![0; MAX_TRUAPI_FRAME_BYTES]) + .expect("response within byte budget should queue"); + } + assert!( + state.queue_truapi_response(vec![0; 1]).is_err(), + "byte-budget overflow must fail closed" + ); + + state.truapi_requests.push_back(vec![1, 2, 3]); + state.truapi_request_bytes = 3; + assert_eq!(state.take_truapi_request(), Some(vec![1, 2, 3])); + assert_eq!(state.truapi_request_bytes, 0); + } #[test] fn launch_limits_reject_unbounded_native_inputs() { assert!(validate_program_configuration(0, 1).is_err()); diff --git a/rust/crates/pvm-runtime/src/wasm.rs b/rust/crates/pvm-runtime/src/wasm.rs index e553572..a9287b6 100644 --- a/rust/crates/pvm-runtime/src/wasm.rs +++ b/rust/crates/pvm-runtime/src/wasm.rs @@ -37,6 +37,7 @@ struct BrowserHost { tri2d: Option, gpu_batch: Option, audio: Option, + truapi_request: Option>, log: Option, save: Option>, translation: Vec, @@ -52,6 +53,7 @@ impl BrowserHost { tri2d: None, gpu_batch: None, audio: None, + truapi_request: None, log: None, save: None, translation: Vec::new(), @@ -70,6 +72,7 @@ impl BrowserHost { self.frame = None; self.tri2d = None; self.gpu_batch = None; + self.truapi_request = None; self.audio = None; self.log = None; self.save = None; @@ -263,6 +266,14 @@ pub extern "C" fn pvm_browser_send_gpu_event() -> u32 { }) } +#[no_mangle] +pub extern "C" fn pvm_browser_send_truapi_response() -> u32 { + status(|host| { + let bytes = std::mem::take(&mut host.staging); + host.running()?.send_truapi_response(bytes) + }) +} + #[no_mangle] pub extern "C" fn pvm_browser_init() -> u32 { status(|host| host.running()?.init()) @@ -411,6 +422,33 @@ pub extern "C" fn pvm_browser_gpu_batch_length() -> u32 { .map_or(0, |batch| batch.bytes.len() as u32) }) } + +#[no_mangle] +pub extern "C" fn pvm_browser_take_truapi_request() -> u32 { + HOST.with(|host| { + let mut host = host.borrow_mut(); + host.truapi_request = match &mut host.phase { + Phase::Running(runtime) => runtime.take_truapi_request(), + _ => None, + }; + u32::from(host.truapi_request.is_some()) + }) +} + +#[no_mangle] +pub extern "C" fn pvm_browser_truapi_request_pointer() -> u32 { + HOST.with(|host| { + host.borrow() + .truapi_request + .as_ref() + .map_or(0, |frame| frame.as_ptr() as usize as u32) + }) +} + +#[no_mangle] +pub extern "C" fn pvm_browser_truapi_request_length() -> u32 { + HOST.with(|host| host.borrow().truapi_request.as_ref().map_or(0, Vec::len) as u32) +} #[no_mangle] pub extern "C" fn pvm_browser_take_audio() -> u32 { HOST.with(|host| {