Complete Manual

Every panel, every button, every option in PassthroughForge - explained in detail.

InstallDownload & extract

PassthroughForge ships as a single ZIP. Download PassthroughForge-vX.YY.zip from the website, then right-click it and pick Extract All... to unpack into a folder of your choice. The extracted folder contains the executable, the OpenXR API layer DLL, the layer manifest and the installer scripts.

Downloaded ZIP being extracted in Windows Explorer.
Downloaded ZIP being extracted in Windows Explorer.
FilePurpose
Install PassthroughForge.batDouble-click this to start the installer. Wraps the PowerShell script with the right permissions flags.
install.ps1PowerShell installer that copies binaries to C:\Program Files\PassthroughForge\, registers the OpenXR API layer in the Windows registry, and creates the Start-menu / Startup shortcuts.
PassthroughForge.exeThe desktop application (mask editor + tray controller).
passthrough_forge_layer.dllOpenXR API layer that injects mask quads into the runtime's composition stack.
passthrough_forge_layer.jsonAPI layer manifest read by the OpenXR loader.

InstallRun the installer

Double-click Install PassthroughForge.bat. A console window opens and immediately requests Administrator rights - the installer needs them to write to C:\Program Files and to add the OpenXR API layer entry under HKLM\SOFTWARE\Khronos\OpenXR\1\ApiLayers\Implicit.

Install PassthroughForge.bat icon highlighted in the extracted folder.
Install PassthroughForge.bat in the extracted folder.

InstallConfirm the Administrator prompt

Windows will show a User Account Control dialog asking permission to run the installer with elevated rights. Click Yes to continue. If you click No, nothing is changed on your system - you can re-run the installer at any time.

Windows UAC prompt asking permission to run install.ps1 as Administrator.
Windows UAC prompt during the installer launch.
SmartScreen warning? If Windows Defender SmartScreen flags the installer because it isn't code-signed yet, click More info in the blue dialog and then Run anyway. The installer only touches its own Program Files folder and the OpenXR layer registry key - nothing else on the system is changed.

InstallWhat gets installed

The installer console prints each step as it goes. When it finishes you'll see a green [OK] line for every action and a Done summary at the end.

Installer console showing successful install steps with [OK] markers.
Installer output after a successful install.
LocationWhat's there
C:\Program Files\PassthroughForge\Application executable, API layer DLL, support DLLs, layer manifest JSON.
Start menuPassthroughForge shortcut for normal launches.
Startup folderOptional auto-start shortcut so PassthroughForge launches with Windows. Remove the shortcut to disable auto-start.
HKLM\SOFTWARE\Khronos\OpenXR\1\ApiLayers\ImplicitRegistry value pointing at the layer JSON. This is what makes DCS / MSFS / IL-2 / War Thunder / Falcon BMS load PassthroughForge automatically when they create their OpenXR session.
%APPDATA%\PassthroughForge\Per-user runtime data: profile JSONs, settings, logs. Only created on first launch, not by the installer.

InstallFirst launch

Start PassthroughForge from the Start menu (or just reboot - the auto-start will bring it up). The desktop window opens with an empty default profile. Now start your sim - DCS, MSFS, IL-2, War Thunder or Falcon BMS - and the OpenXR layer activates automatically inside the game's render loop. Add a mask, position it with the sliders or in Trace VR, and the chroma-key cutout appears in your headset.

PassthroughForge first launch with the empty default profile.
PassthroughForge first launch screen.
Virtual Desktop only: for the chroma-key passthrough to work, three things must match:
  1. In Virtual Desktop on the Quest: Streaming → VR Passthrough is ON
  2. VD's chroma-key Color matches the hex value PassthroughForge shows in Settings → Passthrough Color. Default is #0000B4 (dark blue)
  3. VD's chroma-key Similarity / Threshold sliders are wide enough to absorb stream-compression colour drift - see the troubleshooting section if the mask renders as a solid block.

InstallUninstall

Right-click the installer file again and pick Run with PowerShell, then add the -Uninstall flag - or simpler: open an elevated PowerShell window in the install folder and run:

powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall

The script removes the API layer registry entry, deletes the Start-menu and Startup shortcuts, and clears the install folder. Your profiles and settings under %APPDATA%\PassthroughForge\ are kept; delete that folder yourself if you want to wipe them too.

Uninstaller console output confirming each removal step.
Uninstaller console output.

Start hereFirst-time walkthrough — your first 15 minutes with PassthroughForge

Welcome. PassthroughForge has a lot of panels and a lot of options, but you don't need most of them on day one. This walkthrough takes you from a fresh install to a single working mask in your real cockpit, so you can see the whole pipeline (editor → OpenXR layer → Virtual Desktop → real-world passthrough) actually work end-to-end before you invest time in detailed cockpit cutouts.

Before you put the headset on

Required Virtual Desktop settings. The chroma-key passthrough only works if Virtual Desktop is configured for it. Open the Virtual Desktop streamer on the Quest and set:

  • Streaming → Environment: enabled (checked)
  • Passthrough color (RGB): 0, 0, 180 (deep blue) as your starting point
  • Similarity: 15
  • Smoothness: 15
  • Passthrough: 100

These values are a known-good baseline. You can fine-tune Similarity and Smoothness later if your room lighting causes pink/blue rim artifacts at mask edges, but get them set to these defaults first — most "it doesn't work at all" reports turn out to be a Virtual Desktop default that's far away from these numbers.

Heads up: the same color (0, 0, 180) must also be set as the Passthrough Color inside PassthroughForge (Settings → Passthrough Color). The two values must match exactly — the editor paints masks in that color, and Virtual Desktop only swaps that exact color for camera pixels. A mismatch is the #1 reason masks appear solid.

Other prerequisites:

  • Virtual Desktop running on the Quest and connected to your PC
  • DCS World (or any OpenXR title) installed and ready to launch
  • PassthroughForge installed and the OpenXR layer registered (see Run the installer above)
  • Plan 30 minutes. Most of it is positioning, not setup.

If you plan to bind a mask to a controller (real-time controller tracking) — disable Hand Tracking AND Body Tracking before you start. Both Quest 3 + Virtual Desktop and Meta's native OpenXR runtime feed an inferred hand pose through the same action space the controller uses when those modes are on — so the moment you put a controller down the mask starts following your hand instead. PassthroughForge cannot tell the two apart from inside the API layer (we tried; the POSITION_TRACKED_BIT discriminator is set on both poses on Quest 3 + VDXR). Turn them off at the source:

  • Virtual Desktop streamer → Streaming tab → Hand Tracking: OFF
  • Meta Quest headset → Settings → Movement Tracking: Controllers only
  • Meta Quest headset → Settings → Movement Tracking → Body Tracking: OFF (this one alone causes the symptom even with Hand Tracking off)

Full background and a quick-test procedure live under Troubleshooting → a controller-tracked mask locks to my hand instead of the controller.

About the trial: the open beta runs as a 44-day evaluation window from your first launch. The remaining days are shown in the title-area banner. After 44 days you continue with an activation code, which the dev team sends out in response to your beta-program signup. Setting your system clock backward to extend the trial is detected and blocked — but legitimate clock corrections (DST, NTP, a flat CMOS battery) won't cost you any trial time.

Start hereTape the proximity sensor — and use the right tape

This is the single most useful tip for anyone setting up masks for the first time.

The Quest 3 has a small infrared proximity sensor between the lenses that pauses the headset and re-centers the playspace whenever you take it off. While you're building masks, you'll constantly switch between the headset (to check placement) and the desk (to adjust polygon points). Every time you take the headset off, the proximity sensor fires, and your playspace can shift — your masks then no longer line up with your real cockpit, even though nothing about your setup actually changed.

Use light, reflective tape — never black. The sensor works by emitting infrared light and measuring what bounces back. Black tape absorbs IR instead of reflecting it, so the sensor still reads "no object near" and the headset goes into standby anyway. Use white masking tape, silver duct tape, or aluminum tape — anything light-coloured or metallic. A small folded piece of white paper held in place with a strip of regular tape also works.

Cover the sensor with that piece of tape. The headset will keep tracking and stay in its current playspace orientation while you flip it up onto your forehead to look at the screen. Remove the tape when you're done — proximity sensing is genuinely useful for normal flying because it pauses the headset to save battery.

(See also: Troubleshooting → a controller-tracked mask drifts when the controller is still, where the same tip applies.)

‼️ The permanent fix — “Center to Playspace” (the single most important setting for World-locked masks):

In Virtual Desktop → Streaming, enable “Center to Playspace”. This anchors the OpenXR reference space to your playspace centre (your guardian), not to wherever the headset happens to be — so it stays consistent across headset off/on cycles and across restarts.

Once you have set up, aligned and saved a World mask in PassthroughForge, the mask effectively becomes the central reference object. On the next session it already sits almost perfectly in place — all you do is fire your DCS “Recenter VR View” shortcut once to centre the real cockpit, and everything lines up. No re-placing, no per-session fiddling.

And within a session, a plain World mask only ever needs one Recenter All Masks (Ctrl+Del) after start — from then on it is rock-solid.

Start hereStart with the simplest possible mask

Don't open PassthroughForge for the first time and try to mask a complete HOTAS, keyboard and kneeboard. You'll spend an hour, and if anything is misconfigured (chroma key wrong, layer not loaded, Virtual Desktop in the wrong mode) you won't know which step broke it.

Instead: pick one rectangular real-world object you can see clearly — a coffee mug, a single keyboard, a notepad — and create a single rectangular mask over it. Goal for the first attempt is just to see real-world passthrough through that one rectangle while the rest of VR stays opaque. That proves the whole chain works. After that, every additional mask is just more polygons.

Start hereStep-by-step: your first mask in VR

  1. Confirm the Virtual Desktop settings above (Environment on, color 0, 0, 180, Similarity 15, Smoothness 15, Passthrough 100).
  2. Launch PassthroughForge before you start VR. The OpenXR layer activates per-process; it must be loaded by the time DCS starts.
  3. Verify Settings → Passthrough Color is also 0, 0, 180 (matches Virtual Desktop).
  4. Click "+ New" in the Profiles panel to create an empty profile.
  5. Start your VR session (Virtual Desktop → DCS in VR). Put the headset on.
  6. The default new mask is a small rectangle floating in front of you. Look at it. If you see real-world passthrough inside the rectangle, the chroma key + Virtual Desktop combo is working.
  7. Lift the headset (proximity sensor taped, remember). Use the Position panel sliders or the keyboard shortcuts (Ctrl+Arrow to nudge, Ctrl+Shift+Arrow for size) to move and resize the rectangle until its outline matches the object.
  8. Put the headset back on, check alignment, repeat.
  9. When happy, press Ctrl+S to save the profile.

