Interface control document
1. Identification
| Item | Value |
|---|---|
| Document | ICD-CMHTCGAZE-001 rev 1 |
| Product | HTC Vive gaze module (HEAT.HTCGazeModule, display name HTC Vive SDK) |
| Product versions covered | 32bbc15 |
| Peer | HTC Vive Streaming SDK (VS_PC_SDK.dll) and, through it, the Vive Business Streaming service and the headset eye tracker |
| Host | HEAT Capture, HEAT.Capture.ModuleSDK 2.0.5 |
| Runtime | .NET 10, Windows x64 |
| Data model | SimulationFrame carrying one Entity with a GazeComponent and a PupillometryDataComponent |
| Date | 2026-09-02 |
| Owner | VRAI Simulation |
At this revision the module does not use SRanipal. Eye data is obtained through HTC’s Vive
Streaming SDK, whose managed declarations are in HTC Vive/VS_PC_SDK.cs. Any earlier reference to
an SRanipal interface for this module is incorrect.
2. Scope
This document defines the native calls the module makes into VS_PC_SDK.dll, the callback
interface the native library uses to deliver status transitions and eye-data payloads, the payload
layout and its validity rules, and the gaze and pupillometry fields published to HEAT Capture.
The module is receive-only with respect to measurement data. Its only outbound native calls are library initialisation, release, callback registration, and start and stop of face tracking. It does not configure the headset, calibrate the eye tracker, or write anything back on behalf of HEAT Capture.
3. Interface inventory
| ID | Name | Direction | Transport | Peer |
|---|---|---|---|---|
| IF-CMHTCGAZE-01 | Vive Streaming SDK control | Module to native library | In-process P/Invoke, cdecl | VS_PC_SDK.dll |
| IF-CMHTCGAZE-02 | Status and eye-data callback | Native library to module | In-process function pointer callbacks | VS_PC_SDK.dll |
| IF-CMHTCGAZE-03 | Frame publication | Module to host | Module SDK publication path | HEAT Capture |
4. IF-CMHTCGAZE-01 Vive Streaming SDK control
4.1 Transport and addressing
| Item | Value |
|---|---|
| Binding | DllImport("VS_PC_SDK.dll"), CallingConvention.Cdecl |
| Addressing | None. The library is loaded from the module’s own output directory by name; there is no host, port, path or device identifier to configure. |
| Shipped native payload | VS_PC_SDK.dll, VSWPipeVarClient64U_MT.dll and RRServerManageAPI.dll, all marked CopyToOutputDirectory=Always in the project file and copied into the distributable by the packaging script |
| Process capability | The module declares process.unrestricted, so HEAT Capture launches it without the default restricted token |
4.2 Entry points used
| Entry point | Signature in module | Used for |
|---|---|---|
VS_SetCallbackFunction | void(VS_StatusUpdateCallback, VS_SettingChangeCallback, VS_LoggerCallback) | Registers the three callbacks. Called before VS_Init so initialisation failures are still reported. |
VS_Init | int() | Starts the SDK. 0 is success; any other value is a failure code reported verbatim in the log. |
VS_Version | int() | Read once, only to compose the success log line. |
VS_StartFaceTracking | bool() | Requests eye and facial tracking. The return value is discarded. |
VS_StopFaceTracking | bool() | Stops tracking. The return value is discarded. |
VS_Release | int() | Releases the SDK. The return value is discarded. |
VS_SDKVersion is declared in HTC Vive/VS_PC_SDK.cs but never called at this revision, so the
SDK version string is not logged or published.
4.3 Connection lifecycle
InitialiseModule runs the clean-up path first, so VS_StopFaceTracking (when a tracker was
already initialised) and VS_Release are called before VS_Init. It then assigns the three
callback delegates to fields, registers them, and calls VS_Init. On 0 it logs
Init ViveStreamingSDK <version> success. followed by a trace-level Waiting for server., and
allocates the sample buffer. On any other value it logs Init ViveStreamingSDK failed(<code>). and
returns false, which fails module startup.
StartCapture logs Starting HTC Gaze Data Session and calls the tracking-start helper. That call
is a no-op until the native library has reported the streaming state, because the helper returns
immediately while no client connection is recorded. Tracking therefore begins from the status
callback described in 5.2, not from capture start.
HandleCapture calls the tracking-start helper on every cycle. This is the retry mechanism: while
the headset is connected and tracking has not yet produced data, each cycle may start one
initialisation worker thread.
EndCapture runs the clean-up path only when the tracker initialisation flag is set. If tracking
never produced data during the session, VS_Release is not called at capture end; the release
happens on the next InitialiseModule instead.
The InitialiseModule and StartCapture calls are bounded by the SDK StartupTimeout of 5
seconds, which this module does not override. VS_Init is synchronous and runs inside that budget.
The per-cycle HandleCapture call has no timeout. See the
Module SDK ICD for the shared contract.
4.4 Tracking start, retry and stop
| Item | Value |
|---|---|
| Precondition | The status callback must have reported server state 0; otherwise the start helper returns without doing anything |
| Retry budget | MAX_RETRY_COUNT = 1000 start attempts per module instance. Once exceeded, the helper returns permanently until the module restarts. |
| Concurrency guard | At most one initialisation worker thread at a time; a new one is created only when the previous thread is null or no longer alive |
| Worker behaviour | Logs Starting face tracking., calls VS_StartFaceTracking, then polls the tracker initialisation flag in 100 ms sleeps for up to FACE_TRACKER_DATA_TIMEOUT_MS = 3000 ms |
| Worker outcome log | Face tracking started or Face tracking failed to start |
| Stop helper | Calls VS_StopFaceTracking only when the initialisation flag is set, then clears the flag, the new-sample flag and the retained payload string; always resets the retry counter; logs Stopping face tracking |
| Clean-up helper | Logs Performing clean up of ViveStreamingSDK, runs the stop helper, calls VS_Release, resets the retry counter |
The worker’s outcome log is derived from the first-payload flag rather than the tracker
initialisation flag, and that flag is never cleared for the lifetime of the module instance. After
the first payload of a session, every later worker logs Face tracking started whether or not the
attempt succeeded. Treat the outcome log as unreliable and use arriving frames as the evidence that
tracking is live.
4.5 Authentication
None. The interface is an in-process native call into a library shipped with the module. There is no credential, token, pairing step or peer identity check in either direction.
5. IF-CMHTCGAZE-02 Status and eye-data callback
5.1 Transport
The native library invokes three managed delegates registered through VS_SetCallbackFunction:
| Delegate | Parameters | Handled |
|---|---|---|
VS_StatusUpdateCallback | two LPWStr strings | Yes. The module treats the first as a status code and the second as its value. |
VS_SettingChangeCallback | one LPWStr string | Registered, body empty. Setting changes are ignored. |
VS_LoggerCallback | one LPWStr string | Registered, body empty. Native log lines are discarded, including the initialisation-failure detail the pre-VS_Init registration was meant to capture. |
Callbacks arrive on the native library’s own thread. In native-rate mode the module builds and publishes the frame inside the status callback, so publication happens on that thread rather than on the SDK capture cycle.
5.2 Status codes acted on
| Code | Constant | Handling |
|---|---|---|
2004 | HMD_NAME | Logs VS_PC_SDK: HMD: <value> when the value is non-empty |
2007 | VS_SERVER_VERSION | Logs Vive Streaming Server v<value> connected. |
2105 | VS_SERVER_STATE | Connection state machine, see below |
5001 | EYE_DATA | Eye-data payload |
2498 | none | Also handled as an eye-data payload, in the same case as 5001 |
| any other | none | Ignored silently |
Server state values, as documented in the module source:
| Value | Meaning | Module action |
|---|---|---|
0 | Streaming | On the first transition into this state: records the client connection, logs VS_PC_SDK: HMD connected., starts tracking, logs Starting face tracking. Repeat notifications are ignored. |
1 | No frame received from SteamVR | None. Not logged. |
2 | Standby, waiting for a VBS client to connect | When a client connection was recorded: clears it, calls VS_StopFaceTracking directly, logs VS_PC_SDK: HMD disconnected. |
3 | VBS driver not found | None. Not logged. |
4 | SteamVR not running | None. Not logged. |
States 1, 3 and 4 produce no log line and no state change. A workstation stuck in any of
them presents as a module that never delivers data.
The standby handler calls the native stop function directly instead of the stop helper, so the
tracker initialisation flag stays set. Because the start helper only starts a worker when that flag
is clear, tracking is never restarted when the server returns to state 0, and gaze delivery ends
for the remainder of the session. This is audit finding HTC-01 and it is a property of this
revision, not of the Vive Streaming SDK. Recovery requires restarting the module.
5.3 Wire format
An eye-data value is a single string of comma-separated decimal numbers. The module splits it on
,, parses each element positionally, and stores the result in a fresh 37-element float array.
Position defines meaning; there is no field name, length prefix or version marker in the payload.
| Index | Field | Published as |
|---|---|---|
| 0-2 | COMBINE_EYE_ORIGIN_X/Y/Z | gaze_origin |
| 3-5 | COMBINE_EYE_DIRECTION_X/Y/Z | gaze_direction |
| 6-8 | LEFT_EYE_ORIGIN_X/Y/Z | left_gaze_origin |
| 9-11 | LEFT_EYE_DIRECTION_X/Y/Z | left_gaze_direction |
| 12-14 | RIGHT_EYE_ORIGIN_X/Y/Z | right_gaze_origin |
| 15-17 | RIGHT_EYE_DIRECTION_X/Y/Z | right_gaze_direction |
| 18 | LEFT_EYE_OPENNESS | left_eyelid_openness |
| 19 | RIGHT_EYE_OPENNESS | right_eyelid_openness |
| 20 | LEFT_BLINK | left_eye_occluded, and blink when either blink flag is set |
| 21 | LEFT_WIDE | not published |
| 22 | RIGHT_BLINK | right_eye_occluded, and blink when either blink flag is set |
| 23 | RIGHT_WIDE | not published |
| 24 | LEFT_SQUEEZE | not published |
| 25 | RIGHT_SQUEEZE | not published |
| 26 | LEFT_DOWN | not published |
| 27 | RIGHT_DOWN | not published |
| 28 | LEFT_OUT | not published |
| 29 | RIGHT_IN | not published |
| 30 | LEFT_IN | not published |
| 31 | RIGHT_OUT | not published |
| 32 | LEFT_UP | not published |
| 33 | RIGHT_UP | not published |
| 34 | TIMESTAMP | not published; marked unused in the module source |
| 35 | LEFT_PUPIL_DIAMETER | left_eye_pupil_diameter, and half of pupil_diameter |
| 36 | RIGHT_PUPIL_DIAMETER | right_eye_pupil_diameter, and half of pupil_diameter |
The twelve facial-expression fields at indices 21 and 23 to 33 are parsed into memory and never read out. They are not reachable through HEAT Capture at this revision.
5.4 Sample validity
| Rule | Behaviour |
|---|---|
| Empty value string | The whole callback is ignored; no state change, no publication |
| First non-empty payload of the module instance | Discarded unread. It only sets the first-payload flag. The tracker initialisation flag is not set, so the initialisation worker does not see this payload as success. |
| Null, empty or whitespace element | Rejected; the element keeps the zero-initialised value of the fresh array |
| Non-numeric element | Rejected on the same terms |
NaN element | Rejected on the same terms |
| Decimal separator | Parsed with CultureInfo.InvariantCulture, so a comma-decimal workstation locale does not corrupt values |
| More than 37 elements | Not handled. The parse loop is bounded by the element count, not by the array length. |
The rejection path is documented in the source as keeping the previous value, but the array is
newly allocated for each payload, so a rejected element is published as 0 rather than as the last
good value. A consumer cannot distinguish a rejected element from a genuine zero.
There is no per-field validity or confidence flag in the payload and the module does not synthesise one. Blink state is the only quality-adjacent signal, and it is a blink flag rather than a tracking-validity flag.
5.5 Timing and cadence
| Mode | Cadence |
|---|---|
SendAtNativeRate = true (default) | One frame published per accepted eye-data callback, at whatever rate the native library delivers. The module neither defines nor caps this rate; the authority is HTC’s own documentation for the headset and the Vive Streaming SDK. |
SendAtNativeRate = false | HandleCapture returns a frame built from the most recently retained payload, once per SDK Interval, 1000 ms by default. If no new payload arrived, the previous sample is republished with a new capture timestamp. |
In native-rate mode HandleCapture returns null every cycle, which publishes nothing for that
cycle, so frames come only from the callback. Intermediate payloads are never buffered in either
mode: polled mode discards everything except the latest payload at the moment of the cycle.
6. IF-CMHTCGAZE-03 Frame publication and data model reference
Each publication is a SimulationFrame containing exactly one entity.
| Item | Value |
|---|---|
| Entity name | HTC_Data |
| Entity id | Guid.NewGuid().GetHashCode() masked to 31 bits, computed once per module instance. It is not derived from the entity name and is not stable across module restarts. |
| Components | GazeComponent and PupillometryDataComponent |
| Capture time | UTC Unix milliseconds taken when the frame is built. The payload’s own timestamp field is not used. |
6.1 Gaze component
| Published field | Source |
|---|---|
gaze_origin, gaze_direction | Combined-eye origin and direction, indices 0-5 |
left_gaze_origin, left_gaze_direction | Indices 6-11 |
right_gaze_origin, right_gaze_direction | Indices 12-17 |
left_eye_occluded | LEFT_BLINK compared against exactly 1 |
right_eye_occluded | RIGHT_BLINK compared against exactly 1 |
blink | Logical or of the two blink comparisons |
Occlusion is the blink flag, not an occlusion measurement. Because the comparison is against
exactly 1, any other non-zero blink encoding the native library might use would read as
not blinking.
IPD, combined_eye_position, the per-eye position fields, every gaze quality field,
has_glasses, gaze_point and gaze_screen_point are left at their protobuf defaults.
6.2 Pupillometry component
| Published field | Source |
|---|---|
left_eyelid_openness | Index 18 |
right_eyelid_openness | Index 19 |
left_eye_pupil_diameter | Index 35 |
right_eye_pupil_diameter | Index 36 |
pupil_diameter | Arithmetic mean of indices 35 and 36, computed by the module |
The combined eyelid_openness field, every quality field and other_pupil_data are left at their
protobuf defaults. The mean pupil diameter is unweighted and does not exclude an eye that is
blinking or untracked, so during a one-eyed blink it is roughly half the open eye’s diameter.
6.3 Packet envelope
The SDK wraps the frame as a DataPacket with TimestampUnixMs set to UTC now and the frame
packed into Payload. It is published with NodeId when OverrideNodeId is set, otherwise -1.
No EntityEvent is published by this module.
7. Units and conventions
| Concept | Convention |
|---|---|
| Gaze origin and direction | Three-component float vectors, passed through unmodified. The module applies no normalisation, scaling, axis remap or handedness conversion, so the coordinate frame is exactly the one the Vive Streaming SDK emits. |
| Eyelid openness | Native scalar, passed through unmodified. No range is asserted or enforced. |
| Pupil diameter | Native scalar, passed through unmodified. The unit is whatever the SDK reports; the module neither converts nor labels it. |
| Blink | Boolean, true only when the native field equals 1 |
| Time base | Host wall clock, UTC Unix milliseconds, applied when the frame is built. No device timestamp is carried, and no latency compensation is applied for callback delivery or for the republication delay in polled mode. |
No field is range-checked or clamped. The authority for the meaning, units and coordinate frame of every value is HTC’s own Vive Streaming SDK documentation.
8. Error behaviour
| Condition | Detection | Log | Effect |
|---|---|---|---|
VS_Init returns non-zero | Return code | Init ViveStreamingSDK failed(<code>)., error level | Module start fails |
| Native library or its dependencies missing | .NET fails to resolve the DllImport | Host-level exception, no module-specific message | Module start fails |
Server state 1, 3 or 4 | Status callback | None | Silent wait; no data and no diagnostic |
Standby transition, server state 2 | Status callback | VS_PC_SDK: HMD disconnected. | Tracking stops and, per HTC-01, does not restart for the session |
| Tracking does not produce data within 3000 ms | Initialisation worker poll | Face tracking started or Face tracking failed to start, the outcome string being unreliable per 4.4 | Next capture cycle starts another worker until the 1000-attempt budget is exhausted |
| Retry budget exhausted | Retry counter | None | The start helper returns silently for the rest of the module instance |
| Worker thread cannot be created | Exception | InitFaceTrackerWorker exception <message>, error level | That attempt is lost; the retry counter is not incremented for it |
| Unparsable payload element | Per-element parse | None | Element published as 0 |
| Payload with more than 37 elements | Not detected | None | IndexOutOfRangeException from the parse loop, propagating on the native callback thread |
Exception thrown from HandleCapture | SDK | Error in module | The capture loop continues |
The frame builder contains a no-data guard that logs No Gaze data available and returns no frame.
It is unreachable at this revision, because the sample buffer is allocated during initialisation
and replaced, never cleared to null. An operator will not see that warning.
Loss of the eye tracker while the server stays in state 0 produces no log line and no state
change. In native-rate mode, frames stop. In polled mode, the last payload is republished
indefinitely with fresh capture timestamps, which a consumer cannot distinguish from a wearer
holding a fixed gaze. Correlate with payload values changing rather than with frame arrival.
9. Configuration affecting the interface
| Setting | Section | Default | Interface effect |
|---|---|---|---|
SendAtNativeRate | HEAT.HTCGazeModule | true | Selects callback-driven publication or per-cycle polled publication, as in 5.5 |
Enabled | HEAT.HTCGazeModule | true | Whether the module runs at all |
Interval | HEAT.HTCGazeModule | 1000 ms | Publication cadence in polled mode; in native-rate mode it only sets how often a tracking-start retry can occur |
OverrideNodeId, NodeId | HEAT.HTCGazeModule | false, 0 | Node attribution on the published packet |
Timeout | HEAT.HTCGazeModule | 30 s | Not read by this module |
SendAtNativeRate is read from the settings monitor at each decision point, so it takes effect
without restarting the module. The settings identity registered for localisation is HtcVive, with
the label key module.HtcVive.sendAtNativeRate.label; the configuration section name is
HEAT.HTCGazeModule. See
Manage capture modules for the generic settings.
The retry budget, the 3000 ms initialisation timeout, the 100 ms poll interval and the status codes are compile-time constants and are not configurable.
10. Compatibility and versioning
There is no version negotiation on either side of the native interface. VS_Init is called without
a requested version, and the value returned by VS_Version is only logged. The module therefore
accepts whatever VS_PC_SDK.dll is present in its own directory, and the shipped copy is the only
version the module has been built against.
The eye-data payload is positional, unversioned and unlength-checked. A Vive Streaming SDK release that inserts, removes or reorders a field silently misattributes every value after the change point; a release that appends fields beyond index 36 raises an index-out-of-range exception on the callback thread. Revalidate the index table in 5.3 against HTC’s documentation before adopting a new SDK build.
Prerequisites for the interface to come up at all: Windows x64, .NET 10, SteamVR running and streaming, and HTC’s Vive Business Streaming driver installed. The module connects to the VBS service, not to the headset directly.
The package manifest produced by the module’s own packaging script names the module
HEAT.HTCGazeModule with display name HTC Vive SDK. The central module registry records the same
package with a trailing space, audit finding REG-03. Where a deployment matches packages by exact
name, treat HEAT.HTCGazeModule as the authoritative identity and trim whitespace before
comparing, otherwise the registry entry and the installed package can be seen as two products.
11. Security considerations
The interface is entirely local: an in-process native call into a bundled DLL, which in turn talks to the Vive Business Streaming service on the same workstation. There is no network endpoint, credential or encryption at this boundary, and the module performs no peer identity check. Because the DLL is resolved by name from the module’s own directory, write access to that directory is equivalent to code execution inside the module process; treat the installation directory as integrity-protected.
The module declares process.unrestricted, which is an explicit opt-out from HEAT Capture’s
default per-module process restriction and widens what the module process can do relative to other
modules. Gaze, blink and pupil diameter are biometric personal data and are directly identifying in
combination with session metadata: restrict access to the workstation and to captured sessions
according to the deployment’s own data-handling policy.
12. Conformance
- With SteamVR streaming and the VBS driver installed, start a capture session. Confirm the log
records
Init ViveStreamingSDK <version> success., thenVive Streaming Server v<value> connected., thenVS_PC_SDK: HMD connected.. - Confirm one entity named
HTC_Dataappears in the captured data with both aGazeComponentand aPupillometryDataComponent. - With the headset worn, confirm
gaze_directionand the per-eye directions change across successive frames, and thatleft_eyelid_opennessandright_eyelid_opennessare non-zero. - Blink deliberately and confirm
blink,left_eye_occludedandright_eye_occludedassert. - Leave
SendAtNativeRateattrueand confirm frames arrive faster than the configuredInterval. Set it tofalseand confirm the frame rate drops to one perIntervaland that a static payload is republished with advancing capture timestamps. - Verify HTC-01 before relying on the session: put the streaming server into standby, confirm
VS_PC_SDK: HMD disconnected., then return to streaming and check whether any new frame arrives. Until HTC-01 is fixed, no frame will arrive and the capture session must be restarted. Do not run a session where a standby transition is expected. - Confirm that a reading of
0is not accepted as a measurement without corroboration, given the rejected-element behaviour in 5.4.
13. Revision history
| Rev | Date | Product version | Change |
|---|---|---|---|
| 1 | 2026-09-02 | 32bbc15 | Initial issue. |