// M1 first light: S1 (Tundra IMU) + S2 (NVAPI DirectMode presenter) glued in // one process. Head-locked test pattern from a cold boot, with an IMU-driven // needle so both subsystems are eyeball-verifiable at once: // // - per-eye color fields (left green / right red) + synced sweeping bar — // the S2 structure-revealing pattern; any DSC/packing fault shows. // - a white "needle" near the bottom of each eye deflected by raw gyro: // rotate your head, the needle kicks. IMU dead => needle frozen center. // // No tracking claim here (no AHRS, no calibration, no warp — M2). The needle // is liveness, not pose. // // sauna_first_light [seconds] [--imu-serial LHR-XXXXXXXX] // // seconds 0 or omitted = run until Ctrl+C. --imu-serial picks the Beyond // when other Watchman devices (wired Index controller, Tundra tracker) are // attached; with one device it is optional. // // Exit codes: 0 ok, 2 IMU device missing/ambiguous, 3 display init failed, // 4 display lost mid-run (hot-unplug) — exited cleanly, restart to recover. #include "device/mcu_prox.h" #include "device/tundra_imu.h" #include "present/nvapi_d3d12.h" #include #include #include #include #include #include #include namespace { std::atomic g_stop{false}; BOOL WINAPI ctrlHandler(DWORD type) { if (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT || type == CTRL_CLOSE_EVENT) { g_stop.store(true); return TRUE; } return FALSE; } } // namespace int main(int argc, char** argv) { setbuf(stdout, nullptr); setbuf(stderr, nullptr); double seconds = 0.0; const char* imuSerial = ""; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--imu-serial") == 0 && i + 1 < argc) imuSerial = argv[++i]; else seconds = atof(argv[i]); } SetConsoleCtrlHandler(ctrlHandler, TRUE); sauna::TundraImu imu; if (!imu.start(imuSerial)) return 2; // Probe the stream before lighting panels: a Tundra that SteamVR put to // sleep on exit enumerates fine but delivers nothing (S1 proved cold-boot // streaming; post-SteamVR state is a different animal). std::this_thread::sleep_for(std::chrono::milliseconds(1500)); { auto is = imu.stats(); if (is.samples == 0) { fprintf(stderr, "WARNING: Tundra enumerated but 0 samples in 1.5 s " "(%llu read timeouts) — asleep after a SteamVR exit? " "Continuing; needle will stay dead.\n", (unsigned long long)is.read_timeouts); } else { sauna::ImuSample s; imu.latest(&s); printf("IMU stream up: %.0f Hz, accel=(%d,%d,%d) gyro=(%d,%d,%d)\n", is.samples / 1.5, s.accel[0], s.accel[1], s.accel[2], s.gyro[0], s.gyro[1], s.gyro[2]); } } // Prox is advisory in first light: doff/don transitions are logged and // shown in the status line; free-run detection stays the present-loop // backstop. Absence (e.g. MCU busy) is non-fatal. sauna::McuProx prox; bool haveProx = prox.start(); sauna::NvapiPresenterConfig cfg; // defaults = the S2 validated link config sauna::NvapiD3d12Presenter presenter(cfg); if (!presenter.init()) return 3; // Per-second status line from a side thread: presenter fps + IMU rate. std::thread status([&] { sauna::ImuStats prevImu{}; sauna::PresentStats prevP{}; bool prevWorn = haveProx && prox.worn(); while (!g_stop.load()) { std::this_thread::sleep_for(std::chrono::seconds(1)); auto is = imu.stats(); auto ps = presenter.stats(); if (haveProx && prox.worn() != prevWorn) { prevWorn = prox.worn(); printf("prox: headset %s (distance %u)\n", prevWorn ? "DONNED" : "DOFFED", prox.proxDistance()); } sauna::ImuSample s{}; double age = -1; imu.latest(&s, &age); double amag = sqrt((double)s.accel[0] * s.accel[0] + (double)s.accel[1] * s.accel[1] + (double)s.accel[2] * s.accel[2]); char proxStr[32] = "prox=n/a"; if (haveProx) snprintf(proxStr, sizeof(proxStr), "prox=%s(%u)", prox.worn() ? "worn" : "away", prox.proxDistance()); printf(" fps=%5.1f imu=%4lluHz age=%.2fs gaps=%llu stalls=%llu " "|a|=%.0f gyro=(%d,%d,%d) %s%s\n", (double)(ps.frames - prevP.frames), (unsigned long long)(is.samples - prevImu.samples), age, (unsigned long long)is.seq_gaps, (unsigned long long)is.read_timeouts, amag, s.gyro[0], s.gyro[1], s.gyro[2], proxStr, ps.freeRunning ? " [FREE-RUN: vsync lost, throttled]" : ""); prevImu = is; prevP = ps; } }); presenter.run(seconds, [&](const sauna::FrameContext& fc) { auto* list = static_cast(fc.cmdList); auto* rtv = static_cast(fc.rtv); LONG W = (LONG)fc.width, H = (LONG)fc.height, half = W / 2; // S2 structure-revealing base: per-eye fields + synced sweeping bar. float lc[4] = {0.0f, 0.25f, 0.08f, 1.0f}; float rc[4] = {0.25f, 0.0f, 0.08f, 1.0f}; float wc[4] = {0.4f, 0.4f, 0.4f, 1.0f}; D3D12_RECT rl{0, 0, half, H}, rr{half, 0, W, H}; list->ClearRenderTargetView(*rtv, lc, 1, &rl); list->ClearRenderTargetView(*rtv, rc, 1, &rr); LONG barW = 64; LONG bx = (LONG)(fmod(fc.timeSec * 0.25, 1.0) * (half - barW)); D3D12_RECT bars[2] = {{bx, 0, bx + barW, H}, {half + bx, 0, half + bx + barW, H}}; list->ClearRenderTargetView(*rtv, wc, 2, bars); // IMU liveness needle: gyro MAGNITUDE deflection (axis-mapping-proof — // any head rotation kicks it right from the left rest position). Stale // stream (last sample > 0.25 s old, e.g. Tundra asleep after a SteamVR // exit) shows as a RED track with no needle — distinct from "at rest". sauna::ImuSample s; double age = 1e9; bool have = imu.latest(&s, &age) && imu.running(); bool fresh = have && age < 0.25; LONG trackH = H / 24, trackY = H - 2 * trackH; float trackC[4] = {0.03f, 0.03f, 0.03f, 1.0f}; float deadC[4] = {0.35f, 0.0f, 0.0f, 1.0f}; D3D12_RECT tracks[2] = {{0, trackY, half, trackY + trackH}, {half, trackY, W, trackY + trackH}}; list->ClearRenderTargetView(*rtv, fresh ? trackC : deadC, 2, tracks); if (fresh) { // 2000 LSB gyro magnitude -> full-track deflection. Raw units; scale // is unverified (S1) and irrelevant for a liveness check. float gmag = sqrtf((float)s.gyro[0] * s.gyro[0] + (float)s.gyro[1] * s.gyro[1] + (float)s.gyro[2] * s.gyro[2]); float defl = gmag / 2000.0f; if (defl > 1.0f) defl = 1.0f; LONG margin = 64, needleW = 24; LONG span = half - 2 * margin - needleW; LONG nx = margin + (LONG)(defl * span); float nc[4] = {0.9f, 0.9f, 0.9f, 1.0f}; D3D12_RECT needles[2] = { {nx, trackY, nx + needleW, trackY + trackH}, {half + nx, trackY, half + nx + needleW, trackY + trackH}}; list->ClearRenderTargetView(*rtv, nc, 2, needles); } if (g_stop.load()) presenter.stop(); }); g_stop.store(true); status.join(); prox.stop(); imu.stop(); auto is = imu.stats(); auto ps = presenter.stats(); printf("IMU RESULT: %llu samples (%llu reports), %llu seq gaps, %llu stalls\n", (unsigned long long)is.samples, (unsigned long long)is.reports, (unsigned long long)is.seq_gaps, (unsigned long long)is.read_timeouts); if (ps.displayLost) { printf("VERDICT: DISPLAY LOST (cable pulled?) — exited cleanly; restart " "after reconnect\n"); return 4; } bool pass = ps.frames > 0 && ps.presentErrors == 0 && is.samples > 0; printf("VERDICT: %s\n", pass ? "PASS (eyeball check still required)" : "FAIL"); return 0; }