Documentation
    Preparing search index...

    Class Application

    Hierarchy (View Summary)

    Index
    assets: AssetManager<never> | null

    The §76 asset manager this application was given, or null (§45, §76).

    const { icon } = await app.assets!.load({ icon: "/images/icon.png" });
    

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

    physics: PhysicsWorld | null

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

    poses: PoseBuffer = ...

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

    scene: Scene

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

    scheduler: Scheduler

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

    stats: FrameStats | null

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

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

    views: Viewport[] = []

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

    • get pluginContext(): PluginContext | null

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

      Returns PluginContext | null

    • get reducedMotion(): boolean

      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.

      Returns boolean

    • get renderer(): Renderer | null

      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.

      Returns Renderer | null

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

      Returns void

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

      Returns Promise<void>

      FourError if the application has been disposed. Thrown synchronously, like every other lifecycle precondition here.

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

      Returns number

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

      Returns void

    • Removes every listener, or every listener of one type. Used by teardown paths that must not leave detached nodes retaining listeners (§83).

      Type Parameters

      Parameters

      • Optionaltype: K

      Returns void

    • Resizes 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:

      1. the size is recorded (Application.width / Application.height / Application.resolution), so a headless application still knows how big it is;
      2. the renderer is told — renderer.resize(width, height, resolution), which makes the drawing buffer width · resolution × height · resolution device pixels and re-resolves every normalized viewport rectangle (§61);
      3. every full-surface viewport whose camera is a PerspectiveCamera has its 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.

      Parameters

      • width: number

        surface width in logical pixels; finite and >= 0

      • height: number

        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.

      Returns void

      FourError if the application has been disposed

      RangeError if any argument is out of range

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

      Returns void

      FourError if initialize() has not completed, or if the application has been disposed.

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

      Parameters

      • elapsedSeconds: number

      Returns void

      FourError if the application is disposed, not initialized, not running, or already inside a step (a nested step would advance one TimeState re-entrantly and interleave two frames' events, which no replay could reproduce — WP-1.12 decision, matching SystemRegistry.runFixedStep's guard).

      RangeError if elapsedSeconds is not a finite number >= 0 (validated by the scheduler).

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

      Returns void