Skip to content
Merged
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
38 changes: 28 additions & 10 deletions docs/runtime/polkavm-app-abi-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,18 @@ The Host instantiates a fresh program, calls `init` exactly once, and calls
`update` zero or more times while the App is running. Calls are serialized; the
Host MUST NOT enter the same program concurrently.

The Host selects and enforces a nonzero gas budget for each call. A trap, gas
exhaustion, invalid guest-memory access, or Host-call budget failure fails the
current execution. ABI v1 does not restart a failed program transparently.
The Host selects and enforces a nonzero gas budget for each call. It MAY
execute that budget as smaller internal quanta, returning to its scheduler and
resuming the same call between quanta. This preserves the program counter,
registers, memory, remaining call budget, scheduling requests, and per-call
Host-service bounds; it is not a transparent restart. A hostcall scheduling
quantum ending MUST NOT refill the guest's gas. The translated browser runtime
keeps the VM's remaining gas across hostcall yields and charges each exhausted
gas quantum's refill against the call's configured slice allowance. Reduced
initialization budgets do not receive additional gas quanta.
Exhausting the complete call budget, a trap, invalid guest-memory access, or a
Host-call budget failure fails the current execution. ABI v1 does not restart
a failed program transparently.

The Host owns scheduling and presentation. Returning from `update` yields
control to the Host; it does not imply that a frame was presented.
Expand Down Expand Up @@ -115,17 +124,24 @@ host_update_after(delay_ms: u32) -> ()

Importing `host_update_after` opts a cooperative application guest into
demand-driven updates. The Host performs the first `update` after `init`
automatically. Before each later update, the Host clears the previous request.
Calls made during that Host update select the smallest requested delay.
automatically, including when initialization completes through continuations;
an initialization scheduling request does not postpone that first update.
Before each later logical update, the Host clears the previous request.
Calls made during that update, including all of its gas and hostcall
continuations, select the smallest requested delay.

The CoreVM compatibility path recognizes the same import and applies equivalent
behavior to the initial `_pvm_start` slice and each later resume. This is Host
compatibility behavior, not part of the portable CoreVM contract.
behavior between intentional frame yields: a frame yield starts a new
scheduling and resource-budget boundary, but a gas or hostcall quantum does
not. This is Host compatibility behavior, not part of the portable CoreVM
contract.

`delay_ms == 0` requests another update as soon as the Host can schedule it.
`delay_ms == u32::MAX` requests no timer; the Host waits until input, a
Host-frame response, a GPU event, or another external event is queued for the
guest. Every such event MUST wake an opted-in guest promptly.
guest. Every such event MUST wake an opted-in guest promptly. A foreground wake
received between execution quanta MUST remain pending until a new logical
update starts; completing the interrupted call does not consume that wake.

A guest that does not import this call retains Host-defined continuous
scheduling for compatibility. Scheduling does not weaken per-update gas or
Expand Down Expand Up @@ -1255,8 +1271,10 @@ conforming Host must provide.
## Failure and shutdown

A successful `init` does not guarantee that later updates will succeed. The
Host stops the execution on an unhandled guest trap, gas exhaustion, invalid
memory access, unrecoverable profile error, or Host transport failure.
Host stops the execution on an unhandled guest trap, exhaustion of the complete
call gas budget, invalid memory access, unrecoverable profile error, or Host
transport failure. An internal execution quantum ending is not gas exhaustion
at this contract boundary.