Start hereSave your work as a profile

Profiles store everything: mask shapes, positions, sizes, tracking targets, even reference images. Each aircraft / cockpit gets its own profile; switch with one click in the Profile dropdown when you change planes.

Once this simple example works, you've proven the whole pipeline. From here, the rest of the manual covers how to draw real polygon shapes, bind masks to controllers (real-time tracking), mirror live windows into a mask, and more.

EditorDrawing Tools

Choose the tool you want to draw your mask with.

Drawing Tools card with all six tool radio buttons.
Drawing Tools card with all six tool radio buttons.
ToolEffect
NoneDefault. Canvas clicks/drags do nothing - safe state for adjusting position via shortcuts without accidentally drawing.
PolygonClick to place vertices, right-click on empty canvas to apply, right-click on point for context menu (Bezier toggle, Fillet, Delete).
RectangleClick + drag to fill an axis-aligned rectangle with the passthrough color.
BrushClick + drag to paint a soft circle with the size from the slider that appears next to the radio buttons.
EraserSame as brush but removes pixels (alpha = 0).
FillBucket flood-fill: click any region and the connected area is filled with the passthrough color. Use Eraser to remove a fill.
Tip: the brush size slider only appears when Brush or Eraser is selected.

Add / Subtract draw mode

Right of the tool radios (when a painting tool is selected) is a Mode toggle with two options: Add (default — tool fills with the passthrough colour) and Subtract (tool clears pixels in the affected region instead). Subtract works for Brush, Rectangle, Polygon and Fill, turning every painting tool into a "cut a hole" tool. Classic use case: filled rectangle in Add for the outer shape, switch to Subtract, drag a smaller rectangle inside → instant frame in two clicks. The standalone Eraser tool is still there for muscle memory; it is always-erase regardless of mode.

EditorFiles card

Files card
Files card
Import PNGLoads any image and uses its alpha channel as the mask shape. Pixels with alpha > 128 become the passthrough color, everything else is transparent. Good for stencils / silhouettes.
Import 3DLoads an STL or FBX file, projects its silhouette to a 2D shape and uses that as the mask. Useful for projecting hardware models.
Export PNGWrites the current mask pixels (RGBA) to a PNG file - good for backups or sharing.

EditorCanvas / Edit cards

Canvas / Edit cards
Canvas / Edit cards
Fill AllFills the entire texture with the passthrough color (full passthrough quad). Mostly used to start with a "blank passthrough sheet" you then carve away from with the Eraser.
ClearWipes all pixels to alpha=0 (fully transparent / no passthrough).
UndoReverts the last drawing operation.
RedoRe-applies an undone operation.

EditorView card

View card
View card
Zoom Canvas - / +Discrete zoom buttons. Each press multiplies/divides zoom by 1.25.
1:1Reset zoom and pan to default centered view.
Grid checkboxToggles a divisions grid overlay on the canvas - useful for symmetric polygon placement.
Mask α sliderEditor-only mask transparency on the canvas (0.05–1.0). Drop below 1.0 and the mask becomes semi-transparent in the editor so a reference image (or any canvas-only background) shows through while you paint — the difference between guessing edges and tracing them. Does not affect VR rendering: the mask in the headset stays at its real opacity regardless of where the slider sits. Click 100% to snap back to fully opaque.

Mouse alternative: Ctrl + Wheel zooms continuously and centers on the cursor; Ctrl + Middle Drag pans.

EditorReference Image card

Loads any image (typically a cockpit photo) as a tracing background. The image is fitted to the canvas with correct aspect ratio and centered.

Reference image loaded behind the canvas for tracing.
Reference image loaded behind the canvas for tracing.
Load ReferenceOpen file dialog to pick an image.
Show / HideToggle visibility of the loaded reference.
Opacity slider0 = invisible, 1 = fully opaque.
Rotate 90 / 180 / 270Rotates the reference if your photo is in portrait or upside-down.

EditorActions card

Actions card
Actions card
TRACE VRToggles a special render mode: every painted pixel of the SELECTED mask is drawn yellow in VR (instead of using passthrough), all other masks become invisible. Used for fine-aligning the painted shape with the real cockpit.
Face MeRotates the currently selected mask so its surface normal points exactly at your head. Disables Auto-Face mode (which keeps the rotation live).
Trace VR caveat: the upload to the GPU happens at 30 Hz for 8K masks while in trace mode (vs. event-driven in normal mode), so the editor UI may feel slightly heavier. Turn it off when not aligning.

EditorPolygon tool detail

The polygon tool is the most powerful drawing tool. Each click places a vertex; right-clicking on the canvas applies the polygon (rasterizes it to the mask pixels). After applying, the polygon vertices are saved with the mask so you can re-edit later.

Polygon tool in action - vertices, bezier handles, mirror line.
Polygon tool in action - vertices, bezier handles, mirror line.

Per-vertex actions

Per-vertex actions
Per-vertex actions
Drag a vertexRepositions it.
Right-click on a vertexOpens context menu: Corner / Bezier toggle (controls handle visibility), Fillet radius (adds a rounded corner on apply), Delete.
Right-click on an edgeInserts a new vertex at the click position.
Drag a Bezier handleAdjusts the curve tangent at that vertex.
Hover + DelDeletes hovered vertex.
EscCancel the in-progress polygon (no apply).

Mirror mode

Mirror mode
Mirror mode

The Mirror checkbox creates a real-time mirrored preview of the polygon along a vertical axis through the first placed point. On apply, both the original and the mirrored shape are rasterized in one go. Bezier handles and fillet radii are mirrored correctly.

Right Panel[Pr] Profiles

Manages saved profiles - each profile holds a complete mask layout, color, and transform offsets.

[Pr] Profiles section with profile list and management buttons.
[Pr] Profiles section with profile list and management buttons.
Profile name textboxThe name used by Save As / + New. Pre-filled with the loaded profile's name; edit it to rename or create variations.
Save AsSaves the current state under the typed name. If the name already exists it is overwritten without asking.
+ NewSame as the menu-bar version: creates a fresh profile (typed name preferred, otherwise auto "Profile N").
Profile list (one row each)Click to load a profile. The currently loaded profile is highlighted green. The default (auto-loaded) profile gets a star prefix.
Set Auto / Auto labelMarks a profile as the auto-load default for next launch. Only one profile can have this.
DeletePermanently deletes the profile JSON. No undo.

Right Panel[L] Layer - Mask

The mask list. Each row is one mask in the current profile. The order is the rendering order (top of list = drawn under everything else).

[L] Layer - Mask list with reorder arrows and visibility toggles.
[L] Layer - Mask list with reorder arrows and visibility toggles.
+ Add MaskCreates a new empty mask, spawned about 1 m in front of you, centred on your recentered forward direction. Several freshly-added masks are staggered slightly in depth so they don't flicker into each other.
DuplicateCreates an exact copy of the selected mask, including pixel content.
MirrorCreates a mirrored child of the selected mask. The child syncs its position (X negated), rotation (Y / Z negated) and pixel content (horizontally flipped) from the source. Edit only the source.
(o) toggleMask visibility checkbox - same as Ctrl+F1..F8 hotkey for that mask.
v / ^ arrowsMove the mask up or down in the render order (Z-order).
Mask nameClick to select and edit. Press Enter to commit the new name.
X (close button on tab)Removes the mask. Confirmation dialog appears.

Right Panel[G] Global Offset

Translation and rotation applied to ALL world-anchored masks. Modified automatically by Snap to View, can be edited manually for fine group-level adjustments.

[G] Global Offset sliders for translate and rotate.
[G] Global Offset sliders for translate and rotate.
Offset X / Y / ZTranslates the whole group. Held Alt = fine, Shift = fast.
G.Pitch / G.Yaw / G.RollEuler-angle rotation of the group around world axes (around the pivot if a Snap is active, otherwise around origin).
Reset All OffsetsClears every offset to zero / identity. Same as Settings -> Reset Snap.

Right Panel[M] Mirror & Group

Per-mask mirror controls. Visible only when a mask is selected.

[M] Mirror & Group with Create Mirror / distance slider.
[M] Mirror & Group with Create Mirror / distance slider.
Create MirrorSame as the Mirror button in the mask list - spawns a linked mirror child.
Unlink MirrorVisible only on mirror children. Detaches the child from its source - the child becomes an independent editable mask with its current state frozen.
Mirror Distance sliderVisible when a mirror pair is selected. Sets the X-axis distance between the source and mirror child centers in one slider.

Right Panel[V] Visibility & Activation

[V] Visibility & Activation
[V] Visibility & Activation
Gaze ActivatedWhen checked, the mask only fades in while you're looking within an angular cone of its center. Cone width controlled by the slider next to it (5..90 degrees).
Gaze angle sliderHalf-angle of the activation cone. 30 deg is the default and works for most cockpit panels.
Render on top of other masksForces this mask to draw above all others in the OpenXR composition layer order, even if geometrically further away. Use for PDF / document overlays.

Right Panel[T] Tracking

Per-mask tracking target. Determines whether the mask sits in world-space or follows a controller in real time.

