// Phase 15: MUST appear before any windows.h include or legacy // winsock.h symbols leak in via the implicit windows.h pull. Project headers // (device_provider.h pulls windows.h) are included AFTER winsock2.h. #ifdef ENABLE_BACKGLOW #include // WSAStartup, WSACleanup, sockets — ALWAYS before windows.h #include // inet_pton, inet_ntop #endif #include "device_provider.h" #include "driverlog.h" #include "../hid/hid_device.h" #include "../hid/user_signature.h" #include "../hid/proximity_algorithm.h" #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_BACKGLOW #include "../led/led_controller.h" #include "../led/led_transport.h" #include "../led/wled_serial.h" #include "../led/wled_ddp.h" #include "../led/com_port_scan.h" #include #include #include // GUID_DEVINTERFACE_COMPORT #endif static const char* kSettingsSection = "driver_BeyondProximity"; static constexpr float kPi = 3.14159265358979323846f; // Constructor and destructor must be defined here where HidDevice is a // complete type (unique_ptr requires complete type for deletion/construction) DeviceProvider::DeviceProvider() = default; DeviceProvider::~DeviceProvider() = default; vr::EVRInitError DeviceProvider::Init( vr::IVRDriverContext* pDriverContext ) { // MUST be called before any VR* functions VR_INIT_SERVER_DRIVER_CONTEXT( pDriverContext ); InitDriverLog( vr::VRDriverLog() ); DriverLog( "Beyond Proximity driver initializing\n" ); // Read configuration from steamvr.vrsettings vr::EVRSettingsError settingsErr; int32_t reportRateMs = vr::VRSettings()->GetInt32(kSettingsSection, "report_rate_ms", &settingsErr); if (settingsErr != vr::VRSettingsError_None) reportRateMs = 200; if (reportRateMs < 50) reportRateMs = 50; if (reportRateMs > 5000) reportRateMs = 5000; int32_t logVerbosity = vr::VRSettings()->GetInt32(kSettingsSection, "log_verbosity", &settingsErr); if (settingsErr != vr::VRSettingsError_None) logVerbosity = 0; if (logVerbosity < 0) logVerbosity = 0; if (logVerbosity > 1) logVerbosity = 1; int32_t movingAvgLength = vr::VRSettings()->GetInt32(kSettingsSection, "moving_avg_length", &settingsErr); if (settingsErr != vr::VRSettingsError_None) movingAvgLength = 8; if (movingAvgLength < 1) movingAvgLength = 1; if (movingAvgLength > 64) movingAvgLength = 64; m_logVerbose = (logVerbosity > 0); m_reportRateMs = reportRateMs; m_movingAvgLength = movingAvgLength; DriverLog("Config: report_rate_ms=%d, log_verbosity=%d, moving_avg_length=%d\n", reportRateMs, logVerbosity, movingAvgLength); // Initialize HIDAPI library if (hid_init() != 0) { DriverLog("HID: hid_init() failed\n"); // Continue without HID -- graceful degradation } else { // Create HidDevice and start reader thread (handles open/reconnect internally) m_pHidDevice = std::make_unique(movingAvgLength, m_logVerbose); DriverLog("HID: Starting reader thread (rate=%ums)\n", reportRateMs); m_pHidDevice->StartReading(0x35BD, 0x0101, static_cast(reportRateMs)); } #ifdef ENABLE_BACKGLOW // Phase 15: WSAStartup pairing for DDP transport. Must precede InitBackglow // so WledDdpTransport's socket calls succeed. Failure is non-fatal — // backglow falls into degraded mode but driver init proceeds (LHWD-03). { WSADATA wsa{}; if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { DriverLog("Backglow: WSAStartup failed (err=%d) -- DDP transport unavailable\n", ::WSAGetLastError()); m_bWsaInitialized = false; } else { m_bWsaInitialized = true; } } // Phase 14: Initialize backglow LED subsystem (VRSettings, LedController, hotplug) InitBackglow(); #endif // Create named pipe for debug control CreatePipeServer(); #ifdef ENABLE_BACKGLOW // Phase 16 VRCH-01 fix: dedicated daemon pipe (see header comment). Must // exist BEFORE InitBackglow's success-tail SpawnBackglowDaemon() so the // daemon's 100 ms * 50 retry window can find it. But InitBackglow was // already called above (line ~110). That's fine: daemon's retry covers a // ~5 s window from spawn, we create the pipe within microseconds of // returning from CreatePipeServer. Order is: // 1. InitBackglow opens transport, eventually calls SpawnBackglowDaemon // 2. SpawnBackglowDaemon CreateProcess returns; daemon begins retry // 3. (back in Init) CreatePipeServer creates CLI pipe // 4. (here) CreateDaemonPipeServer creates daemon pipe (<1 ms later) // 5. Daemon's next retry (within 100 ms) succeeds. CreateDaemonPipeServer(); #endif // Read initial IPD from HMD container (HARD-02) { vr::PropertyContainerHandle_t hmdProps = vr::VRProperties()->TrackedDeviceToPropertyContainer(vr::k_unTrackedDeviceIndex_Hmd); vr::ETrackedPropertyError propErr; float ipd = vr::VRProperties()->GetFloatProperty(hmdProps, vr::Prop_UserIpdMeters_Float, &propErr); if (propErr == vr::TrackedProp_Success && ipd > 0.0f) { m_fCurrentIpd = ipd; #ifdef ENABLE_IPD_PERSIST m_fStartupIpd = ipd; #endif DriverLog("IPD: Initial IPD from HMD container: %.4fm (%.1fmm)\n", ipd, ipd * 1000.0f); } else { DriverLog("IPD: No initial IPD available from HMD container (err=%d)\n", propErr); } } // Attempt eager lighthouse config load (rotation matrix cache) // Note: HID reader thread may not have tracking serial yet (async), // so this may fail -- lazy load on first ipd command will retry if (m_pHidDevice) { if (!LoadLighthouseConfig()) { DriverLog("IPD: Eager config load deferred -- serial not yet available from HID\n"); } } DriverLog( "Beyond Proximity driver initialized successfully\n" ); return vr::VRInitError_None; } void DeviceProvider::Cleanup() { #ifdef ENABLE_IPD_PERSIST PersistIpdToConfig(); // D-01: persist on shutdown, before log cleanup #endif DestroyPipeServer(); #ifdef ENABLE_BACKGLOW // Phase 16 VRCH-01 D-06: stop the daemon BEFORE the LedController pipe // server starts tearing down, so the daemon stops issuing pipe writes // before we close the server side. 3-phase shutdown: close stdin (EOF // -> graceful exit) -> 250 ms bounded wait -> Job Object close (force-kill). StopBackglowDaemon(); DestroyDaemonPipeServer(); // Phase 14 (LHWD-03): shut LEDs off before any other teardown so a lingering // writer never sees a half-torn-down transport. ShutdownAllOff joins the // writer thread and sends a final all-black Adalight frame + {"on":false}. if (m_pLedController) { m_pLedController->ShutdownAllOff(); m_pLedController.reset(); } StopHotplugWatcher(); // Phase 15: WSACleanup is LAST — after the LedController writer thread is // joined inside ShutdownAllOff and the hotplug worker thread is joined in // StopHotplugWatcher, so any winsock-dependent transport teardown has // completed and no SetupAPI / socket call can race against DLL detach // (Pitfall 7 loader-lock mitigation). if (m_bWsaInitialized) { ::WSACleanup(); m_bWsaInitialized = false; } #endif if (m_pHidDevice) m_pHidDevice->StopReading(); // Join reader thread first m_pHidDevice.reset(); // Then destroy HidDevice hid_exit(); // Then finalize HIDAPI CleanupDriverLog(); } const char* const* DeviceProvider::GetInterfaceVersions() { return vr::k_InterfaceVersions; } void DeviceProvider::TryCreateProximityComponent() { if (m_bProximityComponentAttempted) return; vr::PropertyContainerHandle_t hmdProps = vr::VRProperties()->TrackedDeviceToPropertyContainer(vr::k_unTrackedDeviceIndex_Hmd); if (hmdProps == vr::k_ulInvalidPropertyContainer) return; // HMD not ready yet — try again next frame m_bProximityComponentAttempted = true; vr::EVRInputError err = vr::VRDriverInput()->CreateBooleanComponent( hmdProps, "/proximity", &m_hProximityComponent); if (err == vr::VRInputError_None) { DriverLog("Proximity: Created /proximity component on HMD (handle=%llu)\n", (uint64_t)m_hProximityComponent); } else { DriverLog("Proximity: CreateBooleanComponent failed (err=%d), using property fallback\n", (int)err); m_hProximityComponent = vr::k_ulInvalidInputComponentHandle; } } void DeviceProvider::CheckHmdSerial() { if (m_bHmdSerialChecked) return; if (!m_pHidDevice) return; CalibrationData cal = m_pHidDevice->GetCalibration(); if (cal.hmd_serial[0] == '\0') return; // not yet read from HID m_bHmdSerialChecked = true; m_bIsBeyond1 = (strncmp(cal.hmd_serial, "BS1", 3) == 0); DriverLog("HMD Serial: %s -> %s\n", cal.hmd_serial, m_bIsBeyond1 ? "Beyond 1 (IPD disabled)" : "Beyond 2 (IPD enabled)"); } bool DeviceProvider::ApplyIpd(float mm) { if (mm < 48.0f || mm > 75.0f) return false; float ipdMeters = mm / 1000.0f; // Loop guard (per D-07): skip if already at this IPD if (fabsf(ipdMeters - m_fCurrentIpd) < 0.0001f) return true; if (!m_bLhConfigLoaded) { if (!LoadLighthouseConfig()) return false; } // Build HmdMatrix34_t with preserved rotation + new IPD translation vr::HmdMatrix34_t left = {}, right = {}; for (int r = 0; r < 3; r++) for (int c = 0; c < 3; c++) { left.m[r][c] = m_cachedLeftRot[r][c]; right.m[r][c] = m_cachedRightRot[r][c]; } left.m[0][3] = -ipdMeters / 2.0f; right.m[0][3] = +ipdMeters / 2.0f; vr::VRServerDriverHost()->SetDisplayEyeToHead( vr::k_unTrackedDeviceIndex_Hmd, left, right); vr::PropertyContainerHandle_t hmdProps = vr::VRProperties()->TrackedDeviceToPropertyContainer( vr::k_unTrackedDeviceIndex_Hmd); vr::VRProperties()->SetFloatProperty(hmdProps, vr::Prop_UserIpdMeters_Float, ipdMeters); m_fCurrentIpd = ipdMeters; #ifdef ENABLE_IPD_PERSIST // Capture startup IPD on first successful apply (Init() read may miss it // if lighthouse driver hasn't set the property yet at that point) if (m_fStartupIpd <= 0.0f) m_fStartupIpd = ipdMeters; #endif // Sync to VRSettings so the settings tab slider reflects current IPD vr::VRSettings()->SetFloat(kSettingsSection, "ipd_mm", mm); m_fLastSettingsIpd = mm; return true; } void DeviceProvider::TrySetSliderProperties() { if (m_bSliderPropsAttempted) return; if (m_bIsBeyond1) return; // per D-09 vr::PropertyContainerHandle_t hmdProps = vr::VRProperties()->TrackedDeviceToPropertyContainer( vr::k_unTrackedDeviceIndex_Hmd); if (hmdProps == vr::k_ulInvalidPropertyContainer) return; m_bSliderPropsAttempted = true; // --- Track B, Part 1: IpdUIRange properties (per D-01) --- vr::ETrackedPropertyError err; err = vr::VRProperties()->SetFloatProperty(hmdProps, vr::Prop_IpdUIRangeMinMeters_Float, 0.048f); DriverLog("IPD Slider: SetFloatProperty(IpdUIRangeMin=0.048) err=%d\n", (int)err); err = vr::VRProperties()->SetFloatProperty(hmdProps, vr::Prop_IpdUIRangeMaxMeters_Float, 0.075f); DriverLog("IPD Slider: SetFloatProperty(IpdUIRangeMax=0.075) err=%d\n", (int)err); vr::ETrackedPropertyError boolErr; boolErr = vr::VRProperties()->SetBoolProperty(hmdProps, vr::Prop_DriverDisplaysIPDChanges_Bool, false); DriverLog("IPD Slider: SetBoolProperty(DriverDisplaysIPDChanges=false) err=%d\n", (int)boolErr); // --- Track B, Part 2: Component-based handle probing (per D-02) --- vr::VRInputComponentHandle_t ipdHandle = vr::k_ulInvalidInputComponentHandle; vr::EVRInputError inputErr; // Probe 1: /input/ipd/value -- scalar analog component for IPD inputErr = vr::VRDriverInput()->CreateScalarComponent( hmdProps, "/input/ipd/value", &ipdHandle, vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedOneSided); DriverLog("IPD Slider: CreateScalarComponent(/input/ipd/value) err=%d handle=%llu\n", (int)inputErr, (unsigned long long)ipdHandle); // Probe 2: /input/ipd_adjust/value -- alternative naming convention vr::VRInputComponentHandle_t ipdAdjHandle = vr::k_ulInvalidInputComponentHandle; inputErr = vr::VRDriverInput()->CreateScalarComponent( hmdProps, "/input/ipd_adjust/value", &ipdAdjHandle, vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedOneSided); DriverLog("IPD Slider: CreateScalarComponent(/input/ipd_adjust/value) err=%d handle=%llu\n", (int)inputErr, (unsigned long long)ipdAdjHandle); // Probe 3: /input/eye_distance/value -- another possible component name vr::VRInputComponentHandle_t eyeDistHandle = vr::k_ulInvalidInputComponentHandle; inputErr = vr::VRDriverInput()->CreateScalarComponent( hmdProps, "/input/eye_distance/value", &eyeDistHandle, vr::VRScalarType_Absolute, vr::VRScalarUnits_NormalizedOneSided); DriverLog("IPD Slider: CreateScalarComponent(/input/eye_distance/value) err=%d handle=%llu\n", (int)inputErr, (unsigned long long)eyeDistHandle); } void DeviceProvider::RunFrame() { // Deferred proximity component creation (HMD container not ready at Init time) TryCreateProximityComponent(); CheckHmdSerial(); TrySetSliderProperties(); PollPipe(); #ifdef ENABLE_BACKGLOW PollDaemonPipe(); #endif // Poll for IPD-related VR events (detect IpdChanged) { vr::VREvent_t event; while (vr::VRServerDriverHost()->PollNextEvent(&event, sizeof(event))) { if (event.eventType == vr::VREvent_IpdChanged) { float newIpdMeters = event.data.ipd.ipdMeters; float newMm = newIpdMeters * 1000.0f; DriverLog("IPD Event: VREvent_IpdChanged ipd=%.4fm (%.1fmm)\n", newIpdMeters, newMm); // Auto-apply (per D-05, D-06) with loop guard inside ApplyIpd (per D-07) if (!m_bIsBeyond1 && newMm >= 48.0f && newMm <= 75.0f) { ApplyIpd(newMm); } } } } // Track A: Poll settings tab slider for IPD changes (per D-03) if (!m_bIsBeyond1) { vr::EVRSettingsError settingsErr; float ipdMm = vr::VRSettings()->GetFloat( kSettingsSection, "ipd_mm", &settingsErr); if (settingsErr == vr::VRSettingsError_None && ipdMm >= 48.0f && ipdMm <= 75.0f) { if (fabsf(ipdMm - m_fLastSettingsIpd) > 0.01f) { m_fLastSettingsIpd = ipdMm; DriverLog("IPD: Settings slider changed to %.1fmm\n", ipdMm); ApplyIpd(ipdMm); } } } // Wire algorithm output to SteamVR HMD proximity if (m_pHidDevice && !m_bManualOverride) { bool detected = m_pHidDevice->GetPersonDetected(); SetHmdProximity(detected); // state-change guard inside if (m_logVerbose) { auto diag = m_pHidDevice->GetAlgorithmDiag(); DriverLog("Prox: avg=%u thresh=%u detected=%s samples=%u\n", diag.averaged_prox, diag.effective_threshold, diag.detected ? "true" : "false", diag.total_samples); } } } bool DeviceProvider::ShouldBlockStandbyMode() { return false; } void DeviceProvider::EnterStandby() { } void DeviceProvider::LeaveStandby() { } void DeviceProvider::CreatePipeServer() { m_hPipe = CreateNamedPipeA( "\\\\.\\pipe\\beyond_proximity_ctl", PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_NOWAIT, 1, // max 1 instance 512, 512, // output/input buffer sizes 0, // default timeout nullptr); // default security if (m_hPipe == INVALID_HANDLE_VALUE) { DriverLog("Pipe: Failed to create named pipe (error %lu)\n", GetLastError()); } else { DriverLog("Pipe: Created \\\\.\\pipe\\beyond_proximity_ctl\n"); } } void DeviceProvider::PollPipe() { if (m_hPipe == INVALID_HANDLE_VALUE) return; if (!m_bClientConnected) { // PIPE_NOWAIT: ConnectNamedPipe returns immediately ConnectNamedPipe(m_hPipe, nullptr); DWORD err = GetLastError(); if (err == ERROR_PIPE_CONNECTED || err == ERROR_NO_DATA) { m_bClientConnected = true; } } if (m_bClientConnected) { char buf[512]; DWORD bytesRead = 0; if (ReadFile(m_hPipe, buf, sizeof(buf) - 1, &bytesRead, nullptr) && bytesRead > 0) { buf[bytesRead] = '\0'; HandlePipeCommand(buf, bytesRead); } else if (GetLastError() == ERROR_BROKEN_PIPE) { // Client disconnected -- reset for next connection DisconnectNamedPipe(m_hPipe); m_bClientConnected = false; } } } void DeviceProvider::HandlePipeCommand(const char* cmd, DWORD /*len*/) { char response[1024] = {0}; // Phase 15: widened for multi-line `backglow status` (Pattern 5) if (strcmp(cmd, "proximity on") == 0) { m_bManualOverride = true; SetHmdProximity(true); snprintf(response, sizeof(response), "OK proximity=true source=manual"); } else if (strcmp(cmd, "proximity off") == 0) { m_bManualOverride = true; SetHmdProximity(false); snprintf(response, sizeof(response), "OK proximity=false source=manual"); } else if (strcmp(cmd, "proximity auto") == 0) { m_bManualOverride = false; DriverLog("Proximity: manual override cleared, algorithm driving\n"); snprintf(response, sizeof(response), "OK source=algorithm"); } else if (strcmp(cmd, "status") == 0) { const char* hidStateStr = "not_connected"; if (m_pHidDevice) { int connState = m_pHidDevice->GetConnectionState(); if (connState == 1) hidStateStr = "open"; else if (connState == 2) hidStateStr = "reconnecting"; } char ipdBuf[16] = "unknown"; if (m_fCurrentIpd > 0.0f) snprintf(ipdBuf, sizeof(ipdBuf), "%.1fmm", m_fCurrentIpd * 1000.0f); if (m_pHidDevice) { CalibrationData cal = m_pHidDevice->GetCalibration(); auto diag = m_pHidDevice->GetAlgorithmDiag(); snprintf(response, sizeof(response), "proximity=%s hid=%s source=%s prox_raw=%u cal=%u thresh=%u hyst=%u trim=%d " "averaged_prox=%u detected=%s eff_thresh=%u samples=%u " "report_rate_ms=%d log_verbosity=%d moving_avg_length=%d " "ipd=%s lh_config=%s", m_bProximity ? "true" : "false", hidStateStr, m_bManualOverride ? "manual" : "algorithm", m_pHidDevice->GetProxDistance(), cal.programmed_cal, cal.proximity_threshold, cal.proximity_hysteresis, cal.user_trim, diag.averaged_prox, diag.detected ? "true" : "false", diag.effective_threshold, diag.total_samples, m_reportRateMs, m_logVerbose ? 1 : 0, m_movingAvgLength, ipdBuf, m_bLhConfigLoaded ? "loaded" : "not_loaded"); } else { snprintf(response, sizeof(response), "proximity=%s hid=not_connected source=%s " "report_rate_ms=%d log_verbosity=%d moving_avg_length=%d " "ipd=%s lh_config=%s", m_bProximity ? "true" : "false", m_bManualOverride ? "manual" : "algorithm", m_reportRateMs, m_logVerbose ? 1 : 0, m_movingAvgLength, ipdBuf, m_bLhConfigLoaded ? "loaded" : "not_loaded"); } } else if (strncmp(cmd, "ipd ", 4) == 0) { if (m_bIsBeyond1) { snprintf(response, sizeof(response), "ERR ipd not supported on Beyond 1"); } else { float mm = 0.0f; if (sscanf(cmd + 4, "%f", &mm) == 1) { HandleIpdSet(mm, response, sizeof(response)); } else { snprintf(response, sizeof(response), "ERR ipd requires numeric argument (e.g. ipd 63.5)"); } } } else if (strcmp(cmd, "ipd?") == 0) { if (m_bIsBeyond1) { snprintf(response, sizeof(response), "ERR ipd not supported on Beyond 1"); } else { HandleIpdQuery(response, sizeof(response)); } } else if (strncmp(cmd, "load_lh_config ", 15) == 0) { HandleLoadLhConfig(cmd + 15, response, sizeof(response)); } else if (strncmp(cmd, "test_handle ", 12) == 0) { uint64_t handle = 0; int value = 0; if (sscanf(cmd + 12, "%llu %d", &handle, &value) == 2) { HandleTestHandle(handle, value != 0, response, sizeof(response)); } else { snprintf(response, sizeof(response), "ERR usage: test_handle <0|1>"); } } #ifdef ENABLE_BACKGLOW else if (strncmp(cmd, "backglow ", 9) == 0) { HandleBackglowCommand(cmd + 9, response, sizeof(response)); } #endif else { snprintf(response, sizeof(response), "ERR unknown command"); } // Send response back to client DWORD written = 0; WriteFile(m_hPipe, response, (DWORD)strlen(response), &written, nullptr); // After responding, disconnect the pipe so it can accept the next client. // Each CLI invocation is a single command-response exchange. FlushFileBuffers(m_hPipe); DisconnectNamedPipe(m_hPipe); m_bClientConnected = false; } void DeviceProvider::DestroyPipeServer() { if (m_hPipe != INVALID_HANDLE_VALUE) { if (m_bClientConnected) DisconnectNamedPipe(m_hPipe); CloseHandle(m_hPipe); m_hPipe = INVALID_HANDLE_VALUE; m_bClientConnected = false; DriverLog("Pipe: Destroyed\n"); } } #ifdef ENABLE_BACKGLOW // ============================================================================= // Phase 16 VRCH-01 fix: Dedicated daemon pipe. // ============================================================================= // The Phase 14/15 CLI pipe (\\.\pipe\beyond_proximity_ctl) is request/response: // each command triggers WriteFile(response) + FlushFileBuffers + DisconnectNamedPipe. // FlushFileBuffers on a named pipe BLOCKS until the client reads the response — // the daemon writes fire-and-forget and never reads, so at 90 Hz the first write // from the daemon hangs server_main inside FlushFileBuffers, and vrserver's own // 12 s watchdog aborts the driver (the original UAT failure mode). // // The daemon pipe below is purpose-built: inbound message mode, multi-message, // no per-message response, no per-message disconnect. The daemon holds the // connection for the whole session and streams "backglow {fill|bri|off}" lines. // HandleBackglowCommand is reused but the response is discarded. // ============================================================================= void DeviceProvider::CreateDaemonPipeServer() { // WR-02 (iter-4) fix: the default named-pipe ACL grants FILE_GENERIC_WRITE // to Everyone on a PIPE_ACCESS_INBOUND server, which means any local user // (including low-integrity browser sandboxes) can connect and inject // `backglow fill|bri|off` frames at up to the PollDaemonPipe rate. The // daemon pipe ultimately reaches the HMD transport, so restrict writers // to SYSTEM + Administrators + the caller's user (the vrserver process). // D-20 keeps OSC UDP open (loopback firewall rules cover it), but the pipe // is a higher-value target and gets an explicit DACL. // // SDDL breakdown of "D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;IU)": // D: discretionary ACL follows // (A;;GA;;;SY) Allow GENERIC_ALL to LocalSystem // (A;;GA;;;BA) Allow GENERIC_ALL to Builtin Administrators // (A;;GA;;;IU) Allow GENERIC_ALL to the Interactive user (the current // desktop session — covers the normal dev-mode case // where vrserver runs unelevated under the user SID). PSECURITY_DESCRIPTOR pSd = nullptr; SECURITY_ATTRIBUTES sa = {}; SECURITY_ATTRIBUTES* psa = nullptr; HMODULE hAdvapi = LoadLibraryA("advapi32.dll"); using PFN_CSSD2SD = BOOL (WINAPI*)(LPCSTR, DWORD, PSECURITY_DESCRIPTOR*, PULONG); PFN_CSSD2SD pfnConvert = hAdvapi ? (PFN_CSSD2SD)GetProcAddress(hAdvapi, "ConvertStringSecurityDescriptorToSecurityDescriptorA") : nullptr; if (pfnConvert && pfnConvert("D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;IU)", 1 /*SDDL_REVISION_1*/, &pSd, nullptr)) { sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = pSd; sa.bInheritHandle = FALSE; psa = &sa; } else { DriverLog("Backglow: DACL build failed (err=%lu) — falling back to default ACL\n", GetLastError()); } m_hDaemonPipe = CreateNamedPipeA( "\\\\.\\pipe\\beyond_backglow_daemon", PIPE_ACCESS_INBOUND, // daemon writes only PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_NOWAIT, // non-blocking reads 1, // max 1 instance (one daemon per driver load) 0, 4096, // no outbound buffer; 4 KB inbound buffer 0, // default timeout (irrelevant under PIPE_NOWAIT) psa); // restricted DACL (SYSTEM + Admins + Interactive user) if (m_hDaemonPipe == INVALID_HANDLE_VALUE) { DriverLog("Backglow: daemon pipe create failed (err=%lu)\n", GetLastError()); } else { DriverLog("Backglow: daemon pipe created \\\\.\\pipe\\beyond_backglow_daemon (restricted DACL)\n"); } if (pSd) LocalFree(pSd); if (hAdvapi) FreeLibrary(hAdvapi); } void DeviceProvider::PollDaemonPipe() { if (m_hDaemonPipe == INVALID_HANDLE_VALUE) return; if (!m_bDaemonPipeClientConnected) { // PIPE_NOWAIT: ConnectNamedPipe returns immediately. ConnectNamedPipe(m_hDaemonPipe, nullptr); DWORD err = GetLastError(); if (err == ERROR_PIPE_CONNECTED) { m_bDaemonPipeClientConnected = true; DriverLog("Backglow: daemon pipe client connected\n"); } // ERROR_PIPE_LISTENING / ERROR_NO_DATA / ERROR_PIPE_NOT_CONNECTED = // "still listening, no client yet" — fine, try again next frame. } if (!m_bDaemonPipeClientConnected) return; // Drain as many messages as arrived this tick. PIPE_NOWAIT ReadFile // returns immediately if no data (with ERROR_NO_DATA on fail). We cap at // 32 messages per RunFrame tick to bound server_main time spent here // even under a daemon write burst (e.g. startup animation's first second: // 1 fill + ~90 bri ~= 91 messages in ~1 s = ~1 message per RunFrame tick // average; burst headroom of 32 is ample). char buf[512]; char dummyResp[1024]; for (int i = 0; i < 32; ++i) { DWORD bytesRead = 0; BOOL ok = ReadFile(m_hDaemonPipe, buf, sizeof(buf) - 1, &bytesRead, nullptr); DWORD err = GetLastError(); if (ok && bytesRead > 0) { buf[bytesRead] = '\0'; // Only "backglow ..." commands are legal on this pipe. Anything else // is silently dropped (defense-in-depth — the pipe is loopback-only // message-mode and the daemon only emits backglow verbs, but a // hypothetical misbehaving peer shouldn't crash us). if (std::strncmp(buf, "backglow ", 9) == 0) { HandleBackglowCommand(buf + 9, dummyResp, sizeof(dummyResp)); // Response discarded intentionally — no WriteFile, no // FlushFileBuffers, no DisconnectNamedPipe. This is the // whole point of having a separate pipe. } continue; } if (err == ERROR_BROKEN_PIPE) { // Daemon exited / pipe handle closed on the client side. DisconnectNamedPipe(m_hDaemonPipe); m_bDaemonPipeClientConnected = false; DriverLog("Backglow: daemon pipe client disconnected\n"); break; } if (err == ERROR_MORE_DATA) { // WR-07 fix: under PIPE_READMODE_MESSAGE, if a daemon message // exceeds our buffer, ReadFile returns FALSE with // ERROR_MORE_DATA and leaves the partial bytes in `buf`. Treating // this as "no more data" would cause the next ReadFile to pick up // the TAIL of the oversized message as a fresh message and corrupt // parsing. Today the longest legitimate daemon line (~74 bytes) // fits well under sizeof(buf), so this path is a safety net: drain // the rest of the oversized message to realign the next ReadFile // to a message boundary, and complain loudly so we notice if the // daemon ever grows past 512 bytes. DriverLog("Backglow: daemon sent message >%zu bytes (WR-07) — draining tail\n", sizeof(buf) - 1); // Drain remaining fragments until ReadFile succeeds (final chunk) // or we hit a terminal error. while (true) { BOOL ok2 = ReadFile(m_hDaemonPipe, buf, sizeof(buf) - 1, &bytesRead, nullptr); DWORD err2 = GetLastError(); if (ok2) break; // last fragment consumed if (err2 != ERROR_MORE_DATA) break; // broken pipe / etc. } break; } // ERROR_NO_DATA / ERROR_PIPE_LISTENING / 0 bytes under PIPE_NOWAIT = // "nothing more to read right now" — stop this tick. break; } } void DeviceProvider::DestroyDaemonPipeServer() { if (m_hDaemonPipe != INVALID_HANDLE_VALUE) { if (m_bDaemonPipeClientConnected) DisconnectNamedPipe(m_hDaemonPipe); CloseHandle(m_hDaemonPipe); m_hDaemonPipe = INVALID_HANDLE_VALUE; m_bDaemonPipeClientConnected = false; DriverLog("Backglow: daemon pipe destroyed\n"); } } #endif // --- HMD proximity signaling --- void DeviceProvider::SetHmdProximity(bool on) { if (on == m_bProximity) return; // Only update on state change m_bProximity = on; if (m_hProximityComponent != vr::k_ulInvalidInputComponentHandle) { // Primary: input component — apps (VRChat, OpenXR) read this vr::EVRInputError err = vr::VRDriverInput()->UpdateBooleanComponent( m_hProximityComponent, on, 0.0); DriverLog("Proximity: UpdateBooleanComponent(%s) = %d\n", on ? "true" : "false", (int)err); } else { // Fallback: property toggling (v1.0 behavior, before input component ready) vr::PropertyContainerHandle_t hmdProps = vr::VRProperties()->TrackedDeviceToPropertyContainer( vr::k_unTrackedDeviceIndex_Hmd); vr::VRProperties()->SetBoolProperty(hmdProps, vr::Prop_ContainsProximitySensor_Bool, on); DriverLog("Proximity: SetBoolProperty fallback (%s)\n", on ? "true" : "false"); } } // --- Config reading helpers (reused from spike, now reading from config.json files) --- // Helper: parse 9 floats from "[[f,f,f],[f,f,f],[f,f,f]]" pattern starting at pos static bool ParseEyeToHead3x3(const std::string& data, size_t startPos, float out[3][3]) { // Find the opening "eye_to_head" value -- a nested JSON array: [[r00,r01,r02],[r10,...],[r20,...]] // The config is pretty-printed with newlines/spaces, so scan floats one at a time. size_t pos = data.find('[', startPos); if (pos == std::string::npos) return false; // Skip past the outer '[' pos++; int count = 0; for (int r = 0; r < 3 && pos < data.size(); r++) { // Find inner row '[' pos = data.find('[', pos); if (pos == std::string::npos) return false; pos++; for (int c = 0; c < 3 && pos < data.size(); c++) { // Skip whitespace while (pos < data.size() && (data[pos] == ' ' || data[pos] == '\t' || data[pos] == '\n' || data[pos] == '\r')) pos++; char* end = nullptr; float val = strtof(data.c_str() + pos, &end); if (end == data.c_str() + pos) return false; // no float parsed out[r][c] = val; count++; pos = end - data.c_str(); // Skip comma/whitespace after value while (pos < data.size() && (data[pos] == ',' || data[pos] == ' ' || data[pos] == '\t' || data[pos] == '\n' || data[pos] == '\r')) pos++; } // Skip past row closing ']' and any comma/whitespace if (pos < data.size() && data[pos] == ']') pos++; while (pos < data.size() && (data[pos] == ',' || data[pos] == ' ' || data[pos] == '\t' || data[pos] == '\n' || data[pos] == '\r')) pos++; } return (count == 9); } // Helper: extract a JSON string value for a given key from openvrpaths.vrpath // Looks for "key" : [ "value" ] and returns the first value with escaped backslashes cleaned. static std::string ExtractVrPathValue(const std::string& content, const char* key) { std::string searchKey = std::string("\"") + key + "\""; size_t keyPos = content.find(searchKey); if (keyPos == std::string::npos) return ""; size_t bracketPos = content.find('[', keyPos); if (bracketPos == std::string::npos) return ""; size_t q1 = content.find('"', bracketPos + 1); size_t q2 = content.find('"', q1 + 1); if (q1 == std::string::npos || q2 == std::string::npos) return ""; std::string raw = content.substr(q1 + 1, q2 - q1 - 1); // Clean escaped backslashes and forward slashes std::string clean; for (size_t i = 0; i < raw.size(); i++) { if (raw[i] == '\\' && i + 1 < raw.size() && raw[i + 1] == '\\') { clean += '\\'; i++; } else if (raw[i] == '/') { clean += '\\'; } else { clean += raw[i]; } } return clean; } // --- IPD methods --- bool DeviceProvider::ReadEyeToHeadFromConfigFile(const std::string& configPath) { std::ifstream cfgStream(configPath); if (!cfgStream.is_open()) { DriverLog("IPD: Failed to open config file: %s\n", configPath.c_str()); return false; } std::string cfgContent((std::istreambuf_iterator(cfgStream)), std::istreambuf_iterator()); cfgStream.close(); DriverLog("IPD: Read config file (%zu bytes): %s\n", cfgContent.size(), configPath.c_str()); // Parse tracking_to_eye_transform -> eye_to_head for both eyes size_t tetPos = cfgContent.find("tracking_to_eye_transform"); if (tetPos == std::string::npos) { DriverLog("IPD: No tracking_to_eye_transform in config file\n"); return false; } float eye0Rot[3][3] = {}; float eye1Rot[3][3] = {}; size_t eth0Pos = cfgContent.find("eye_to_head", tetPos); if (eth0Pos == std::string::npos) { DriverLog("IPD: No eye_to_head[0] in config file\n"); return false; } if (!ParseEyeToHead3x3(cfgContent, eth0Pos, eye0Rot)) { DriverLog("IPD: Failed to parse eye_to_head[0] matrix\n"); return false; } size_t eth1Pos = cfgContent.find("eye_to_head", eth0Pos + 11); if (eth1Pos == std::string::npos) { DriverLog("IPD: No eye_to_head[1] in config file\n"); return false; } if (!ParseEyeToHead3x3(cfgContent, eth1Pos, eye1Rot)) { DriverLog("IPD: Failed to parse eye_to_head[1] matrix\n"); return false; } // Validate eye order: left eye should have positive yaw, right negative // Intrinsic XYZ: yaw = atan2(R[0][2], sqrt(R[0][0]^2 + R[0][1]^2)) float yaw0 = atan2f(eye0Rot[0][2], sqrtf(eye0Rot[0][0] * eye0Rot[0][0] + eye0Rot[0][1] * eye0Rot[0][1])); if (yaw0 > 0.0f) { // eye0 has positive yaw -> left eye (correct order) memcpy(m_cachedLeftRot, eye0Rot, sizeof(float) * 9); memcpy(m_cachedRightRot, eye1Rot, sizeof(float) * 9); } else { // eye0 has negative yaw -> swap (eye0 is right, eye1 is left) DriverLog("IPD: WARNING eye order swapped in config, correcting\n"); memcpy(m_cachedLeftRot, eye1Rot, sizeof(float) * 9); memcpy(m_cachedRightRot, eye0Rot, sizeof(float) * 9); } m_bLhConfigLoaded = true; // Log Euler angles for diagnostics (intrinsic XYZ decomposition) float lPitch = atan2f(-m_cachedLeftRot[1][2], m_cachedLeftRot[2][2]) * 180.0f / kPi; float lYaw = atan2f(m_cachedLeftRot[0][2], sqrtf(m_cachedLeftRot[0][0]*m_cachedLeftRot[0][0] + m_cachedLeftRot[0][1]*m_cachedLeftRot[0][1])) * 180.0f / kPi; float lRoll = atan2f(-m_cachedLeftRot[0][1], m_cachedLeftRot[0][0]) * 180.0f / kPi; float rPitch = atan2f(-m_cachedRightRot[1][2], m_cachedRightRot[2][2]) * 180.0f / kPi; float rYaw = atan2f(m_cachedRightRot[0][2], sqrtf(m_cachedRightRot[0][0]*m_cachedRightRot[0][0] + m_cachedRightRot[0][1]*m_cachedRightRot[0][1])) * 180.0f / kPi; float rRoll = atan2f(-m_cachedRightRot[0][1], m_cachedRightRot[0][0]) * 180.0f / kPi; DriverLog("IPD: Cached rotations L pitch=%.2f yaw=%.2f roll=%.2f R pitch=%.2f yaw=%.2f roll=%.2f\n", lPitch, lYaw, lRoll, rPitch, rYaw, rRoll); return true; } bool DeviceProvider::LoadLighthouseConfig() { // 1. Get tracking serial — prefer SteamVR property (most reliable), fall back to HID flash std::string serial; // Try Prop_SerialNumber_String on HMD container (set by lighthouse driver, e.g. "LHR-1F8E25F1") { vr::PropertyContainerHandle_t hmdProps = vr::VRProperties()->TrackedDeviceToPropertyContainer(vr::k_unTrackedDeviceIndex_Hmd); vr::ETrackedPropertyError propErr; char serialBuf[64] = {}; vr::VRProperties()->GetStringProperty(hmdProps, vr::Prop_SerialNumber_String, serialBuf, sizeof(serialBuf), &propErr); if (propErr == vr::TrackedProp_Success && serialBuf[0] != '\0') { serial = serialBuf; DriverLog("IPD: Got serial from SteamVR property: %s\n", serial.c_str()); } else { DriverLog("IPD: Prop_SerialNumber_String not available (err=%d)\n", propErr); } } // Fall back to HID user flash tag 0x09 if property not available if (serial.empty()) { serial = m_pHidDevice ? m_pHidDevice->GetTrackingSerial() : ""; if (!serial.empty()) DriverLog("IPD: Got serial from HID flash: %s\n", serial.c_str()); else DriverLog("IPD: No tracking serial available from any source\n"); } if (serial.empty()) return false; m_sTrackingSerial = serial; // 2. Read openvrpaths.vrpath to find config directory char localAppData[MAX_PATH] = {}; if (!GetEnvironmentVariableA("LOCALAPPDATA", localAppData, MAX_PATH)) { DriverLog("IPD: Failed to get LOCALAPPDATA\n"); return false; } std::string vrpathFile = std::string(localAppData) + "\\openvr\\openvrpaths.vrpath"; std::ifstream vrpathStream(vrpathFile); if (!vrpathStream.is_open()) { DriverLog("IPD: Failed to open %s\n", vrpathFile.c_str()); return false; } std::string vrpathContent((std::istreambuf_iterator(vrpathStream)), std::istreambuf_iterator()); vrpathStream.close(); std::string configDir = ExtractVrPathValue(vrpathContent, "config"); if (configDir.empty()) { DriverLog("IPD: No 'config' key in openvrpaths.vrpath\n"); return false; } // 3. Lowercase the serial for folder matching std::string lowerSerial = serial; std::transform(lowerSerial.begin(), lowerSerial.end(), lowerSerial.begin(), ::tolower); // 4. Build config path: \lighthouse\\config.json std::string configPath = configDir + "\\lighthouse\\" + lowerSerial + "\\config.json"; DriverLog("IPD: Attempting config load from: %s\n", configPath.c_str()); // 5. Read and parse config file bool result = ReadEyeToHeadFromConfigFile(configPath); if (result) { DriverLog("IPD: Lighthouse config loaded successfully from %s\n", configPath.c_str()); } else { DriverLog("IPD: Failed to load lighthouse config from %s\n", configPath.c_str()); } return result; } bool DeviceProvider::LoadLighthouseConfigFromPath(const std::string& path) { DriverLog("IPD: Loading lighthouse config from explicit path: %s\n", path.c_str()); return ReadEyeToHeadFromConfigFile(path); } void DeviceProvider::HandleIpdSet(float mm, char* response, size_t responseSize) { if (mm < 48.0f || mm > 75.0f) { snprintf(response, responseSize, "ERR ipd out of range (48-75mm)"); return; } if (!ApplyIpd(mm)) { snprintf(response, responseSize, "ERR lh_config not loaded"); return; } snprintf(response, responseSize, "OK ipd=%.1fmm", mm); } void DeviceProvider::HandleIpdQuery(char* response, size_t responseSize) { if (m_fCurrentIpd <= 0.0f) { snprintf(response, responseSize, "OK ipd=unknown"); } else { snprintf(response, responseSize, "OK ipd=%.1fmm", m_fCurrentIpd * 1000.0f); } } void DeviceProvider::HandleLoadLhConfig(const char* path, char* response, size_t responseSize) { if (LoadLighthouseConfigFromPath(std::string(path))) { snprintf(response, responseSize, "OK lh_config=loaded"); } else { snprintf(response, responseSize, "ERR failed to read config: file not found or parse error"); } } // --- Spike: proximity handle discovery --- // Debug: test_handle command (retained from spike for diagnostics) void DeviceProvider::HandleTestHandle(uint64_t handle, bool value, char* response, size_t responseSize) { vr::EVRInputError err = vr::VRDriverInput()->UpdateBooleanComponent( (vr::VRInputComponentHandle_t)handle, value, 0.0); DriverLog("TestHandle: handle=%llu value=%s err=%d\n", handle, value ? "true" : "false", (int)err); snprintf(response, responseSize, "OK handle=%llu value=%s err=%d", handle, value ? "true" : "false", (int)err); } // --- IPD Persistence (Phase 13) --- #ifdef ENABLE_IPD_PERSIST std::string DeviceProvider::FindLighthouseConsole() { // Read openvrpaths.vrpath to find SteamVR runtime directory char localAppData[MAX_PATH] = {}; DWORD len = GetEnvironmentVariableA("LOCALAPPDATA", localAppData, MAX_PATH); if (len == 0 || len >= MAX_PATH) { DriverLog("IPD Persist: Failed to get LOCALAPPDATA\n"); return ""; } std::string vrpathFile = std::string(localAppData) + "\\openvr\\openvrpaths.vrpath"; std::ifstream vrpathStream(vrpathFile); if (!vrpathStream.is_open()) { DriverLog("IPD Persist: Failed to open %s\n", vrpathFile.c_str()); return ""; } std::string vrpathContent((std::istreambuf_iterator(vrpathStream)), std::istreambuf_iterator()); vrpathStream.close(); std::string runtimeDir = ExtractVrPathValue(vrpathContent, "runtime"); if (runtimeDir.empty()) { DriverLog("IPD Persist: Could not extract runtime path from openvrpaths.vrpath\n"); return ""; } std::string exePath = runtimeDir + "\\tools\\lighthouse\\bin\\win64\\lighthouse_console.exe"; if (!std::filesystem::exists(exePath)) { DriverLog("IPD Persist: lighthouse_console.exe not found at %s\n", exePath.c_str()); return ""; } DriverLog("IPD Persist: Found lighthouse_console at %s\n", exePath.c_str()); return exePath; } // Helper: spawn lighthouse_console, send serial + command, read stdout response, // send exit, wait for process to finish. Follows HMDUtility write_to_stdin pattern. // Returns true if process completed successfully. static bool RunLighthouseCommand(const std::string& exePath, const std::string& serialCmd, const std::string& command) { SECURITY_ATTRIBUTES sa = {}; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; HANDLE hStdinRead, hStdinWrite, hStdoutRead, hStdoutWrite; if (!CreatePipe(&hStdinRead, &hStdinWrite, &sa, 0) || !SetHandleInformation(hStdinWrite, HANDLE_FLAG_INHERIT, 0)) return false; if (!CreatePipe(&hStdoutRead, &hStdoutWrite, &sa, 0) || !SetHandleInformation(hStdoutRead, HANDLE_FLAG_INHERIT, 0)) { CloseHandle(hStdinRead); CloseHandle(hStdinWrite); return false; } STARTUPINFOA si = {}; si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES; si.hStdInput = hStdinRead; si.hStdOutput = hStdoutWrite; si.hStdError = hStdoutWrite; PROCESS_INFORMATION pi = {}; if (!CreateProcessA(exePath.c_str(), NULL, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { DriverLog("IPD Persist: Failed to start lighthouse_console (err=%lu)\n", GetLastError()); CloseHandle(hStdinRead); CloseHandle(hStdinWrite); CloseHandle(hStdoutRead); CloseHandle(hStdoutWrite); return false; } // Close child-side handles in parent (not needed after CreateProcess) CloseHandle(hStdinRead); CloseHandle(hStdoutWrite); auto writeStr = [&](const std::string& s) -> bool { DWORD written = 0; return WriteFile(hStdinWrite, s.c_str(), (DWORD)s.size(), &written, NULL) && written == s.size(); }; bool ok = false; // Send serial select command if (!writeStr(serialCmd)) goto done; // Send the actual command (downloadconfig or uploadconfig) if (!writeStr(command)) goto done; // Read stdout -- blocks until lighthouse_console produces output (command done) { char buf[4096]; DWORD bytesRead = 0; if (!ReadFile(hStdoutRead, buf, sizeof(buf), &bytesRead, NULL)) goto done; } ok = true; done: // Send exit command and wait for process to finish (HMDUtility pattern) writeStr("exit\r\n"); // Close stdin to signal EOF CloseHandle(hStdinWrite); // Poll for process exit (HMDUtility pattern: Sleep(1) loop) { // WR-03 (Phase 15 review): initialise exitCode to STILL_ACTIVE so that // if GetExitCodeProcess fails on the very first call (handle invalid, // process already reaped, etc.) we fall through to the pessimistic // TerminateProcess path — which is a no-op on an already-exited // process. Without this initialisation the post-loop compare against // STILL_ACTIVE reads an indeterminate value (UB). DWORD exitCode = STILL_ACTIVE; int iterations = 0; while (GetExitCodeProcess(pi.hProcess, &exitCode) && exitCode == STILL_ACTIVE && iterations < 10000) { Sleep(1); iterations++; } if (exitCode == STILL_ACTIVE) { DriverLog("IPD Persist: WARNING -- lighthouse_console did not exit, terminating\n"); TerminateProcess(pi.hProcess, 1); } } CloseHandle(hStdoutRead); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return ok; } void DeviceProvider::PersistIpdToConfig() { // D-03: Change detection -- skip if IPD didn't change during session if (m_fCurrentIpd <= 0.0f || m_fStartupIpd <= 0.0f || fabsf(m_fCurrentIpd - m_fStartupIpd) < 0.0001f) { DriverLog("IPD Persist: No change during session (startup=%.4f current=%.4f), skipping\n", m_fStartupIpd, m_fCurrentIpd); return; } // SLIDER-04, D-09: Need tracking serial for lighthouse_console serial command if (m_sTrackingSerial.empty()) { DriverLog("IPD Persist: WARNING -- tracking serial not available, cannot persist\n"); return; } // D-06: Find lighthouse_console via openvrpaths.vrpath runtime key std::string exePath = FindLighthouseConsole(); if (exePath.empty()) { DriverLog("IPD Persist: lighthouse_console.exe not found, cannot persist\n"); return; } float ipdMm = m_fCurrentIpd * 1000.0f; // Create temp file paths for download/upload char tempDir[MAX_PATH] = {}; DWORD tempLen = GetTempPathA(MAX_PATH, tempDir); if (tempLen == 0 || tempLen >= MAX_PATH) { DriverLog("IPD Persist: Failed to get temp directory\n"); return; } std::string dlPath = std::string(tempDir) + "beyond_ipd_dl.json"; std::string ulPath = std::string(tempDir) + "beyond_ipd_ul.json"; std::string serialCmd = "serial " + m_sTrackingSerial + "\r\n"; // Delete temp files if they exist from a previous attempt DeleteFileA(dlPath.c_str()); DeleteFileA(ulPath.c_str()); // Step 1: Download config (separate process invocation, per HMDUtility pattern) DriverLog("IPD Persist: Downloading config for %s\n", m_sTrackingSerial.c_str()); std::string dlCmd = "downloadconfig " + dlPath + "\r\n"; if (!RunLighthouseCommand(exePath, serialCmd, dlCmd)) { DriverLog("IPD Persist: downloadconfig failed\n"); goto cleanup; } if (!std::filesystem::exists(dlPath)) { DriverLog("IPD Persist: downloadconfig completed but file not created\n"); goto cleanup; } // Step 2: Read and modify config { std::ifstream dlStream(dlPath); if (!dlStream.is_open()) { DriverLog("IPD Persist: Failed to open downloaded config\n"); goto cleanup; } std::string configStr((std::istreambuf_iterator(dlStream)), std::istreambuf_iterator()); dlStream.close(); // Find "default_mm" and replace its value size_t pos = configStr.find("\"default_mm\""); if (pos == std::string::npos) { DriverLog("IPD Persist: default_mm not found in config, skipping\n"); goto cleanup; } size_t colonPos = configStr.find(':', pos); if (colonPos == std::string::npos) { DriverLog("IPD Persist: malformed config -- no colon after default_mm\n"); goto cleanup; } // Skip whitespace after colon size_t valStart = colonPos + 1; while (valStart < configStr.size() && (configStr[valStart] == ' ' || configStr[valStart] == '\t')) valStart++; // Find end of numeric value (digits, dot, minus) size_t valEnd = valStart; while (valEnd < configStr.size() && (isdigit(configStr[valEnd]) || configStr[valEnd] == '.' || configStr[valEnd] == '-')) valEnd++; if (valEnd == valStart) { DriverLog("IPD Persist: malformed config -- no numeric value for default_mm\n"); goto cleanup; } // Replace with new value (Pitfall 6: one decimal place) char buf[32]; snprintf(buf, sizeof(buf), "%.1f", ipdMm); configStr.replace(valStart, valEnd - valStart, buf); // Write modified config to upload path std::ofstream ulStream(ulPath); if (!ulStream.is_open()) { DriverLog("IPD Persist: Failed to create upload config file\n"); goto cleanup; } ulStream << configStr; ulStream.close(); } // Step 3: Upload config (separate process invocation, per HMDUtility pattern) { DriverLog("IPD Persist: Uploading modified config (%.1fmm)\n", ipdMm); std::string ulCmd = "uploadconfig " + ulPath + "\r\n"; if (!RunLighthouseCommand(exePath, serialCmd, ulCmd)) { DriverLog("IPD Persist: uploadconfig failed\n"); goto cleanup; } } DriverLog("IPD Persist: Successfully wrote %.1fmm to headset config\n", ipdMm); cleanup: DeleteFileA(dlPath.c_str()); DeleteFileA(ulPath.c_str()); } #endif // ENABLE_IPD_PERSIST // --- Backglow LED subsystem (Phase 14) --- #ifdef ENABLE_BACKGLOW // Message-only window support for RegisterDeviceNotification (Pattern 5, D-15). // A single global pointer lets the WndProc fan out to the owning DeviceProvider. // Only one DeviceProvider instance ever exists inside a running vrserver, so a // single static pointer is safe — mirrors how the rest of this driver assumes a // singleton provider. static const char kHotplugClassName[] = "BeyondBackglowHotplug"; static DeviceProvider* s_hotplugOwner = nullptr; // set in Start, cleared in Stop static LRESULT CALLBACK HotplugWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_DEVICECHANGE && wParam == DBT_DEVICEARRIVAL && s_hotplugOwner) { auto* hdr = reinterpret_cast(lParam); if (hdr && hdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { s_hotplugOwner->OnHotplugArrival(); } } return DefWindowProcA(hWnd, msg, wParam, lParam); } // Parse a 6-char hex string "RRGGBB" into 3 bytes. Rejects anything else // (T-14.3-01). Third copy of this helper project-wide (spike, harness, driver) // per plan note — acceptable for a ~15-line function. static bool ParseHex6(const char* s, size_t len, uint8_t& r, uint8_t& g, uint8_t& b) { if (len != 6) return false; for (size_t i = 0; i < 6; ++i) { if (!std::isxdigit(static_cast(s[i]))) return false; } char buf[7] = {0}; std::memcpy(buf, s, 6); unsigned long v = std::strtoul(buf, nullptr, 16); r = static_cast((v >> 16) & 0xFF); g = static_cast((v >> 8) & 0xFF); b = static_cast( v & 0xFF); return true; } void DeviceProvider::InitBackglow() { vr::EVRSettingsError sErr; // 1. Read existing Phase 14 keys. // D-12 / LHWD-02: brightness ceiling (default 50, clamp [1..255]). int32_t ceiling = vr::VRSettings()->GetInt32(kSettingsSection, "backglow_brightness_ceiling", &sErr); if (sErr != vr::VRSettingsError_None) ceiling = 50; if (ceiling < 1) ceiling = 1; if (ceiling > 255) ceiling = 255; m_backglowCeiling = ceiling; char portBuf[64] = {0}; vr::VRSettings()->GetString(kSettingsSection, "backglow_com_port", portBuf, sizeof(portBuf), &sErr); m_backglowPort = (sErr == vr::VRSettingsError_None) ? std::string(portBuf) : std::string(); // 2. Read NEW Phase 15 keys. char tBuf[16] = {0}; vr::VRSettings()->GetString(kSettingsSection, "backglow_transport", tBuf, sizeof(tBuf), &sErr); std::string transportStr = (sErr == vr::VRSettingsError_None && tBuf[0]) ? std::string(tBuf) : std::string("usb"); // T-15-04: whitelist transport string. Unknown values default to "usb". if (transportStr == "ddp") m_backglowTransport = BackglowXp::DDP; else if (transportStr == "auto") m_backglowTransport = BackglowXp::AUTO; else m_backglowTransport = BackglowXp::USB; // includes "usb" + unknown char hBuf[64] = {0}; vr::VRSettings()->GetString(kSettingsSection, "backglow_ddp_host", hBuf, sizeof(hBuf), &sErr); m_backglowDdpHost = (sErr == vr::VRSettingsError_None) ? std::string(hBuf) : std::string(); DriverLog("Backglow: transport=%s ddp_host='%s' ceiling=%d com_port='%s'\n", transportStr.c_str(), m_backglowDdpHost.c_str(), m_backglowCeiling, m_backglowPort.c_str()); // 3. Factory branching. std::unique_ptr transport; std::string portArg; // what we pass to LedController::Start bool chosenIsDdp = false; m_backglowDisabledReason.clear(); auto tryUsb = [&]() -> bool { auto t = std::make_unique(); std::string p = m_backglowPort; // D-09 step 1: try configured port first if non-empty. bool opened = false; if (!p.empty()) { opened = t->Open(p); if (!opened) { DriverLog("Backglow: configured COM port '%s' failed (err=%lu); falling through to VID/PID scan\n", p.c_str(), t->LastErrorCode()); } } // D-09 step 2: scan if no config OR Open failed. if (!opened) { auto matches = ScanForUsbComPorts(0x303A, 0x1001); // ESP32-C3 if (matches.empty()) { m_backglowDisabledReason = "scan_no_match"; return false; } // D-10 multi-match WARN. if (matches.size() > 1) { std::string allMatches; for (size_t i = 0; i < matches.size(); ++i) { if (i) allMatches += ", "; allMatches += matches[i]; } DriverLog("Backglow: scan matched multiple ESP32-C3 ports [%s]; using lowest %s. " "Pin a specific port via 'backglow_com_port' if needed.\n", allMatches.c_str(), matches.front().c_str()); } else { DriverLog("Backglow: scan matched VID_303A&PID_1001 -> %s\n", matches.front().c_str()); } p = matches.front(); opened = t->Open(p); if (!opened) { char tok[32] = {0}; std::snprintf(tok, sizeof(tok), "open_err_%lu", t->LastErrorCode()); m_backglowDisabledReason = tok; return false; } } // Probe-Open succeeded. Close it; LedController::Start re-Opens via writer thread. t->Close(); transport = std::move(t); portArg = p; m_backglowPort = p; // remember for status / hotplug recovery chosenIsDdp = false; return true; }; auto tryDdp = [&]() -> bool { if (m_backglowDdpHost.empty()) { m_backglowDisabledReason = "no_ddp_host_configured"; return false; } auto t = std::make_unique(); if (!t->Open(m_backglowDdpHost)) { m_backglowDisabledReason = "ddp_probe_timeout"; return false; } // D-08: WARN if WLED reports a strip length other than 10. int reported = t->ReportedLedCount(); if (reported != kBackglowMaxLeds) { DriverLog("Backglow: WLED reports leds.count=%d, controller is locked at %d; " "encoder will clamp.\n", reported, kBackglowMaxLeds); } t->Close(); transport = std::move(t); portArg = m_backglowDdpHost; chosenIsDdp = true; return true; }; bool ok = false; if (m_backglowTransport == BackglowXp::USB) { ok = tryUsb(); } else if (m_backglowTransport == BackglowXp::DDP) { ok = tryDdp(); } else { // BackglowXp::AUTO (D-04 startup-only fallback) ok = tryUsb(); if (!ok) { DriverLog("Backglow: auto USB failed (%s); falling through to DDP\n", m_backglowDisabledReason.c_str()); m_backglowDisabledReason.clear(); ok = tryDdp(); } } if (!ok) { m_backglowDisabled = true; if (!m_backglowDisabledLogged) { DriverLog("Backglow: disabled (reason=%s); registering hotplug watcher\n", m_backglowDisabledReason.c_str()); m_backglowDisabledLogged = true; } StartHotplugWatcher(); // D-11 hotplug rescan still arms even when disabled return; } m_pLedController = std::make_unique( std::move(transport), static_cast(m_backglowCeiling)); m_pLedController->Start(portArg); if (m_pLedController->GetConnectionState() == 0) { m_backglowDisabled = true; if (!m_backglowDisabledLogged) { DriverLog("Backglow: LedController failed to Start on '%s'\n", portArg.c_str()); m_backglowDisabledLogged = true; } StartHotplugWatcher(); } else { m_backglowDisabled = false; DriverLog("Backglow: online via %s on %s\n", chosenIsDdp ? "DDP" : "USB", portArg.c_str()); StartHotplugWatcher(); // D-11: always arm so disabled-state recovery works // Phase 16 VRCH-01 D-02: spawn daemon ONLY on success path, never in // degraded state. Spawn failure is logged but non-fatal — driver stays // healthy (backglow still works via the pipe without the VRChat bridge). SpawnBackglowDaemon(); } } // Phase 16 VRCH-01 anchor: a static free function that lives inside this DLL. // Used as the address argument to GetModuleHandleExW(FROM_ADDRESS, ...) so we // resolve THIS DLL's module handle (not kernel32, not an importing EXE). A // member-function pointer cannot be reinterpret_cast to LPCWSTR (illegal // per-standard), so we anchor on a free function instead. static void BackglowDllAnchor() {} // ============================================================================= // Phase 16 VRCH-01: Backglow daemon lifecycle (spawn + watchdog + graceful stop). // Spawns beyond_backglow_ctl.exe under a Windows Job Object with // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the daemon cannot outlive vrserver.exe // (kernel-enforced). Watchdog respawns on crash with 1s/2s/4s backoff capped // at 3 per session; an uptime >= 60 s resets the counter. Graceful stop closes // the daemon's stdin (EOF -> daemon self-exits), bounded-waits 250 ms, then // closes the Job Object to force-kill anything still running. See 16-RESEARCH // §Example 1 / §Example 2 / §Pattern 3 for pattern provenance. // ============================================================================= bool DeviceProvider::SpawnBackglowDaemon() { // Resolve absolute daemon path from this DLL's location (D-24 — prevents DLL hijack). wchar_t dllPath[MAX_PATH] = {}; HMODULE hMod = NULL; if (!GetModuleHandleExW( GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(&BackglowDllAnchor), &hMod)) { DriverLog("Backglow: GetModuleHandleExW failed (err=%lu)\n", GetLastError()); return false; } if (GetModuleFileNameW(hMod, dllPath, MAX_PATH) == 0) { DriverLog("Backglow: GetModuleFileNameW failed (err=%lu)\n", GetLastError()); return false; } std::wstring daemonPath(dllPath); size_t slash = daemonPath.find_last_of(L"\\/"); if (slash == std::wstring::npos) { DriverLog("Backglow: cannot derive daemon dir from '%ls'\n", dllPath); return false; } daemonPath.resize(slash); daemonPath += L"\\beyond_backglow_ctl.exe"; // WR-01 (re-review): build the job handle in a local first, only publish // to the atomic member AFTER the full spawn succeeds. Keeps Stop() from // seeing a half-initialised Job between CreateJobObjectW and // AssignProcessToJobObject. HANDLE hJobLocal = CreateJobObjectW(NULL, NULL); if (!hJobLocal) { DriverLog("Backglow: CreateJobObjectW failed (err=%lu)\n", GetLastError()); return false; } JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {}; jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; if (!SetInformationJobObject(hJobLocal, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli))) { DriverLog("Backglow: SetInformationJobObject failed (err=%lu)\n", GetLastError()); CloseHandle(hJobLocal); return false; } // Create stdin pipe so driver can signal graceful-exit by closing the write end (D-06). SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE }; HANDLE hStdinRead = NULL, hStdinWrite = NULL; if (!CreatePipe(&hStdinRead, &hStdinWrite, &sa, 0) || !SetHandleInformation(hStdinWrite, HANDLE_FLAG_INHERIT, 0)) { DriverLog("Backglow: CreatePipe failed (err=%lu)\n", GetLastError()); if (hStdinRead) CloseHandle(hStdinRead); if (hStdinWrite) CloseHandle(hStdinWrite); CloseHandle(hJobLocal); return false; } // IN-02 (iter-4) fix: explicitly mark the vrserver stdout/stderr handles // inheritable before CreateProcess. Most vrserver startups already have // inheritable console handles, but there's no documented guarantee — if // they are NOT inheritable, the child daemon's `fprintf(stderr, ...)` in // DaemonLog becomes a silent no-op and the entire `Backglow-daemon:` grep // path in vrserver.txt disappears. If GetStdHandle returns NULL (vrserver // started without a console), SetHandleInformation is skipped — the loss // of the daemon log stream in that case is an already-accepted degradation // (noted in daemon_log.h). HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); HANDLE hErr = GetStdHandle(STD_ERROR_HANDLE); if (hOut && hOut != INVALID_HANDLE_VALUE) { SetHandleInformation(hOut, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); } if (hErr && hErr != INVALID_HANDLE_VALUE) { SetHandleInformation(hErr, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); } STARTUPINFOW si = { sizeof(si) }; si.dwFlags = STARTF_USESTDHANDLES; si.hStdInput = hStdinRead; si.hStdOutput = hOut; si.hStdError = hErr; PROCESS_INFORMATION pi = {}; DWORD flags = CREATE_SUSPENDED | CREATE_NO_WINDOW | CREATE_BREAKAWAY_FROM_JOB; BOOL ok = CreateProcessW(daemonPath.c_str(), NULL, NULL, NULL, TRUE, flags, NULL, NULL, &si, &pi); if (!ok && GetLastError() == ERROR_ACCESS_DENIED) { // Containing job forbids breakaway — retry without the flag (Pitfall 2). flags = CREATE_SUSPENDED | CREATE_NO_WINDOW; ok = CreateProcessW(daemonPath.c_str(), NULL, NULL, NULL, TRUE, flags, NULL, NULL, &si, &pi); } if (!ok) { DriverLog("Backglow: CreateProcess failed (err=%lu)\n", GetLastError()); CloseHandle(hStdinRead); CloseHandle(hStdinWrite); CloseHandle(hJobLocal); return false; } CloseHandle(hStdinRead); // child owns its copy if (!AssignProcessToJobObject(hJobLocal, pi.hProcess)) { DriverLog("Backglow: AssignProcessToJobObject failed (err=%lu)\n", GetLastError()); TerminateProcess(pi.hProcess, 1); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); CloseHandle(hStdinWrite); CloseHandle(hJobLocal); return false; } ResumeThread(pi.hThread); CloseHandle(pi.hThread); // IN-01 (re-review) fix: bail out of the spawn without publishing handles // if Stop() has raced in while we were mid-spawn. Without this check a // watchdog-triggered respawn that overlaps a driver Stop() leaks the // just-created stdin pipe handle — Stop's phase-1 exchange() captured null // BEFORE we got here, so the fresh handle we'd publish below would never be // closed by Stop's remaining phases until DLL unload. Closing locals here // is safe because nothing else can observe them yet (handles are not yet // stored into the atomic members). if (m_bStopBackglowWatchdog.load(std::memory_order_acquire)) { DriverLog("Backglow: spawn raced with Stop() — discarding fresh handles\n"); TerminateProcess(pi.hProcess, 1); CloseHandle(pi.hProcess); CloseHandle(hStdinWrite); CloseHandle(hJobLocal); return false; } // CR-01/WR-03 + WR-01 (re-review) fix: atomic stores with release ordering // so the watchdog thread (which reads these via acquire loads) observes a // consistent handle before entering WaitForSingleObject. Publish the job // handle atomically too so Stop()'s phase-3 exchange(nullptr) is the sole // owner of the close. m_hBackglowJob.store(hJobLocal, std::memory_order_release); m_hBackglowProcess.store(pi.hProcess, std::memory_order_release); m_hBackglowStdinWrite.store(hStdinWrite, std::memory_order_release); m_backglowSpawnTime = std::chrono::steady_clock::now(); DriverLog("Backglow: daemon spawned (pid=%lu)\n", pi.dwProcessId); // Launch watchdog (only if not already running for a prior spawn). if (!m_backglowWatchdogThread.joinable()) { m_bStopBackglowWatchdog.store(false); m_backglowWatchdogThread = std::thread(&DeviceProvider::BackglowWatchdogThreadFunc, this); } return true; } void DeviceProvider::BackglowWatchdogThreadFunc() { static const int kBackoffMs[] = { 1000, 2000, 4000 }; // D-04 // WR-01 (iter-4) fix: track *attempt* count separately from the successful- // respawn count. m_backglowRespawnCount is only incremented on a successful // spawn, so if every spawn fails permanently (daemon binary missing / // quarantined / policy-blocked) the null-handle retry branch below would // loop forever at 500 ms cadence — 7200 attempts/hour, each emitting a // DriverLog line and flooding vrserver.txt. Cap consecutive spawn failures // hard so a pathological environment triggers a clean give-up. static const int kMaxConsecutiveFailures = 3; int consecutiveSpawnFailures = 0; // invariant: watchdog is single-owner. SpawnBackglowDaemon only re-creates // watchdog if !joinable(). m_hBackglowProcess is updated before // SpawnBackglowDaemon returns, so hProc reload on each iteration is safe. while (!m_bStopBackglowWatchdog.load()) { // Re-read m_hBackglowProcess at the TOP of each iteration (not captured // once). After a respawn, SpawnBackglowDaemon has stored the new handle // into m_hBackglowProcess by the time control returns here — we must // observe the fresh handle, not a stale copy from a prior iteration. // CR-01/WR-03 fix: acquire-ordered load for defined cross-thread read. HANDLE hProc = m_hBackglowProcess.load(std::memory_order_acquire); if (!hProc) { // WR-03 (iter-3) fix: a null handle here is NOT necessarily a // teardown signal — it also occurs when a prior SpawnBackglowDaemon() // failed below, leaving the process slot empty. Previously the loop // broke on the next iteration, permanently disabling the daemon // for the rest of the SteamVR session on any transient spawn // failure (file-system glitch, CreateProcessW stall, etc.). // Instead, treat null-with-stop-not-requested as a respawn retry: // back off briefly, bail if the respawn cap was already reached, // and otherwise try SpawnBackglowDaemon() again. The top-of-loop // m_bStopBackglowWatchdog check still drains the teardown case. if (m_backglowRespawnCount.load() >= 3) { DriverLog("Backglow: respawn cap reached (3); giving up for this session\n"); break; } // WR-01 (iter-4) fix: also cap on consecutive spawn *failures*, not // just successful-respawn count. If the binary is permanently // missing or blocked, m_backglowRespawnCount never increments and // the cap above can't fire; without this check the loop spins at // 500 ms cadence for the whole SteamVR session. if (consecutiveSpawnFailures >= kMaxConsecutiveFailures) { DriverLog("Backglow: %d consecutive spawn failures — giving up for this session\n", consecutiveSpawnFailures); break; } std::this_thread::sleep_for(std::chrono::milliseconds(500)); if (m_bStopBackglowWatchdog.load()) break; if (!SpawnBackglowDaemon()) { ++consecutiveSpawnFailures; DriverLog("Backglow: respawn retry failed (%d/%d) — backing off\n", consecutiveSpawnFailures, kMaxConsecutiveFailures); // Fall through; next iteration re-enters this branch, sleeps // another 500 ms, retries until cap or stop flag fires. } else { consecutiveSpawnFailures = 0; // recovery: reset failure streak } continue; } WaitForSingleObject(hProc, INFINITE); if (m_bStopBackglowWatchdog.load()) break; // normal shutdown path DWORD exitCode = 0; GetExitCodeProcess(hProc, &exitCode); auto now = std::chrono::steady_clock::now(); auto uptimeMs = std::chrono::duration_cast( now - m_backglowSpawnTime).count(); // CR-01/WR-03 + WR-01 (re-review) fix: exchange(nullptr) so only ONE // path ever CloseHandles a given handle value. If Stop() also raced // here, only the thread that won the exchange sees a non-null handle // and calls CloseHandle. Applies to job handle too. HANDLE hProcOwned = m_hBackglowProcess.exchange(nullptr, std::memory_order_acq_rel); if (hProcOwned) CloseHandle(hProcOwned); HANDLE hStdinOwned = m_hBackglowStdinWrite.exchange(nullptr, std::memory_order_acq_rel); if (hStdinOwned) CloseHandle(hStdinOwned); HANDLE hJobOwned = m_hBackglowJob.exchange(nullptr, std::memory_order_acq_rel); if (hJobOwned) CloseHandle(hJobOwned); if (uptimeMs >= 60000) { m_backglowRespawnCount.store(0); // reset counter — daemon was stable } int n = m_backglowRespawnCount.load(); if (n >= 3) { DriverLog("Backglow: respawn cap reached (3); giving up for this session\n"); break; } int delayMs = kBackoffMs[n]; DriverLog("Backglow: daemon exited code=%lu uptime=%lldms — respawn %d/3 in %dms\n", exitCode, (long long)uptimeMs, n + 1, delayMs); std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); if (m_bStopBackglowWatchdog.load()) break; // WR-03 (iter-3) fix: increment the respawn counter only on a // SUCCESSFUL spawn. Previously the counter was bumped pre-emptively // before SpawnBackglowDaemon(), which meant a transient CreateProcessW // glitch burned a respawn slot and the user hit the cap one spawn // sooner than the /3 budget intended. On failure we leave n unchanged // and loop back into the null-handle retry path above, which will // try again after a 500 ms back-off until the cap or stop flag fires. if (SpawnBackglowDaemon()) { m_backglowRespawnCount.store(n + 1); consecutiveSpawnFailures = 0; // WR-01 iter-4: reset on success } else { ++consecutiveSpawnFailures; // WR-01 iter-4: count this attempt DriverLog("Backglow: respawn %d failed — entering retry loop (cap=%d)\n", n + 1, 3); // Next iteration's top-of-loop load returns nullptr; the new WR-03 // retry branch takes over (back off 500 ms + retry Spawn). } } } void DeviceProvider::StopBackglowDaemon() { m_bStopBackglowWatchdog.store(true); // CR-01/WR-03 fix: we must not CloseHandle(m_hBackglowProcess) before // joining the watchdog thread, because the watchdog may be blocked in // WaitForSingleObject on that exact handle — closing a handle while another // thread waits on it is Win32 UB. Use exchange(nullptr) everywhere so only // ONE path CloseHandles any given handle value, and join the watchdog // BEFORE touching the process/job handles (so the watchdog's post-Wait // teardown completes first). // Phase 1: close stdin write-end — daemon reads EOF, initiates graceful // shutdown. exchange(nullptr) means the watchdog's own // m_hBackglowStdinWrite exchange will become a no-op on this value. HANDLE hStdinOwned = m_hBackglowStdinWrite.exchange(nullptr, std::memory_order_acq_rel); if (hStdinOwned) CloseHandle(hStdinOwned); // Phase 2: bounded wait for graceful exit (250 ms). Use a LOCAL copy via // .load() — we do NOT exchange here because the watchdog may still be // waiting on this handle; we only CloseHandle after the watchdog is // joined in Phase 4. bool gracefully = false; HANDLE hProcLocal = m_hBackglowProcess.load(std::memory_order_acquire); if (hProcLocal) { DWORD w = WaitForSingleObject(hProcLocal, 250); gracefully = (w == WAIT_OBJECT_0); } // Phase 3: close Job Object — kernel terminates anything still running // (D-03). This unblocks the watchdog's WaitForSingleObject if the daemon // hadn't already exited gracefully. // WR-01 (re-review) fix: exchange(nullptr) discipline — if the watchdog // already closed the job in its post-Wait teardown, our exchange returns // nullptr and we skip the close. Mirrors the process/stdin handle pattern. HANDLE hJobOwned = m_hBackglowJob.exchange(nullptr, std::memory_order_acq_rel); if (hJobOwned) CloseHandle(hJobOwned); // Phase 4: JOIN THE WATCHDOG FIRST, BEFORE touching m_hBackglowProcess. // On its post-Wake path the watchdog will exchange(nullptr) + CloseHandle // the process handle itself. Only after join is it safe for Stop() to // exchange and (if the watchdog lost the race and got nullptr) close the // process handle ourselves. if (m_backglowWatchdogThread.joinable()) { m_backglowWatchdogThread.join(); } // Phase 5: any remaining handle value — typically the watchdog already // exchanged nullptr in its teardown, so this is usually a no-op. HANDLE hProcOwned = m_hBackglowProcess.exchange(nullptr, std::memory_order_acq_rel); if (hProcOwned) CloseHandle(hProcOwned); if (gracefully) DriverLog("Backglow: daemon stopped gracefully\n"); else DriverLog("Backglow: daemon force-killed via Job Object close\n"); } void DeviceProvider::HandleBackglowCommand(const char* args, char* response, size_t responseSize) { // CR-01 (Phase 15 review): lock backglow shared state for the duration of // the handler. The hotplug thread (OnHotplugArrival) may concurrently // reconstruct m_pLedController and rewrite m_backglowPort / disabled // flags, which without synchronisation is C++ UB (torn reads, use-after- // free on the controller pointer). Critical sections are short (string // copy / pointer deref / snprintf), so a plain lock_guard is fine. std::lock_guard lk(m_backglowMtx); // Tokenise verb early so we can route `status` BEFORE the degraded-mode bail // (status is queryable in any state — D-13/D-15). char buf[256] = {0}; std::strncpy(buf, args, sizeof(buf) - 1); char* ctx = nullptr; char* verb = strtok_s(buf, " \t\r\n", &ctx); if (!verb) { snprintf(response, responseSize, "ERR backglow requires subcommand"); return; } // --- status (D-13/D-14/D-15) — answerable in any state --- if (std::strcmp(verb, "status") == 0) { const char* tStr = (m_backglowTransport == BackglowXp::DDP) ? "ddp" : (m_backglowTransport == BackglowXp::AUTO) ? "auto" : "usb"; // Connection state: prefer LedController state if present, else fall // back to m_backglowDisabled flag (covers degraded-pre-ctor case). const char* cStr = "disabled"; if (m_pLedController) { int s = m_pLedController->GetConnectionState(); if (s == 1) cStr = "open"; else if (s == 2) cStr = "reconnecting"; // s == 0 -> disabled (covered) } if (m_backglowDisabled) cStr = "disabled"; // Choose port: line — `port:` for USB-resolved, `host:` for DDP-resolved. // Auto mode reports the actual transport in use (USB if USB succeeded, // DDP if it fell through). Detect by which of m_backglowPort or // m_backglowDdpHost was actually adopted by the factory. bool usingDdp = (m_backglowTransport == BackglowXp::DDP) || (m_backglowTransport == BackglowXp::AUTO && m_backglowPort.empty()); const char* targetLabel = usingDdp ? "host" : "port"; const char* targetValue = usingDdp ? m_backglowDdpHost.c_str() : m_backglowPort.c_str(); unsigned ceilingV = m_pLedController ? m_pLedController->GetCeiling() : static_cast(m_backglowCeiling); // IN-05 (Phase 15 review): use %-9s for the dynamic label so alignment // is self-documenting and cannot silently break if a future label is // longer than the current 4-char "port"/"host". int n = std::snprintf(response, responseSize, "transport: %s\n" "conn: %s\n" "%-9s: %s\n" "bri: %u\n" "ceiling: %u\n" "leds: %d\n", tStr, cStr, targetLabel, targetValue, (unsigned)m_lastReportedBri, ceilingV, kBackglowMaxLeds); // D-15 err: token appended only when not healthy. const bool unhealthy = (std::strcmp(cStr, "open") != 0); if (unhealthy && !m_backglowDisabledReason.empty() && n > 0 && static_cast(n) < responseSize) { std::snprintf(response + n, responseSize - static_cast(n), "err: %s\n", m_backglowDisabledReason.c_str()); } return; } // --- All other verbs require an active controller (existing Phase 14 contract) --- // D-15: degraded mode yields a uniform error for every subcommand so the // caller sees a stable signal. if (m_backglowDisabled || !m_pLedController || m_pLedController->GetConnectionState() == 0) { snprintf(response, responseSize, "ERR backglow disabled (%s)", m_backglowDisabledReason.empty() ? "no port" : m_backglowDisabledReason.c_str()); return; } // --- fill: polymorphic 1-hex or kBackglowMaxLeds-hex form (D-03) --- if (std::strcmp(verb, "fill") == 0) { uint8_t rgb[kBackglowMaxLeds * 3] = {0}; int n = 0; uint8_t singleR = 0, singleG = 0, singleB = 0; for (char* tok = strtok_s(nullptr, " \t\r\n", &ctx); tok && n < kBackglowMaxLeds; tok = strtok_s(nullptr, " \t\r\n", &ctx)) { uint8_t r, g, b; if (!ParseHex6(tok, std::strlen(tok), r, g, b)) { snprintf(response, responseSize, "ERR fill expects 1 or 10 hex values"); return; } if (n == 0) { singleR = r; singleG = g; singleB = b; } rgb[n * 3 + 0] = r; rgb[n * 3 + 1] = g; rgb[n * 3 + 2] = b; ++n; } // Reject extra tokens past kBackglowMaxLeds too (count != 1 && != 10). // Note: if a token came after kBackglowMaxLeds we already exited the loop // with n == kBackglowMaxLeds and left an extra token in ctx. Verify. if (n == kBackglowMaxLeds) { char* extra = strtok_s(nullptr, " \t\r\n", &ctx); if (extra) { snprintf(response, responseSize, "ERR fill expects 1 or 10 hex values"); return; } } if (n == 1) { for (int i = 1; i < kBackglowMaxLeds; ++i) { rgb[i * 3 + 0] = singleR; rgb[i * 3 + 1] = singleG; rgb[i * 3 + 2] = singleB; } m_pLedController->QueueFrame(rgb, kBackglowMaxLeds); snprintf(response, responseSize, "OK fill=%02X%02X%02X leds=%d", singleR, singleG, singleB, kBackglowMaxLeds); } else if (n == kBackglowMaxLeds) { m_pLedController->QueueFrame(rgb, kBackglowMaxLeds); snprintf(response, responseSize, "OK fill=per-led leds=%d", kBackglowMaxLeds); } else { snprintf(response, responseSize, "ERR fill expects 1 or 10 hex values"); } return; } // --- set (D-04) --- if (std::strcmp(verb, "set") == 0) { char* idxTok = strtok_s(nullptr, " \t\r\n", &ctx); char* hexTok = strtok_s(nullptr, " \t\r\n", &ctx); if (!idxTok || !hexTok) { snprintf(response, responseSize, "ERR set requires <6-hex>"); return; } int idx = std::atoi(idxTok); uint8_t r, g, b; if (idx < 0 || idx >= kBackglowMaxLeds || !ParseHex6(hexTok, std::strlen(hexTok), r, g, b)) { snprintf(response, responseSize, "ERR set requires <6-hex>"); return; } m_pLedController->QueueLedSet(idx, r, g, b); snprintf(response, responseSize, "OK set idx=%d rgb=%02X%02X%02X", idx, r, g, b); return; } // --- bri (D-05, D-13) --- if (std::strcmp(verb, "bri") == 0) { char* tok = strtok_s(nullptr, " \t\r\n", &ctx); unsigned v = 0; if (!tok || std::sscanf(tok, "%u", &v) != 1 || v > 255) { snprintf(response, responseSize, "ERR bri requires 0..255"); return; } // Belt-and-suspenders ceiling clamp for the response value (LedController // re-clamps internally before WLED JSON). Reported 'clamped' is the // effective post-ceiling master brightness. uint8_t ceiling = m_pLedController->GetCeiling(); unsigned clamped = (v > ceiling) ? ceiling : v; m_pLedController->QueueBrightness(static_cast(v)); m_lastReportedBri = static_cast(clamped); // Phase 15: surface in `status` (D-14) snprintf(response, responseSize, "OK bri=%u ceiling=%u", clamped, (unsigned)ceiling); return; } // --- off (D-06) --- if (std::strcmp(verb, "off") == 0) { m_pLedController->QueuePower(false); snprintf(response, responseSize, "OK off"); return; } // --- on (D-06 complement; aids UAT) --- if (std::strcmp(verb, "on") == 0) { m_pLedController->QueuePower(true); snprintf(response, responseSize, "OK on"); return; } snprintf(response, responseSize, "ERR unknown backglow subcommand"); } // --- Hotplug (D-15 + Pattern 5 Message-Only Window) --- void DeviceProvider::StartHotplugWatcher() { m_bHotplugStop.store(false); s_hotplugOwner = this; m_hotplugThread = std::thread(&DeviceProvider::HotplugThreadFunc, this); } void DeviceProvider::StopHotplugWatcher() { m_bHotplugStop.store(true); if (m_hHotplugWindow) { // Post WM_QUIT to unblock GetMessageA in the thread loop. PostMessageA(m_hHotplugWindow, WM_QUIT, 0, 0); } if (m_hotplugThread.joinable()) { m_hotplugThread.join(); } s_hotplugOwner = nullptr; } void DeviceProvider::HotplugThreadFunc() { WNDCLASSA wc{}; wc.lpfnWndProc = HotplugWndProc; wc.hInstance = GetModuleHandleA(nullptr); wc.lpszClassName = kHotplugClassName; // Already registered? Ignore the error and proceed to CreateWindow. RegisterClassA(&wc); m_hHotplugWindow = CreateWindowExA(0, kHotplugClassName, "", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, wc.hInstance, nullptr); if (!m_hHotplugWindow) { DriverLog("Backglow hotplug: CreateWindowExA failed (err=%lu); watcher disabled\n", GetLastError()); return; } DEV_BROADCAST_DEVICEINTERFACE_A filter{}; filter.dbcc_size = sizeof(filter); filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; filter.dbcc_classguid = GUID_DEVINTERFACE_COMPORT; m_hHotplugNotify = RegisterDeviceNotificationA(m_hHotplugWindow, &filter, DEVICE_NOTIFY_WINDOW_HANDLE); if (!m_hHotplugNotify) { DriverLog("Backglow hotplug: RegisterDeviceNotificationA failed (err=%lu)\n", GetLastError()); } MSG msg; while (!m_bHotplugStop.load()) { BOOL r = GetMessageA(&msg, nullptr, 0, 0); if (r <= 0) break; // 0 = WM_QUIT, -1 = error TranslateMessage(&msg); DispatchMessageA(&msg); } if (m_hHotplugNotify) { UnregisterDeviceNotification(m_hHotplugNotify); m_hHotplugNotify = nullptr; } if (m_hHotplugWindow) { DestroyWindow(m_hHotplugWindow); m_hHotplugWindow = nullptr; } UnregisterClassA(kHotplugClassName, wc.hInstance); } void DeviceProvider::OnHotplugArrival() { // D-15 / Pitfall 2: 500 ms debounce — composite USB devices need a moment // for the CDC child to fully enumerate after WM_DEVICECHANGE fires. // Debounce BEFORE taking the lock so HandleBackglowCommand is not blocked // for 500 ms on the main thread. Sleep(500); // CR-01 (Phase 15 review): lock backglow shared state for the remainder // of the handler. Writes to m_backglowDisabled, m_backglowDisabledReason, // m_backglowPort and m_pLedController must be serialised against // HandleBackglowCommand on the vrserver RunFrame thread. std::lock_guard lk(m_backglowMtx); // D-11: only react when transport=USB (or AUTO that fell into USB) AND we // are currently disabled. DDP transport ignores hotplug; live USB sessions // are not disturbed. if (m_backglowTransport != BackglowXp::USB && m_backglowTransport != BackglowXp::AUTO) { return; } if (!m_backglowDisabled) { return; // already healthy, nothing to do } // Re-run the VID/PID scan. D-09 step 2: rescan covers the case where no // port was previously found AND the previous scan returned empty. auto matches = ScanForUsbComPorts(0x303A, 0x1001); if (matches.empty()) { // Still no match — leave disabled state in place; do not flap reason token. return; } if (matches.size() > 1) { std::string allMatches; for (size_t i = 0; i < matches.size(); ++i) { if (i) allMatches += ", "; allMatches += matches[i]; } DriverLog("Backglow hotplug: scan matched [%s]; using lowest %s\n", allMatches.c_str(), matches.front().c_str()); } else { DriverLog("Backglow hotplug: scan matched VID_303A&PID_1001 -> %s\n", matches.front().c_str()); } m_backglowPort = matches.front(); if (!m_pLedController) { // Controller was never constructed (init failed before make_unique). // Construct it now with a fresh USB transport. auto t = std::make_unique(); m_pLedController = std::make_unique( std::move(t), static_cast(m_backglowCeiling)); } m_pLedController->Start(m_backglowPort); if (m_pLedController->GetConnectionState() == 1) { m_backglowDisabled = false; m_backglowDisabledReason.clear(); DriverLog("Backglow: hotplug re-init succeeded on %s\n", m_backglowPort.c_str()); } } #endif // ENABLE_BACKGLOW