Skip to Content
This documentation is provided with the HEAT environment and describes this HEAT instance.

Interface control document

1. Identification

ItemValue
DocumentICD-CMVARJO-001 rev 2
ProductVarjo gaze module (Varjo SDK in the module registry, package HEAT.VarjoGazeModule)
Product versions coveredManaged module b2618c7, native bridge dbbef2a
Managed repositoryhttps://github.com/VRAIie/cm-varjo-gaze.git
Bridge repositoryhttps://github.com/VRAIie/CaptureApp-VarjoGazeBridge.git
PeerVarjoGazeBridge.dll, and through it the Varjo Base runtime and compositor
HostHEAT Capture, HEAT.Capture.ModuleSDK 2.0.5
Data modelHEAT ProtoLib SimulationFrame, serialized by the bridge and reparsed by the module
Target architectureWindows x64
Date2026-09-02
OwnerVRAI Simulation

This document is the contract required by audit findings VARJO-01 and VBRIDGE-01: before it, neither the managed module nor the bridge had an ICD specifying exports, buffer ownership, lifecycle, framing or error behaviour. The bridge has no ICD of its own by design; it is documented here, through the module that owns and ships it.

2. Scope

The product spans two repositories and one process. The managed module is loaded by HEAT Capture, P/Invokes into a native bridge DLL that is vendored inside the module package, and the bridge holds the Varjo session, drives the compositor frame loop, reads gaze, encodes per-eye video and serializes complete SimulationFrame messages. The managed module reparses those bytes and hands them to the Module SDK.

This document defines the managed-to-native ABI, the framing and ownership rules of the frame handoff, the lifecycle across both sides, the published data, timing, and error behaviour on both sides. It does not define the Varjo SDK itself; the authority for Varjo semantics is Varjo’s own documentation, and the SDK headers vendored in the bridge repository under varjo-sdk/include/.

3. Interface inventory

IDNameDirectionTransportPeer
IF-CMVARJO-01Bridge control ABIModule to bridgeExported C functions in VarjoGazeBridge.dll, cdeclNative bridge
IF-CMVARJO-02Serialized frame handoffBridge to moduleCaller-supplied byte buffer drained from a bounded in-process FIFONative bridge
IF-CMVARJO-03Varjo runtime sessionBridge to runtimeVarjo SDK session, compositor layers and mirror swap chainVarjo Base
IF-CMVARJO-04Frame publicationModule to hostModule SDK publication pathHEAT Capture
IF-CMVARJO-05Bridge diagnostic logBridge to filesystemAppended text fileWorkstation

4. IF-CMVARJO-01 Bridge control ABI

4.1 Exports

The bridge exports exactly five functions, declared extern "C" with __declspec(dllexport) and no explicit calling-convention specifier (VarjoGazeBridge.h:6-12). There is one integer calling convention on Windows x64, and the managed side declares CallingConvention.Cdecl (VarjoInterop.cs:17-30).

ExportNative signatureManaged declaration
Initializeint Initialize(bool enableVideoCapture, int outputWidth, int outputHeight)int Initialize(bool, int, int)
Shutdownvoid Shutdown()void Shutdown()
GetFrameint GetFrame(uint8_t* buffer, int bufferSize)int GetFrame(IntPtr buffer, int bufferSize)
GetGazeTrackingStatusint GetGazeTrackingStatus()int GetGazeTrackingStatus()
RequestGazeCalibrationvoid RequestGazeCalibration(GazeTracking::CalibrationType)void RequestGazeCalibration(CalibrationType)

The DLL is loaded by plain module name, VarjoGazeBridge.dll (VarjoInterop.cs:15), so the default Windows loader search applies. There is no export that reports a bridge version, build identity or ABI revision, and no handshake of any kind. See section 12.

outputWidth and outputHeight are accepted and ignored. The bridge derives its encode resolution from the Varjo view description instead (VarjoGazeBridge.cpp:696-697, :110-114, :184-193), and the managed side passes 0, 0 deliberately (VarjoGazeDataModule.cs:38-45).