[T] Tracking dropdown - World / Left / Right Controller.
[T] Tracking dropdown - World / Left / Right Controller.
None (World)Static world position. Default. Profile-stored coords + global offset.
Real Time Tracking Left ControllerMask pose = (left controller pose) * (mask's stored offset). The mask follows your left hand.
Real Time Tracking Right ControllerSame for right hand.
Reset Offset (when tracked)Sets the offset to identity - mask sits exactly at the controller's grip pose.
Motion rig tip: strap a controller to your motion-rig frame. Switch a mask to tracked mode and the cutout follows every pitch / roll / yaw the platform produces, keeping the cockpit panel cutout aligned even when the rig moves.

TrackingReal-time controller tracking

Real-time controller tracking pins a mask to a Quest / Touch controller so it follows the controller live. It is ideal for anchoring a kneeboard to a controller you have mounted somewhere in your cockpit: the mask sits exactly where the controller is, and stays glued to it. This page covers the one-time Virtual Desktop setup you need first, then how to switch a mask to controller tracking inside PassthroughForge.

Step 1 — Configure Virtual Desktop (do this first)

Controller tracking only reaches PassthroughForge if Virtual Desktop is set up to forward it. Open the Virtual Desktop Streamer menu in your headset and set the following. These are one-time settings — once they are right you never touch them again.

VD tabSettingRequired state
INPUT Automatically hide controllers OFF (un-ticked). If this is on, Virtual Desktop stops reporting a controller the moment you put it down — so the mask would lose its anchor.
STREAMING Track controllers ON (ticked).
STREAMING Forward tracking data to PC ON (ticked). This is what actually sends the controller pose to the PC. Requires a restart of the game / SteamVR to take effect.
Virtual Desktop INPUT tab with 'Automatically hide controllers' un-ticked.
Virtual Desktop → INPUT: “Automatically hide controllers” must be OFF.
Virtual Desktop STREAMING tab with 'Track controllers' and 'Forward tracking data to PC' ticked.
Virtual Desktop → STREAMING: “Track controllers” and “Forward tracking data to PC” both ON.

Step 2 — Switch a mask to controller tracking

⚠️ You must be in a running VR application to select a Real Time Tracking mode — the choice will not “stick” otherwise. The tracking target is a live connection between your controllers and PassthroughForge, and that connection only exists while a VR environment is running (DCS in VR, or any OpenXR/VR scene). If you open the dropdown on the flat desktop with no VR app running, the controller is not being tracked, so the selection is rejected and falls back to None (World). Start DCS (or any VR app), put the headset on, and make the selection from inside VR.

First, make sure the controller is actually tracking. Turn the controller ON and be in a VR environment — DCS running in VR, or any VR scene — because PassthroughForge reads the controller pose from the live VR runtime. Then check the controller status: only once the L: and R: indicators are GREEN (the controller is powered on and tracked) can you choose a Real Time Tracking mode. If they aren't green yet, switch on / wake the controller and look at it so the headset cameras pick it up.

In PassthroughForge, controller tracking is set per mask in the [T] Tracking panel:

  1. Place the mask first, in World mode. While the mask's Tracking Target is None (World), position it exactly where you want it in the cockpit (move/rotate it until it sits right).
  2. Open the [T] Tracking panel and set Tracking Target to Real Time Tracking Left Controller or Real Time Tracking Right Controller — whichever controller you mounted.
  3. The mask stays exactly where it is (it does not jump to the controller) and from now on follows that controller live. Mount the controller in its place and the mask is anchored to your cockpit.
PassthroughForge [T] Tracking panel with the Tracking Target dropdown showing the Real Time Tracking options.
The [T] Tracking panel — pick Real Time Tracking Left / Right Controller.

Good to know

  • No jump on switch. Switching from World to a controller keeps the mask's current position to the millimetre, so you can place it comfortably in World first and then lock it to the controller.
  • Mount the controller statically and you're set. Fix the controller rigidly to your cockpit frame and use real-time tracking, and the masks stay in exactly the same place. It is no problem if the controller goes into standby — at the start of your session just give it a quick tap and the masks come to life.
  • Mount it where the headset cameras can always see it — that is the one thing that matters. As long as the headset can see the controller, it re-tracks instantly. Short look-aways are fine: even a ~30-second pause is no problem, so when you check your six in a dogfight the mask simply stays where it is and is right there again when you look forward.
A Quest controller mounted rigidly on the cockpit frame, kept in view of the headset cameras.
A good controller mount: rigid on the frame, and always visible to the headset cameras.

Right Panel[P] Position (head-relative)

The position sliders show head-relative coordinates: X = right of player's view, Y = up, Z = forward (positive) / behind (negative). The internal mask.transform stays in stage-space; this panel converts on the fly so values feel intuitive.

[P] Position sliders, head-relative coordinates.
[P] Position sliders, head-relative coordinates.
X (right)Drag to move along the player's right-axis. Held Alt = fine, Shift = fast.
Y (up)Up/down in world coords (gravity-aligned).
Z (forward)Forward / back relative to player's yaw.

Keyboard alternative: Ctrl + Arrows for X/Y, Ctrl + PgUp/PgDn for Z.

Right Panel[R] Rotation (world axes)

[R] Rotation (world axes)
[R] Rotation (world axes)
PitchRotation around world X axis (front-to-back tilt).
YawRotation around world Y axis (vertical / heading).
RollRotation around world Z axis (banking).
Implementation: rotations apply as world-frame quaternion left-multiplications, so they stay gimbal-lock free as you stack them. Editing one slider does not unexpectedly change the others.

Right Panel[S] Size & Resolution

[S] Size & Resolution
[S] Size & Resolution
Width / Height (m)Physical size of the mask in meters as it appears in VR.
Lock aspectWhen checked, editing one dimension scales the other proportionally.
Texture resolutionInternal pixel resolution of the mask buffer. Higher = sharper edges but more GPU bandwidth. Default 8192x8192. Document Masks usually need lower (e.g. 2048x2048).

Right Panel[3D] Preview

Wireframe preview of the mask group around the player's head pose. Updates live. Useful for understanding the spatial relationship between masks and validating where Recenter All Masks will land them.

[3D] Preview wireframe of mask group around head pose.
[3D] Preview wireframe of mask group around head pose.
Show all checkboxIf unchecked, only the selected mask is drawn (simplifies the preview when you have many masks).

WorkflowShortcut configuration

Reachable via Settings -> Mask Shortcuts.... Opens a modal where every action's binding can be customized.

Multiple bindings per action (v2.73+): every action can carry any number of bindings at the same time — a keyboard combo (with Ctrl/Shift/Alt modifiers) and a joystick / HOTAS button and a hat-switch direction, across several physical devices. Any one of them triggers the action, so an in-cockpit pilot can map e.g. Toggle All Masks to Ctrl+F9 (for a Stream Deck) and a stick button and a throttle button without ever switching hands. Click Add on a row, then press a key or a joystick button to add a binding; each binding is listed on its own line as the key combo or Joy[<Device>] Btn 7 / Joy[<Device>] Hat1 Up, with its own x to remove just that one. Joystick bindings work in tray-mode and coexist with DCS bindings on the same physical button (PassthroughForge polls non-exclusively).

Upgrading from an older build keeps your existing setup: each previous single binding is migrated automatically to the new list on first launch — nothing to reconfigure, you can simply add more.

Shortcut configuration window with rebindable action table.
Shortcut configuration window with rebindable action table.

Step mode controls

Step mode controls
Step mode controls

Three buttons at the top set the current step multiplier directly: Normal (x1, default), Fine (x0.1), Coarse (x5). The toggles also fire automatically on the configured Toggle keys (Q / E by default).

Action table

Action table
Action table

Each row shows an action and all of its bindings, one per line (e.g. Ctrl+RightArrow, Joy[Warthog] Btn 7), plus Add / Clear buttons. An action with no bindings shows <unbound> and is inactive.

AddClick, then press the desired key or joystick/HOTAS button to add another binding to this action (existing ones are kept). Modifier keys (Ctrl / Shift / Alt) are part of the binding - LeftArrow and Ctrl+LeftArrow are different bindings. Press Esc to cancel. Duplicate bindings are ignored.
xThe small x next to a binding removes just that one binding, leaving the others intact.
ClearRemoves all bindings for the action. It becomes inactive.

Other buttons

Other buttons
Other buttons
Reset to DefaultsRestores all bindings to factory defaults (Ctrl + arrows / numpad / Q / E).
Show Cheat Sheet in VRGenerates a 1024x768 image with all current bindings rendered as a panel and creates a "Shortcuts Help" mask floating ~1 m in front of you.

WorkflowTray mode

Click the Minimize button (top-right of the title bar) to send PassthroughForge to the system tray. While in tray:

Tray icon and the right-click context menu.
Tray icon and the right-click context menu.
  • The window is hidden but the layer keeps publishing mask data to your sim.
  • All keyboard shortcuts are registered as Win32 RegisterHotKeys.
  • A low-level WH_KEYBOARD_LL hook is also installed to intercept keys before sims (DCS) which use DirectInput / RawInput see them.
  • The hook only fires when a known flight sim has foreground (DCS, MSFS, IL-2, War Thunder, Falcon BMS). Plain typing in Notepad / browser passes through normally.

X (close button) always quits the application - only the Minimize button sends to tray. The tray icon's right-click menu also has "Show PassthroughForge" (restore) and "Exit".

WorkflowVR cheat sheet

Settings -> Mask Shortcuts -> Show Cheat Sheet in VR builds a high-res image with the current keyboard bindings and creates a dedicated mask called "Shortcuts Help". It floats ~1 m in front of you, large enough to read at a glance.

In-VR cheat sheet floating in front of the player.
In-VR cheat sheet floating in front of the player.

It's a normal mask - you can move it, hide it via Ctrl+F<n>, snap it to a controller, or delete it like any other. Re-press the menu item to regenerate it after rebinding.

WorkflowStatus bar

Bottom of the desktop window. Shows live state:

Status bar at the bottom of the desktop window.
Status bar at the bottom of the desktop window.
  • SHM Active / Disconnected: green if the layer is connected, gray otherwise.
  • Masks: N: number of masks in the current profile.
  • Color: : the current passthrough color.
  • Step: Fine / Coarse: appears in green / orange when not in Normal step mode.

DCS IntegrationDCS Messages mask

PassthroughForge can display DCS in-game messages — multiplayer chat, mission trigger texts, and AI radio chatter — on a freely placeable A4 panel inside your cockpit. Useful for keeping JTAC briefings, ATC instructions, or wingman calls in your peripheral vision instead of cluttering the HUD.

Two ingredients make this work:

  1. A small Lua script (PassthroughForge.lua) that lives inside DCS's Scripts\Hooks\ folder and forwards messages over a local UDP socket (127.0.0.1:31090, never leaves your PC).
  2. The PassthroughForge desktop app, which buffers incoming messages and renders them on any mask you marked as a "Messages" layer.

Add a Messages mask via Layer — Mask → + Messages. The panel spawns ~1 m in front of your head; reposition with the standard mask transform shortcuts. Only traffic addressed to the player is shown — your own outbound radio calls (lines starting with Spieler: / Player:) are filtered out.

DCS IntegrationInstall the DCS hook (automatic)

The PassthroughForge installer handles the hook for you. From version 2.46 onwards, running install.ps1 performs step [3.5/5] Installing DCS message hook…:

  • It scans every Windows user profile under C:\Users\ for Saved Games\DCS, DCS.openbeta, DCS.dev, DCS.earlyaccess.
  • For each match, it creates the Scripts\Hooks\ folder if missing and copies PassthroughForge.lua into it.
  • The uninstaller removes the same files.

If you installed PassthroughForge before DCS: the Saved Games folder didn't exist yet, so the installer skipped this step. Just run DCS once, then re-run install.ps1 — it's safe to run repeatedly.

DCS IntegrationManual hook install

If you'd rather not run the installer (or you're using a portable build), install the hook by hand:

  1. Open File Explorer and paste this into the address bar:
    %USERPROFILE%\Saved Games\DCS\Scripts
    For OpenBeta use %USERPROFILE%\Saved Games\DCS.openbeta\Scripts instead.
  2. If a folder named Hooks doesn't exist inside Scripts\, create it.
  3. Copy PassthroughForge.lua from dist\PassthroughForge\ (next to the installer) into the Hooks folder.
  4. Start PassthroughForge, then start DCS. To verify the hook loaded, open %USERPROFILE%\Saved Games\DCS\Logs\dcs.log and search for the line:
    PassthroughForge: Hook loaded — forwarding messages to 127.0.0.1:31090
  5. In PassthroughForge, open Settings → DCS Messages (Beta)… for a live debug feed. The "received" counter should tick up while DCS is running and traffic is happening.

