Logo Mibo

AdaptiveHeadless<'Frame> Type

Runs an adaptive program with explicit frame stepping.

The runner owns the frame boundary. Each AdaptiveHeadless.Step: (1) applies cross-thread posts, drains the next-frame lane and the pre-step lane (subscription events, e.g. input) at the frame boundary, then compares the program's subscription projection against the attached table, (2) writes the current game time into the time root (once, or once per fixed step when AdaptiveProgram is set), (3) runs the program's Update phase, (4) drains the intent queue until empty — posted work runs in post order, and work posted during the drain runs in the same drain (after each sub-step's Update under fixed-step), (5) forces the frame builder — the frame's projections recompute exactly once if any of their dependencies moved, and not at all otherwise — and (6) notifies the observers with the forced frame. Draw code reads the returned frame: reads are O(1) until the next write.

Before the first step, the runner drains the intent queue once at startup, right after Init returns and before the first frame is forced: work Init posted through its context runs before the first frame, like the Cmd returned from the MVU init function.

The adaptive graph is confined to the thread that creates it: no locks, allocation-free steady state. The runner creates the graph lazily on the first user — the thread that first calls Step/StepN/StepUntil/Run, or the dedicated game thread of RunAsync. All graph work then happens on that thread. Cross-thread writes go through cval.Post — drained by Posting.pump at the start of every step — and external work goes through AdaptiveContext.Intents (the post lane is thread-safe; the runner drains it after Update).

Constructors

Constructor Description

AdaptiveHeadless(program, ?width, ?height, ?context, ?profiler)

Full Usage: AdaptiveHeadless(program, ?width, ?height, ?context, ?profiler)

Parameters:
Returns: AdaptiveHeadless<'Frame>
program : AdaptiveProgram<'Frame>
?width : int
?height : int
?context : GameContext
?profiler : FrameProfiler
Returns: AdaptiveHeadless<'Frame>

Instance members

Instance member Description

this.Dispose

Full Usage: this.Dispose

Detach all subscriptions, dispose program disposables and observers, and clean up resources.

this.Frame

Full Usage: this.Frame

Returns: 'Frame

The last forced frame. Valid after the first AdaptiveHeadless.Step.

Returns: 'Frame

this.GameTime

Full Usage: this.GameTime

Returns: GameTime

Total elapsed virtual time.

Returns: GameTime

this.Post

Full Usage: this.Post

Parameters:
    work : unit -> unit

Thread-safe external injection: posts work to the post lane, where it runs on the owner thread at the next post drain, in post order, drained until empty, before the frame is forced. For work posted after initialization, that drain follows the next step's Update (after each sub-step's Update under fixed-step); for work posted before the first step, it is the startup drain, right after Init and before the first frame is forced. A convenience for foreign code (tests, network callbacks, AI drivers) that holds the runner but not the Update context: the same as IntentQueue.post.

work : unit -> unit

this.Run

Full Usage: this.Run

Parameters:
    interval : TimeSpan - Tick interval (e.g. TimeSpan.FromMilliseconds(16) for 60fps).
    ?ct : CancellationToken - Optional cancellation token to stop the loop early.

Returns: (GameTime * 'Frame) seq A sequence of (GameTime * 'Frame) snapshots, paced by the interval.

Run the simulation synchronously, yielding each frame as a sequence.

Uses a spin-wait with Thread.Sleep(1) to pace the loop. This is the standard pattern for game servers — the Stopwatch controls timing precision while Sleep yields the CPU between ticks.

This advances the runner's internal state. Do not mix with Step/StepN/StepUntil on the same runner — they all advance the simulation and using them together will produce simulation corruption.

interval : TimeSpan

Tick interval (e.g. TimeSpan.FromMilliseconds(16) for 60fps).

?ct : CancellationToken

Optional cancellation token to stop the loop early.