4.2 Return values

ExportReturn
Initialize1 on success, 0 on failure. Note the inversion relative to a C exit code: the managed side treats 0 as failure (VarjoGazeDataModule.cs:47-52)
GetFrameByte count written to the caller’s buffer, or 0 for “nothing delivered”. Never negative
GetGazeTrackingStatus-1 bridge not initialised, 1 gaze not allowed, 2 no HMD connected, 3 calibrating, 4 calibrated, 5 not calibrated (VarjoGazeBridge.cpp:833-842)

The GetGazeTrackingStatus codes are the bridge’s own numbering, not Varjo’s; they are derived from four Varjo session properties in that fixed precedence order. The managed module logs the same mapping verbatim at session start (VarjoGazeDataModule.cs:54-55).

4.3 Buffer ownership

No bridge-allocated memory ever crosses the boundary, and there is no free or release function to call. Ownership is one-directional and total.

ItemRule
GetFrame bufferAllocated and owned by the managed caller. The bridge copies into it and does not retain the pointer beyond the call (FrameQueue.cpp:21-34)
Buffer allocationOne 4 MiB byte[] per capture session, allocated in StartCapture and released in EndCapture (VarjoGazeDataModule.cs:24-28, :63, :132-133)
PinningThe array is pinned only for the duration of each call, with fixed inside unsafe (VarjoGazeDataModule.cs:85-90)
CallbacksNone. The bridge never calls back into managed code, so no delegate lifetime has to be kept alive
StringsNone cross the boundary
Bridge-side frame storageOwned by the bridge’s FIFO, freed when an entry is popped or when Shutdown clears the queue (VarjoGazeBridge.cpp:824-825)

4.4 Thread affinity

ExportAffinity
InitializeAny thread. It creates the Varjo session, the D3D11 device and the transparent swap chain on the caller’s thread, then starts the bridge’s own capture thread and returns (VarjoGazeBridge.cpp:698-786)
GetFrameAny thread. The FIFO is mutex-guarded (FrameQueue.cpp:21-22)
ShutdownAny thread, but it blocks: it clears the run flag and joins the capture thread (VarjoGazeBridge.cpp:790-792)
GetGazeTrackingStatus, RequestGazeCalibrationCalled on the caller’s thread and touch the Varjo session directly (:833-848) while the capture thread is also using that session

Video setup, encoder activation, submit, drain and teardown all happen on the bridge’s capture thread, which establishes an MTA apartment for itself because hardware H.264 encoder MFTs fail to activate from an STA thread (VarjoGazeBridge.cpp:250-259, :96-101, :235-248). That is why Initialize defers video setup rather than doing it inline: its caller is a GUI thread.

The two session-touching queries in the last row are issued from the Module SDK’s threads while the capture thread is inside the Varjo frame loop. The Varjo SDK headers vendored in this revision state no thread-safety contract for a session handle, so this document cannot assert that the concurrent use is safe. It is recorded here as a property of the audited revision.

4.5 Enumeration mapping

CalibrationType is passed by value as a 32-bit enum. The two sides do not declare the same set.

Managed valueNumericNative equivalent
Fast0GazeTracking::CalibrationType::FAST
OneDot1GazeTracking::CalibrationType::ONE_DOT
None2No native equivalent exists (GazeTracking.h:22-25)

None is a managed-only sentinel: the module skips the call entirely rather than passing 2 across the boundary (VarjoGazeDataModule.cs:56-61). A caller that did pass 2 would land on the native default: branch and get FAST (GazeTracking.cpp:36-42), silently. Treat None as “do not call this export”, not as a value the ABI accepts.

4.6 Authentication

None, on either interface. The ABI is an in-process function call. Any code in the HEAT Capture process can call the same exports, and the bridge accepts whichever Varjo session the local runtime hands it. Video capture requires a Varjo Pro licence, but neither side checks for one in code; enforcement, if any, is inside Varjo’s runtime.

