The package is a data_consumer. It does not own simulation authority, import a
simulator SDK, or receive a backend object, host path, or file descriptor. Installing
the package only registers its component catalog and runtime factory. A Run pays the
recording cost only when plugin://fastsim/record is selected.
Status
Version 0.3.12 aligns Record with FastSim Core >=0.1.0a36,<0.2 and
fastsim-recording-format[images]==0.3.3. It adds optional scene-geometry capture while
keeping that path disabled by default. Its component schema matches the current
prepare contract: mandatory global sources, incomplete preservation, and the empty
postprocessor set are rejected during compilation when authored otherwise. It uses Core's exact
FinalizerRegistrationRequest, FinalizerResult, and TerminalSemanticCut values,
commits only after consuming every selected source through the cut, and reports its
required finalizer result back to the Core terminal coordinator. Real-backend DROID
recording and cross-backend Replay are tracked by the separate integration gate. When
Core exposes fluid.audit, Record subscribes to compact accepted emission batches and
writes the required fluid.emissions / fastsim-fluid-emission-batch/1 stream. JSON
encoding remains in the writer worker, and runs without that optional service retain
the previous stream set and cost profile.
Pure numeric observations are captured once per simulation tick as one atomic FSR record. A scene exposing 90 numeric channels therefore contributes 120 queue items per second at 120 Hz, not 10,800. Camera RGB and calibration data keep independent channel records and their existing codecs.
Core 0.1.0a33 additionally supplies the actual portable byte size of lifecycle and
plugin-event envelopes. Record uses that producer-computed fact for lossless critical
lane admission instead of charging every small event at the 1 MiB service ceiling.
Older compatible Core releases retain the conservative ceiling as a safe fallback.
Camera RGB now defaults to JPEG quality 92 with 4:4:4 chroma sampling. This keeps
ordinary simulation datasets substantially smaller while preserving a high-quality
RGB signal. Pixel-exact users can select lossless PNG with one encoding: png field.
Both codecs run only in the Record worker; capture callbacks and the physics loop do
not encode images. Depth, segmentation, normals, and other numeric sensor channels
are not routed through the RGB JPEG encoder.
The default writer uses FSR binary 1.2 with structural-validation. It validates the
container, canonical metadata, exact offsets and lengths, sequence and generation
projections, index, footer, truncation, and atomic output transaction without hashing or
rescanning payload bytes. This removes full-payload digest work from both recording and
default post-seal readback. integrity: fast and integrity: strong remain explicit,
tamper-evident compatibility modes and require Core RunOutputPolicy(integrity="sha256").
Record produces only the primary FSR artifact; derived video, evaluation, and upload
workers consume the committed artifact outside simulation and Record threads.
Record can also capture an optional render.state trajectory at a bounded configured
rate. Disabled runs pay no render-state participant or per-tick capture cost. Enabled
runs retain poses and velocities plus packed float32 particle positions for later
camera-only Replay, and the writer compresses that state with zlib level 1.
The 0.3.1 release changed package compatibility only; its recording hot path and
persistence contract remained unchanged from 0.3.0.
The 0.3.0 release adds ordered per-channel observation capture and native FSR camera
streams. Within each calibration epoch, RGB, intrinsics, world translation, and world
quaternion records share the corresponding calibration digest while retaining
independent stream sequences. Descriptor drift, missing required samples, or an
invalid calibration contract fails closed. The catalog retains
all historical manifests for existing locks, while new compilation selects 0.3.12.
Calibration is tracked independently for every camera and Runtime generation. RGB and extrinsics may forward-reference a new digest only within the same committed tick; the matching intrinsics sample must declare it before the next tick, reset barrier, or terminal cut. Every intrinsics record carries the complete bounded calibration profile in record metadata, while its payload carries the actual K matrix. Offline Replay can therefore reconstruct generation-to-digest calibration without relying on the stream's initial attributes.
Installation
Python 3.11 and 3.12 are supported. Install the matching Core and format packages before the plugin:
python -m pip install "fastsim>=0.1.0a36,<0.2" \
"fastsim-recording-format[images]==0.3.3" \
"fastsim-plugin-record==0.3.12"
For a source checkout:
python -m pip install /path/to/fastsim-next
python -m pip install /path/to/fastsim-plugins/packages/fastsim-recording-format
python -m pip install /path/to/fastsim-plugins/packages/fastsim-plugin-record
The installation publishes two entry points:
fastsim.components / fastsim.recordexposes the component catalog.fastsim.plugins / fastsim.recordexposes the managed plugin factory.
Minimal configuration
Add the plugin to a normal FastSim v2 Run. Component defaults are merged by the FastSim compiler:
schema: fastsim/2
name: recorded-run
backend: isaaclab
scenario:
scene:
robots:
robot:
use: robot://example/robot
plugins:
record:
use: plugin://fastsim/record
The default observation mode is all_numeric. If the scene declares no numeric
channels, Record does not request an Observation grant or create an Observation pump.
To record only lifecycle, control, and plugin events, make that choice explicit:
plugins:
record:
use: plugin://fastsim/record
config:
streams:
observations: {mode: none, include: {}}
Advanced configuration
Record once, render later
Enable the optional render.state trajectory when the recording will later be used
for camera-only rendering. The capture rate is selected before Core reads backend
state or allocates a global record sequence. Disabled runs therefore create no
render-state subscription or per-tick capture work.
plugins:
record:
use: plugin://fastsim/record
config:
output: {name: source.fsr}
streams:
observations: {mode: none, include: {}}
cameras: {}
render_state: {enabled: true, rate_hz: 30}
The required render.state stream stores articulation joint position and velocity,
optional articulation root pose and twist, rigid pose and linear/angular velocity,
and particle-fluid positions as packed little-endian float32 XYZ values. The Record
writer encodes metadata plus those position buffers with standard-library zlib level
- Capture and simulation threads do not compress or quantize the state.
Use the recorded FSR as the Replay input and run Replay together with a second Record
instance to create a new camera-bearing FSR. A complete example with explicit camera
channels is examples/render-replay/run.yaml.
The output footer records mode: render and the exact source FSR SHA-256. Its terminal
fence includes the complete render-state source sequence.
Optional 3D geometry for FSR Viewer
Geometry is not recorded by default. Enable it only when the FSR must support the Viewer's simulator-free 3D workspace:
plugins:
record:
use: plugin://fastsim/record
config:
streams:
geometry: {enabled: true, rate_hz: 30}
Record reads geometry only through scene.geometry@2. Visual geometry is preferred;
collision or planning geometry is used only when the backend exposes no visual
catalog. Mesh resources are deduplicated, split into bounded chunks and stored once.
The configured rate controls only dynamic and kinematic Transform samples. Static
geometry is not repeated per frame, materials are excluded, and particle-fluid
particles never enter this geometry asset set. When enabled remains false, there
is no catalog/resource read and no geometry Transform query.
The following is a currently executable configuration. It records one required joint channel and keeps all lanes lossless:
plugins:
record:
use: plugin://fastsim/record
config:
integrity: structural
output:
name: episode.fsr
streams:
lifecycle: true
events: true
control: true
observations:
mode: explicit
include:
joints:
channel: robot.joint_position
required: true
every_n_samples: 1
cameras:
overview:
rgb: sensors.overview.camera.rgb
intrinsics: sensors.overview.camera.intrinsics
translation: sensors.overview.camera.extrinsics.translation
quaternion_xyzw: sensors.overview.camera.extrinsics.quaternion_xyzw
required: true
every_n_samples: 1
# Omit encoding and quality for the JPEG quality-92 default.
queues:
critical:
max_items: 4096
max_bytes: 67108864
overload: fail_run
numeric:
max_items: 2048
max_bytes: 268435456
overload: fail_run
media:
max_items: 64
max_bytes: 536870912
overload: fail_run
batching:
max_items: 256
max_bytes: 16777216
max_sim_duration_s: 1.0
finalize:
requested_timeout_s: 60
preserve_incomplete: true
postprocessors: {}
The camera alias creates cameras.overview.rgb, cameras.overview.intrinsics,
cameras.overview.extrinsics.translation, and
cameras.overview.extrinsics.quaternion_xyzw streams in the FSR file. Capture cadence
is configured through Core runtime.sensor_hz and rate_policy, before a global
record sequence is allocated. Consequently every_n_samples is fixed to 1 in this
release; values above 1 fail validation rather than creating sequence holes.
JPEG is the default even when the encoding field is omitted. Use lossless PNG only
when exact RGB pixels are required:
cameras:
overview:
# The channel and calibration fields are unchanged.
rgb: sensors.overview.camera.rgb
intrinsics: sensors.overview.camera.intrinsics
translation: sensors.overview.camera.extrinsics.translation
quaternion_xyzw: sensors.overview.camera.extrinsics.quaternion_xyzw
required: true
every_n_samples: 1
encoding: png
For JPEG, quality is an optional exact integer from 1 through 100 and defaults to
92. PNG rejects quality because it is always lossless. The FSR stream records its
media type, payload codec, dimensions, color space, lossy flag, quality, JPEG chroma
policy, and the PNG lossless compression level, so offline readers do not infer the
representation. PNG uses lossless level 1 to favor sustained recording throughput.
lifecycle, events, and control currently must remain true; the global capture
source mask is compiled before plugin startup. Postprocessors and
preserve_incomplete: false are rejected by the 0.3.8 component schema, with runtime
checks retained as defense in depth.
Lossy policies (degrade_recording, drop_oldest, and drop_newest) are valid only
when every stream in the numeric or media lane is optional. The critical lane and any
lane containing a required stream use fail_run.
Asynchronous and performance semantics
Record uses three bounded lanes:
| Lane | Content | Overload rule |
|---|---|---|
critical |
lifecycle, control audit, control results, plugin events | fail_run only |
numeric |
selected non-media observations | lossless or declared optional loss |
media |
selected RGB camera envelopes | lossless or declared optional loss |
Each source has a dedicated pump. Selected observations share one ordered per-channel
subscription. A pump validates an immutable, already stamped envelope and calls
non-waiting try_publish; it performs no encoding,
compression, hashing, file access, or worker join. Lifecycle, plugin events, and
observations request atomic history-plus-live subscriptions from global recording
sequence 1, so activity published before Record's prepare boundary is not lost.
Control subscribes before control producers start.
The component Manifest declares Record as the sole ordered capture owner through
recording_sources. Core assigns contiguous capture_sequence values only to the
selected observation channels and generation barriers; Record uses that sequence for
source continuity and the terminal fence. The independent live channel_sequence is
retained only as diagnostic metadata and never determines persistence continuity.
One worker thread owns the opaque write lease. It merges all lanes by global sequence, encodes payloads exactly once, and writes bounded batches. Immutable packed RGB buffers remain untouched on the capture path; any JPEG/PNG conversion and its required worker- side buffer allocation happen only after queue admission. A batch is flushed when any configured limit is reached:
- item count;
- exact encoded payload byte count; or
- simulation-time span across sorted records in one generation.
Generation changes close the current batch. Finalization always flushes the remaining partial batch. A slow or stalled output cannot block simulation publication; required queue overflow is latched as a fatal recording error. An enabled-only monitor turns a latched pump, lane, or worker error into one normal Core completion request. If another source already won completion, the required finalizer still reports the failure and prevents false success.
The finalizer summary reports payload bytes, batch/finalize/readback elapsed time, and per-active-lane capacity, depth, high-water mark, enqueue/dequeue, loss, failure, and degradation counters. These values come from existing lane counters and batch boundaries; no per-tick timer is added. With no selected observations or cameras, Record creates neither a numeric nor a media lane and performs no observation polling.
Output, validation, and failure behavior
The intended output transaction is:
open write lease -> write/flush/close -> seal
-> open exact opaque readback lease
-> structural FSRReader validation; payload bytes are seek-skipped
-> close readback lease
-> confirm_validation(exact_lease, immutable_receipt)
-> commit
The validation receipt identifies the validator schema/profile, validated byte count, policy integrity, optional content SHA-256, FSR footer identity, and completion identity. The default structural receipt has no content digest. Explicit fast/strong modes compute one lazy full-file digest after sealing and can commit only under Core's SHA-256 output policy. Commit is never attempted after corruption, an unexplained global sequence hole, a missing terminal cut, encoder bound violation, worker timeout, or wrong readback-lease identity.
Declared optional loss is written as an exact ordered GapRecord and marks the primary
artifact degraded. Required-stream loss is fatal. Calling close before successful
finalization aborts the output transaction. Preservation and publication of an
.incomplete staging artifact are controlled by trusted Core RunOutputPolicy, not by
a plugin-selected host path. The authored preserve_incomplete field currently must
remain true and does not override deployment policy.
When the Host cancels a finalizer at its absolute deadline, Record cancels its pumps and writer but does not call the public unbounded transaction-abort path. Core owns the deadline-bounded lease revocation and incomplete-file quarantine. This keeps a stalled filesystem from converting terminal cleanup into an unbounded application wait.
Verification
From the fastsim-plugins repository:
PYTHONPATH=/path/to/fastsim-next/src:\
packages/fastsim-recording-format/src:\
packages/fastsim-plugin-record/src \
python -m pytest packages/fastsim-plugin-record/tests -q
python -m ruff check \
packages/fastsim-plugin-record/src \
packages/fastsim-plugin-record/tests
cd packages/fastsim-plugin-record
PYTHONPATH=/path/to/fastsim-next/src:../fastsim-recording-format/src:src \
python -m mypy --config-file pyproject.toml
python -m build
Run the DROID joint-state functional gate on a separate short recording; do not reuse
the position-only performance A/B. The source Run configuration must explicitly
select both channels as shown in
acceptance/configs/record-droid-joint-state-explicit.yaml:
PYTHONPATH=packages/fastsim-recording-format/src \
python acceptance/scripts/validate_record_joint_state.py \
--config /path/to/exact-functional-run.yaml \
--fsr /path/to/committed-functional-run.fsr \
--report /path/to/joint-state-acceptance.json
This gate distinguishes manifest declarations from evidence: both position and velocity must have nonzero real record counts, and every decoded sample must match the locked DROID entity, float64 dtype, 13-element shape, units, byte length, and finite- value contract.
The current package suite covers bounded overload and exact gaps, concurrent loss
accounting, out-of-order concurrent source arrival, global ordered merge,
per-channel reset barriers, default JPEG and explicit PNG worker encoding, legacy
raw-RGB decoding, exact RGB/K/translation/
quaternion FSR output, zero media-lane allocation when cameras are disabled,
reset-generation seek, all four batch-flush paths,
slow and stalled writers, worker-only encoding, startup-history replay against real
Core source objects, exact terminal cuts, normal/failed/cancelled/stopped completion,
required-finalizer precedence, finalizer timeout, capture-failure propagation,
fault-injected prepare cleanup, corrupt readback, exact lease identity, idempotent
stop/close, dual-Python source tests, isolated Wheel dependency resolution, pip check, installed entry-point discovery, and source-isolation checks. Real simulator
Record and cross-backend Replay evidence belongs to the integrated backend acceptance
phase and is not implied by these package gates.
License and authorship
Author: Hofee lexhofee@gmail.com
The package currently uses LicenseRef-Pending until the repository license is
finalized.