Network safety: The hook only sends data to 127.0.0.1 (loopback). Nothing leaves your PC; no firewall rule is needed.

Multiplayer Integrity Check (IC): DCS treats anything in Scripts\Hooks\ as a client-side mod. Strict-IC servers may flag this hook. If your server is strict, leave the file out for those sessions — PassthroughForge keeps working, you just won't get the message feed.

Crash safety: Every callback in the hook is wrapped in pcall; a bug in the script can never crash DCS itself.

DCS IntegrationRemoving the hook

Either run the PassthroughForge uninstaller (Programs & Features → PassthroughForge → Uninstall — it removes the hook from every DCS profile it finds), or delete the file manually:

%USERPROFILE%\Saved Games\DCS\Scripts\Hooks\PassthroughForge.lua

DCS picks up the change on the next launch.

DCS IntegrationAuto-switch profile on aircraft change (v2.65+)

Most multi-jet pilots end up with one PassthroughForge profile per airframe — "Hornet-Setup" with HOTAS-shaped cutouts and a DDI region capture, "Hog-Setup" with different panel positions, "Apache-Setup" with two MFDs side by side. Auto-switch makes the right profile load itself the moment DCS reports you've slotted into the matching aircraft. No menu clicks, no manual dropdown, no profile-mix-up between sorties.

Prerequisites (one-time)

  1. The DCS GUI hook must be installed. The PassthroughForge installer drops it automatically into every Saved Games\DCS*\Scripts\Hooks\ folder it finds (see Install the DCS hook). You can verify it ran by launching DCS and looking for "PassthroughForge: Hook loaded" in Saved Games\DCS\Logs\dcs.log.
  2. In PassthroughForge: open Settings menu and tick Auto-switch profile on DCS aircraft change. (Default: on.) You can flip this off any time — the per-profile aircraft assignments stay saved either way.

Step 1. Build one profile per jet

Standard PassthroughForge workflow: launch the app, design the masks for one specific aircraft (positions / sizes / capture regions / panel layouts), then click Save As in the right-side Profiles panel. Give it a descriptive name — e.g. "Hornet-Cockpit", "Hog-CCIP", "Apache-MPDs".

Repeat for every aircraft you want covered. There's no limit on profile count.

Step 2. Bind each profile to an aircraft

Each profile carries an optional Default for aircraft field. When the DCS hook reports a slot change matching that aircraft ID, PassthroughForge auto-loads that profile.

In the Profiles panel:

  1. Click the profile name in the list to load it (so "Current:" shows the right profile at the top of the panel).
  2. Scroll down to the new DCS auto-switch: section.
  3. Type the DCS aircraft ID into the Default for aircraft input field and press Enter. Or, if you have DCS open and you're in that aircraft right now, click the "Use 'XYZ'" button next to the field — it fills in the exact ID DCS just reported.

That's it. The setting is written back to the profile's JSON immediately (no Save needed for the assignment itself).

Step 3. Use it

Start DCS, slot into any aircraft you've bound. Within a second of slotting, PassthroughForge swaps to the matching profile and every mask layout / capture / kneeboard adjusts accordingly. Slot to a different jet mid-mission (multiplayer slot picker, or "Restart Mission" into another aircraft) and the profile switches again.

If you slot into an aircraft no profile is bound to, PassthroughForge stays on the currently-loaded profile — nothing happens, no error.

DCS aircraft IDs (cheat sheet)

These are the IDs DCS uses internally, exactly as they appear in Saved Games\DCS\KNEEBOARD\<id> folders and in our auto-switch hook. The "Use 'XYZ'" button is the safe way to avoid typos — but for reference:

Aircraft DCS ID
F/A-18C Hornet FA-18C_hornet
A-10C II Warthog A-10C_2
A-10C (original) A-10C
F-16C Viper F-16C_50
F-14B Tomcat F-14B
F-14A Tomcat F-14A-135-GR
F-15E Strike Eagle F-15E
F-15C F-15C
AH-64D Apache AH-64D_BLK_II
UH-60L Black Hawk UH-60L
Ka-50 Black Shark Ka-50_3
Mi-8 / Mi-24 Mi-8MT / Mi-24P
M-2000C Mirage M-2000C
JF-17 Thunder JF-17
MiG-21bis MiG-21Bis
Mosquito FB Mk VI MosquitoFBMkVI
P-51D Mustang P-51D
Spitfire LF Mk IX SpitfireLFMkIX

Don't see your jet? The "Use 'XYZ'" button is the safe path. Open DCS, slot into the aircraft, hop back to the PassthroughForge window, click the button. The exact ID DCS uses is auto-filled. Third-party modules (Heatblur F-4, Razbam M-2000, etc.) follow the same convention — DCS gives us their internal string and we use it.

Worked example: switching between Hornet and Hog

  1. Build a profile for the F/A-18C with HOTAS / DDI / kneeboard cutouts placed for your physical Hornet sim setup. Save as "Hornet-Setup".
  2. In DCS, fire up an Instant Action Hornet mission. Slot into the cockpit.
  3. In the PassthroughForge Profiles panel, with "Hornet-Setup" loaded as current, click "Use 'FA-18C_hornet'".
  4. Build a second profile for the A-10C II. Save as "Hog-Setup".
  5. Restart your DCS mission as a Hog. Slot in.
  6. In PF, with "Hog-Setup" loaded, click "Use 'A-10C_2'".
  7. From now on, every time you slot into the Hornet, PF auto-loads Hornet-Setup. Every time you slot into the Hog, PF auto-loads Hog-Setup. Multiplayer slot picker, instant-action restart, campaign next-mission — all trigger the switch automatically.

Limitations + edge cases

  • One profile per aircraft. If you bind two profiles to the same DCS ID, the first one in alphabetical order wins. To work around: give each profile a different Default for aircraft assignment, or leave one empty (manual-only).
  • Unsaved changes on the current profile won't survive a switch. Profile auto-switch loads the target profile fresh from disk. If you were mid-edit on the previously-loaded profile and hadn't clicked Save, those edits are discarded. Save before you slot into a different jet.
  • Spectator slots don't trigger a switch. Going into the multiplayer Spectator does NOT clear or change the current profile — it just keeps whatever was loaded.
  • The first slot of a mission does trigger. When DCS reports the initial slot at mission start, PF acts on it the same way it acts on a later switch.
  • Auto-switch toggle in Settings is global. Disable it and ALL profile auto-switching is paused (per-profile assignments stay saved). Re-enable any time.

Verification + troubleshooting

The PassthroughForge app log records every transition:

%APPDATA%\PassthroughForge\logs\passthrough_forge_app.log

Look for lines like:

DcsMessageListener: aircraft '' -> 'FA-18C_hornet'
App: auto-switching profile to 'Hornet-Setup' (matched defaultAircraft=FA-18C_hornet)

If you see the first line but not the second:

  • No profile has Default for aircraft set to that exact DCS ID. Double-check spelling.
  • Or Auto-switch profile on DCS aircraft change is disabled in the Settings menu.

If you see neither line:

  • The DCS hook didn't load. Check Saved Games\DCS\Logs\dcs.log for "PassthroughForge: Hook loaded". If missing, re-run the PassthroughForge installer.
  • Or the DCS Lua hook is loading but Export.LoGetSelfData() isn't available on your DCS revision — rare, but possible on very old beta builds. The hook tolerates this gracefully (no aircraft event); you'll just have to switch profiles manually.

DCS IntegrationDCS Auto-Kneeboard (Aircraft / Theater / Mission / Briefing) — v2.65+, multi-tab from v2.66

The shortest summary: point a mask at one or more DCS content sources by ticking checkboxes, and PassthroughForge fills it with the right pages automatically — refreshed whenever DCS reports a new slot or loads a new mission. With v2.66 multiple sources on the same mask are presented as tabs so a single cockpit panel can carry your Hornet charts, your Syria theatre reference, the current mission's baked images, and the auto-rendered briefing — all switchable with a HOTAS button.

The four source modes

Source Pages come from Refreshes on
DCS Aircraft Saved Games\DCS\KNEEBOARD\<aircraft>\ — whatever PNG/JPG you've dropped in there. Use the same folders DCS already supports natively. Slot change
DCS Theater Saved Games\DCS\KNEEBOARD\<theatre>\ — e.g. Syria, Caucasus, Afghanistan. Mission load (different theatre)
DCS Mission (.miz) The KNEEBOARD/IMAGES/ folder baked into the active .miz file by the mission designer. Unzipped on the fly to %TEMP%\PassthroughForge\miz-cache\. Mission load (different .miz)
DCS Briefing Rendered live from the mission's Lua table. Includes Sortie, Mission (theatre + start), Your Flight (callsign + frequency), Bullseye, full waypoint list with altitude (m + ft) and speed (kts), AI assets (AWACS / Tankers / CAP / SEAD / CAS with frequencies), the briefing prose, per-coalition tasking, weather, and — if enabled in Settings — an enemy Threats section. Mission load (different .miz)

Prerequisites

The PassthroughForge DCS GUI hook must be installed (the standard installer does this automatically into every Saved Games\DCS*\Scripts\Hooks\ it finds; see Install the DCS hook). Without it PassthroughForge has no way to learn which aircraft / theatre / mission is active, so the auto-sources have nothing to refresh from.

Step 1. Pick a mask + open the editor

Any mask can carry auto-source content. Click the mask's tab in the editor, or create a new one with + Add Mask. In the editor scroll to the Files card — below the "Load PDF / Images" button you'll see an Auto-source section with one checkbox per DCS source mode.

Step 2. Tick one or more sources

Each checkbox you tick enables that source for this mask. Next to each checkbox PassthroughForge shows a live page count ("(8 pages)") so you immediately see whether the source has content for your current DCS state. Empty sources are not hidden — you can still keep them ticked, see Empty tabs below.

Single-source mode (one checkbox ticked). The mask behaves exactly like a regular document mask with that source's pages. No tab strip in VR, no new shortcuts needed — just paginate with the existing Doc Next / Previous Page bindings.

Multi-source mode (two or more ticked). A tab strip appears at the top of the mask in VR with one tab per enabled source. Switching tabs swaps the visible content instantly; each tab remembers its own current page across switches.

Step 3. Use it in VR

