On this page
The iRacing extension is the Director App's most complex integration. It reads data directly from iRacing's shared memory using koffi (a Node.js FFI library) to access Windows system DLLs — no SDK wrapper, no REST API, no WebSocket. This post explains the full data pipeline: from memory-mapped files through YAML parsing and telemetry variable reading to the RaceState object that drives AI sequence generation.
Two Windows DLLs
The extension interfaces with two Windows system libraries via koffi:
| DLL | Functions | Purpose |
|---|---|---|
| user32.dll | FindWindowA, PostMessageA, RegisterWindowMessageA | Detect the sim window, send broadcast commands (camera switches, replay control) |
| kernel32.dll | OpenFileMappingA, MapViewOfFile, UnmapViewOfFile, CloseHandle | Read iRacing's shared memory for session metadata and live telemetry |
The command layer (user32) sends messages to iRacing. The data layer (kernel32) reads from iRacing. These are independent — you can read telemetry without sending commands, and vice versa.
Shared Memory Layout
iRacing exposes all session and telemetry data via a memory-mapped file named Local\IRSDKMemMapFileName. The extension opens it read-only with FILE_MAP_READ:
┌─────────────────────────────────────────────────┐
│ Header (48 bytes — 12 × int32) │
│ [0] ver SDK version │
│ [1] status Connection status │
│ [2] tickRate Ticks per second │
│ [3] sessionInfoUpdate Change counter (YAML) │
│ [4] sessionInfoLen YAML buffer size │
│ [5] sessionInfoOffset Byte offset to YAML │
│ [6] numVars Number of telemetry vars │
│ [7] varHeaderOffset Offset to var headers │
│ [8] numBuf Number of data buffers │
│ [9] bufLen Bytes per data buffer │
│ [10..11] padding │
├─────────────────────────────────────────────────┤
│ Buffer Headers (16 bytes each × numBuf) │
│ { tickCount, bufOffset, pad[2] } │
├─────────────────────────────────────────────────┤
│ Session Info YAML (at sessionInfoOffset) │
│ CameraInfo, DriverInfo, WeekendInfo, │
│ SessionInfo, etc. │
├─────────────────────────────────────────────────┤
│ Telemetry Variable Headers (144 bytes each) │
│ { type, offset, count, name, desc, unit } │
├─────────────────────────────────────────────────┤
│ Telemetry Data Buffers (triple-buffered) │
│ Per-tick snapshots of all telemetry variables │
└─────────────────────────────────────────────────┘
The entire structure is read through raw pointer arithmetic with koffi.decode(). There is no deserialization library — the extension reads int32 arrays, float32 arrays, and byte sequences directly from memory offsets.
Two Data Channels
The shared memory contains two fundamentally different kinds of data:
Channel 1: Session Info (YAML)
A large YAML string (~500KB) embedded in the memory-mapped file. It contains static or slowly-changing data:
- CameraInfo.Groups[] — All camera groups (Nose, Gearbox, Cockpit, TV1, TV2, TV3, Scenic, Blimp, Chase, Pit Lane, etc.)
- DriverInfo.Drivers[] — All drivers (carIdx, carNumber, userName, teamName, carName, carClassName)
- WeekendInfo — Track name, track type, series name
- SessionInfo.Sessions[] — Session types, lap counts, time limits
The YAML only changes when something structural happens: a new session loads, a driver joins or leaves, etc. The extension tracks the sessionInfoUpdate counter in the header and only re-parses when it changes:
function readSessionInfo(director: ExtensionAPI): SessionInfoResult | null {
const header = readFullHeader();
if (!header) return null;
// Skip re-parse if session info hasn't changed
if (header.sessionInfoUpdate === lastSessionInfoUpdate) {
return null; // No change
}
// Read raw bytes at the YAML offset
const rawBytes = koffi.decode(pBase, header.sessionInfoOffset, 'uint8', header.sessionInfoLen);
// ... find null terminator, decode to string, parse with js-yaml ...
}When the YAML changes, the extension extracts camera groups and drivers, caches them locally, and emits events:
director.emitEvent('iracing.cameraGroupsChanged', { groups: result.cameraGroups });
director.emitEvent('iracing.driversChanged', { drivers: result.drivers });Channel 2: Telemetry Variables (Binary, Triple-Buffered)
High-frequency numerical data updated every tick. iRacing uses triple-buffering — three data buffers are rotated so the reader always has a consistent snapshot. The extension finds the latest buffer by comparing tickCount values:
function getLatestBuffer(): { offset: number; tickCount: number } | null {
const BUF_HEADER_OFFSET = 48;
let bestTick = -1;
let bestOffset = -1;
for (let i = 0; i < header.numBuf; i++) {
const bhBase = BUF_HEADER_OFFSET + i * 16;
const bh = koffi.decode(pBase, bhBase, 'int32', 2);
if (bh[0] > bestTick) {
bestTick = bh[0];
bestOffset = bh[1];
}
}
return bestOffset >= 0 ? { offset: bestOffset, tickCount: bestTick } : null;
}Telemetry Variable Headers
Before reading telemetry data, the extension parses the variable header table. Each of iRacing's ~400 telemetry variables has a 144-byte descriptor:
Bytes 0-3: type (int32) — BOOL=1, INT=2, BITFIELD=3, FLOAT=4, DOUBLE=5
Bytes 4-7: offset (int32) — byte position within the data buffer
Bytes 8-11: count (int32) — array length (1 for scalars, 64 for per-car arrays)
Bytes 12-15: countAsTime (int32)
Bytes 16-47: name (char[32]) — e.g., "CarIdxPosition"
Bytes 48-111: desc (char[64]) — human description
Bytes 112-143: unit (char[32]) — e.g., "m/s", "rad", "%"
The extension parses these once on connect and caches them in a Map<string, VarHeader>. It re-parses only if numVars changes.
Key Telemetry Variables Used
The extension reads these variables to build the race state:
| Variable | Type | Count | Description |
|---|---|---|---|
CarIdxPosition | int32 | 64 | Overall race position per car index |
CarIdxClassPosition | int32 | 64 | Class position per car index |
CarIdxLapDistPct | float32 | 64 | Track position as 0.0–1.0 fraction |
CarIdxF2Time | float32 | 64 | Gap to leader in seconds |
CarIdxOnPitRoad | bool | 64 | Whether each car is on pit road |
CarIdxLapCompleted | int32 | 64 | Laps completed per car |
CarIdxLastLapTime | float32 | 64 | Last lap time per car |
CarIdxBestLapTime | float32 | 64 | Best lap time per car |
CamCarIdx | int32 | 1 | Which car iRacing's camera is focused on |
SessionFlags | bitfield | 1 | Flag state (green, yellow, red, checkered, etc.) |
SessionLapsRemainEx | int32 | 1 | Laps remaining |
SessionTimeRemain | float64 | 1 | Time remaining in seconds |
All per-car variables are indexed by carIdx (0–63). The driver roster from the session YAML maps car numbers and names to these indices.
Reading Telemetry Data
The variable readers use koffi.decode() with the header's type and offset information:
function readVarFloat(varName: string, bufOffset: number): number[] | null {
const vh = varHeaders.get(varName);
if (!vh || !pBase) return null;
const off = bufOffset + vh.offset;
if (vh.type === IRSDK_FLOAT) {
return Array.from(koffi.decode(pBase, off, 'float32', vh.count));
} else if (vh.type === IRSDK_DOUBLE) {
return Array.from(koffi.decode(pBase, off, 'float64', vh.count));
}
return null;
}
function readVarInt(varName: string, bufOffset: number): number[] | null {
const vh = varHeaders.get(varName);
if (!vh || !pBase) return null;
const off = bufOffset + vh.offset;
if (vh.type === IRSDK_INT || vh.type === IRSDK_BITFIELD) {
return Array.from(koffi.decode(pBase, off, 'int32', vh.count));
}
return null;
}These functions return raw arrays. A per-car variable like CarIdxPosition returns a 64-element int32 array where index N is the position of the car with carIdx N. A scalar like CamCarIdx returns a 1-element array.
Building RaceState
Every 250ms (4Hz), the telemetry polling loop calls buildRaceState(). This function merges cached driver info (from the session YAML) with live telemetry (from the data buffer):
interface RaceState {
cars: RaceCarState[];
focusedCarIdx: number;
sessionFlags: number;
sessionLapsRemain: number;
sessionTimeRemain: number;
leaderLap: number;
totalSessionLaps: number;
trackName: string;
}
interface RaceCarState {
carIdx: number;
carNumber: string;
driverName: string;
carClass: string;
position: number;
classPosition: number;
lapDistPct: number;
gapToLeader: number;
gapToCarAhead: number;
onPitRoad: boolean;
lapsCompleted: number;
lastLapTime: number;
bestLapTime: number;
}The build process:
- Get latest buffer — Find the triple-buffer with the highest tick count
- Read per-car arrays — Position, class position, lap %, gap to leader, pit status, lap times
- Read scalars — Focused car, flags, laps remaining, time remaining
- Merge with drivers — Loop through cached drivers, skip pace car and invalid entries, build
RaceCarStateper driver - Sort by position — Cars sorted by overall race position
- Compute deltas — Gap-to-car-ahead calculated from sorted leader gaps
The resulting RaceState is emitted as iracing.raceStateChanged. The DirectorOrchestrator forwards this to Race Control as part of the raceContext in sequence poll requests, where the AI uses it to make camera selection decisions.
Two Polling Loops
The extension runs two independent polling loops:
Session Data Poll (2Hz)
checkInterval = setInterval(() => {
checkConnection(director);
}, 2000);Checks whether iRacing is running (via FindWindowA), opens/closes shared memory as needed, and reads the session info YAML when the sessionInfoUpdate counter changes. This is the "slow" loop that detects session loads, driver changes, and camera group updates.
Telemetry Poll (4Hz)
telemetryInterval = setInterval(() => {
pollTelemetry(director);
}, 250);Reads live telemetry variables from the data buffer and builds the RaceState. Started when iRacing connects, stopped when it disconnects. At 250ms intervals, this is fast enough for race tower updates without being wasteful.
Camera Groups
iRacing exposes dozens of camera groups per track. The extension parses these from the session YAML:
interface CameraGroup {
groupNum: number;
groupName: string;
isScenic?: boolean;
}Camera groups vary by track but follow consistent naming patterns:
| Category | Groups | Description |
|---|---|---|
| On-car | Nose, Gearbox, Cockpit, Roll Bar, LF/LR/RF/RR Susp, Gyro | Mounted on the car, follow the focused driver |
| Broadcast | TV1, TV2, TV3, TV4, TV Mixed | Track-side cameras, the bread and butter of race broadcasts |
| Scenic | Scenic | Helicopter/aerial scenic shots |
| Special | Blimp, Chopper, Chase, Far Chase, Rear Chase | Specialized tracking cameras |
| Pit | Pit Lane, Pit Lane 2 | Fixed pit road cameras |
When the broadcast.showLiveCam intent fires, the iRacing extension translates the camera group name from the sequence step into the numeric groupNum, finds the target car's carIdx from the car number, and sends the switch command via broadcastMessage() through user32.dll's PostMessageA.
The Command Layer
Camera switches and replay control use Windows broadcast messages:
function broadcastMessage(cmd: number, var1: number, var2: number, var3: number = 0) {
if (!msgId || !PostMessageA) return;
const wParam = (cmd & 0xffff) | ((var1 & 0xffff) << 16);
const lParam = (var2 & 0xffff) | ((var3 & 0xffff) << 16);
PostMessageA(HWND_BROADCAST, msgId, wParam, lParam);
}The msgId is obtained by calling RegisterWindowMessageA('IRSDK_BROADCASTMSG') at startup. Commands are packed into wParam and lParam as 16-bit fields. The key commands:
| Command | Constant | Purpose |
|---|---|---|
| Camera switch by position | IRSDK_CAM_SWITCHPOS = 0 | Switch to car at position N |
| Camera switch by number | IRSDK_CAM_SWITCHNUM = 1 | Switch to car number N |
| Set replay speed | IRSDK_REPLAY_SETSPEED = 3 | Change playback speed |
| Set replay position | IRSDK_REPLAY_SETPOS = 4 | Jump to frame |
| Search replay | IRSDK_REPLAY_SEARCH = 5 | Search for events |
| Set replay state | IRSDK_REPLAY_SETSTATE = 6 | Play/pause |
Session Flags Bitfield
The SessionFlags telemetry variable is a bitfield. The extension and Race Control both decode it:
| Bit | Flag | Meaning |
|---|---|---|
0x00000001 | Checkered | Race is over |
0x00000002 | White | Final lap |
0x00000004 | Green | Racing |
0x00000008 | Yellow | Caution |
0x00000010 | Red | Session stopped |
Race Control uses these flags in the raceContext to influence sequence generation. A yellow flag triggers caution-specific sequences (pit lane cameras, replay of the incident). A green flag returns to battle-focused sequences.
Why This Matters for AI Context
The iRacing telemetry pipeline is the source of truth for everything the AI knows about the live race:
- RaceState.cars tells the AI who's in what position, who's pitting, who has the fastest lap
- sessionFlags tells the AI whether it's green, yellow, or checkered
- focusedCarIdx tells the AI which car the broadcast is currently watching
- gapToCarAhead reveals battles — cars within 1-2 seconds of each other
- Camera groups constrain what camera angles the AI can request
Without this pipeline, the AI would have no live race data. Every sequence it generates — every camera switch, every announcement, every timing decision — is grounded in the RaceState that flows through this path.