5. IF-CMVARJO-02 Serialized frame handoff

5.1 Framing

There is no envelope, length prefix, sequence number or delimiter. A single GetFrame call delivers exactly one complete, self-delimiting serialized SimulationFrame message, and its return value is that message’s length. The unit of transfer is one message, so the managed side never has to reassemble or scan for a boundary.

The bytes are produced natively by the HEAT ProtoLib C API through HEAT::Proto::Builders::SimulationFrameBuilder::serialize (VarjoGazeBridge.cpp:326-331, :661-667) and reparsed by SimulationFrame.Parser.ParseFrom on the managed side (VarjoGazeDataModule.cs:95). Both sides must therefore be built against compatible ProtoLib schemas; see section 12.

5.2 Queue semantics

ItemValue
StructureBounded FIFO of complete serialized frames, mutex-guarded (FrameQueue.h:10-32)
Capacity90 entries, chosen as roughly one second at the ~90 Hz headset cadence (VarjoGazeBridge.cpp:70-71)
ProducersThe capture thread, for both gaze frames and per-eye video frames
OverflowThe oldest entry is dropped, and both encoders are forced to produce a fresh IDR keyframe (FrameQueue.cpp:11-18, VarjoGazeBridge.cpp:667-672)
DrainGetFrame pops the single oldest entry per call and returns its size (FrameQueue.cpp:21-34)
ClearedOn Shutdown (VarjoGazeBridge.cpp:824-825)

Overflow is a data-loss event with a visible consequence: the discarded entry may be a gaze frame or a video access unit, and the forced keyframe means the affected eye’s video shows a discontinuity rather than a delay.

5.3 Undersized buffer

If the oldest queued frame is larger than the buffer the caller supplied, popInto returns 0 and leaves that frame at the head of the queue (FrameQueue.cpp:26-28). The return value is therefore ambiguous by construction: 0 means either “queue empty” or “head entry does not fit”, and the caller cannot tell which.

The managed drain loop treats any non-positive return as end-of-queue and stops (VarjoGazeDataModule.cs:92). An oversized head entry consequently blocks the queue permanently for the rest of the session: nothing behind it can ever be delivered, no diagnostic is emitted on either side, and the module’s own parse-error counter does not move. The 4 MiB managed buffer is sized against an IDR access unit at the bridge’s ~8 Mbit/s per-eye CBR budget (VarjoGazeDataModule.cs:24-28, VarjoGazeBridge.cpp:51-53), so the condition is not expected in normal operation, but nothing in either side enforces the bound or detects it being exceeded.

5.4 Drain contract

The managed module drains the whole queue on every capture cycle rather than taking one frame per cycle (VarjoGazeDataModule.cs:78-118). Of the frames drained in one cycle, all but the last are published directly through the SDK’s Publish, and the last is returned from HandleCapture so the base class publishes it on the normal path (:112-115, :127). Every successfully parsed frame is published exactly once; nothing is coalesced or discarded on the managed side.

Draining a full queue in one cycle is required, not an optimisation. A single GetFrame per cycle cannot keep pace with the bridge’s producer rate, and the queue would sit at its overflow threshold permanently.

6. IF-CMVARJO-03 Varjo runtime session

6.1 Session setup

Initialize performs, in order: varjo_SessionInit; a session priority of 1 so the compositor layers the bridge above a target application at default priority 0; gaze tracking initialisation with the standard output filter and the maximum supported output frequency; adapter selection by matching the Varjo-reported LUID; D3D11 device creation with video and BGRA support; D3D11 multithread protection; discovery of the left and right context view indices; and creation of a 4x4 transparent swap chain (VarjoGazeBridge.cpp:698-773).

The transparent swap chain is submitted as a render layer on every tick for one reason: it keeps the compositor servicing the bridge’s session (:765, :637-679). It carries no visible content.

When video capture is enabled, the capture thread additionally starts Media Foundation, reads the native view size, creates a double-width mirror swap chain, registers both eyes’ viewports with varjo_SetMirrorConfig, creates two per-eye encode textures, and initialises two encoders (:104-232).