In multi-source mode the mask renders something like this in your headset (real version is full-resolution image content below the strip):

┌───────────┬──────────┬──────────┬──────────────┐
│ Aircraft  │ Theater  │ Mission  │  Briefing    │   ← tab strip
│  3 of 8   │  2 of 4  │  5 of 45 │   1 of 7     │     (page indicator)
├───────────┴──────────┴──────────┴──────────────┤
│                                                │
│           (page content for active tab)        │
│                                                │
└────────────────────────────────────────────────┘

The active tab is highlighted in blue with bright white text. The other tabs are darker grey with off-white text. Each tab carries a "current page / total pages" counter underneath its name; the counter updates live as you flip pages within the active tab.

Step 4. Bind tab navigation shortcuts

Open Settings → Mask Shortcuts…. Two new actions are available:

  • Next Kneeboard Tab — cycle forward through the enabled sources of the currently selected mask. Wraps around.
  • Previous Kneeboard Tab — cycle backward.

Click the binding row and press any key or HOTAS button to assign it. For a typical hat-switch setup most pilots end up with:

  • Hat Up / Down → Doc Previous / Next Page (paginate inside the active tab)
  • Hat Left / Right → Previous / Next Kneeboard Tab (jump between sources)

Both shortcut families work in tray mode while DCS is in the foreground — no need to alt-tab.

Optional: hide the tab strip

If you prefer a cleaner look in VR and don't need the visible tab indicators — you can still cycle via shortcut — un-tick Show tab strip in VR in the editor. The mask then renders just the active tab's content with no overlay; only the shortcut bindings work for switching.

Empty tabs

It's normal for a source to have zero pages — e.g. a stock DCS mission without a baked KNEEBOARD/IMAGES/ folder, or a theatre you've never dropped any chart into. Empty tabs don't get hidden; instead the tab strip still shows them and the page area shows "(no content for this source)" — a status hint rather than an error. You can leave the tab enabled and the moment DCS loads a mission that does have content for it, the pages pop in.

Briefing Threats section (v2.67+, opt-in)

The rendered DCS Briefing source can append a Threats section listing every enemy SAM / AAA / MANPAD group the mission designer placed in the .miz. Each line shows the NATO reporting name, the group's name, and a bullseye-relative bearing + distance, grouped by engagement range:

THREATS
────────────────────────────────────────────
Long-range SAM (60+ nm)
   SA-10 Grumble (LN)    - RU S-300 Bty (Al Assad)   BE 233 / 76 nm
Medium-range SAM (15-40 nm)
   SA-6 Gainful (LN)     - Palmyra battery           BE 105 / 38 nm
Short-range SAM / AAA / MANPAD (< 15 nm)
   ZSU-23-4 Shilka (AAA) - RU Shilka (Al Assad)      BE 233 / 76 nm

"BE 233 / 76 nm" = bearing 233° from your coalition's bullseye, 76 nautical miles out. Same reference frame AWACS and your wingmen use for contact calls, so it lines up without any coordinate conversion. The bullseye itself is printed one section higher.

How to enable it: open Settings and tick Briefing: include enemy threats (SAM/AAA/MANPAD). Any open Briefing-mode mask re-renders immediately — no mission reload needed. The setting is remembered in settings.json.

OFF by default — read this before enabling for multiplayer. This surfaces information some MP servers deliberately hide (an IADS spoiler / "SAM cheat"). The data is read client-side from the mission file you already have on disk — for single-player or your own missions it's entirely fair game and hugely useful for SEAD / strike planning. For public servers, check the server's stance first. PassthroughForge ships it disabled so the choice is always an explicit one you make.

Detection details

  • ~80 common DCS unit-type strings are mapped directly to NATO names. Uncatalogued or community-mod units fall back to a prefix match (e.g. an unknown S-400… still lands in "Long-range SAM (uncatalogued)") so nothing is silently dropped.
  • A multi-component SAM site (search radar + tracker + launchers + command post) is collapsed to one line, tagged with the longest-range category present in the group.
  • Each range category is capped at 30 entries with a "… N more in this category" tail, so a heavily-defended mission still fits a readable page.
  • Only the enemy coalition (opposite of your slot) is scanned. If you're in a neutral slot the section is skipped.

Profile persistence

The set of enabled sources, which one is currently active, the "Show tab strip in VR" flag, and each tab's last-viewed page are all stored in the profile JSON under %APPDATA%\PassthroughForge\profiles\<name>.json. Older v2.65 profiles with the previous single-source format are auto-migrated on load — one ticked checkbox, no behaviour change.

Worked example: one cockpit panel, everything you need

  1. Create a regular mask, place it where your physical kneeboard sits (knee, dashboard, whatever).
  2. In the editor, tick all four checkboxes: Aircraft, Theater, Mission, Briefing.
  3. Save the profile and assign it to your jet (see Auto-switch profile).
  4. Bind Hat Left / Right to Previous / Next Kneeboard Tab, Hat Up / Down to Doc Previous / Next Page.
  5. Load any mission. The tab strip appears; you spawn on Aircraft tab and see your usual checklists. Hat-Right to Theater for a Syrian airfield reference. Hat-Right again to Mission for the briefing pictures the designer baked in. Hat-Right once more to Briefing for the auto-rendered objectives + waypoints + frequencies + weather. Hat-Up / Down paginates within each. Total: one panel, one stick movement to reach any reference, zero re-configuration between missions.

Troubleshooting

App log lines that confirm or deny each step (%APPDATA%\PassthroughForge\logs\passthrough_forge_app.log):

DcsMessageListener: aircraft '' -> 'FA-18C_hornet'
DcsMessageListener: theater '' -> 'Syria'
DcsMessageListener: mission '' -> 'D:\Steam\..\test.miz'
App: mask 0 source aircraft -> 8 page(s) from C:\..\KNEEBOARD\FA-18C_hornet
App: mask 0 source mission  -> 45 page(s) from C:\..\miz-cache\<hash>
App: mask 0 source briefing -> 7 page(s) from C:\..\miz-cache\<hash>
MizExtractor: test.miz (cached=no) -> 45 kneeboard image(s)
BriefingRenderer: rendered 7 page(s)

If you don't see those lines:

  • The DCS GUI hook didn't load — check Saved Games\DCS\Logs\dcs.log for "PassthroughForge: Hook loaded"; if missing, re-run the PassthroughForge installer.
  • You haven't actually slotted into a mission yet (auto-sources can't refresh before DCS publishes which aircraft / mission you're in).
  • The mask doesn't have any source ticked (check the editor's Auto-source section).

Tablet & Phone NotesWrite on a Whiteboard kneeboard

The Whiteboard (Notes) tab turns a DCS-Kneeboard mask into a plain white page you can write and draw on — with a graphics tablet (Wacom / Huion / XP-Pen) or with your phone over Wi-Fi. Ink appears live on the kneeboard in VR, so you can jot down a frequency, a steerpoint or a quick sketch without leaving the headset.

  • It works while DCS has focus — no alt-tabbing.
  • Pressure-sensitive: harder strokes draw thicker (stylus / pen).
  • Notes are not saved to disk. The page starts blank each session, like a real grease-pencil board.

Tablet & Phone NotesAdd the Whiteboard (Notes) tab

The Whiteboard is one of the DCS-Kneeboard tabs, so it lives next to Aircraft / Theater / Mission / Briefing / Nearest Airfields / Moving Map.

  1. Create or select a DCS Kneeboard mask (+ Add Mask → DCS Kneeboard).
  2. In the editor's Auto-source section, tick “Whiteboard (tablet / phone notes)”.
  3. In VR, flip the kneeboard tab strip to “Notes” — you'll see a white page.

Ink lands on whichever kneeboard is currently showing its Notes tab, so just flip to Notes when you want to write and flip back to your charts when you're done.

Tablet & Phone NotesUse a graphics tablet

PassthroughForge reads a graphics tablet through one of two paths, picked automatically. Which one you use matters a lot once you're flying, so read this before you set up:

WinTab (vendor driver) — great for the desktop, NOT for flight

The driver that ships with Wacom / Huion / XP-Pen tablets. If it's installed, PassthroughForge uses it with zero extra setup and the status line shows “Connected: WinTab”. The catch: WinTab only delivers pen data to the window that currently has focus. So it works while the PassthroughForge window is in front (setting up your kneeboard on the desktop), but once you're in a DCS mission — DCS has focus, PassthroughForge sits in the tray — WinTab sends nothing, and your pen just moves the Windows mouse. This is a WinTab limitation, not a bug.

OpenTabletDriver + OTD-IPC — the in-cockpit path (recommended)

To write during a flight with a tablet, use OpenTabletDriver (OTD) with its OTD-IPC plugin. OTD takes over the tablet and streams pen data over a local pipe, so it's focus-independent (works while DCS is in front and PF is in the tray) and the pen no longer drifts the Windows mouse. This is the same approach OpenKneeboard uses.

  1. Install OpenTabletDriver from opentabletdriver.net and confirm it sees your tablet.
  2. Install the OTD-IPC plugin via Plugins → Open Plugin Manager, then enable it under “Tools” / “Filters”. Installing alone is not enough — the connection only opens once the OTD-IPC tool is ticked/active.
  3. OpenTabletDriver must be running whenever you fly. Its background “Daemon” does the work; you can close the OTD window, but don't quit it. Tip: enable OTD's “Run on startup” so the daemon launches with Windows.
  4. Start (or restart) PassthroughForge — the status line under Settings → Tablet / Whiteboard should switch to “Connected: OpenTabletDriver” (auto-detected and preferred over WinTab). If it still shows WinTab, OTD-IPC wasn't active yet — enable the tool, then restart PassthroughForge.
  5. Flip a kneeboard to its Notes tab and write — in the cockpit, mid-mission.

Stop the pen from clicking in the sim: if both the vendor driver and OTD run at once, the pen also drives the Windows mouse (and a tip-press can click cockpit controls). Stop the vendor service so only OTD runs — Win+R → services.msc → “Wacom Professional Service” / “WTabletServicePro” → Stop (set it to Manual to keep it off). In OpenTabletDriver you can also clear the Tip binding so the pen never left-clicks, while still writing on the kneeboard.

Which should I use?

ScenarioWinTab (vendor driver)OTD-IPCPhone (Wi-Fi)
Desktop setup (PF in focus)
In a DCS mission (PF in tray)✘ focus-bound
Pen stops moving the mousen/a
Extra install needednoneOTD + pluginnone

Bottom line: WinTab is fine for laying out masks on the desktop, but for actually writing while you fly install OTD-IPC — or use your phone (next section), which needs no install and also works in the tray.

Download links

For in-flight tablet writing (focus-independent):

Vendor WinTab drivers (fine for desktop setup):

