Documentation
    Preparing search index...

    Interface ApplicationOptions

    The current subset of §45's ApplicationOptions.

    §45 lists thirteen options; eleven of them ship. Omitted numeric options take the Appendix A normative defaults.

    antialias landed 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. alpha and powerPreference are still absent because RendererOptions (§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.

    interface ApplicationOptions {
        antialias?: boolean;
        assets?: AssetManager<never>;
        autoResize?: boolean;
        canvas?: unknown;
        depthRange?: DepthRange;
        fixedTimeStep?: number;
        height?: number;
        maximumSubSteps?: number;
        now?: () => number;
        onRendererCapabilityShortfall?: (
            report: RendererCapabilityShortfall,
        ) => void;
        onRendererFallback?: (report: RendererFallbackReport) => void;
        physics?: false | PhysicsWorld | PhysicsWorldFactory;
        plugins?: readonly FourPlugin[];
        poseInterpolation?: boolean;
        reducedMotion?: boolean | "auto";
        reducedMotionSource?: () => boolean;
        renderer?: false | Renderer | RendererSelection;
        rendererCapabilities?: RendererCapabilityDeclaration;
        rendererRegistry?: RendererRegistry;
        resolution?: number;
        stats?: boolean;
        surfaceObserver?: SurfaceObserver;
        views?: readonly Viewport[];
        width?: number;
    }
    Index
    antialias?: boolean

    Requests 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.

    assets?: AssetManager<never>

    The §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).

    autoResize?: boolean

    Whether 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.

    canvas?: unknown

    The 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.

    depthRange?: DepthRange

    Clip-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.

    fixedTimeStep?: number

    Fixed 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.

    height?: number

    Initial surface height in logical pixels (§45). See ApplicationOptions.width.

    maximumSubSteps?: number

    Maximum fixed steps per Application.step (§10, §45). Default 5 (Appendix A).

    now?: () => number

    Monotonic 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.

    onRendererCapabilityShortfall?: (report: RendererCapabilityShortfall) => void

    Called 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.

    onRendererFallback?: (report: RendererFallbackReport) => void

    Called 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.

    physics?: false | PhysicsWorld | PhysicsWorldFactory

    The §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 });

    §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."

    1. Registers one SimulationSystem at 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).
    2. Initializes it from Application.initialize, when world.initialized is still false — a WebAssembly solver loads its module there (§37), which is why that call is asynchronous.
    3. Measures it, when ApplicationOptions.stats is on: §84's physicsStepTime and activeBodies. See Application.stats.
    4. Disposes it only if it built it — the factory form. See Application.dispose.
    plugins?: readonly FourPlugin[]

    The §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.

    §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.

    poseInterpolation?: boolean

    Whether 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.

    reducedMotion?: boolean | "auto"

    §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.

    reducedMotionSource?: () => boolean

    The 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.

    renderer?: false | Renderer | RendererSelection

    The 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:

    1. Application.renderer is 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.
    2. The registry initializes the renderer it built. §62's fallback is defined in terms of initialize failing, so the selection has to be the one calling it; this class does not call it a second time.
    3. Passing an instance stays fully supported, and is what §45 requires ("the application must permit advanced users to construct and own these systems independently").

    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.

    rendererCapabilities?: RendererCapabilityDeclaration

    §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.

    rendererRegistry?: RendererRegistry

    The §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.

    resolution?: number

    Device 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.

    stats?: boolean

    Whether 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.

    surfaceObserver?: SurfaceObserver

    The 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.

    views?: readonly Viewport[]

    The 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));
    
    width?: number

    Initial 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.