6.2 What the video actually is

The video is the compositor mirror: the fully composited scene as presented to each eye, captured through varjo_SetMirrorConfig (VarjoGazeBridge.cpp:128-149). It is not imagery of the user’s eyes. The bridge does not include Varjo_datastream.h and opens no Varjo data stream, so no eye-camera, colour-camera or environment-cubemap stream is read at this revision. The stream names eye.left and eye.right denote which eye’s view was rendered, not which camera was read.

6.3 Encoder configuration

ItemValue
CodecH.264, one independent stream per eye
BackendNVENC on NVIDIA adapters, otherwise a Media Foundation H.264 MFT; NVENC init failure falls back to the MFT (VarjoGazeBridge.cpp:194-210)
Bitrate8,000,000 bit/s per eye, constant bitrate (:51, MftEyeEncoder.cpp:97-99, NvencEyeEncoder.cpp:117-118)
GOP90 frames (:52, MftEyeEncoder.cpp:100, NvencEyeEncoder.cpp:114)
B-framesNone (MftEyeEncoder.cpp:103-104, NvencEyeEncoder.cpp:115)
Declared frame rate90 (VarjoGazeBridge.cpp:53)
Encode resolutionNative view size scaled so the larger dimension is at most 1088, then floored to a multiple of 16, minimum 16 (:184-193)
Colour pathRGBA mirror texture colour-converted to NV12 by the D3D11 video processor before encoding
MFT profileH.264 Baseline (MftEyeEncoder.cpp:72)
NVENC packagingIn-band SPS/PPS repeated on every IDR (NvencEyeEncoder.cpp:126-127)

The MFT backend sets no explicit NAL-packaging attribute, so its output packaging is whatever the selected Media Foundation H.264 encoder produces by default. A consumer must not assume the two backends are byte-identical in their parameter-set placement.

7. IF-CMVARJO-04 Frame publication and data model reference

7.1 Entity

Every frame the bridge produces carries exactly one entity, and the managed module republishes it untouched. It does not rebuild the frame, does not assign an entity id, and does not reinterpret any field.

ItemValue
Entity id0x56415246, a fixed compile-time constant (VarjoGazeBridge.cpp:73)
Entity nameVarjo_Data (:316, :622)

7.2 Frame kinds

The bridge emits two independent kinds of frame into the same queue. They are distinguished only by which components the entity carries.

KindEmittedComponents
Gaze frameOnce per capture tick, when the tick has valid gaze (:433, :264-266)SpatialComponent, GazeComponent, PupillometryDataComponent
Video frameOnce per eye per completed H.264 access unit (:583-679)SpatialComponent, StreamComponent, plus GazeComponent and PupillometryDataComponent when that tick’s snapshot had valid gaze

Gaze and video are fully decoupled producers. Gaze is published whether or not video capture is running, and is not gated on an encoder or on a metadata match.

A video frame’s gaze copy is the same sample already delivered on that tick’s gaze frame, stashed per tick so the encoder-delayed video carries temporally correct gaze (:479-497, :625-656). It is not a second, independent reading. Do not average or double-count the two.

7.3 Published fields

ComponentFieldSource
SpatialComponentpose.positionTranslation column of varjo_FrameGetPose(varjo_PoseType_Center) (:346-350)
SpatialComponentpose.orientationQuaternion derived from the same matrix, with the Y term negated at emission (:352-380, :433)
GazeComponentipdvarjo_EyeMeasurements.interPupillaryDistanceInMM (:393)
GazeComponentgaze_origin, left_gaze_origin, right_gaze_originvarjo_Gaze.gaze/leftEye/rightEye.origin (:394-403)
GazeComponentgaze_direction, left_gaze_direction, right_gaze_directionThe corresponding forward rays with Z negated (:404-411)
GazeComponentleft_eye_occluded, right_eye_occludedleftStatus/rightStatus equal to varjo_GazeEyeStatus_Invalid (:412-413)
GazeComponentblinkleft_eye_occluded OR right_eye_occluded (:414)
PupillometryDataComponentleft_eyelid_openness, right_eyelid_opennessleftEyeOpenness, rightEyeOpenness (:415-416)
PupillometryDataComponentleft_eye_pupil_diameter, right_eye_pupil_diameterleftPupilDiameterInMM, rightPupilDiameterInMM (:417-418)
StreamComponentOne VideoSource per frame: name (eye.left or eye.right), codec (H264), width, height, frame_rate, video_dataEncode dimensions and the drained access unit (:604-609)
SimulationFramecapture_time.timestamp_msHost system_clock milliseconds taken on the capture tick (:426-428, :325-328, :658-660)

