Skip to main content

File layout

When a LABO run starts, each enabled stream stands ready but doesn't open a file yet. The CSV is created the first time the writer actually emits a row — see Lazy file creation below. This page is the reference for where those files live, how they're named, and what governs the layout.

The run directory

Every session (one "run" of an experience) gets its own directory created under the configured data store path. All streams for that session write into the same directory.

<dataStorePath>/
└── <runDirectory>/
├── UpdateLoop.csv
├── FixedInterval.csv
├── ParticipantTracking.csv
├── EyeTracking.csv
├── BodyTracking.csv
├── FaceTracking.csv
├── LeftHandTracking.csv
├── RightHandTracking.csv
├── InputCapture.csv
├── EventPoints.csv
├── Variables.csv
├── Expressions.csv
├── AgentsData.csv
├── InteractivesData_<ObjectName>.csv (one per running interactive)
└── ...

The run directory is created by DataDirectoryUtility when the run initialisation completes. Each writer subscribes to the RunInitializationCompleted event, marks itself ready, and waits — the file is opened lazily on first row emission.

File names

Default file names per stream:

StreamDefault filenameNotes
ExperienceState (render loop)UpdateLoop.csvDriven by UpdateLoopDataCapture.
ExperienceState (physics loop)FixedInterval.csvDriven by FixedIntervalDataCapture. Independent file.
ParticipantParticipantTracking.csvConfigurable on the SO.
BodyBodyTracking.csv
FaceFaceTracking.csv
Hand (left)LeftHandTracking.csvTwo separate writers, one per hand.
Hand (right)RightHandTracking.csvFallback UnknownHandTracking.csv if skeleton-type is null.
EyeEyeTracking.csv
EventsEventPoints.csv
VariablesVariables.csv
ExpressionsExpressions.csv
AgentsAgentsData.csvOnly when USING_AGENTS is defined.
InputInputCapture.csvConfigurable on the SO.
InteractivesInteractivesData_<ObjectName>.csvOne file per running interactive.

Streams whose config SO exposes a fileName field (most do) allow overriding the default. If the configured name is blank, the writer falls back to the default.

Update loop & Fixed interval — two files, one writer class

Unlike the historical "Both" mode (one writer producing two files), the current pipeline uses two independent configs (UpdateLoopDataCapture + FixedIntervalDataCapture) and creates two writer instances sharing the LaboExperienceStateDataWriter class. Each writes its own file:

  • UpdateLoop.csv — one row per LateUpdate (render frame)
  • FixedInterval.csv — one row per FixedUpdate (physics tick)

Enable one, the other, or both — they're independent. Joining the two together is usually not what you want — they're on different clocks. Pick the one that matches what you're analysing:

  • Render-rate continuous signals → UpdateLoop.csv
  • Physics-rate continuous signals → FixedInterval.csv

Hand tracking — two files

Hand tracking uses two independent writer instances, one per hand, because the left and right hands may have different skeletons assigned (or only one hand may be assigned). Each writer produces its own file:

  • LeftHandTracking.csv
  • RightHandTracking.csv

If only one hand is configured, only that one file exists. The filename is derived from skeleton.skeletonType — fallback is UnknownHandTracking.csv if the skeleton is null. To produce a combined per-frame view, join both files to UpdateLoop.csv on FrameNumber.

Lazy file creation — writers with no data leave no file

SessionBoundCsvWriterBase does not open the CSV file when the run initialises. The file is created only the first time WriteDataRow() is actually called. Consequences:

  • A writer that's enabled but never produces a row leaves no file behind. Empty CSVs do not exist.
  • "Where's my file?" usually has two possible answers: (a) the writer was gated out at startup, or (b) the writer was enabled but no triggers fired during the run (irregular streams) / no samples qualified (regular with recordMode filtering).

This change post-dates earlier docs that described eager file creation on run-init. If you see references to "files created at session start," they're stale.

Headers and append behaviour

  • The header row is written once when the file is first opened (i.e. on the first row emission), via the writer's BuildHeader(). Header is never rewritten mid-run.
  • Rows are appended as they're produced. Writes are batched and flushed every N rows (configurable per writer via flushEveryNRows on the config SO; default 30). A crash or force-quit can lose at most the most recent unflushed batch.
  • Files are closed cleanly when the writer's OnDisable / OnDestroy fires — normally when the run ends (RunInitializationReset event) or the editor exits play mode.
  • After a run ends, the next run opens fresh files in a new run directory. Same writer instances survive across runs.

Gated streams — files may not exist

A writer only produces a file if its gate conditions allow at least one row. If the gate fails, no file is created.

StreamGate
UpdateLoop / FixedIntervalRespective config SO present, enabledLogging = true
BodyExperienceUtilities.CaptureBodyData enabled and a SilicoSkeletonBody is assigned
FaceExperienceUtilities.CaptureFaceData enabled and a SilicoSkeletonFace is assigned
Hand (per side)Hand capture enabled in DataCaptureSettings, skeleton assigned, skeleton initialized == true
EyeEye capture enabled, eyes tracked, head skeleton assigned
ParticipantParticipantTrackingDataCapture SO present, enabledLogging = true
InputKeyCodeDataCapture SO present, enabledLogging = true
Events / Variables / Expressions / AgentsRespective config SO present, enabledLogging = true, plus USING_AGENTS for Agents
InteractivesPer-object: interactive.isRunning AND shared InteractivesDataCapture.enabledLogging = true

Plus the master gate: DataDirectoryUtility.SaveData must be true. In editor, that requires the application state to be running (not None / View) and saveSettings.saveTestData to be On. In builds, always true.

If an analysis script expects a file that doesn't exist for this run, the most common causes are: gated out, never triggered, or editor saveTestData was off.

Where the run directory lives

The data store root is resolved at runtime from StateUtility.State.dataStore.runDataPath. It's configured in the LABO editor settings; the default varies by platform but is typically under the project's local Application.persistentDataPath or a user-configured folder.

Each run directory is typically named with a timestamp and/or run identifier — check DataDirectoryUtility for the exact format if you need to parse or filter directories in bulk.

Encoding

  • File encoding: UTF-8 without BOM (StreamWriter default).
  • Line endings: native to the platform the run was recorded on. Expect \r\n on Windows recordings, \n on macOS/Linux. If you process recordings cross-platform, pandas and R handle both transparently; raw string diffs may not.
  • Field separator: comma. Commas inside string values are replaced with semicolons at write time (see Column conventions).
  • Quoting: none. No field is double-quoted; the comma-to-semicolon substitution is the only escaping LABO does.

Further reading