Returns: (GameTime * 'Frame) seq

A sequence of (GameTime * 'Frame) snapshots, paced by the interval.

this.RunAsync

Full Usage: this.RunAsync

Parameters:
Returns: IAsyncEnumerable<StepOutcome<'Frame>> An async sequence of StepOutcome snapshots: the game time and the forced frame of each step.

Run the simulation asynchronously, yielding each frame as an async enumerable.

The world loop runs on a dedicated background game thread — the graph is confined to its creating thread, and async continuation threads are not it. The async enumerable is a consumer of that thread: it receives the outcomes the loop produces, in order. The for .. in syntax in F# 8+ can iterate over IAsyncEnumerable directly:

 for outcome in world.RunAsync hz30 do
   render outcome.Frame
val outcome: obj
The consumer renders the packed frame — it never touches the world's graph. Background work started with IntentQueue.postTask / postAsync runs off the world thread, and its completion posts back into the world's intent queue (thread-safe) and runs at the next post drain on the game thread.

At most one enumerator may consume a runner at a time: a second GetAsyncEnumerator while one is active throws. An exception on the game thread (from Init, Update, or a projection) is not thrown on the background thread — it is stored and rethrown from MoveNextAsync so the consumer sees a normal error.

This advances the runner's internal state. Do not mix with Step/StepN/StepUntil on the same runner — they all advance the simulation and using them together will produce simulation corruption.

interval : TimeSpan

Tick interval.

?ct : CancellationToken

Optional cancellation token to stop the loop.

Returns: IAsyncEnumerable<StepOutcome<'Frame>>

An async sequence of StepOutcome snapshots: the game time and the forced frame of each step.

this.ShouldQuit

Full Usage: this.ShouldQuit

Returns: bool

Whether the runner has received an exit request.

Returns: bool

this.Step

Full Usage: this.Step

Parameters:
    elapsed : TimeSpan - Frame delta (e.g. TimeSpan.FromMilliseconds(16) for 60fps). Negative values are clamped to zero.

Returns: 'Frame

Advance the program by one frame and return the forced frame.

When AdaptiveProgram is set, the frame delta is converted into zero or more fixed-size steps: the time root is written, Update runs, and the intent queue drains until empty once per step. The frame is forced once at the end regardless, so intermediate steps are integrated but not observed by the frame. This mirrors the MVU TickFrame fixed-step loop, calling Update instead of dispatching a mapped message. The next-frame lane drains once per Step at the boundary — never per sub-step.

This mutates the runner's internal state (time root, program roots, frame). Do not mix Step/StepN/StepUntil with Run/RunAsync on the same runner — they all advance the simulation and using them together will produce simulation corruption.

elapsed : TimeSpan

Frame delta (e.g. TimeSpan.FromMilliseconds(16) for 60fps). Negative values are clamped to zero.

Returns: 'Frame

this.StepN

Full Usage: this.StepN

Parameters:
    count : int - Number of frames to run.
    elapsed : TimeSpan - Frame delta per step.

Returns: 'Frame

Advance the simulation by N frames and return the last forced frame.

This mutates the runner's internal state. Do not mix with Run/RunAsync on the same runner — they all advance the simulation and using them together will produce simulation corruption.

count : int

Number of frames to run.

elapsed : TimeSpan

Frame delta per step.

Returns: 'Frame

this.StepUntil

Full Usage: this.StepUntil

Parameters:
    predicate : 'Frame -> bool - Condition to check after each frame.
    elapsed : TimeSpan - Frame delta per step.
    ?maxFrames : int - Safety limit to prevent infinite loops.

Returns: bool True if predicate was met, false if maxFrames was reached.

Advance until a predicate on the forced frame returns true.

This mutates the runner's internal state. Do not mix with Run/RunAsync on the same runner — they all advance the simulation and using them together will produce simulation corruption.

predicate : 'Frame -> bool

Condition to check after each frame.

elapsed : TimeSpan

Frame delta per step.

?maxFrames : int

Safety limit to prevent infinite loops.

Returns: bool

True if predicate was met, false if maxFrames was reached.

Type something to start searching.