No other field of these components is populated. Notably absent: every *_quality field, has_glasses, gaze_point, gaze_screen_point, the combined eyelid_openness and pupil_diameter fields, and other_pupil_data. A consumer must not read an absent field as a measured zero.

left_eye_occluded and right_eye_occluded come from Varjo’s per-eye gaze validity status, not from an eyelid-openness threshold, and blink is derived from them. That is the reverse of the derivation order used by some other eye-tracking modules in this catalogue.

7.4 Publication envelope

Each published frame is wrapped by the Module SDK as a DataPacket whose TimestampUnixMs is UTC now at publication and whose Payload is the packed SimulationFrame, with NodeId when OverrideNodeId is set and -1 otherwise. See the SDK contract. A frame therefore carries two timestamps with different meanings: the bridge’s capture tick inside capture_time, and the host’s publication instant on the packet. They are not interchangeable, and the gap between them includes the queue dwell time, the encoder delay for video frames, and one poll interval.

8. Timing and cadence

ItemValue
Bridge tick pacingvarjo_WaitSync, so the compositor’s frame cadence (VarjoGazeBridge.cpp:337)
Assumed tick rate~90 Hz, hard-coded into the queue capacity, GOP, declared frame rate and log intervals (:52-53, :70)
Gaze sampling requestedStandard output filter, maximum supported output frequency (:706, GazeTracking.cpp:15-31)
Gaze samples used per tickOne. getGazeDataWithEyeMeasurements returns every sample buffered since the previous read, and the bridge keeps only the newest (VarjoGazeBridge.cpp:388-391)
Video access-unit latencyNVENC drains with a fixed output delay of 3 frames (NvencEyeEncoder.cpp:212, :280-293)
Forced keyframeWall-clock, whenever 1000 ms have elapsed since the last one, in addition to the frame-count GOP (VarjoGazeBridge.cpp:466-472)
Managed poll intervalInterval, default 8 ms for this module against the SDK default of 1000 ms (Config/VarjoGazeSettings.cs:12)
Managed diagnosticsA [VarjoDiag] line every 120 capture cycles (VarjoGazeDataModule.cs:120-126)
Bridge heartbeatA log line every 90 ticks (VarjoGazeBridge.cpp:341-343)

Because only the newest gaze sample per tick survives, the effective published gaze rate is the compositor tick rate, not the requested maximum sampling rate. When the runtime samples faster than the compositor ticks, the intermediate samples are discarded silently and no counter records the loss. Anyone using this module for saccade-scale analysis needs to know that the published series is decimated, not raw.

9. Connection lifecycle

StepManaged sideBridge side
InitialiseInitialiseModule logs and returns true; it touches no native code (VarjoGazeDataModule.cs:31-35)Nothing
StartStartCapture reads EnableVideoCapture, calls Initialize, logs the returned status, logs the gaze tracking status, optionally requests calibration, allocates the 4 MiB buffer, resets counters, sets the capturing flag (:37-70)Initialize builds the session, device and transparent swap chain, then starts the capture thread; video setup runs on that thread (VarjoGazeBridge.cpp:694-786)
RunHandleCapture drains the queue every Interval (:72-127)The capture thread runs sync, pose, gaze, begin frame, mirror copy and submit, end frame, and drain, once per tick (:334-685)
StopEndCapture clears the capturing flag, releases the buffer, calls Shutdown, logs Varjo session ended (:129-137)Shutdown clears the run flag, joins the capture thread, resets gaze tracking, resets mirror config, frees both swap chains, shuts down the session, releases the D3D objects, clears both queues (:788-826)

