ReadonlyassetsReadonlyphysicsThe world this application steps (§45), or null when it has no physics.
app.physics?.addBody(node); // §23: register a body
app.physics?.gravity; // §21: what the solver was given
Stepped once per fixed step at §39 step 6, initialized by Application.initialize, and disposed by Application.dispose only when this application constructed it — see ApplicationOptions.physics for both forms and what each implies.
A field rather than a getter because both forms are settled in the
constructor: the factory is called there, so this never changes after
construction (unlike Application.renderer, whose string form has
nothing to hold until initialize).
ReadonlyposesThe engine's single previous/current pose store (§37, §43).
Always present, and empty until something tracks a node —
app.poses.track(node). It is captured once per fixed step (§39 step 10)
only when pose interpolation is on, which is the default whenever a
renderer is configured; see ApplicationOptions.poseInterpolation.
Not cleared by Application.dispose, for the reason the scene is not
destroyed either: the buffer may outlive the application that stepped it.
ReadonlysceneThe default scene (§45). Constructed and owned by the application; the root of everything Application.step resolves and, from Phase 3, of everything the renderer draws.
ReadonlyschedulerThe simulation scheduler (§10, §45). Exposed because it is the only way to
reach the time record and the time scale, and because §45 requires these
systems to be usable directly: app.scheduler.timeScale = 0.5 is slow
motion, app.scheduler.time is §9's TimeState.
Its three callbacks belong to this class — reassigning
scheduler.onFixedStep, onUpdate, or onRender unwires the application
(register a SimulationSystem or an event listener instead, D5).
Readonlystats§84's runtime statistics for the frame just stepped, or null when
statistics are off — which is the default (A-1, 2026-08-07; A-6 recorded
this member as absent until then).
const app = new Application({ renderer, stats: true });
// … a frame later …
app.stats?.cpuFrameTime; // seconds of CPU work in the last step
app.stats?.drawCalls; // what the backend actually submitted
One record, rewritten in place every Application.step — the
same discipline §9's TimeState follows, and for the same reason (plan D7:
the loop allocates nothing per frame). Retaining a reference retains
nothing; copy a frame's values with copyFrameStats from
@fourjs/diagnostics.
A field reading NaN was not measured this frame, as distinct from a
0 that was measured. physicsStepTime and activeBodies joined the
measured set with A-6's Application.physics (2026-08-08) and are
filled only when a world is attached — the first is seconds inside
world.step summed over the frame's fixed steps, the second §32's awake
count after the frame. contacts is the solver's live manifold count
(SolverStatistics.contactCount) after the same walk. gpuFrameTime
copies Renderer.lastGpuFrameTimeSeconds when the backend publishes
a finite number — WebGL 2 via EXT_disjoint_timer_query_webgl2, WebGPU
via timestamp-query — and stays NaN when the member is absent, still
in flight, or disjoint. Reading that getter is what arms measurement;
a renderer that never exposes it is left unmeasured.
textureMemory and bufferMemory joined the measured set with A-5's §83
resource accounting (2026-08-07); they are levels — the bytes every
live geometry, texture, and render target in the process holds at the end
of the frame — and are reported with or without a renderer, because a
resource is memory the engine holds whether or not anything drew it.
FrameStats documents each counter
and what it waits on. drawCalls/triangles/instances are NaN with no
renderer, or with a backend that does not report statistics, and 0 when a
reporting backend drew nothing.
What is deliberately not here: §10's fixed-step count, dropped time, and
every §9 clock. They are on app.time already, and §84's list does not
name them — a statistics block that re-published the time record would be
two places to read one number.
Always null in a production build (A-4, 2026-08-07): §85 permits a
production build to drop expensive diagnostics, and this whole path is
gated on @fourjs/core's DEV. The declared type is the same in both
builds — a program that reads app.stats?.drawCalls compiles and runs
either way and simply gets undefined — so nothing here is a public-shape
change. See ApplicationOptions.stats.
ReadonlysystemsThe §39 simulation-system registry, attached to Application.scheduler.
This is where per-fixed-step engine work is registered (D5): every later
feature registers a system here, in PRIORITY_* order, and nothing edits
the loop.
ReadonlyviewsThe viewports drawn each frame, in order (§48). Mutable: push, splice, and reorder it at any time; the next frame uses whatever it holds.
Empty by default, and an empty list draws and clears nothing (§61).
The array itself is handed to the renderer without being copied, so a
backend must read it during the call (the Renderer.render contract).
Whether Application.dispose has run. Disposal is terminal.
The surface height in logical pixels. See Application.width.
Whether Application.initialize has completed.
The scheduler's pause flag (§10). See Application.pause.
The §81 plugin context once Application.initialize has installed
ApplicationOptions.plugins, and null before that (or forever, for
an application that configured none).
It is sealed: plugins and capabilities read back for diagnostics,
and get/require refuse, because registration is legal only during a
plugin's own install. context.plugins is in install order — topological
over declared dependencies, ties broken by the order they were listed in
(§33: install order decides equal-priority system order, so it is a
specified property rather than an emergent one).
Whether this application's animation should be reduced (§45, §75).
const duration = app.reducedMotion ? 0 : 0.3; // §14, opt-in
The resolved policy, not the option: reducedMotion: true | false reads
back as itself, and "auto" — the default (Appendix A) — is the platform
preference from ApplicationOptions.reducedMotionSource, or false
when no source was supplied.
Recomputed on every read, so a user who turns the preference on mid-session
is honoured without this class subscribing to anything (§75 requires the
policy, not a change event). Nothing in the engine consults it yet: §75
makes it the application's policy and hands consumption to @fourjs/ui's
widgets and, on an opt-in basis, to §14's animation API — both of which
reach it through this property.
The backend this application draws with (§45, §61), or null when it is
headless.
Constructed by the application author — or, for the string form, by the
§62 registry on its behalf (R-2/A-8) — and merely driven here; see
ApplicationOptions.renderer. It is initialized by
Application.initialize, called once per frame after the render
event, and not disposed by Application.dispose.
A getter rather than a readonly field since 2026-08-07, because a string
selection has nothing to hold at construction: with renderer: "auto" this
reads null until initialize() resolves, and the resolved backend from
then on. With an instance it is set in the constructor and never changes,
exactly as before.
Device pixels per logical pixel, as last given to
Application.resize. 1 until one is supplied.
Whether the application is started (§45 start/stop).
The live §9 time record. The same object the events carry; copy it with
copyTimeState to retain a frame's values.
The surface width in logical pixels, as last given to
Application.resize. 0 before any resize.
Tears the application down (§45, §83): stops it, unwires the scheduler, disposes every registered system, and removes every listener.
Idempotent and terminal — a disposed application cannot be initialized or
started again, and stepping it throws. The scene is deliberately not
destroyed: Node has no dispose (nothing yet holds a GPU or solver
resource on a node), and the scene may well outlive the application that
was stepping it. The packet that gives nodes disposable resources owns that
decision (§83).
The renderer is not disposed either (§83, decision WP-3.6). It arrives as an instance the application author constructed (ApplicationOptions.renderer), so the author disposes it — ownership follows construction, and an application that destroyed a renderer it was merely lent would break the perfectly ordinary case of one renderer outliving, or being shared between, applications. Disposing it is one line at the same call site:
app.dispose();
renderer.dispose();
The physics world is disposed only when this application built it (A-6): the factory form of ApplicationOptions.physics means the world exists for this application alone, so it goes with it, adapter included; an instance handed in is the author's and is left alone, exactly like the renderer. Application.assets is never disposed, for the same reason — you constructed it.
Application.views and Application.poses are left intact for
the same reason the scene is; the snapshot system that captured into the
buffer is disposed with every other registered system. An autoResize
subscription is released first of all, so no host report can reach a
half-torn-down application.
The scheduler's callbacks are cleared before systems are disposed, so a
dispose that touches the scheduler cannot re-enter the loop or reach a
listener that has been told the application is gone. A system whose
dispose throws still leaves the application fully unwired: the registry
re-throws the first failure, and listener removal runs in a finally.
Delivers event to the listeners registered for type in registration
order. Re-entrant emission of type is deferred (rule 4).
Prepares the application for stepping (§45), asynchronously and exactly once.
With a renderer configured this awaits renderer.initialize({ canvas }) —
§61's context or device acquisition, which is genuinely asynchronous for
WebGPU and may compile pipelines for any backend — and the application
counts as initialized only once that resolves. A rejected renderer
initialization therefore rejects this call, leaves initialized false, and
leaves start() refusing to run: a program that failed to acquire a GPU
should stop at the line that says so, not at the first frame (decision,
WP-3.6). The rejection is remembered, so a retry needs a new application.
With no renderer there is still nothing to await, and this resolves on the
next microtask. It has always been async because §45's usage is
await app.initialize(); app.start();, and the later subsystems (WASM
solver loading §37, asset preloads §75) are asynchronous too.
Idempotent: repeated or concurrent calls return the same promise and initialize once.
Number of live registrations for type (diagnostics, §83).
Number of live listener registrations across every event type (§83).
Used by teardown diagnostics — a detached node that still holds listeners pins its subtree until those listeners are removed.
Removes the earliest still-registered registration of listener for
type. Removing a listener that is not registered is a no-op.
Registers listener for type. Returns a function that removes it.
The same listener may be registered more than once; each registration is independent and is delivered once per dispatch.
Registers listener for a single delivery. Returns an unsubscriber.
Pauses simulation (§10, §45): the accumulator stops accumulating and
deltaTime is 0, while unscaledDeltaTime, update, and render
continue — so a paused application still renders, and a paused frame is
exactly a timeScale = 0 frame with timeScale preserved.
A pure proxy for scheduler.paused; pausing a stopped or uninitialized
application is legal and is how an application starts paused.
Removes every listener, or every listener of one type. Used by teardown paths that must not leave detached nodes retaining listeners (§83).
Optionaltype: KResizes the drawing surface (§45's seventh lifecycle method, 2026-08-06
A-7; "eighth" until 2026-08-07 — §45 lists initialize, start, stop, pause,
resume, step, resize, dispose, and resize is the seventh of those eight).
Three things happen, in this order:
renderer.resize(width, height, resolution),
which makes the drawing buffer width · resolution × height · resolution device pixels and re-resolves every normalized viewport
rectangle (§61);aspect set to width / height and
its projection rebuilt; every full-surface viewport whose camera
instead accepts a surface size — §47's ScreenCamera, or any camera
implementing SurfaceSizedCamera — is handed (width, height, resolution) and rebuilt too (R-37, 2026-08-21). That is how a
screen-space UI pass learns its pixel rectangle; the push happens here
rather than in the view loop because a projection should change when a
size changes, not when a frame is drawn, and because a headless
application (§45) never runs a view loop at all.Why this class updates cameras and the renderer does not (decision,
A-7; the sentence here quoted §61 until 2026-08-07 — "a camera's aspect
is the application's to set, because only the application knows which
camera belongs to which viewport" — and §61 says no such thing: it defines
Renderer.resize(width, height, resolution) and nothing about cameras. The
reasoning stands on its own and is restated as the decision it is). §47
gives the camera its projection and §48 maps one camera to one viewport
rectangle; a renderer is handed the finished Viewport[] and has no way to
know which of those rectangles a given camera was authored for. This class
is exactly that knowledge — Application.views is the mapping — so
the aspect update belongs here. §47 keeps projection recomputation
explicit, so the rebuild is a call to updateProjectionMatrix, made with
ApplicationOptions.depthRange.
Full-surface means normalized with the rectangle (0, 0, 1, 1) —
what createFullscreenViewport builds. A partial viewport is left alone
because its aspect is its rectangle's, not the surface's, and the rectangle
may be in pixels the application maintains itself — a screen camera in a
partial viewport is sized with setSurfaceSize by whoever owns that
rectangle; an orthographic camera is left alone because its extent is an
authoring decision (how much world to show), not a consequence of the
window size.
Two viewports sharing one camera update it twice with the same value, which
is idempotent.
Headless is a no-op for step 2 only. With no renderer the size is still recorded and the cameras are still updated: a headless application that feeds a custom draw path or takes a screenshot needs both, and "no renderer" is not "no surface".
Legal at any point in the lifecycle, including before initialize and
while stopped — a window resizes whether or not a frame is running. Not
legal after dispose.
surface width in logical pixels; finite and >= 0
surface height in logical pixels; finite and >= 0
Optionalresolution: number
device pixels per logical pixel; finite and > 0.
Defaults to the current value, so app.resize(w, h) after
app.resize(w, h, 2) keeps the 2× buffer rather than silently halving it.
Resumes simulation (§10, §45). The proxy inverse of Application.pause.
Starts the application (§45): from here Application.step is legal.
Phase 1 installs no driver — no requestAnimationFrame, no timer, no
DOM (plan D4: manual stepping is the headless mode). The host owns the
cadence and calls step(elapsedSeconds); start establishes that doing so
is intended, which is what makes an accidental step before setup an error
instead of a silently advanced clock. The browser driver arrives with the
renderer (Phase 3) and will attach here.
Calling start on a running application is a no-op.
Advances the loop by elapsedSeconds of injected real time — one frame
(§10, §45).
This is the whole driver in Phase 1: the host decides the cadence, which is
what determinism (§33) and replay (§34) require. elapsedSeconds is real
(unscaled) time; timeScale and paused are applied by the scheduler.
With statistics on (ApplicationOptions.stats) this call is also
§84's frame boundary: Application.stats is reset before the frame
and finished after it, so it always describes the last completed step. A
frame that throws records nothing — its numbers would describe half a
frame, and half a frame's drawCalls is worse than no answer.
Stops the application (§45). Application.step becomes an error
again; nothing else is torn down, so start() resumes stepping with the
time record, accumulator, systems, and listeners exactly as they were.
Idempotent, and legal before start — "stopped" is the initial state.
The §76 asset manager this application was given, or
null(§45, §76).Constructed by the application author and merely published here; see ApplicationOptions.assets. Nothing in the frame loop reads it, and Application.dispose does not dispose it (§83).