No tablet? Your phone needs none of these — see the next section.

Tablet & Phone NotesUse your phone over Wi-Fi

No app to install — PassthroughForge serves a small web page that your phone's browser draws on. Your phone and this PC must be on the same Wi-Fi / local network.

  1. Open Settings → Tablet / Whiteboard and tick “Use my phone as a tablet”.
  2. The panel shows one or more addresses like http://192.168.1.57:8723/?k=ABC123. If several are listed (a PC wired and on Wi-Fi has more than one), pick the one whose first numbers match your phone's network. Use Copy and paste it into the phone, or just type the http://<ip>:<port> shown.
  3. On the phone you get a full-screen pad. Write with a finger or stylus (stylus pressure supported). Pen / Erase / Clear buttons sit along the top.
  • Firewall: the installer opens the needed port automatically — nothing to confirm. (If you run a portable build, allow PassthroughForge through Windows Firewall once.)
  • Screen stays awake: the page holds a wake-lock so the phone doesn't sleep and drop the connection while you fly.
  • Security: the key in the URL keeps other devices on your network from drawing. It stays the same every session; only the New key button changes it (then reload the page on the phone).
  • Can't connect? Make sure both devices are on the same Wi-Fi (not a “guest” network with client isolation), and that you used the address whose subnet matches the phone.

Tablet & Phone NotesInk settings, calibration & pen buttons

Everything lives under Settings → Tablet / Whiteboard:

  • Colour & width — pick the ink colour and base stroke width (pressure scales it).
  • Calibration (graphics tablet only) — tap Calibrate, then touch the top-left and bottom-right corners of the area you want mapped onto the page. Reset maps the whole tablet. Phone input needs no calibration (the screen already maps 1:1).
  • Pen buttons — map a barrel button to Erase and another to Clear page. On the phone, use the on-screen Erase / Clear buttons instead.

The eraser paints white (it doesn't punch a hole), so the page always stays a clean white sheet.

Live & Web MasksCapture a window or monitor region

Besides drawn passthrough masks, a mask can show live desktop content: + Add Mask → Capture window… mirrors one application window, Capture monitor region… mirrors a whole monitor (with an editable crop rectangle in the right panel). Both update at ~30 Hz fully on the GPU.

  • Gamma slider (v2.97+): captured content used to look washed out in VR. Every capture mask now has a Gamma control above the toolbar — the default 2.2 matches the VR pipeline; set 1.0 for the old raw look.
  • The captured window may be covered by other windows — but it must not be minimised (Windows stops delivering frames for minimised windows).
  • Painting tools are disabled on capture masks; position / scale / opacity work as usual.

Live & Web MasksWebcam as a mask (Camera Feed)

+ Add Mask → Capture webcam… shows a live camera as a mask — a passthrough substitute for headsets without passthrough cameras, or a dedicated cockpit camera you can place freely in VR.

  • Device & resolution are picked in the panel above the toolbar. 4K streams are heavy — 1440p is the VR sweet spot.
  • Gamma: default 2.2 matches VR colours (same control as capture masks).
  • True 3D with a stereo camera: set Stereo → Side-by-Side (3D) for cameras that deliver a left|right image — each eye gets its own half, fused into real depth. Side-by-Side → single eye shows one half flat instead. A swap toggle fixes reversed eyes.
  • Shape stencil: Load shape… clips the feed to an image — transparent (or dark) areas hide the camera, white/opaque areas show it. Great for cutting the feed to a cockpit opening. With side-by-side stereo the shape is applied per eye, so you see ONE fused shape in VR.
  • Several masks, one camera: multiple camera masks of the same device share a single stream. Each mask has its own Crop (X/Y/W/H sliders) — split one camera across several surfaces. A per-mask coloured calibration border helps lining the pieces up edge-to-edge in VR.
  • Camera pixels are never saved to disk; profiles only store the device, resolution and settings.

Live & Web MasksWeb page (browser) mask

+ Add Mask → Web page (browser)… shows any URL as a live mask in VR — charts, SimBrief, checklists, YouTube (with sound). The page renders off-screen through Windows' built-in WebView2 engine: there is no browser window anywhere, and the page keeps running while PassthroughForge is in the background or minimised to the tray.

  • Interact on the canvas: the editor canvas IS the page — click, drag and right-click directly on it. The mouse wheel scrolls the page (Ctrl+wheel still zooms the canvas).
  • Navigate with the URL bar above the toolbar (Enter or Go), plus Reload and Back.
  • Typing into web forms is not supported yet — navigation via the URL bar covers most cockpit use cases.
  • The page URL and size are saved in the profile; the browser starts again automatically when the profile loads.
  • Requires the WebView2 Runtime (preinstalled on Windows 11 and current Windows 10). If it is missing, the mask panel shows a download link.

Live & Web MasksShow PassthroughForge inside VR

Drawing masks normally means peeking at your monitor through the transparent canvas — which only works if the monitor is in front of you. + Add Mask → Show PassthroughForge in VR mirrors the app itself onto a mask, including your mouse pointer, so you can operate PassthroughForge entirely from inside the headset.

  • While the mirror mask is enabled, the real PassthroughForge window is kept always-on-top — blind clicks always land in PSF, never in a window covering it. Disable or delete the mask and the window behaves normally again.
  • Keep mouse inside this window (checkbox in the mask panel) confines the cursor to the PSF window while it has focus, so the pointer can't get lost outside the mirror. It releases automatically when another app (e.g. DCS) takes focus.
  • Selecting the mirror mask itself in the editor shows a mirror-in-mirror tunnel — that's normal.
  • New mirror masks default to Render on top so they never hide behind other masks in VR.

Community MasksShare masks with other pilots

You don't have to build every mask from scratch. Community Masks lets you share a mask you made and import masks other pilots have shared — right from the editor's Files card: Share Mask to upload one, Get Community Masks to browse and import.

  • An activated license is required for both sharing and importing.
  • Reviewed before it goes public — every shared mask is checked before it appears in the gallery, so the library stays clean.
  • Tagged by aircraft — each mask carries an aircraft type, so you can find exactly what fits your jet.

Community MasksShare a mask

Select the mask you want to share, open the Files card and click Share Mask. Fill in:

  • Title — a short, descriptive name.
  • Your name — shown as the author credit.
  • Aircraft type — so others can filter for it.
  • Description (optional) — what it covers and how it's meant to sit in the cockpit.
  • Preview (optional) — a thumbnail shown in the gallery.

Submit it and the mask goes into the review queue; it appears in the public gallery once approved. Note that some masks can't be shared — live window/region captures and the DCS-Messages mask have Save / Share greyed out, because their content is live rather than a drawn/static mask.

The Share Mask dialog with title, author, aircraft type, description and preview fields.
The Share Mask dialog — title, your name, aircraft type, description & preview.

Community MasksGet community masks

Open the Files card and click Get Community Masks to browse the gallery:

  • Preview thumbnails for every shared mask.
  • Filter by aircraft to narrow it down to your jet.
  • One-click import drops the chosen mask straight into a new layer in your current profile — ready to position.

An imported mask behaves like any other: move, rotate, scale it and bind it to a controller exactly as if you had drawn it yourself.

The Get Community Masks gallery with preview thumbnails and an aircraft filter.
The Get Community Masks gallery — preview thumbnails, filter by aircraft, one-click import.

Files & StorageWhere your data lives

PassthroughForge keeps all of its user-side state under a single AppData folder, completely separate from the installation directory. Reinstalling, updating to a new version, or moving the install directory has no effect on your profiles — they stay put.

User folder

%APPDATA%\PassthroughForge\ (full path: C:\Users\<you>\AppData\Roaming\PassthroughForge\)

Quick way to open it: press Win+R, type %APPDATA%\PassthroughForge, hit Enter.

Path Contents
settings.json Global app settings: default profile, passthrough colour, mask fade duration, capture refresh rate, Auto-recenter toggle. NOT per-profile.
profiles\<name>.json One file per profile. Contains every mask: its position, rotation, size, tracking mode, pixels — everything you see in VR for that profile. Largest file in the system; can easily reach several hundred MB when you have multiple high-resolution masks (the pixel buffer is embedded as Base64, so 8K masks alone are ~256 MB each).
logs\ passthrough_forge_app.log — main app log
passthrough_forge_layer.log — OpenXR layer log
PassthroughForge_LayerDiag.log — per-frame diag CSV
PassthroughForge_SwimDiag.log — pose-jitter CSV (v2.64+)
All rotate at 5 MB × 3 archives.
.license, .clock_status, .sentinel Beta-key bookkeeping (license validity, install-date integrity check). Hidden by their leading dot. Do not edit — tampering invalidates your beta access.

Installation folder (program files)

C:\Program Files\PassthroughForge\ — the binaries (EXE + OpenXR layer DLL + dependencies), installed by install.ps1. No user data lives here. Replaced on every version upgrade.

Files & StorageProfile file structure

A profile is a JSON document. The top level holds session-wide values (passthrough colour, global offsets from a Recenter), and a masks array holds one entry per mask in the order they stack from back to front in VR.

{
  "version": 1,
  "name":   "my-cockpit",
  "passthroughColor": [r, g, b],
  "positionOffset":   [x, y, z],          // global Recenter translation
  "rotationOffset":   [w, x, y, z],       // global Recenter quaternion
  "masks": [
    {
      "name":              "ND Display",
      "group":             "F-16 cockpit",
      "enabled":           true,
      "texWidth":          8192,
      "texHeight":         8192,
      "position":          [x, y, z],     // metres in OpenXR LOCAL space
      "rotation":          [w, x, y, z],  // quaternion
      "sizeMeters":        [w, h],
      "trackingTarget":    0,             // 0=World, 1=LeftCtrl, 2=RightCtrl
                                           //  (legacy 3/4 = old Mask Locker -> migrated to 1/2 on load)
      "renderOnTop":       false,         // force-on-top regardless of depth
      "mirrorOf":          -1,            // -1 standalone, else mirror source idx
      "gazeActivated":     false,
      "gazeAngleThreshold": 30.0,
      "pixels":            "<base64 RGBA>",
      "savedPolygon":      [ {x, y, bez, hix, hiy, hox, hoy}, ... ],

      // Optional, only present when applicable:
      "isDocumentMask":     true,
      "documentPages":      ["page1.png", "page2.png"],
      "currentDocumentPage": 0,
      "documentSourcePath": "C:\\...\\source.pdf",

      "source":             "window_capture" | "screen_region",
      "captureWindowTitle": "Chrome",
      "captureProcessName": "chrome.exe",
      "captureMonitorIndex": 0,
      "captureRegionX":      0,  "captureRegionY":      0,
      "captureRegionW":      0,  "captureRegionH":      0,

      "isDcsMessageMask":   true
    },
    ...
  ]
}

What each field controls

  • Where the mask sits in space: position (x, y, z in metres, OpenXR LOCAL coordinate space) and rotation (quaternion w, x, y, z).
  • How big it is in VR: sizeMeters [width, height]. Independent of texture resolution — a mask can be 2×2 m physical and 4K×4K texture.
  • Texture resolution: texWidth / texHeight. The actual pixel buffer is in pixels (Base64 RGBA, top-left origin).
  • Why it follows your controller / cockpit / head: trackingTarget. 0 = world-anchored (most common), 1/2 = real-time tracking bound to the left/right controller (the mask follows that controller live). Switching a mask from World to a controller keeps its current position (no jump). (Legacy values 3/4 from the old “Mask Locker” mode are migrated to 1/2 on load.)
  • Stack order (z-depth): determined by the array position — masks[0] renders first (furthest back), masks[N] last (on top). For document overlays that need to sit on top of passthrough cutouts even when geometrically further away, set renderOnTop: true to force a second-pass render.
  • Mirror link: mirrorOf — the index of the source mask whose pixels are copied + horizontally flipped each frame. -1 means independent.
  • Globally shifted by a Recenter event: the top-level positionOffset + rotationOffset (NOT per-mask values). Applied additively in the layer so individual mask coordinates stay untouched in the file.

Where the OpenXR layer itself stores its data: nowhere. The layer DLL (passthrough_forge_layer.dll) reads mask poses from a shared-memory channel the app writes every frame. It has no on-disk state of its own beyond its registration in the Windows registry under HKLM\SOFTWARE\Khronos\OpenXR\1\ApiLayers\Implicit.

Files & StorageBackup & share profiles

Because a profile is a single self-contained JSON file with every mask's pixels embedded, you can:

  • Back up all your work by copying the profiles\ folder to a USB stick. Restoring is the reverse copy.
  • Share a cockpit setup with another pilot by sending them a single <name>.json file. They drop it into their own %APPDATA%\PassthroughForge\profiles\ and pick it from the profile dropdown.
  • Migrate to a new PC by copying the entire %APPDATA%\PassthroughForge\ folder (profiles + settings + license). The fresh install on the new machine picks up everything automatically.

Tip for very large profiles: profile JSONs can easily reach several hundred MB because the pixel buffer for each mask is embedded as Base64. If you only want to share the layout (positions, sizes, names) without the pixels, you can strip them with a one-liner: python -c "import json; d=json.load(open('default.json')); [m.pop('pixels',None) for m in d['masks']]; json.dump(d, open('layout-only.json','w'), indent=2)" — the result is < 100 KB and easy to diff/version-control. The recipient still needs to fill in the pixels themselves.

TroubleshootingMask appears as a solid block (no passthrough)

You see the mask in VR but the area that should reveal the real world stays as a solid colour (typically the blue passthrough fill, or yellow when Trace VR is on). A small transparent border around the very edge of the quad suggests Virtual Desktop's general passthrough is working - it's specifically the chroma-key replacement that fails.

Why it happens:

Virtual Desktop streams the PC frame to the Quest through a lossy video codec (H.264 / H.265 / AV1). A pixel sent as (0, 0, 180) can arrive on the Quest as e.g. (0, 1, 178) after encode/decode. If VD's chroma-key Threshold is too tight, those near-misses are not recognised as passthrough and stay opaque. The drift is bigger on lower bitrates and varies between PCs depending on encoder, GPU and codec.

How to fix:

  1. Increase Similarity / Threshold in Virtual Desktop's chroma-key settings on the Quest: Settings → Streaming → Chroma Key. Two to three times the default value usually absorbs codec drift without leaking colours from your in-game scene.
  2. Raise the streaming bitrate on the Streamer app on your PC (200 Mbit+ over a strong 5 GHz Wi-Fi link). Higher bitrate = less compression = less colour drift.
  3. Switch to a maximally-saturated key colour. Magenta #FF00FF or pure green #00FF00 sit at the corners of the colour cube and barely move under compression. In PF: Settings → Passthrough Color → pick the preset, copy the new hex value, paste it as VD's chroma-key colour on the Quest.

Important: the chroma-key setting lives in the Virtual Desktop app on the Quest, not on the PC - so it's tied to the headset, not the streaming PC. Once configured for one PC it carries over to any other PC the Quest streams from.

TroubleshootingPink / blue rim around the mask edges

You can see the cockpit through the mask, but there's a coloured halo right at the edge - usually pink, sometimes blue, a few pixels wide.

Why it happens:

This is a Virtual Desktop limitation, not a PassthroughForge bug. VD's chroma-key replacement is per-pixel; at antialiased edges where the rendered pixel partially blends the passthrough colour with the surrounding image, the mixed pixel falls inside VD's chroma-key tolerance from one direction and outside from the other, producing a colour fringe.

How to reduce it:

  • Bump VD's chroma-key Smoothing / Edge feathering slider up - smooths the transition between keyed and un-keyed pixels.
  • Use a colour that contrasts strongly with the typical pixels next to the mask edge. Dark blue against a typically-grey cockpit produces a pink rim; magenta against a grey cockpit produces a green rim. Pick the colour whose complement is least visible in your sim.
  • Cannot be fixed in PassthroughForge - the mask antialiasing is the same on the PC side, the colour fringe is created on the Quest side during compositing.
  • HIDE it with a bezel overlay — the most effective workaround. See Bezel overlay workflow below for step-by-step instructions.

Visual PolishHide the chroma-key edge with a bezel overlay

The pink/blue chroma rim happens on the Quest side and can't be removed in PassthroughForge directly. What you can do is drop a second mask on top of the first that contains a cockpit-style bezel image with a transparent centre — the bezel covers the rim, the centre shows your passthrough cutout through it. From normal cockpit viewing angles this looks like a proper 3D frame around the instrument; from extreme side-views it goes flat (it's a 2D PNG), but cockpit ergonomics keep your head pointed roughly forward anyway.