Both InitialiseModule and StartCapture are bounded by the SDK’s StartupTimeout, 5 seconds by default, which this module does not override. HandleCapture has no timeout.

There is no reconnect logic on either side. Nothing retries varjo_SessionInit, and nothing detects a session that dies mid-capture: the capture thread keeps calling into a session the runtime may have abandoned. Recovery is a capture restart, which is what makes the ordering below matter.

9.1 Failed start does not tear down

Initialize has four failure returns, and three of them occur after the Varjo session already exists: no matching DXGI adapter (VarjoGazeBridge.cpp:720), D3D11 device creation failure (:736-737), and transparent swap-chain creation failure (:773). None of them undoes the session, the gaze tracking object or any D3D object created up to that point.

On the managed side a 0 return logs Varjo Initialize failed and returns false without calling Shutdown (VarjoGazeDataModule.cs:47-52), and the SDK does not call EndCapture when StartCapture returns false (BaseDataModule.cs:123-128). The native session therefore stays alive with no owner. A subsequent start calls Initialize again, which overwrites the session handle with a fresh varjo_SessionInit (VarjoGazeBridge.cpp:698), so the previous session and its D3D objects are leaked for the lifetime of the process. Restart HEAT Capture rather than only the capture session after a failed Varjo start.

9.2 Video failure is silent to the managed side

Video setup runs on the capture thread after Initialize has already returned 1. Every failure in that path (Media Foundation startup, mirror swap chain, encode textures, DXGI device manager, either encoder) leaves g_mirrorEnabled false and is reported only to the bridge’s own log file (VarjoGazeBridge.cpp:104-232). Gaze continues normally and no video frame is ever produced. The managed module is never told, its videoEnabled debug indicator reflects the setting rather than the outcome (VarjoGazeDataModule.cs:174), and the only evidence is IF-CMVARJO-05.

10. Error behaviour

ConditionBridge behaviourManaged behaviour
varjo_SessionInit failsInitialize returns 0 immediatelyLogs Varjo Initialize failed, returns false, module does not start
Adapter, D3D or swap-chain failure during InitializeReturns 0 leaving the session and gaze tracking aliveSame as above, and Shutdown is never called: see 9.1
Any video-path setup failureLogged to the bridge log, video stays off, gaze unaffectedNot notified: see 9.2
No valid gaze on a tickNo gaze frame is pushed for that tickNothing published for that tick from that source
Encoder submit returns false for an eyeThat eye’s frame is dropped for the tick; pending metadata is pushed only if at least one eye submittedNot notified
Access unit drained with no matching tick metadataDropped rather than emitted with a wrong timestamp, and logged (VarjoGazeBridge.cpp:589-595)Not notified
Frame queue at capacity on pushOldest entry dropped, both encoders forced to a fresh IDRNot notified; sees a gap and a keyframe
Head queue entry larger than the supplied bufferReturns 0 and leaves the entry queuedTreats 0 as end of queue; the queue stalls permanently: see 5.3
Malformed or truncated frame bytesNot detectedIncrements the parse-error counter, logs Failed to parse Varjo SimulationFrame ({Written} bytes); skipping, and continues draining the rest of the cycle (VarjoGazeDataModule.cs:98-105)
Exception escaping HandleCaptureNot applicableSDK logs Error in module and the capture loop continues
Shutdown while the capture thread is inside varjo_WaitSyncThe join waits for the current wait to return, so Shutdown blocks for up to one frame periodEndCapture blocks for the same period
GetFrame or GetGazeTrackingStatus after ShutdownReturns 0 and -1 respectively, without crashingReports Stopped in the debug view

A single malformed frame is deliberately not fatal to the drain cycle. Aborting the drain would discard every other frame queued that cycle, gaze included.

