Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions docs/runtime/polkavm-app-abi-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ input and audio. A Host call made outside its declared capability MUST fail
with that call's unavailable or invalid-state result. The Host MUST NOT
silently reinterpret a submission as another graphics profile.

Device-input ABI v1 may gain additive Host imports behind declared feature
names. Adding a feature does not change the existing input-record layout or the
semantics of existing imports. A Host that does not recognize a requested
feature MUST reject the manifest before instantiating the guest. A Host that
recognizes an optional feature MUST still define its import when the current
device cannot provide data; the import reports unavailability as specified
below.

## Host imports

### Framebuffer presentation
Expand Down Expand Up @@ -265,6 +273,60 @@ The device-input contract defines code values, coordinate interpretation, and
surface-metric scaling. ABI v1 does not define touch, wheel, UTF-8 text, IME,
or focus events.

#### Optional motion tilt

An App requests fused, display-relative tilt without making it a launch
requirement by declaring:

```json
{
"deviceInput": {
"abiVersion": 1,
"requiredFeatures": ["pointer"],
"optionalFeatures": ["motion-tilt"]
}
}
```

The feature adds:

```text
host_motion_read(pointer: u32, capacity: u32) -> i32
```

The Host retains only the newest calibrated sample. The call returns `40` after
writing one complete sample, `0` when motion is unavailable, inactive, stale,
or not authorized, and `-40` when `capacity` is too small. It never writes a
partial sample.

Every ABI v1 Host MUST resolve `host_motion_read`, including Hosts without a
motion source. An unavailable Host returns `0`; it does not reject the import or
trap the guest. This keeps pointer fallback deterministic across desktop and
sensor-capable Hosts.

The 40-byte `PMT1` sample is:

```text
offset type field
0 [u8;4] magic "PMT1"
4 u16 version 1
6 u16 flags
8 u32 byte length 40
12 u32 nonzero sequence
16 u64 monotonic timestamp in microseconds
24 f32 normalized horizontal tilt in [-1, 1]
28 f32 normalized vertical tilt in [-1, 1]
32 f32 azimuth in radians, or zero when unavailable
36 u32 zero
```

Flag bit 0 means the sample is calibrated and MUST be set. Bit 1 means azimuth
is valid. All other bits are zero. Float fields are finite. Motion tilt is
lossy state, not an event stream: Hosts SHOULD sample near display cadence,
coalesce updates, stop sampling when the App is not visible, and MUST NOT
persist samples. Pointer input remains available as the fallback and MAY
temporarily override tilt while a pointer gesture is active.

### Time