Step 1. Make the bezel image (PNG with transparency)

Three ways to produce one, easiest first:

  1. From a DCS screenshot (15 minutes, no 3D skill needed). Take a high-resolution screenshot of your aircraft's cockpit with the instrument or panel you want to frame visible. Open it in GIMP (free), Photopea (free, browser-based), or Photoshop. Use the magic-wand or polygonal-lasso to select the area INSIDE the bezel (the screen / glass), then Edit → Clear so the inside becomes transparent. Crop to the bezel + a generous frame margin. Export as PNG with alpha channel.
    Tip: include 50–100 px of opaque frame around the cutout — that's the part that has to cover VD's chroma rim.
  2. From a real cockpit photo. Same workflow as above but with a photo. Works particularly well for fixed-glass instruments (MFDs, RWR, RMI) where the real-world frame matches what your eye expects to see in VR.
  3. 3D-render from Blender (1–2 hours, polished look). Model a simple ring/frame in Blender, set the render output to RGBA PNG, set Film → Transparent to true, place the camera so the frame is centred. Render. The result is a sharp clean bezel image with transparent inside and outside.
    Resolution rule of thumb: same as your target mask (1024×1024, 2048×2048, or 4096×4096 are good defaults).

Step 2. Import the bezel as a second mask in PassthroughForge

  1. Open PassthroughForge. Make sure the underlying passthrough mask (the one with the chroma-key cutout) is set up and working.
  2. Click "+" to add a new mask. Name it something like "<mask name> bezel" so you can tell them apart.
  3. In the editor, use File → Import PNG to load your bezel PNG. Confirm the dimensions match (PassthroughForge preserves aspect ratio).
  4. In the right-side panel, copy the underlying mask's position, rotation, and size values into this new bezel mask — they need to be co-located. Quickest way: select the original mask, write down the numbers; select the bezel, type them in.
  5. Make the bezel slightly larger than the underlying mask (e.g. +5 cm on each side) so the opaque frame extends past the chroma rim.
  6. In [V] Visibility & Activation tick Render on top. Without this the bezel will z-fight with the underlying mask — both layers are at the same depth.

