OptionalantialiasRequests multisampled anti-aliasing from the backend (§45, §61).
Forwarded verbatim as RendererOptions.antialias — to the string form's
construction and initialization, and to an instance's initialize. A
hint, not a requirement: §61 forbids a backend from failing initialization
over it.
Ignored when no renderer is configured.
OptionalassetsThe §76 asset manager this application publishes as Application.assets.
const app = new Application({ assets: new AssetManager() });
const { robot } = await app.assets!.load({ robot: "/models/robot.glb" });
§45's prose says the Application owns assets and §76's example reads
app.assets.load(…), but §45's option block lists no assets option and
AssetManager takes host seams of its own (fetch, a timer, §96 limits),
so it arrives here constructed — the same rule as
ApplicationOptions.renderer's instance form. Constructing a default
one would also make four/application import @fourjs/assets in every
bundle, which is the cost the whole file is written to avoid.
Held, not driven: nothing in the frame loop touches it, and Application.dispose leaves it alone (§83 — you constructed it, you dispose it, exactly as with the renderer).
OptionalautoWhether the application resizes itself when the host reports a new surface size (§45, §48).
Defaults to true when ApplicationOptions.surfaceObserver is
supplied and false otherwise, so there is no way to be quietly wrong: an
observer is never accepted-and-ignored, and autoResize: true without one
throws at construction rather than silently never resizing. Pass
autoResize: false alongside an observer to keep it unsubscribed (a program
that owns its own resize policy but shares its options object).
When on, every report becomes an Application.resize call with the
host's numbers — so the renderer's drawing buffer and every full-surface
perspective camera follow the surface, with the same validation an explicit
resize gets. The subscription is made in the constructor, because a window
resizes whether or not a frame is running, and released by
Application.dispose.
OptionalcanvasThe drawing surface handed to the renderer as initialize({ canvas })
(§45, §61).
Typed unknown for the same reason RendererOptions.canvas is: this
package compiles with no DOM lib, and each backend narrows and validates
the value itself (the WebGL 2 backend rejects a non-canvas with
RENDERER_INITIALIZATION_FAILED). Ignored when no renderer is configured.
OptionaldepthClip-space depth convention the projections this application recomputes are written with (plan D8).
Application.resize rebuilds the projection of any perspective camera
filling the surface, and §47 makes that an explicit recomputation whose
depth convention belongs to the renderer, not to the camera. Defaults to
"negative-one-to-one", matching Camera.updateProjectionMatrix and the
WebGL 2 MVP (§120); a WebGPU application passes "zero-to-one", exactly as
it does to PointerInput.
OptionalfixedFixed simulation step in seconds (§10, §45). Default 1/60 (Appendix A).
§45 spells this fixedTimeStep while §9/§10's time record spells the same
quantity fixedDeltaTime; this option is the §45 name and is passed
straight through to the scheduler's fixedDeltaTime.
OptionalheightInitial surface height in logical pixels (§45). See ApplicationOptions.width.
OptionalmaximumMaximum fixed steps per Application.step (§10, §45). Default 5 (Appendix A).
OptionalnowMonotonic clock, in seconds, used only by the §84 statistics
(ApplicationOptions.stats). Defaults to performance.now()
divided by 1000; a host without performance measures nothing and the two
durations read NaN rather than being filled from a non-monotonic
Date.now() this repository bans outright (§33 — see
@fourjs/diagnostics' createMonotonicClock).
Injected rather than reached for, the way PointerInput takes a
PointerSurface: this class names no host object, so a test can measure a
frame it controls (now: () => t), a profiler can supply its own timebase,
and a worker or an exotic host is not a special case. Read at most twice
per Application.step, and never at all when stats is off — so a
clock with a cost (a syscall, a GPU query) is only paid for when its
numbers are wanted.
Seconds, like every other time in this engine (§7a, §9). A clock that reports milliseconds must divide.
OptionalonCalled once per declared capability a tried backend did not answer true
(§62's diagnostics report, capability half; WP-R1.9) — forwarded verbatim
as RendererResolveOptions.onCapabilityShortfall, on
ApplicationOptions.onRendererFallback's terms: a callback because
the frozen §3.1 matrix gives @fourjs/render no diagnostics edge, unread
for an instance.
OptionalonCalled for each backend renderer: "auto" passes over, with the §62 reason
("unsupported" or "initialization-failed").
This is §62's "emits a diagnostics event", delivered as a callback: the
frozen §3.1 matrix gives @fourjs/render no @fourjs/diagnostics edge, so the
report is handed to the application, which is the one place that already
has both. Forwarding it to a diagnostics channel is one line.
Never called for an instance, or for an explicitly named backend — naming one means it must work, so its failure is thrown rather than reported.
OptionalphysicsThe §20/§37 world this application simulates (§45), or false (the
default) for an application with no physics at all.
Two forms, and which one you use decides who owns the world (§83):
// The factory form. The application constructs the world, so it disposes it.
const app = new Application({
physics: ({ poses }) =>
new PhysicsWorld({ dimension: "3d", adapter: new Rapier3dAdapter(), poses }),
});
await app.initialize(); // awaits world.initialize() too
app.physics?.addBody(node);
// The instance form. You constructed it, so you dispose it.
const world = new PhysicsWorld({ dimension: "2d", adapter });
const app = new Application({ physics: world });
PhysicsWorldOptions§45 spells this option PhysicsWorldOptions | false, which would have this
class call new PhysicsWorld(…) and new <solver>Adapter(…) — and that is
the renderer's deferred string form again, with more at stake (R-2/A-8, and
MEMORY 2026-08-01). four/application would import @fourjs/physics
statically, so every program that composes an application — the headless
ones, the determinism harness, the user-interface examples — would carry a
rigid-body world it never constructs. The renderer answer was a registry the
backend opts into; @fourjs/physics has the same registry for solvers
(registerSolver, PH-19), but the world class itself has no such front
door, so the honest form today is the one §45's own closing sentence
requires anyway: "the application must permit advanced users to construct
and own these systems independently."
PRIORITY_PHYSICS_SOLVE (§39 step 6) that calls world.step(fixedDeltaTime)
and then world.dispatchEvents() — the two passes, in the order
PhysicsSystem runs them for a single world. This class cannot construct a
PhysicsSystem (that is a runtime import of @fourjs/physics, see above),
so it registers the equivalent; do not also track this world in a
PhysicsSystem, which would step it twice per fixed step and advance
the simulation at double rate (§10, §34).world.initialized is still false — a WebAssembly solver loads its module
there (§37), which is why that call is asynchronous.physicsStepTime and activeBodies. See Application.stats.OptionalpluginsThe §81 plugins this application installs, in order (RFC 0002, gap A-3).
const app = new Application({ plugins: [windPlugin, telemetryPlugin] });
await app.initialize(); // installs, in dependency order
app.pluginContext?.plugins; // what installed, in install order
§45's own option block did not list it, and the §40 packet's rule is that
adding an option §45 does not list is inventing API. RFC 0002's first open
question is exactly that collision, and the owner decided it (2026-08-21,
register row 5): amend §45. The precedent turned on units being
absent from the spec's own list for §45, whereas §81 is a spec section
that requires an install lifecycle and §45 owns the lifecycle — §81 has
nowhere else to live. Specification revision 1.9 records the amendment;
this option is the shipped form of it.
initialize, and it must§81 types install as void | Promise<void>, so installation is
potentially asynchronous and cannot happen in a constructor.
Application.initialize installs the plugins after the renderer
and the physics world are ready, so a plugin's install sees a fully
constructed application. A refusal — a duplicate name, a missing
dependency, a dependency cycle, an engine-version mismatch, a capability
this host does not provide — rejects initialize and leaves the
application uninitialized, exactly as a failed renderer does.
Two, and only two, because they are the only registries the application itself holds:
| Capability | Provided |
|---|---|
SIMULATION_SYSTEMS (§39) — Application.systems |
always |
RENDERER_REGISTRY (§62) — ApplicationOptions.rendererRegistry |
when that option was passed |
A plugin needing a solver registry, a serializer registry, or a render
graph asks for something an Application never holds — naming those here
would put @fourjs/physics, @fourjs/serialization, and a RendererRegistry
in every bundle that composes an application, which is the cost this whole
file is written to avoid. Install such a plugin through a standalone
PluginHost, providing the registries you
own; it takes three lines and is the form RFC 0002's §7 ships. Asking here
is refused loudly and by name, never ignored.
Plugins are values, never names resolved from a document (§96). See
@fourjs/core's plugin.ts for the trust posture in full: a plugin runs
with the application's authority and nothing here sandboxes it.
OptionalposeWhether the frame's draw uses §43 interpolated render poses. Defaults to
true when a renderer is configured, false otherwise.
When on, the application owns a PoseBuffer (Application.poses),
registers the §39 step-10 snapshot system that captures into it, and passes
{ poseBuffer, alpha: time.interpolationAlpha } to every
renderer.render call. Which nodes are interpolated is still opt-in:
the buffer tracks nothing until something calls app.poses.track(node)
(from Phase 5 the physics adapter tracks its bodies), and an untracked node
draws from its live transform. Tracking every node automatically would cost
a copy per node per fixed step for scenery that never moves (decision,
WP-3.6).
Set it to false for a renderer that must draw exactly the simulation
state — a screenshot at a known step, a visual regression baseline — and to
true without a renderer to run the capture for a custom draw path.
Optionalreduced§75's reduced-motion policy, read back as Application.reducedMotion.
"auto" (the default, Appendix A) follows the platform preference through
ApplicationOptions.reducedMotionSource; true and false override
it outright, and the source is then never called. §75 makes this an
application-level policy precisely so that one answer serves @fourjs/ui's
widgets and §14's opt-in animation consultation, rather than each of them
asking the platform separately.
OptionalreducedThe platform preference reducedMotion: "auto" consults — in a browser,
matchMedia("(prefers-reduced-motion: reduce)"):
const query = matchMedia("(prefers-reduced-motion: reduce)");
const app = new Application({ reducedMotionSource: () => query.matches });
A function rather than a media-query object, for the reason ApplicationOptions.now is a function: this package names no host type, and a host with an exotic preference store (a settings file, a URL parameter, a test) is then not a special case. It is called on every Application.reducedMotion read and never cached, so a user who changes the preference mid-session is honoured without this class subscribing to anything; hoist the query object, as above, and the read is a property access.
Absent, "auto" resolves to false — "the platform reported no
preference", which is §75's safe answer and the one a host with no notion of
the setting should get.
OptionalrendererThe backend that draws the scene (§45, §61) — an instance, or §45's string
form — or false (the default) for a headless application that draws
nothing.
import { WebglRenderer } from "@fourjs/render-webgl";
const app = new Application({ renderer: new WebglRenderer(), canvas });
app.views.push(createFullscreenViewport(camera));
await app.initialize(); // awaits renderer.initialize({ canvas })
app.start();
§45 spells this option "auto" | "webgpu" | "webgl2" | "canvas2d" | "svg",
and since 2026-08-01 this class took an instance instead, because
resolving a string to a class would mean four importing every backend
package at runtime: every program — the headless and determinism ones
included — would carry a WebGL renderer it never uses (§91; the deferral is
recorded in MEMORY, and the payload evidence was a 14.88 kB example).
The string now works, and that reason is intact, because four still
imports no backend. A backend package registers itself into
@fourjs/render's §62 registry when the application asks it to, and the
string is resolved against whatever was registered:
import { registerWebglRenderer } from "@fourjs/render-webgl";
registerWebglRenderer(); // the app names its backends
const app = new Application({ renderer: "auto", canvas, antialias: true });
await app.initialize(); // resolves, then initializes
app.renderer; // the WebglRenderer, from here on
"auto" prefers WebGPU, then WebGL 2, then a 2D backend (§62), skipping
any that is unregistered, reports itself unsupported, or fails to
initialize — each skip reported through
ApplicationOptions.onRendererFallback. A named backend fails fast
with RENDERER_INITIALIZATION_FAILED (§62, §89) rather than downgrading,
and so does "auto" when nothing is left, naming every backend that is
registered (§85).
Three consequences worth stating:
null until
Application.initialize resolves when a string is used — there
is no renderer to hold before then. With an instance it is set at
construction, exactly as before.initialize failing, so the selection has to be the
one calling it; this class does not call it a second time.The application initializes the renderer (see Application.initialize) and drives it once per frame, but does not own it: Application.dispose leaves it alone (§83 — whoever created a resource disposes it), including one the registry built on its behalf.
Optionalrenderer§62's "applications may declare required and optional capabilities"
(WP-R1.9), forwarded verbatim as RendererResolveOptions.capabilities for
the string form of ApplicationOptions.renderer.
The selection rule and the tri-state honesty rule live on
RendererCapabilityDeclaration in @fourjs/render: "auto" skips a
backend that does not affirm every required name (reported through
ApplicationOptions.onRendererFallback with reason
"missing-capability"), a named backend that cannot rejects
Application.initialize with RENDERER_INITIALIZATION_FAILED, and
optional shortfalls of the selected backend are reported through
ApplicationOptions.onRendererCapabilityShortfall. A malformed
declaration rejects initialize with a RangeError (§85).
Unread for an instance — an author who constructed the backend has its record in hand and needs no negotiation — and ignored when no renderer is configured.
OptionalrendererThe §62 registry ApplicationOptions.renderer's string form resolves against; the shared one by default.
Pass one to keep a selection scope to itself — the discipline the tests use so that one application's backends are invisible to the next, and the seam a host embedding two independent engines needs. Unread for an instance.
OptionalresolutionDevice pixels per logical pixel (§45, §61) — devicePixelRatio in a
browser. Default 1.
Validated at construction, with or without a width/height pair
(2026-08-07): a non-finite or non-positive value throws a RangeError from
the constructor rather than reaching renderer.resize on some later call.
OptionalstatsWhether to measure §84's runtime statistics into Application.stats.
Defaults to false (A-1, 2026-08-07).
Off by default, and off means off. With statistics off,
Application.stats is null, the clock is never read, the backend's
counters are never assigned, and the frame runs the code it ran before this
option existed apart from three !== null comparisons per frame — no
allocation, and not one GPU call added, removed, or reordered. That is the
point: a diagnostic that changes the thing it measures is not a diagnostic
(§84), and a render gated on a render-affecting flag could not share a
pixel golden with one that is not.
With statistics on, the application owns one FrameStats record,
resets it at the top of every Application.step, measures
cpuFrameTime and simulationTime itself, and — when the renderer reports
them (§61's optional statistics member) — reads drawCalls, triangles,
and instances back off the backend, and reads §83's live-resource totals
for textureMemory/bufferMemory (A-5), and copies
Renderer.lastGpuFrameTimeSeconds into gpuFrameTime when that number
is finite. Counters a given frame cannot measure stay NaN;
FrameStats says which and why.
A production build ignores this option (A-4, 2026-08-07). §84 is a
development tool and §85 lets a production build drop expensive
diagnostics, so the whole path is gated on @fourjs/core's DEV: with
__FOUR_DEV__ defined as false, stats: true still type-checks (the
option and Application.stats's type are unchanged in both builds)
and Application.stats is null. Measure in development, or ship a
build that leaves __FOUR_DEV__ alone — see
docs/guides/performance-optimization.md.
OptionalsurfaceThe host seam ApplicationOptions.autoResize subscribes to — a
ResizeObserver in a browser, and this package names no DOM type. See
SurfaceObserver for the five-line browser adapter.
OptionalviewsThe viewports drawn each frame, in order (§48). Copied into Application.views, which is mutable afterwards.
Defaults to none — and an application with no viewport draws nothing (§61: an empty view list draws and clears nothing), because there is no camera the application could invent. Push a viewport before the first frame:
app.views.push(createFullscreenViewport(camera));
OptionalwidthInitial surface width in logical pixels (§45), applied through Application.resize at construction.
Defaults to 0 together with height, which means "no size has been
declared": nothing is forwarded to the renderer and no camera aspect is
touched until the first resize call. A renderer configured but never
resized keeps whatever size its own initialize established, exactly as
before this option existed.
The current subset of §45's
ApplicationOptions.§45 lists thirteen options; eleven of them ship. Omitted numeric options take the Appendix A normative defaults.
antialiaslanded with the §62 selection packet (R-2/A-8, 2026-08-07), for the reason its TODO gave: it is a device-selection option only a backend constructor can honour, so it had to wait for the code that constructs the backend.alphaandpowerPreferenceare still absent becauseRendererOptions(§61) does not carry them — accepting them here would mean storing values no backend ever reads, which is the accept-and-ignore this repository refuses.Three options here are seams rather than settings —
now, ApplicationOptions.surfaceObserver, and ApplicationOptions.reducedMotionSource. Each is the host object this package refuses to name, handed in as a function; each is read only by the §45 option it serves, and never at all when that option is off.