# Phase 16: VRChat OSC Bridge - Context

**Gathered:** 2026-04-19
**Status:** Ready for planning

<domain>
## Phase Boundary

Deliver `beyond_backglow_ctl.exe` — a Windows Job-Object-tethered daemon process, launched by `DeviceProvider` at driver init, that receives VRChat avatar OSC parameters (per-LED color + global brightness) via OSCQuery + mDNS discovery and translates them into existing `backglow fill|bri|off|on` named-pipe commands to the driver. End-to-end pipeline: world contact senders → avatar contact receivers → unsynced avatar params → VRChat OSC out → daemon → pipe → `LedController` → `ILedTransport` → WS2812B LEDs.

Phase 16 covers: `VRCH-01` (driver-managed bridge daemon, refined — no separate enable toggle, driver-load drives daemon lifecycle) and `VRCH-02` (avatar float params → LED pipe commands, per-LED from day 1).

Phase 16 does **NOT** cover: `VRCH-03` (Unity avatar prefab with VRCContactReceiver components — Phase 17), `VRCH-04` (reference VRChat world with spatial light sources — Phase 17), runtime daemon kill-switch, non-VRChat OSC sources (Resonite/TouchOSC), JSON-configurable param→LED mapping, multi-headset sync, screen-sampling ambilight.

</domain>

<decisions>
## Implementation Decisions

### Daemon lifecycle
- **D-01:** **No separate "Enable Backglow" VRSettings toggle.** Driver being loaded = backglow attempts to function. Refines `VRCH-01` requirement wording and roadmap success criterion #1 — daemon spawn is driven by `DeviceProvider::Init` success, not a user-facing VRSettings boolean. Rationale: SteamVR's built-in "Startup/Shutdown → Manage Add-Ons" already toggles the entire Beyond Proximity driver; a second per-subsystem toggle is redundant UX.
- **D-02:** Daemon spawned at the end of `DeviceProvider::InitBackglow()` on the success path. If backglow enters the Phase 14 D-15 / Phase 15 D-03 degraded-disabled state (no transport, no COM port, DDP probe fails, etc.), the daemon is **NOT** spawned. Degraded state must never publish OSC traffic to a dead pipe.
- **D-03:** **Kernel-enforced parent-kill via Windows Job Object** with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. `CreateJobObject` → `SetInformationJobObject(JobObjectExtendedLimitInformation)` → `CreateProcess` + `AssignProcessToJobObject`. Job handle owned by `DeviceProvider`. Closing the handle (`DeviceProvider::Cleanup()` or vrserver.exe crash) kills the daemon. Prevents orphaned daemons holding OSC sockets after a vrserver crash.
- **D-04:** Crash recovery: watchdog thread on the driver side monitors the daemon process handle via `WaitForSingleObject`. On exit with non-zero status (or on any exit before graceful shutdown begins), relaunch with **exponential backoff 1s → 2s → 4s, capped at 3 respawns per session**. Uptime ≥ 60s resets the counter (treats the daemon as "stable"). After cap → log ERR once, stop respawning for the session.
- **D-05:** Daemon exe deployed alongside `BeyondProximity.dll` + `beyond_prox_ctl.exe` under the external driver directory (`.../Bigscreen Beyond Driver/bin/BeyondProximity/bin/win64/`). `scripts/deploy-backglow-dev.ps1` updated to copy the daemon.
- **D-06:** Graceful stop: `DeviceProvider::Cleanup()` first writes a sentinel exit command to the daemon (via stdin close / signal pipe — planner picks exact mechanism), waits a bounded timeout (~250 ms) for self-exit, then closes the Job Object handle so the kernel finishes the job. This order avoids hard-killing the daemon mid-UDP-send where possible, but the Job Object guarantees termination regardless.

### Avatar OSC parameter surface
- **D-07:** **Per-LED from day 1.** The entire purpose of backglow is emulating light reaching the face from different angles / intensities (user design intent, locked). Uniform-fill-only is rejected. 10 LEDs × 3 channels + 1 global brightness = **31 avatar parameters**.
- **D-08:** Parameter names (PascalCase, zero-based LED index):
  - `BackglowR0` … `BackglowR9` (10 floats)
  - `BackglowG0` … `BackglowG9` (10 floats)
  - `BackglowB0` … `BackglowB9` (10 floats)
  - `BackglowBri` (1 float)
  Daemon subscribes to OSC addresses `/avatar/parameters/Backglow[R|G|B][0-9]` and `/avatar/parameters/BackglowBri`.