The Host may stop an execution when its App surface closes, the Product is
replaced, the user selects a file for a relaunch registration, or platform
Expand Down
28 changes: 25 additions & 3 deletions js/packages/polkavm-browser-runtime/src/polkavm-runtime-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {
const LEGACY_FRAME_INTERVAL_MS = 1000 / 60;
const MAX_GAS_PER_UPDATE = 10_000_000_000;
const MAX_TRANSLATED_LOOPS_PER_UPDATE = 50_000_000;
const MAX_TRANSLATED_GAS_SLICES_PER_UPDATE =
MAX_GAS_PER_UPDATE / MAX_TRANSLATED_LOOPS_PER_UPDATE;
const MAX_PROGRAM_BYTES = 64 * 1024 * 1024;
const MAX_ASSET_FILES = 2048;
const MAX_ASSET_NAME_BYTES = 1024;
Expand Down Expand Up @@ -80,6 +82,7 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {
let pendingFrame = null;
let demandDriven = false;
let tickPending = false;
let updateRequested = false;
let motionAvailability = 0;
let pendingMotionSample = null;
let pointerCaptureSupported = false;
Expand Down Expand Up @@ -653,8 +656,8 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {
}

function hasBackgroundWork() {
// Only cooperative hostcall-budget yields are continuations. CoreVM's
// frame yield is an update boundary, never a reason for an idle spin.
// Gas and hostcall scheduling yields are continuations. CoreVM's frame
// yield is an update boundary, never a reason for an idle spin.
if (translated?.hasPendingContinuation()) {
return backgroundContinuationTicks > 0;
}
Expand Down Expand Up @@ -689,6 +692,7 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {

function wake() {
if (demandDriven && !backgrounded) {
updateRequested = true;
scheduleTick(0);
}
}
Expand All @@ -701,8 +705,10 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {
? Math.min(backgroundServiceTicks + 1, MAX_BACKGROUND_SERVICE_TICKS)
: MAX_BACKGROUND_SERVICE_TICKS;
backgroundContinuationTicks = MAX_BACKGROUND_CONTINUATION_TICKS;
if (backgrounded || demandDriven) {
if (backgrounded) {
scheduleTick(0);
} else {
wake();
}
}

Expand Down Expand Up @@ -764,6 +770,9 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {
backgroundServiceTicks = pendingHostFrameResponses();
backgroundContinuationTicks = MAX_BACKGROUND_CONTINUATION_TICKS;
}
if (!backgrounded) {
updateRequested = true;
}
if (!backgrounded && pendingFrame !== null) {
const { output, transfers } = pendingFrame;
pendingFrame = null;
Expand Down Expand Up @@ -791,6 +800,11 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {
if (!running || paused || (backgrounded && !hasBackgroundWork())) {
return;
}
if (!translated?.hasPendingContinuation()) {
// An external wake belongs to the next logical call, not a continuation
// of a call that may already have polled before that event arrived.
updateRequested = false;
}
const firstUpdate = updateCount === 0;
if (firstUpdate) {
postMessage({ type: "startup", stage: "first-update-started" });
Expand Down Expand Up @@ -854,6 +868,10 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {
}
return;
}
if (translated?.hasPendingContinuation() || updateRequested) {
scheduleTick(0);
return;
}
const requestedDelay = requestedUpdateDelay(completedAt);
if (requestedDelay !== null) {
scheduleTick(requestedDelay);
Expand Down Expand Up @@ -1304,6 +1322,7 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {
motionAvailability,
message.mediatedInputKinds ?? [],
message.fileInput ?? null,
MAX_TRANSLATED_GAS_SLICES_PER_UPDATE,
);
const relaunch = relaunchFile(message);
if (relaunch !== null) {
Expand Down Expand Up @@ -1504,6 +1523,9 @@ globalThis.createPolkaVmRuntime = (endpoint, options = {}) => {
for (const { output, transfers } of pendingOutputs) {
postRuntimeOutput(output, transfers);
}
// Initialization may still be suspended. Its completion cannot consume
// the automatic first update or make that update wait on an init deadline.
updateRequested = true;
scheduleTick(0);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,7 @@
motionAvailability = MOTION_STATUS_UNAVAILABLE,
mediatedInputKinds = [],
fileInput = null,
maxGasSlices = 1,
) {
if (!TranslatedPolkaVmRuntime.isCompiledProgram(program)) {
throw new TypeError("invalid translated PolkaVM compiled program");
Expand Down Expand Up @@ -1006,6 +1007,11 @@
this.uiSemanticsSubmitted = false;
this.uiOutputSubmitted = false;
this.maxGas = BigInt(maxGas);
if (!Number.isSafeInteger(maxGasSlices) || maxGasSlices < 1) {
throw new Error("translated PolkaVM runtime has invalid gas slice count");
}
this.maxGasSlices = maxGasSlices;
this.remainingGasSlices = 0;
this.input = [];
this.coreInput = [];
this.epocaInput = [];
Expand All @@ -1026,6 +1032,7 @@
this.hostcalls = 0;
this.hostcallBytes = 0;
this.resumePending = false;
this.continuationPending = false;
this.stopped = false;
this.coreVm = this.metadata.exports.has("_pvm_start");
if (this.coreVm && graphicsProfile !== "framebuffer") {
Expand Down Expand Up @@ -1080,7 +1087,7 @@
}

hasPendingContinuation() {
return !this.coreVm && this.resumePending;
return this.continuationPending;
}

pendingHostFrameResponses() {
Expand Down Expand Up @@ -1141,17 +1148,16 @@
return;
}
this.timeMs = Math.max(this.timeMs ?? 0, timeMs);
this.updateAfterMs = null;
this.gpuSubmits = 0;
this.hostFrameRequests = 0;
this.uiSemanticsSubmitted = false;
this.uiOutputSubmitted = false;
this.hostFrameRequestBytes = 0;
this.#resetBudget(
this.coreVm && !this.coreVmStarted
? MAX_HOSTCALLS_PER_INIT
: MAX_HOSTCALLS_PER_UPDATE,
);
const hostcalls = this.coreVm && !this.coreVmStarted
? MAX_HOSTCALLS_PER_INIT
: MAX_HOSTCALLS_PER_UPDATE;
if (this.continuationPending) {
// A worker tick is not a new guest call. Only the hostcall scheduling
// quantum restarts; gas, scheduling requests and call bounds survive.
this.hostcalls = hostcalls;
} else {
this.#resetBudget(hostcalls);
}
if (this.coreVm) {
this.#run(this.exports.get("_pvm_start"), true);
this.coreVmStarted = true;
Expand Down Expand Up @@ -2022,6 +2028,12 @@

#resetBudget(hostcalls, gas = this.maxGas) {
this.hostcalls = hostcalls;
this.updateAfterMs = null;
this.gpuSubmits = 0;
this.hostFrameRequests = 0;
this.uiSemanticsSubmitted = false;
this.uiOutputSubmitted = false;
this.hostFrameRequestBytes = 0;
this.hostcallBytes = MAX_HOSTCALL_BYTES;
this.tri2dSubmitted = false;
this.mediatedInputCommands = 0;
Expand All @@ -2032,11 +2044,17 @@
let status;
if (this.resumePending) {
this.resumePending = false;
if (!this.continuationPending) {
this.remainingGasSlices = this.maxGasSlices - 1;
}
this.continuationPending = false;
status = this.pvm.pvm_resume();
} else {
if (entry === undefined) {
throw new Error("translated PolkaVM entrypoint is missing");
}
this.remainingGasSlices =
gas === this.maxGas ? this.maxGasSlices - 1 : 0;
status = this.pvm.pvm_begin(entry, gas);
}
for (;;) {
Expand All @@ -2052,7 +2070,16 @@
);
}
if (status === STATUS_OUT_OF_GAS) {
throw new Error("translated PolkaVM guest ran out of gas");
if (this.remainingGasSlices === 0) {
throw new Error("translated PolkaVM guest ran out of gas");
}
this.remainingGasSlices--;
// Hostcall yields retain the gas already in the VM. Only an exhausted
// gas quantum gets a refill, charged against the complete call budget.
this.pvm.pvm_set_gas(this.maxGas);
this.resumePending = true;
this.continuationPending = true;
return;
}
if (status !== STATUS_ECALL) {
throw new Error(
Expand All @@ -2072,13 +2099,15 @@
: this.#handleCooperativeCall(name);
if (yielded && yieldOnFrame) {
this.resumePending = true;
this.continuationPending = false;
return;
}
if (this.hostcalls === 0) {
// Resume on the next worker tick after completing this ECALL. Large
// assets can require more than one bounded hostcall slice, while
// returning here keeps each slice capped and the worker responsive.
this.resumePending = true;
this.continuationPending = true;
return;
}
status = this.pvm.pvm_resume();
Expand Down
Loading
Loading