```text
Expand Down
68 changes: 68 additions & 0 deletions js/packages/pvm-browser-runtime/src/pvm-runtime-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,50 @@ globalThis.createPvmRuntime = endpoint => {
);
}

function sendMotionTilt(bytes) {
if (!running || bytes.byteLength !== 40) {
return;
}
if (translated) {
translated.sendMotionTilt(bytes);
return;
}
stage(bytes);
check(
pvm.pvm_browser_set_motion_tilt(),
"set PolkaVM browser motion-tilt sample"
);
}

function clearMotionTilt() {
if (!running) {
return;
}
if (translated) {
translated.clearMotionTilt();
return;
}
check(
pvm.pvm_browser_clear_motion_tilt(),
"clear PolkaVM browser motion-tilt sample"
);
}

function sendGpuCapabilities(bytes) {
if (!running || bytes.byteLength < 56 || bytes.byteLength > 4096) {
return;
}
if (translated) {
translated.setGpuCapabilities(bytes);
return;
}
stage(bytes);
check(
pvm.pvm_browser_set_gpu_capabilities(),
"update PolkaVM browser GPU capabilities"
);
}

function sendGpuEvent(bytes) {
if (!running || !pvm || !bytes.byteLength) {
return;
Expand Down Expand Up @@ -568,6 +612,30 @@ globalThis.createPvmRuntime = endpoint => {
postMessage({ type: "error", message: error.message });
postMessage({ type: "terminated" });
}
} else if (message?.type === "motion-tilt") {
try {
sendMotionTilt(new Uint8Array(message.bytes));
} catch (error) {
stopRuntime();
postMessage({ type: "error", message: error.message });
postMessage({ type: "terminated" });
}
} else if (message?.type === "motion-tilt-clear") {
try {
clearMotionTilt();
} catch (error) {
stopRuntime();
postMessage({ type: "error", message: error.message });
postMessage({ type: "terminated" });
}
} else if (message?.type === "gpu-capabilities") {
try {
sendGpuCapabilities(new Uint8Array(message.bytes));
} catch (error) {
stopRuntime();
postMessage({ type: "error", message: error.message });
postMessage({ type: "terminated" });
}
} else if (message?.type === "gpu-event") {
try {
sendGpuEvent(new Uint8Array(message.bytes));
Expand Down
67 changes: 67 additions & 0 deletions js/packages/pvm-browser-runtime/src/pvm-wasm-translated.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
const STATUS_TRAP = -3;
const STATUS_OUT_OF_GAS = -4;
const INPUT_EVENT_BYTES = 8;
const MOTION_TILT_BYTES = 40;
const MAX_INPUT_EVENTS = 4096;
const MAX_HOSTCALLS_PER_INIT = 1024 * 1024;
const MAX_HOSTCALLS_PER_UPDATE = 8192;
Expand Down Expand Up @@ -48,6 +49,33 @@
const decoder = new TextDecoder();
const encoder = new TextEncoder();

function validMotionTilt(bytes) {
if (!(bytes instanceof Uint8Array) || bytes.byteLength !== MOTION_TILT_BYTES) {
return false;
}
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const flags = view.getUint16(6, true);
const tiltX = view.getFloat32(24, true);
const tiltY = view.getFloat32(28, true);
const azimuth = view.getFloat32(32, true);
return (
decoder.decode(bytes.subarray(0, 4)) === "PMT1" &&
view.getUint16(4, true) === 1 &&
(flags & ~3) === 0 &&
(flags & 1) !== 0 &&
view.getUint32(8, true) === MOTION_TILT_BYTES &&
view.getUint32(12, true) !== 0 &&
Number.isFinite(tiltX) &&
tiltX >= -1 &&
tiltX <= 1 &&
Number.isFinite(tiltY) &&
tiltY >= -1 &&
tiltY <= 1 &&
((flags & 2) === 0 || Number.isFinite(azimuth)) &&
view.getUint32(36, true) === 0
);
}

function readMetadata(module) {
const sections = WebAssembly.Module.customSections(
module,
Expand Down Expand Up @@ -258,6 +286,7 @@
this.tri2dSubmitted = false;
this.maxGas = BigInt(maxGas);
this.input = [];
this.motionTilt = null;
this.coreInput = [];
this.epocaInput = [];
this.pointer = null;
Expand Down Expand Up @@ -390,6 +419,29 @@
}
}

sendMotionTilt(bytes) {
if (this.stopped || !validMotionTilt(bytes)) {
throw new Error("invalid translated motion-tilt sample");
}
this.motionTilt = bytes.slice();
}

clearMotionTilt() {
this.motionTilt = null;
}

setGpuCapabilities(bytes) {
if (
this.stopped ||
!(bytes instanceof Uint8Array) ||
bytes.byteLength < 56 ||
bytes.byteLength > 4096
) {
throw new Error("invalid translated WebGPU capabilities");
}
this.gpuCapabilities = bytes.slice();
}

sendGpuEvent(bytes) {
if (
this.stopped ||
Expand Down Expand Up @@ -428,6 +480,7 @@
stop() {
this.stopped = true;
this.input.length = 0;
this.motionTilt = null;
this.coreInput.length = 0;
this.truapiRequests = 0;
this.truapiRequestBytes = 0;
Expand Down Expand Up @@ -744,6 +797,20 @@
this.#setReg(7, BigInt(event.byteLength));
return false;
}
case "host_motion_read": {
if (this.motionTilt === null) {
this.#setReg(7, 0n);
return false;
}
const capacity = this.#u32(a1);
if (capacity < MOTION_TILT_BYTES) {
this.#setReg(7, BigInt(-MOTION_TILT_BYTES));
return false;
}
this.#write(this.#u32(a0), this.motionTilt);
this.#setReg(7, BigInt(MOTION_TILT_BYTES));
return false;
}
case "host_poll_input": {
const capacity = this.#u32(a1);
const count = Math.min(
Expand Down
8 changes: 4 additions & 4 deletions rust/crates/pvm-runtime-assets/assets/SHA256SUMS
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
14c19701d27b037935fb8bd1f1ac2ad350d769da87783e746a030d5b3b9e9fc2 pvm-browser-runtime.wasm
7d9ebbe1189a37dcbd80cc2817a526a801424c82489cfbb9387a8535717607ab pvm-worker.js
2da1dbca9ca067064ba4311c01d627fd6dcec0ebbcf539941b01b611a29eb59c pvm-browser-runtime.wasm
a6b8052746d7b30e6790e54fee6a7a0f35386934f7eefc61d35757f32370564a pvm-worker.js
33418e2c81c117539569eb4cf91af4d058cdfae7dd4556b13f3687f6d6b3bae4 pvm-gpu-worker.js
a49f337b87c614d0427c9bb2845e88ed2fc33c34b878cc36fbda95cf2ceb3e4d pvm-wasm-translated.js
f527c1f530ce326e4d40840a5c0456dc9dad5798818038c8f0fe865040742c50 pvm-runtime-core.js
7b41037579c861aabcc04e28f66650c88cb78c5e77a66bf354f9474c26157715 pvm-wasm-translated.js
11e8df14cb719da9fa29921441c19820a7076dde28a2318f775254d18a960312 pvm-runtime-core.js
9c929f5d5c64a1b75e7e48485d7c3944ed6838112177ea778827a2c407c2d820 pvm-wasm-worker-entry.js
Binary file modified rust/crates/pvm-runtime-assets/assets/pvm-browser-runtime.wasm
Binary file not shown.
68 changes: 68 additions & 0 deletions rust/crates/pvm-runtime-assets/assets/pvm-runtime-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,50 @@ globalThis.createPvmRuntime = endpoint => {
);
}

function sendMotionTilt(bytes) {
if (!running || bytes.byteLength !== 40) {
return;
}
if (translated) {
translated.sendMotionTilt(bytes);
return;
}
stage(bytes);
check(
pvm.pvm_browser_set_motion_tilt(),
"set PolkaVM browser motion-tilt sample"
);
}

function clearMotionTilt() {
if (!running) {
return;
}
if (translated) {
translated.clearMotionTilt();
return;
}
check(
pvm.pvm_browser_clear_motion_tilt(),
"clear PolkaVM browser motion-tilt sample"
);
}

function sendGpuCapabilities(bytes) {
if (!running || bytes.byteLength < 56 || bytes.byteLength > 4096) {
return;
}
if (translated) {
translated.setGpuCapabilities(bytes);
return;
}
stage(bytes);
check(
pvm.pvm_browser_set_gpu_capabilities(),
"update PolkaVM browser GPU capabilities"
);
}

function sendGpuEvent(bytes) {
if (!running || !pvm || !bytes.byteLength) {
return;
Expand Down Expand Up @@ -568,6 +612,30 @@ globalThis.createPvmRuntime = endpoint => {
postMessage({ type: "error", message: error.message });
postMessage({ type: "terminated" });
}
} else if (message?.type === "motion-tilt") {
try {
sendMotionTilt(new Uint8Array(message.bytes));
} catch (error) {
stopRuntime();
postMessage({ type: "error", message: error.message });
postMessage({ type: "terminated" });
}
} else if (message?.type === "motion-tilt-clear") {
try {
clearMotionTilt();
} catch (error) {
stopRuntime();
postMessage({ type: "error", message: error.message });
postMessage({ type: "terminated" });
}
} else if (message?.type === "gpu-capabilities") {
try {
sendGpuCapabilities(new Uint8Array(message.bytes));
} catch (error) {
stopRuntime();
postMessage({ type: "error", message: error.message });
postMessage({ type: "terminated" });
}
} else if (message?.type === "gpu-event") {
try {
sendGpuEvent(new Uint8Array(message.bytes));
Expand Down
Loading
Loading