Step 3. Fine-tune in VR

  • Put the headset on and check the bezel covers the rim from your typical seating position. If you still see pink, edit the PNG to widen the opaque margin and re-import.
  • If the bezel sits visibly in front of or behind the underlying mask (because their positions don't quite match), use the Z-slider on the bezel mask to shift it 1–2 cm closer to your eye — covers the parallax mismatch completely.
  • Don't make the bezel a passthrough mask itself. It's a regular image mask whose visible pixels stay opaque (the chroma-key colour should NOT appear in the bezel image).

Step 4. Save it

Once it looks right, hit Save Profile. The bezel mask is stored exactly like any other mask — you can share the profile with other testers and your bezel goes with it (the PNG pixels are embedded in profiles\<name>.json as Base64, see Profile file structure).

Why this works: Virtual Desktop's chroma-key replacement runs per-pixel on the rendered frame from your PC. The pink rim is generated where the chroma colour meets the surrounding image — that's outside our masked pixels. By rendering a second mask with normal (non-chroma) pixels just slightly further from your eye, you simply put an opaque opaque-coloured layer over the area where the rim would appear. VD never sees a chroma-key transition there anymore, so the rim doesn't get drawn.

TroubleshootingMask never appears in VR (no rectangle, no Trace polygon)

You see the cockpit normally but no mask at all - not even the solid blue or the Trace VR yellow polygon. Suggests the OpenXR API layer never loaded into your VR app's process.

Verify in this order:

  1. VC++ Redistributable: PassthroughForge depends on the Microsoft Visual C++ 2015-2022 Redistributable (x64). On a fresh PC it's often missing. Download: aka.ms/vs/17/release/vc_redist.x64.exe
  2. Layer registration: open PowerShell and run Get-ItemProperty "HKLM:\SOFTWARE\Khronos\OpenXR\1\ApiLayers\Implicit". You should see a line ending in passthrough_forge_layer.json with value 0. If it's missing, re-run install.ps1 as Administrator.
  3. Active OpenXR runtime: PassthroughForge needs the runtime that does chroma-key (Virtual Desktop), not Quest Link's runtime - Quest Link doesn't do chroma-key replacement at all. In Meta Quest Link or Virtual Desktop Streamer set the desired runtime as the default OpenXR runtime.
  4. SHM Active: at the bottom of the PassthroughForge desktop window, the status bar should show a green SHM Active badge. If it stays grey, the app couldn't create its shared memory - try running PassthroughForge as Administrator once.

TroubleshootingA controller-tracked mask drifts when the controller is still

Symptom: you set a mask to Real Time Tracking Left / Right Controller and mount the controller in your cockpit. After a short break the mask is no longer exactly where you put it, and a quick tap of the controller snaps it back into place.

Cause: Quest controllers are tracked by the headset's inside-out cameras looking at the controller's infrared (IR) LEDs. To save battery, a controller that sits still goes into standby after about 30 seconds and switches its tracking LEDs off — so the cameras can no longer see it, even if it is right in front of you. The runtime then falls back to the controller's internal motion sensor (IMU), whose pose drifts. The mask follows that pose live, so it drifts too. This is normal Quest hardware behaviour, not a PassthroughForge bug — a tap (real motion) wakes the controller, the LEDs come back on, and tracking is restored.

How to make it stable

  • Keep the controller in the headset cameras' field of view. In front of the headset works perfectly; tucked behind a monitor or under a keyboard does not.
  • Cover the proximity sensor on the headset (a small piece of tape works) so the headset itself doesn't go to standby.
  • If the mask drifts after a pause, just tap / nudge the bound controller — the LEDs come back on, the cameras re-acquire it, and the mask snaps back to the right cockpit position immediately.
  • To avoid sleep entirely: give the controller a little constant motion — e.g. mount it where it picks up some vibration (a bass-shaker rig, or a small vibration motor on the mount). A controller that never sits perfectly still never goes to standby, so it stays camera-tracked and the mask stays put on its own.

Also check Virtual Desktop: on the INPUT tab, “Automatically hide controllers” must be OFF, and on the STREAMING tab, “Forward tracking data to PC” must be ON — otherwise the controller pose never reaches the PC. See the Real-time controller tracking tutorial.

TroubleshootingA controller-tracked mask locks to my hand instead of the controller

Symptom: you set a mask to Real Time Tracking Left / Right Controller, put the controller down in your cockpit, and the mask doesn't stay there — instead it follows whichever hand you move around the cockpit (the steering hand, the throttle hand, your face when you scratch your nose).

Cause: hand tracking is enabled in your VR stack. Quest 3 + Virtual Desktop (and Meta's native OpenXR runtime) supports a controller-tracking fallback: as soon as a controller goes inactive (you put it down, it sleeps, the battery dies), hand tracking takes over and reports the hand pose through the exact same OpenXR action space the controller was using. From the app's perspective the pose is still "valid" and "from the controller" — the API deliberately hides the source. PassthroughForge sees a perfectly valid pose stream from the configured controller slot and locks the mask to it, just as designed.

How to fix it (~99 % of cases — pick whichever you have):

  • Virtual Desktop: Streamer settings → Streaming tab → turn Hand Tracking OFF. This is by far the most common source on Quest 3 + VD setups.
  • Meta Quest headset: Settings → Movement Tracking → set to "Controllers only" (or disable Hand Tracking entirely).
  • Also in the Meta Quest headset menu: the Body Tracking menu item under Movement Tracking must also be disabled. Body tracking synthesises hand / arm poses from upper-body inference and feeds them through the same OpenXR action spaces as the controllers, so leaving it on causes exactly the same mask-follows-the-hand symptom even after Hand Tracking is off.
  • SteamVR / other runtimes: look for a "controller fallback to hand tracking" toggle in the runtime settings and disable it.

Workaround if you want to keep hand tracking on: keep the controller actively held when you switch the mask to controller tracking — press any button to wake the controller out of standby. While the controller is awake, its pose wins over the hand-tracking fallback in the runtime.

Quick test to confirm hand tracking is the cause: hold the controller, press any button on it to make sure it's awake, then switch the mask to controller tracking. If the mask now correctly sticks to the controller — and only drifts to your hand after you put the controller down — hand tracking is definitely the culprit.

Why we can't detect this in PassthroughForge: our OpenXR API layer asks the runtime for exactly one thing per hand: /user/hand/<left|right>/input/grip/pose. The action set is bound strictly to controller interaction profiles (Touch, Touch Plus, Touch Pro, Index, Vive, WMR, Pico) — we never request hand-tracking poses. The problem is what the runtime does behind that request: when Hand Tracking (or Body Tracking) is enabled and your physical controller is not actively held, the runtime synthesises a hand pose from the headset cameras and writes it into the same grip-pose action space. From the app's side it looks like a normal, valid controller pose — same data type, same coordinate frame, same flags.

We tested the technically-correct discriminator — XR_SPACE_LOCATION_POSITION_TRACKED_BIT, which is supposed to mean "actively tracked, not inferred" — and on Quest 3 + VDXR + Body Tracking ON, the runtime sets that bit on both the real controller pose and the inferred hand pose. It isn't a reliable discriminator in practice, so an app-side fix would only mask the symptom on some setups and break others. The cleanest, headset-runtime-correct fix is to disable Hand / Body Tracking at the source — see the bullet list above.

TroubleshootingMask "swims" relative to the cockpit during fast head motion in a DCS mission

Symptom: you turn your head left/right and the mask appears to lag the world for a fraction of a second before settling. Effect is barely visible in the DCS main menu, but obvious inside a busy mission especially when DCS frame-time stutters.

Cause: PassthroughForge submits masks as OpenXR quad composition layers. When DCS hits a stable frame rate, the headset compositor reprojects the quad against your live head pose at scan-out and the mask sits rock-solid in the cockpit. When DCS frame time starts varying under mission load (planes, weather, dense terrain), the compositor catches the world layer with motion smoothing / Asynchronous SpaceWarp, but the mask layer can lag by a frame or two — that's the "swim".

Tester-validated fix — works in practice

Lowering your VR render resolution until DCS holds a stable frame rate eliminates the swim completely. In testing on Quest 3 + Virtual Desktop the mask went from visibly swimming to perfectly stationary just by stepping the render resolution one notch down. The exact setting depends on your hardware, but the rule of thumb is: if the world feels smooth without motion smoothing artefacts, the mask follows.

In detail — settings to try, in order

  • Drop the VR render resolution one notch. Virtual Desktop → Godlike / Ultra / High: try one step lower. Quest Link → Oculus Debug Tool encode resolution width: try a lower value. This is the highest-leverage change.
  • In DCS, drop Pixel Density to 1.0, turn off MSAA, set Shadows and Visibility Range to Low, reduce Preload Radius. The goal is a stable frame time, not maximum quality.
  • Quest Link (cable) is more stable than Virtual Desktop for the most demanding missions — less frame-time variance.
  • Smaller masks hide the effect better. A 30 cm panel showing only a control surface is far more forgiving than a 2 m×2 m cockpit overlay.

TroubleshootingThe Real Time Tracking selection won't stick (reverts to None)

Symptom: you pick Real Time Tracking Left / Right Controller in the Tracking Target dropdown, but it jumps straight back to None (World) — the selection is simply not accepted.

Cause: you are not in a running VR application. The tracking target is a live connection between your controllers and PassthroughForge, and that connection only exists while a VR environment is running. On the flat desktop with no VR app open, there is no controller pose to attach to, so PSF rejects the choice and falls back to None (World).

Fix: start DCS in VR (or any OpenXR/VR scene), put the headset on, make sure the controller is on and the L: / R: indicators are GREEN, and then choose the Real Time Tracking mode from inside VR. Now it sticks. See the Real-time controller tracking tutorial.

Still reverts even though you are in VR? Then it is almost always an OpenXR layer-ordering problem — another layer (often a motion-rig / motion-compensation layer) is grabbing the controller inputs before PassthroughForge. See Real-time controller tracking does nothing and move PassthroughForge to the top of the layer order.

TroubleshootingReal-time controller tracking does nothing

Symptom: a mask set to Real Time Tracking Left / Right Controller never follows the controller — it stays where it is, as if it were still world-anchored.

First, the basics: the controller must be powered on and tracked (held or in view of the headset). Controller tracking cannot follow a controller that's switched off or asleep on the desk — that alone explains most cases (tap it to wake it).

Then check Virtual Desktop — this is the most common cause. The controller pose only reaches the PC if VD is set up to forward it: INPUT tab → “Automatically hide controllers” OFF; STREAMING tab → “Track controllers” ON and “Forward tracking data to PC” ON (needs a game / SteamVR restart). See the Real-time controller tracking tutorial.

If the controller is on, tracked and forwarded and it still doesn't work, it's almost always another OpenXR API layer conflicting with PassthroughForge. Controller tracking needs PassthroughForge to attach its tracking action set and read controller poses; some other implicit layers interfere with that. Confirmed culprits from tester reports:

  • OpenXR-MotionCompensation / motion-rig layers (a motion platform can consume your controller inputs before PassthroughForge ever sees them)
  • OpenXR-Quad-Views-Foveated
  • OpenXR-Toolkit

Try this first — reorder, don't disable. In most cases you don't have to turn anything off: you just need PassthroughForge to sit at the TOP of the layer order. If a motion-compensation / motion-rig layer is above PSF, it grabs the controller inputs first and PSF gets nothing — so the tracking target won't engage. Open the OpenXR API Layers GUI (below), select passthrough_forge_layer.json, and click “Move Up” until it is on top of the list. Then restart your PC — the new layer order only takes effect after a full reboot, so closing DCS / PassthroughForge alone is not enough. This keeps your motion platform working and restores controller tracking — it is the fix that resolved this for most testers.

Easiest way — OpenXR API Layers GUI: a free tool by Fred Emmott that lists every active OpenXR layer in its real order and lets you enable / disable / reorder them with a click (no registry editing). Download it from github.com/fredemmott/OpenXR-API-Layers-GUI, run it (it needs admin rights), disable the suspect layer (e.g. Quad-Views-Foveated) or move PassthroughForge to the top, then restart your PC and try controller tracking. Any change to the layer list (enable / disable / reorder) only takes effect after a full reboot — not just an app restart. The tool also has a “Fix Them!” button for common layer problems. Don't disable the Virtual Desktop Streamer layer — that one is needed for VD.

Advanced / manual alternative: list your active OpenXR layers from a Command Prompt:

reg query "HKLM\SOFTWARE\Khronos\OpenXR\1\ApiLayers\Implicit"

Each entry is a path to a layer's .json with a value: 0 = enabled, 1 = disabled. To test, disable the suspect layers (admin Command Prompt), then restart your PC and try controller tracking (a registry change to the layer list only applies after a full reboot):

reg add "HKLM\SOFTWARE\Khronos\OpenXR\1\ApiLayers\Implicit" /v "C:\Program Files\OpenXR-Quad-Views-Foveated\openxr-api-layer.json" /t REG_DWORD /d 1 /f

Re-enable later by setting the value back to 0 (or use the tool's own enable/disable switch). Don't disable the Virtual Desktop Streamer layer — that one is needed for VD to work. If tracking returns with the suspect layer off, that layer was the conflict.

No controller at all? You can still place masks reliably as world-locked (recenter) or head-locked — those need no controller tracking.