- **D-09:** **All 31 parameters are declared `Synced = false`** on the avatar's Expression Parameters list (unsynced / local-only). Rationale: backglow is a local-user-only experience (only the wearer of the headset ever sees their own LEDs); no reason to burn synced-param bits. Unsynced params cost **0 bits** of the default 256-bit synced budget, leaving full budget available for VRCFaceTracking / eye tracking / AudioLink / PhysBones. Unsynced params retain full IEEE 754 float precision (no 8-bit quantization). World contact senders still drive them because contacts are computed locally per-client; `/avatar/parameters/*` OSC output reads local animator state regardless of sync flag.
  - **Avatar prefab contract (Phase 17) MUST explicitly document the `Synced = false` requirement.**
- **D-10:** Value ranges: all 31 floats treated as `[0.0, 1.0]`. Daemon clamps out-of-range values to `[0, 1]` before conversion. Bri = 0.0 is functionally "off" (drives `backglow bri 0`); no separate Enable bool.
- **D-11:** No master Enable bool, no per-LED enable bools. Avatar-side menu control is a float slider for `BackglowBri`; world-driven control happens through contact receivers writing to the per-LED R/G/B params. Zero-config for world creators.

### OSC → pipe mapping + throttling
- **D-12:** **Daemon coalesces OSC updates and emits pipe commands at ≈90 Hz (∼11 ms tick).** Overrides Phase 14 D-11 (33 ms / 30 fps LedController writer tick). Planner MUST retune `LedController::m_maxFps` default and cv-wait timeout to target 90 Hz. Bandwidth check: per-LED polymorphic `backglow fill` = "FFFFFF"×10 ≈ 70-byte pipe write; at 90 Hz = 6.3 KB/s over local named pipe — negligible. USB Adalight 36 B/frame × 90 Hz = 3.24 KB/s vs ~11.5 KB/s effective at 115200 baud = 28% bandwidth, fine. DDP UDP at 90 Hz also fine.
- **D-13:** Brightness routed as a **separate `backglow bri <0-255>` pipe command** — daemon computes `round(BackglowBri * 255)` and emits `backglow bri N` whenever the integer value changes. Color params keep full dynamic range. Driver-side `LedController` ceiling clamp (Phase 14 D-12, default 50/255) remains authoritative and is not duplicated in the daemon.
- **D-14:** Per-LED color dispatch: each 90 Hz tick the daemon computes the full 30-byte RGB frame (10 × 3 bytes) via `round(channel * 255)` per float, assembles 10 hex strings, and sends one polymorphic `backglow fill <h0> <h1> … <h9>` pipe command (Phase 14 D-03 per-LED branch). If the 30-byte frame is byte-identical to the last sent frame → skip the tick (8-bit LED dedup; epsilon handled by quantization to uint8).
- **D-15:** **Silence fade.** If no OSC packet of any kind arrives for **3 seconds**, daemon begins a linear ramp from current `BackglowBri` integer value down to 0 over ~500 ms, emitting `backglow bri N` at 90 Hz tick rate. On hitting 0 → emit `backglow off`. Arrival of any new OSC packet **before** the ramp reaches 0 cancels the ramp and resumes normal forwarding from the new OSC values. Rationale: backglow is meant to reflect the *current* virtual environment; stale LEDs when VRChat is closed / paused would be distracting (user's design intent, locked).
- **D-16:** **Startup animation.** On daemon launch and successful pipe connect, daemon emits a scripted ~4-second sequence: `backglow fill FFFFFF FFFFFF … ×10` once, then linear `backglow bri` ramp from 0 → 128 over 1 s (white fades up to 50% brightness), hold briefly, then linear ramp 128 → 0 over 3 s (white fades out), then `backglow off`. After animation completes, daemon idles until first real OSC arrives. Serves as a visible "daemon connected, pipe working" confirmation for the user.
- **D-17:** **Hardcoded param→LED mapping in daemon for Phase 16.** No JSON config file. The mapping is a static table: `{BackglowR|G|B}{N}` → `frame[N].{r|g|b}`. Custom mappings (e.g. per-world themes) are a post-v3.0 ergonomics concern.

### OSC transport + discovery
- **D-18:** **VRChat OSCQuery + mDNS advertise** is the primary OSC receive path. Daemon starts:
  1. A small embedded HTTP server responding to `GET /` (OSCQuery host info) and `GET /{param-path}` (per-param metadata) per the OSCQuery proposal VRChat implements.
  2. An mDNS-SD advertisement for `_oscjson._tcp` (OSCQuery service) and `_osc._udp` (OSC receive endpoint) pointing at dynamically-bound localhost ports.
  VRChat on startup enumerates mDNS OSCQuery services and dispatches OSC to each discovered service's registered UDP port. Coexists cleanly with VRCOSC, OpenShock, VRCFaceTracking, and any other OSCQuery-aware tooling.
- **D-19:** **Fallback to fixed UDP 9001** if OSCQuery / mDNS setup fails (mDNS responder unavailable, firewall blocks HTTP server, etc.). Daemon logs WARN and attempts a classic legacy bind on `127.0.0.1:9001`. If 9001 is already bound by another tool → log ERR + `exit(1)`. Driver's respawn-with-backoff (D-04) then hits its cap and gives up for the session. User remediation: kill the conflicting tool and restart SteamVR.
- **D-20:** **All UDP and HTTP binds loopback-only (`127.0.0.1`).** No `0.0.0.0` exposure. No remote OSC. VRChat always sends to localhost; there is no use case for LAN OSC. Avoids Windows firewall prompts on first run and eliminates remote attack surface.
- **D-21:** OSC message filter: daemon ignores (drops silently) any OSC address that is not `/avatar/parameters/Backglow*`. Malformed OSC (short packet, bad bundle framing) dropped silently. A counter per drop reason kept in-memory for a future `backglow status` daemon-side metrics extension (out of scope for Phase 16).
- **D-22:** mDNS + OSCQuery library choice = Claude's Discretion. Candidates: Windows built-in DNS-SD API (`dnsapi.dll`, `DnsServiceRegister`), `mjansson/mdns` (single-header, public domain), or Bonjour SDK (Apple, heavier runtime dep). OSCQuery HTTP server: a minimal handler built directly on Winsock, or a tiny embedded HTTP lib. Chosen implementations must be header-only or permissively licensed (match oscpp / ISC precedent).

### File layout (delta on Phases 14-15)
- **D-23:** New source tree `src/backglow_ctl/`:
  - `main.cpp` — daemon entry, argv parsing, top-level startup/shutdown.
  - `osc_server.h/.cpp` — OSC UDP listener + `oscpp` message parsing + OSCQuery HTTP responder.
  - `mdns_advertise.h/.cpp` — mDNS-SD service registration + lifecycle.
  - `param_map.h/.cpp` — in-memory per-LED RGB state + global Bri + OSC-address → slot dispatch.
  - `pipe_client.h/.cpp` — named-pipe writer to `\\.\pipe\beyond_proximity_ctl`, connect/reconnect loop, hex frame formatting.
  - `startup_anim.h/.cpp` — scripted 4 s startup ramp (D-16).
  - `silence_fade.h/.cpp` — 3 s silence detection + 500 ms bri ramp-to-zero (D-15). (May live inside `osc_server` or `param_map`; separation is planner's call.)
- **D-24:** `src/driver/device_provider.cpp` gains a `SpawnBackglowDaemon()` path: `CreateJobObjectW` + `SetInformationJobObject` + `CreateProcessW` (daemon exe path resolved relative to the driver DLL's own path via `GetModuleFileName`) + `AssignProcessToJobObject`. Watchdog thread tracks daemon handle and respawn backoff (D-04).
- **D-25:** `CMakeLists.txt` adds new target `beyond_backglow_ctl` (executable, links oscpp + Winsock + WinHTTP/dns api / mdns). Entire target wrapped in the existing `ENABLE_BACKGLOW` compile flag (Phase 14 D-22) so builds without backglow still succeed.
- **D-26:** `LedController::m_maxFps` default retuned from 30 → 90 (D-12). Phase 14 D-11 writer-thread 33 ms cv-wait reduced to ~11 ms. Planner verifies this does not starve the proximity reader thread or cause serial-write contention on sustained-red traffic.

### Claude's Discretion
- mDNS library choice (Windows DNS-SD vs header-only `mdns.h` vs other). D-22.
- OSCQuery HTTP port allocation strategy (bind port 0 → OS-assigned vs probe 9000+N).
- Exact daemon log sink: stderr-only vs rolling file (e.g. `%LOCALAPPDATA%\Beyond Backglow\daemon.log`).
- Watchdog thread ownership (new thread on DeviceProvider vs reuse of existing pipe-poll loop in RunFrame).
- Graceful-exit signaling mechanism between driver and daemon (stdin close vs WM_CLOSE on a hidden window vs a dedicated "ctl" named pipe vs a Windows Event handle).
- Exact startup-anim easing curve (linear suggested; ease-in-out acceptable if it looks better).
- Silence-fade timer implementation (wall-clock vs performance counter; monotonic).
- `oscpp` receive buffer size (planner tunes; 2 KB default likely sufficient).
- Pipe-client reconnect cadence if the driver pipe server briefly disappears.
- Packaging of daemon's OSCQuery JSON endpoint payload structure beyond the minimum VRChat requires.

### Folded Todos
None. The two pending todos (`2026-03-26-explore-input-system-click-handle-probing-on-hmd.md`, `2026-03-23-milestone-2-0-live-ipd-change-and-steamvr-slider.md`) are unrelated to VRChat OSC scope and have been reviewed-but-deferred in every Phase 14/15 context; same disposition here.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Project / Roadmap / Requirements
- `.planning/PROJECT.md` — driver architecture, key decisions log, v3.0 milestone target features
- `.planning/REQUIREMENTS.md` — `VRCH-01`, `VRCH-02` mapped to Phase 16 (wording refined by D-01 — no separate toggle)
- `.planning/ROADMAP.md` §"Phase 16: VRChat OSC Bridge" — goal + success criteria (criterion #1 refined by D-01)
- `.planning/STATE.md` — current progress, outstanding concerns

### Prior Phase Context
- `.planning/phases/14-usb-serial-foundation-and-led-control/14-CONTEXT.md` — **all Phase 14 decisions remain in force**: pipe command surface (D-01..D-07), hex format (D-02), polymorphic fill (D-03), brightness ceiling (D-12), pipe routing (D-21). Phase 16 D-12 / D-26 **override** Phase 14 D-11's 30 fps writer tick to 90 fps.
- `.planning/phases/14-usb-serial-foundation-and-led-control/14-RESEARCH.md` — Adalight framing, COMMTIMEOUTS, reconnect pattern (carries unchanged)
- `.planning/phases/14-usb-serial-foundation-and-led-control/14-SMOKE.md` — USB hardware UAT template
- `.planning/phases/15-wifi-ddp-fallback-and-transport-selection/15-CONTEXT.md` — Phase 15 decisions (D-01..D-19). Phase 16 respects degraded-disabled gating (D-03) and init-only config pattern (D-02).
- `.planning/phases/15-wifi-ddp-fallback-and-transport-selection/15-SMOKE.md` — WiFi/DDP hardware UAT template
- `.planning/phases/15-wifi-ddp-fallback-and-transport-selection/15-VERIFICATION.md` — completion evidence

### v3.0 Milestone Research
- `.planning/research/SUMMARY.md` — recommended stack + top risks; `oscpp` ISC header-only OSC parser already selected
- `.planning/research/ARCHITECTURE.md` §5 — "VRChat Control Daemon: SEPARATE Process", full daemon architecture rationale; §6 Path B — avatar param → OSC → daemon → pipe → driver full pipeline; §7 Build Order table (item 2)
- `.planning/research/FEATURES.md` §"Confirmed Architecture: Avatar OSC Bridge Pattern" — world→avatar-param→OSC bridge idiom; world-Udon-OSC gaps; reference projects (Patstrap, OpenShock, GiggleTech)
- `.planning/research/PITFALLS.md` — OSC-in-driver anti-pattern (reinforces separate-process D-03); vrserver crash containment
- `.planning/research/STACK.md` — `oscpp` usage notes

### Existing Codebase (integration anchors)
- `src/driver/device_provider.h/.cpp` — `InitBackglow`, `HandleBackglowCommand`, hotplug `RegisterDeviceNotification`, pipe server setup at line ~420, `Cleanup` at line ~152, `WSAStartup`/`WSACleanup` ordering (line ~175)
- `src/driver/device_provider.cpp:1041` — HMDUtility `CreateProcess` + stdin-close + poll-for-exit reference pattern (adapt for daemon graceful-exit D-06)
- `src/led/led_controller.h/.cpp` — `m_maxFps` (retune to 90 per D-26), writer thread cv-wait
- `src/ctl/main.cpp` — `beyond_prox_ctl.exe` pipe client pattern (reference for `pipe_client.cpp` D-23)
- `CMakeLists.txt` — existing `ENABLE_BACKGLOW` flag wiring; add `beyond_backglow_ctl` target here

### External Specs / Docs
- VRChat OSC overview: https://docs.vrchat.com/docs/osc-overview
- VRChat OSC Avatar Parameters: https://docs.vrchat.com/docs/osc-avatar-parameters
- VRChat OSCQuery: https://docs.vrchat.com/docs/osc-query
- VRChat avatar Expression Parameters / synced flag: https://creators.vrchat.com/avatars/expression-menu-and-controls
- VRChat Contacts (Contact Sender / Receiver): https://creators.vrchat.com/common-components/contacts/
- OSCQuery Proposal (Vidvox): https://github.com/Vidvox/OSCQueryProposal
- Open Sound Control 1.0 spec: https://opensoundcontrol.stanford.edu/spec-1_0.html
- `oscpp` header-only OSC library (ISC): https://github.com/kaoskorobase/oscpp
- Windows DNS-SD API (`DnsServiceRegister`): https://learn.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsserviceregister
- `mjansson/mdns` single-header mDNS implementation: https://github.com/mjansson/mdns
- Windows Job Objects (`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`): https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
- `CreateJobObject` / `AssignProcessToJobObject`: https://learn.microsoft.com/en-us/windows/win32/api/jobapi2/nf-jobapi2-assignprocesstojobobject
- Reference bridge projects (avatar OSC → hardware pattern):
  - Patstrap: https://github.com/danielfvm/Patstrap
  - OpenShock ShockOSC: https://wiki.openshock.org/guides/shockosc/avatar-setup-vrc
  - VRChat OSCmooth (OSCQuery consumer): https://github.com/regzo2/VRCOSC

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- **Named pipe server** (`device_provider.cpp:420`): `\\.\pipe\beyond_proximity_ctl` already accepts `backglow fill|set|bri|off|on|status`. Daemon is a pipe *client*; zero server-side work beyond the tick-rate retune (D-26).
- **Polymorphic `backglow fill`** (Phase 14 D-03): 10-hex variant consumed directly by daemon (D-14). No new pipe command needed for per-LED output.
- **`LedController::m_maxFps`** (Phase 14): already a tunable; retune to 90 (D-26). Writer thread semantics unchanged.
- **`DeviceProvider::InitBackglow` success path** (`device_provider.cpp:1332`): already owns a well-defined "backglow ready" moment — precisely where `SpawnBackglowDaemon()` hooks in (D-02).
- **HMDUtility `CreateProcess` + stdin-close + `Sleep(1)` exit-poll pattern** (`device_provider.cpp:1041`): direct reference for daemon graceful-stop (D-06). Replace stdin-write with a daemon-specific exit signal mechanism, reuse the handle-wait + TerminateProcess fallback skeleton.
- **Degraded-disabled state** (Phase 14 D-15 / Phase 15 D-03): already wired. D-02 leverages it — if backglow is disabled, `SpawnBackglowDaemon()` never runs.
- **`WSAStartup` / `WSACleanup` in driver** (`device_provider.cpp` lines 5, 175): already in place for Phase 15 DDP UDP. Daemon process runs its own Winsock lifecycle (independent of driver's) — no coupling.

### Established Patterns
- **Pattern 1 — compile-time feature flag `ENABLE_BACKGLOW`**: wraps `src/backglow_ctl/` sources and the daemon target (D-25). Mirrors Phase 14 D-22.
- **Pattern 2 — sidecar process over named pipe**: `beyond_prox_ctl.exe` establishes the precedent. Daemon is a long-lived cousin: pipe client, fire-and-forget sends, reconnect on server drop.
- **Pattern 3 — separate-process-per-command (HMDUtility)**: NOT applicable to the daemon itself (daemon is long-lived), BUT applicable to the *graceful-exit handshake* (D-06).
- **Pattern 4 — init-only config reads**: Phase 14 D-14 (`com_port`), Phase 15 D-02 (`transport`). D-01 extends the pattern by declining to add any new VRSettings key for Phase 16 entirely.
- **Pattern 5 — atomic-state + background thread**: daemon internally uses atomic RGB + Bri state written by OSC listener thread, read by 90 Hz pipe writer thread. Mirrors `LedController` + `HidDevice` writer-thread idiom.

### Integration Points
- **`DeviceProvider::InitBackglow()` success tail** — add `SpawnBackglowDaemon()` call (D-02, D-24).
- **`DeviceProvider::Cleanup()`** — add graceful-stop + Job Object close **before** `WSACleanup()` (Phase 15 lines 169-175) and before `CleanupDriverLog()`.
- **DeviceProvider member additions** — Job Object handle, daemon process handle, watchdog thread handle, respawn-attempt counter.
- **`CMakeLists.txt`** — new `beyond_backglow_ctl` executable target; link `oscpp` (header-only, no actual link), `ws2_32.lib`, `winhttp.lib` (or equivalent for OSCQuery HTTP), `dnsapi.lib` (if using Windows DNS-SD) or inline `mdns.h`.
- **`scripts/deploy-backglow-dev.ps1`** — copy `beyond_backglow_ctl.exe` to external driver `bin/win64/` folder alongside `BeyondProximity.dll` and `beyond_prox_ctl.exe` (D-05).
- **Installer (`installer/`)** — include daemon exe in Inno Setup manifest (match Phase 9 installer pattern).

### Process Lifecycle Boundary
Daemon has **NO** OpenVR SDK dependency. Pure Win32 (Winsock UDP + WinHTTP/WinINet + named pipe client + Job Object). Crashes in the daemon do not affect vrserver.exe; proximity + IPD keep working. This is the single most important architectural boundary of Phase 16 — locked in research ARCH §5 and reinforced here.

</code_context>

<specifics>
## Specific Ideas

### User design intent: backglow = always-present ambient light
> "Assume that the backglow driver will be constantly controlling the LEDs over the course of an entire VR session. They don't just time out, they are an ever-present representation of the light in the virtual environment."

Key implication: the daemon is *always* the authority on LED state while VRChat is active. Once silence-fade (D-15) completes and LEDs turn off, they only resume when OSC traffic returns. No idle animation, no demo mode, no "tried to draw attention" timeout.

### User design intent: per-LED is the product, not a luxury
> "The whole point is being able to emulate how light reaches the face at different angles and intensities."

Uniform-fill-only was considered as a stepping stone and rejected. Phase 16 ships with full per-LED fidelity so Phase 17's avatar prefab can demonstrate the value proposition immediately.

### Unsynced-param insight (D-09)
This is the single most consequential engineering decision in the phase. Synced avatar params carry an 8-bit quantized payload per float and chew through the 256-bit default budget. Unsynced (local-only) params cost **zero** synced bits and preserve full IEEE 754 precision. OSC output exports both synced and unsynced param state — unsynced params work for local-user-only use cases like this one. The avatar prefab in Phase 17 MUST document this requirement; a world creator who copies the BackglowR/G/B pattern but leaves `Synced = true` will exhaust the budget and break any other avatar feature that needs sync (face tracking especially).

### 90 Hz overrides research's "30 fps is imperceptible"
Research ARCHITECTURE §4 reads:
> "VRChat OSC can fire at 90Hz but LED updates above 30fps are imperceptible and waste USB bandwidth."

User explicitly overrides this with D-12. Intent: LEDs should match HMD refresh rhythm so ambient lighting feels "attached" to head motion, not visibly frame-stepped. Bandwidth budget (D-12) confirms 90 Hz is trivial across both USB Adalight and DDP transports. Phase 14 D-11's 30 fps writer tick is retuned by D-26.

### Camera stream available for agent-in-the-loop UAT
Stream URL (view-only): `https://vdo.ninja/?view=JYMW97gq`. Carries forward from Phases 14 and 15 — agent can ask the user to enable the stream during Phase 16 SMOKE UAT (verify startup animation visible, verify per-LED color changes from simulated OSC, verify silence-fade ramp). Ask before assuming it's on. Particularly useful here because all success criteria are visual.

### OSCQuery is the correct path because other bridges also use 9001
OpenShock, VRCOSC, VRCFaceTracking bridges, avatar compression tools — all send/receive OSC with VRChat. A user with any one of these already installed will collide on fixed UDP 9001. OSCQuery + mDNS is how VRChat itself resolves this for multi-client operation; we match the ecosystem rather than demanding exclusive 9001 ownership. Legacy fallback (D-19) exists for dev-time / solo-tool configurations.

### Startup animation as connectivity smoke test
The 4-second white fade (D-16) is not decorative — it's a mandatory on-headset visual indicator that:
1. Driver loaded and `InitBackglow` succeeded,
2. Daemon process spawned,
3. Daemon connected to the pipe,
4. Full path from daemon → pipe → LedController → transport → ESP32 → LEDs is functional.
When a user reports "backglow not working", the first diagnostic question is: "Did you see the white fade at SteamVR startup?" This replaces needing `backglow status` daemon-side telemetry in Phase 16.

</specifics>

<deferred>
## Deferred Ideas

### Phase 17 scope
- **Unity avatar prefab** with VRCContactReceiver components pre-wired to `BackglowR0..9`/`G0..9`/`B0..9` + `BackglowBri`, `Synced = false` flag documented (`VRCH-03`).
- **Reference VRChat world** with spatial colored light zones driving avatar contacts through contact senders (`VRCH-04`).
- **Avatar expression menu** slider control for `BackglowBri` (user manual override).

### Post-v3.0 ergonomics
- **Runtime daemon kill-switch** — e.g. a `backglow daemon off` pipe command that doesn't require SteamVR restart. Rejected for Phase 16 (D-01: driver-load drives lifecycle).
- **JSON config file for custom param→LED mapping** — rejected for Phase 16 (D-17, hardcoded). Revisit if world creators want theme-specific param layouts.
- **`backglow status` daemon metrics extension** — OSC messages/sec, drop count, last-param-received timestamp. Counter already kept in-memory per D-21 but not surfaced on the pipe.
- **Non-VRChat OSC sources** — Resonite / TouchOSC / custom senders. Architecture already enables because OSC is source-agnostic, but only VRChat param paths (`/avatar/parameters/Backglow*`) are whitelisted by D-21.
- **Multi-headset sync** — `ECOS-02`; several users with backglow sharing a scene. Orthogonal to Phase 16 (each user's daemon is independent).
- **Screen-sampling / ambilight mode** — `ADVN-01`; drives LEDs from desktop content when VRChat not running. Completely separate code path, post-v3.0.
- **Preset / effect library** — `ADVN-02`; named configs (campfire, underwater, alert). Not in Phase 16.
- **Avatar-menu self-control** — `ADVN-03`; implemented organically in Phase 17 via `BackglowBri` slider; extended palette / mode switches are post-v3.0.
- **OSCQuery parameter discovery advertising the full backglow param namespace** — Phase 16 advertises only that the daemon exists and receives OSC. Advertising the exact param schema (so other OSCQuery inspectors can auto-discover `BackglowR0..9`) is a nice-to-have for ecosystem interop.

### Rejected during discussion
- **Separate "Enable Backglow" VRSettings toggle** — D-01 rejects it. Driver load is the enable signal.
- **Uniform-fill-only mapping** — rejected in favor of per-LED from day 1 (D-07, D-11).
- **8-bit synced-param encoding** — rejected in favor of unsynced full-precision (D-09).
- **RGB-packed-into-one-float synced param** — rejected once VRChat's 8-bit synced precision was understood. D-09 supersedes.
- **Fixed 9001 + fail-if-busy for Phase 16** — rejected in favor of OSCQuery first-class (D-18) with fixed-9001 as fallback (D-19).
- **Timeout-based silence → instant off** — rejected; user wants slow fade (D-15) because LEDs abruptly going dark would feel more jarring than a 500 ms ramp.
- **Startup state = "send nothing, leave untouched"** — rejected in favor of visible startup animation (D-16) for connectivity smoke.
- **Brightness pre-multiplied into RGB on daemon side** — rejected; D-13 keeps `backglow bri` as a distinct pipe command so the driver-side ceiling remains authoritative.

### Reviewed todos (not folded)
- `2026-03-26-explore-input-system-click-handle-probing-on-hmd.md` — proximity / input-component exploration; unrelated to OSC bridge scope.
- `2026-03-23-milestone-2-0-live-ipd-change-and-steamvr-slider.md` — already shipped in v2.0; stale pending file, cleanup owned separately.

</deferred>

---

*Phase: 16-vrchat-osc-bridge*
*Context gathered: 2026-04-19*