11. Units and conventions

ConceptConvention
Gaze originsMetres, in the Varjo runtime’s own coordinate system, narrowed from double to float without transformation
Gaze directionsUnit rays from the same source with the Z component negated, a deliberate axis conversion applied identically to the combined and both per-eye vectors
Head pose positionMetres, taken from the pose matrix translation without sign change
Head pose orientationQuaternion, with the Y term negated at emission
IPDMillimetres, as the Varjo runtime reports it. The ProtoLib ipd field is specified in metres
Pupil diameterMillimetres, as the Varjo runtime reports it
Eyelid opennessVarjo’s ratio, 1 fully open and 0 fully closed
Occlusion and blinkBooleans derived from Varjo per-eye validity status, not from a measured threshold
Video dimensionsPixels, the encode resolution, not the native view resolution
Frame rate fieldThe bridge’s declared constant 90, not a measured rate
Time baseHost system_clock Unix milliseconds, sampled on the capture tick. Varjo’s own nanosecond captureTime on each gaze sample is read but not carried

Two consequences follow from that table and both belong to the audited revision rather than to the Varjo protocol. ipd is published in millimetres into a field the ProtoLib schema documents as metres, so a consumer reading it as metres is off by a factor of 1000. And because direction vectors are Z-negated while origins and the head-pose position are not, an origin and its direction are not expressed in a single consistent handedness; a consumer that builds a world-space ray from the pair must decide which convention to trust rather than assume they agree.

Beyond the Z negation and the narrowing to float, no value is scaled, filtered, validated or range-checked. The authority for their meaning is Varjo’s own documentation.

12. Configuration affecting the interfaces

SettingDefaultInterface effect
CalibrationTypeFastWhether RequestGazeCalibration is called at all, and which native calibration parameter is requested. None suppresses the call
EnableVideoCapturetruePassed as the enableVideoCapture argument. When false the bridge starts no Media Foundation, creates no mirror swap chain and no encoders, and IF-CMVARJO-02 carries gaze frames only
Interval8 msHow often the queue is drained. This module overrides the SDK default of 1000 ms because at 1000 ms the queue sits at its overflow threshold every cycle
EnabledtrueWhether the module, and therefore the bridge, runs at all

Calibration is requested at every capture start unless CalibrationType is None; the bridge always passes the automatic headset-alignment guidance mode and does not expose a choice (VarjoGazeBridge.cpp:844-848). See Manage capture modules for the generic settings.

13. Compatibility and versioning

The bridge and the managed module must be deployed as a matched pair. This is a hard requirement, and nothing enforces it.

There is no version export, no ABI revision constant and no handshake. A mismatched pair does not fail at load: it fails as a wrong-shaped call or as frames the managed parser rejects, and the only symptom is a sustained parse-error rate or absent data. The bridge repository’s README.md documents an export set that does not exist in this revision (InitializeGazeTracking, ShutdownGazeTracking, GetGazeData, GetPupillometryData); it is stale and must not be used as the ABI authority. This document is the authority.

Pairing is established by vendoring, not by resolution. The module repository commits three prebuilt native binaries under bridge/ (VarjoGazeBridge.dll, VarjoLib.dll, heat_protolib_capi.dll) and marks each CopyToOutputDirectory Always (HEAT.VarjoGazeModule.csproj:18-30), so a given module commit ships whichever bridge build was committed alongside it. The bridge is not separately installable and must not be replaced in a deployed module directory.

Schema compatibility is a third axis. The frame is built by ProtoLib’s native C API on one side and parsed by HEAT.Proto.Simulation from HEAT.Capture.ModuleSDK 2.0.5 on the other. A ProtoLib schema change that renumbers or retypes a field surfaces as parse failures or as silently missing components, not as a load error.

