Programs¶
A program is the behavior attached to a prim. Modelica, Rhai, and optionally
Python programs all use the same LunCoProgramAPI schema and the same
source-backed lifecycle. The schema applies directly to the physical prim when
the behavior is intrinsic, or to a child Scope when the behavior is
separable.
This is a deliberate simplification. There is no global script list, no scene-level controller registry, and no hidden behavior that survives deleting the program prim or the object that owns it.
The program prim¶
def Xform "Rover" (kind = "assembly")
{
def Scope "Controller" (
prepend apiSchemas = ["LunCoProgramAPI"]
)
{
uniform token info:implementationSource = "sourceAsset"
uniform asset info:sourceAsset = @twin://my-rover/models/Drive.mo@
uniform bool lunco:program:realtimeSafe = true
}
}
| Property | Meaning |
|---|---|
info:implementationSource |
"sourceAsset" for a file, "sourceCode" for inline text, "id" for a runtime-implemented driver |
info:sourceAsset |
Asset address of the source file |
info:sourceCode |
Inline source text, when using sourceCode |
lunco:program:realtimeSafe |
Declares that the program can meet the real-time step budget |
The example uses a child Scope, the separable placement: the controller is
bolted onto the rover and can be removed without removing the airframe. Apply
the same schema directly to the physical prim when the program is the thing
itself — a lander's flight-control system, for example — so its inputs: are
the vehicle's control surface. In both placements the ports live on the prim
that carries the schema, and a connection targets those ports.
For a source asset, the backend is selected from its extension:
| Extension | Backend |
|---|---|
.mo |
Modelica |
.rhai |
Rhai |
.py |
Python, when the optional feature is built |
A missing or unrecognized extension is an explicit program error. It is not guessed from the prim name or the folder.
Implementation arms¶
Use sourceAsset for anything a person will edit, review, or reuse:
uniform token info:implementationSource = "sourceAsset"
uniform asset info:sourceAsset = @lunco://models/RoverDrive.mo@
Use sourceCode only for small, generated, or test-only programs:
uniform token info:implementationSource = "sourceCode"
uniform string info:sourceCode = "fn on_start(me, ctx) { notify(\"ready\"); }"
Use id for a driver the runtime already implements, such as range_beam.
The selected arm is authoritative: exactly one of info:id,
info:sourceAsset, or info:sourceCode may be populated. A populated
non-selected arm is an authoring conflict, not a fallback.
A file-backed program participates in the asset pipeline: it reloads when the file changes, resolves its imports through the asset owner, and can be opened in the appropriate editor. Inline source has none of that provenance. It is appropriate for a fixture, not for a project a team maintains.
Attaching a program¶
The typed authoring intent is AttachProgram. It authors the child prim,
declared scalar ports, defaults, and USD connections as one change set. The
same operation is exposed to scripts as assembly_edit::attach_program and to
the UI as the program attachment flow.
Attaching a program is an ordinary USD edit. It enters the journal, can be undone, and is saved with the document. The runtime does not create a program prim behind the author's back.
AttachProgram attaches a child program to any prim that can own one. The
common choices are the assembly root, a controller scope, or a dedicated
behavior prim. Keep the program near the thing it controls so deleting the
assembly deletes the behavior. For an intrinsic program, apply
LunCoProgramAPI to the physical prim directly; that is a schema application
rather than the child-prim flow.
The realtime promise¶
lunco:program:realtimeSafe = true is a declaration, never an inference. No
amount of reading a model's source establishes how long it takes to step.
The promise matters when a program writes a force or torque port on a client-predicted body. Without it, the wiring pass refuses that wire and reports the reason. This prevents a slow solver from being admitted into a path that must meet the presentation clock.
Absent or false means "not promised." A malformed value is ignored with a
warning; it does not become true.
Program lifecycle¶
A source-backed program follows the same sequence in every backend:
- The program prim is discovered during projection. For a child program, the owning prim is projected first.
- The source asset is resolved through the asset owner.
- The source is compiled or parsed into an interface.
- Declared ports and authored defaults are checked against that interface.
- Connected inputs are admitted, possibly through coupled initialization.
- The program starts and participates in the fixed-step clock.
- On detach, hot reload, or scene teardown, the old program stops before a replacement starts.
A terminal interface error faults the participant instead of leaving an unresolved readiness wait. Source replacement refreshes the same checked interface through its owner.
Hot reload¶
Editing a file-backed program can reload it without rebuilding Rust. The
outgoing program's stop hook runs before the new one starts. For Rhai this is
on_stop; for Modelica it is session retirement and recompilation.
Hot reload is not a scene restart. The document, the stage, and unrelated participants stay live. A program that acquired control must release it in its stop hook.
Modelica programs¶
A .mo program is a Modelica class compiled by Rumoca and run on a background
worker. Its public ports come from the checked DAE, not from the source text
alone:
- runtime inputs are writable;
- numeric observables, including authored outputs and local states, are readable;
- constants, runtime inputs, and parameters are not published as outputs.
The program prim's inputs: and outputs: properties are the external
contract. They must match the compiled interface. See
Modelica for the language and
Cosimulation for the step contract.
Rhai programs¶
A .rhai program is a scenario with lifecycle hooks. It observes the
simulation, reacts to events, and issues typed commands. It does not integrate
physics or replace a Modelica model.
def Scope "Mission" (
prepend apiSchemas = ["LunCoProgramAPI"]
) {
uniform token info:implementationSource = "sourceAsset"
uniform asset info:sourceAsset = @twin://my-rover/scenarios/drive.rhai@
}
The asset source is loaded and hot-replaced by the asset layer. A scenario can
also be run directly against an entity with RunScenario, or run as a
file-backed asset with RunScenarioAsset. See Rhai.
Python programs¶
Python is an optional feature. When built, a .py program is loaded through
the same source-backed path. Python does not implement the scenario lifecycle
hooks; it supports the one-shot RunPython command and a participant source
seam. A build without the feature cannot execute or register Python
participants.
Several programs on one prim¶
A prim may own several program children, and it may also carry one intrinsic program of its own. Each program prim is an independent participant with its own source, session, and lifecycle. They interact through the same typed ports and event bus as any other components.
Do not use several programs to express one behavior that should be a single model. Split only when the pieces have genuinely different clocks, languages, or ownership.
Deleting a program¶
Deleting the child prim removes the behavior. Deleting the owning prim removes the program with it. There is no separate "stop script" registry to clean up.
If a program acquired control, its stop hook runs during removal. A program that fails to release control leaves the runtime in a visible held state rather than silently continuing with stale authority.
Rules of thumb¶
- Put the program under the object it controls.
- Prefer
sourceAssetover inlinesourceCode. - Declare
realtimeSafeonly when the program can meet the budget. - Match the prim's
inputs:andoutputs:to the compiled interface. - Release control and stop actuators in the stop hook.
- Treat a source reload as a replacement, not as a mutation of the old program.
Continue with Materials, which bind appearance to the same composed prims.