Release validation must therefore pin and verify a specific bridge and managed pair together, as required by audit finding VBRIDGE-01. Audit finding VARJO-02 makes that harder than it should be: the module’s release workflow selects the full prerelease SemVer for the package version (module-release.yml:75-106) but publishes the release and the manifest download location at the stable base tag v{majorMinorPatch} (:126-134, :150-161). A prerelease and its eventual stable release can therefore occupy the same tag, so a tag alone does not identify a bridge and managed pair. Pin by commit until VARJO-02 is fixed.

14. Security considerations

Both interfaces are local and in-process. There is no network transport, no authentication and no encryption, and no privilege boundary between the module and the bridge: any code in the HEAT Capture process can call the same exports and drain the same queue.

Gaze, pupillometry and eyelid data are biometric personal data. The per-eye video is the composited scene as presented to the user (see 6.2), so it can contain whatever the target application displays, including material that has nothing to do with eye tracking. Treat a captured session as containing both biometric measurements and a screen recording, and restrict access accordingly.

15. IF-CMVARJO-05 Bridge diagnostic log

The bridge appends diagnostics to VarjoBridge.log in the directory named by the TEMP environment variable, falling back to the process working directory when TEMP is unset (VarjoGazeBridge.cpp:75-96). Each line is prefixed with the epoch time in milliseconds.

The file is opened, appended and closed per line under a mutex. It is never rotated, truncated or deleted, so it grows for as long as the workstation keeps running capture sessions. Its content is adapter identity, encoder selection and activation results, per-tick heartbeats, mirror acquisition errors, drain sizes and queue depth. No gaze value, pose value or video payload is written to it.

The code comment marks this log as temporary and to be removed once video capture is confirmed working; it is still present and still unconditional in the audited revision. Treat it as the only diagnostic channel for video setup failures (see 9.2), and as an unbounded file that deployment housekeeping has to account for.

16. Conformance

  1. Start the Varjo Base runtime with a headset connected, then start a capture session. Confirm the log records Initialize returned 1 and a GazeTrackingStatus line.
  2. Confirm the reported status is 4 (calibrated). Any other value is a documented condition, not a fault, and produces no gaze frames.
  3. Confirm one entity, id 0x56415246 and name Varjo_Data, appears in the captured data.
  4. Confirm GazeComponent and PupillometryDataComponent values change across successive frames while the headset is worn, and that removing the headset stops frames rather than producing frames with zeroed fields.
  5. Check the published ipd value. Until the unit mismatch in section 11 is resolved, confirm it is in the tens (millimetres) and not a fraction of one (metres), and that downstream consumers apply the conversion.
  6. With EnableVideoCapture on, confirm two VideoSource streams named eye.left and eye.right appear, each with codec H264, and confirm from the bridge log that g_mirrorEnabled=1 (video ACTIVE). A gaze-only session with the setting on means video setup failed silently; the log is the only evidence.
  7. Confirm the encode resolution reported on the VideoSource matches the bridge log’s encode size line, and that its larger dimension does not exceed 1088.
  8. Decode one eye’s access units in receive order and confirm a keyframe arrives at least once per second of wall clock.
  9. Raise Interval well above its default, confirm the bridge log shows a rising queueDepth and then a video discontinuity, and restore the default. This exercises the documented overflow policy, not a fault.
  10. Stop the capture session. Confirm Varjo session ended is logged and that the bridge heartbeat lines stop.
  11. Validate the release pair, not the sides separately: record a session with the exact bridge and managed build under test, confirm a clean start and stop trace on both sides, and confirm the module’s parse-error counter stays at zero. A non-zero counter on a healthy headset indicates a mismatched pair.
  12. Pin the pair by commit rather than by tag while VARJO-02 is open.

17. Revision history

RevDateProduct versionChange
12026-09-02b2618c7Initial issue.
22026-09-02b2618c7, bridge dbbef2aReplaced the placeholder with the source-derived contract: exports, buffer ownership, thread affinity, framing, queue semantics, lifecycle across both sides, published fields, timing, error behaviour, units and version pairing. Resolves VARJO-01 and VBRIDGE-01.
Opens your browser's print dialog. Choose "Save as PDF" as the destination.