# Bigscreen Beyond — Calibration & Distortion Pipeline

Sources (all first-party, read-only; branch `main` unless noted):

- `calibot` — C:\Users\decid\Documents\projects\calibot (factory tracking-calibration rig)
- `valve-tools-wrapper` — C:\Users\decid\Documents\projects\bigscreen\valve-tools-wrapper (Python wrapper around Valve factory tools + sample per-device configs)
- `big-notebooks` — C:\Users\decid\Documents\projects\bigscreen\big-notebooks (ops/fulfillment notebooks + config flasher)
- Supplementary (found adjacent, directly relevant): `vertical-alignment-proximity` (branch `master`) — C:\Users\decid\Documents\projects\bigscreen\vertical-alignment-proximity (per-unit optical alignment station, "VAP"); `check-canting` — C:\Users\decid\Documents\projects\check-canting; `extended-mode-display` — C:\Users\decid\Documents\projects\extended-mode-display (non-SteamVR display access).

---

## 1. The big picture

The Beyond is, to SteamVR, a **stock lighthouse HMD**. Its tracking SIP is a Tundra Labs module enumerating with Valve's VID/PID (`0x28de:0x2300`, see `calibot/calibrate_diode.py` `VIDS_TUNDRA/PIDS_TUNDRA`); a separate Bigscreen MCU ("Atmel", `0x35bd:0x0101`, product string "Beyond") handles displays/LED/proximity/fan. **All optical + tracking calibration lives in a single Valve-format JSON config stored in the tracking SIP's flash** (~2 KB zlib-compressed, ~11 KB inflated — see notebook output in `big-notebooks/testConfigFlasher.ipynb`: "Read config of 1979 bytes ... inflated to 11004 bytes"). SteamVR's built-in lighthouse driver downloads that JSON from the device at startup, caches it under `C:\Program Files (x86)\Steam\config\lighthouse\<lhr-serial>\`, and implements `IVRDisplayComponent::ComputeDistortion()` from its `tracking_to_eye_transform` block. The compositor samples that function to build its warp mesh. No Bigscreen-authored OpenVR driver exists in any of these repos — the distortion consumer is Valve's closed `driver_lighthouse`.

Factory chain per unit:

1. **calibot station** — flashes a full template config (incl. design-level distortion polynomials) to the SIP, then solves per-unit photodiode geometry with Valve's `lhcalib.exe` and writes it back to SIP flash.
2. **VAP station** — calibrates the proximity sensor (into the MCU's 512-byte TLV config memory) and measures per-unit display/optics rotational misalignment with a camera; writes per-unit `eye_to_head` matrices and `ipd.default_mm` into the SIP JSON config.
3. Results PASS/FAIL go to a factory MES web endpoint (`vertical-alignment-proximity/mes_logger.py`, station IDs "VAP T060, MMI T070, BT T080"). The Bigscreen cloud (`big-notebooks/api.py`) stores order/IPD/face-scan data, **not** optical calibration; no evidence of per-serial calibration in the cloud.

---

## 2. calibot — what it is

`calibot/README.md`: "Software run on calibot - used for steamVR tracking". It is a **2-axis motorized motion rig** (two Trinamic TMCM-1161 stepper modules over serial-TMCL, worm ratio 180:1, axes addressed by global-parameter IDs 159/160 — `calibot/calibot.py`). It slings up to **4 headsets at once** through a randomized motion path in view of **2–3 lighthouse base stations** (`calibot/calibot_config.yaml`: `basestation_count: 3`).

Flow (`calibot/calibrate_diode.py`, the shipping app; `calibot/main.py` is an older variant):

1. USB device-monitor matches each Atmel MCU to its Tundra SIP by shared USB hub port chain (`HeadsetHIDMatching`); headset LED is driven via MCU HID feature report `[0,'L',r,g,b]` for operator status (white=in progress, green=pass, red=fail).
2. `calibot/load_init_json.py` uploads `calibot/init_json_config.json` **wholesale** to the SIP via `lighthouse_console.exe uploadconfig`, with only `device_serial_number` substituted per device. I.e. at this stage **every unit receives identical distortion polynomials** — the optical distortion model is per-design (possibly per lens revision), not measured per unit.
3. Runs Valve's `lhcalib.exe`:
   `lhcalib /bodyserial <LHR-…> /bodycal init_json_config.json <total_hits=800> <hits_per_sensor=80> /bodycalmulti <n_bases> /deletemissingsensors`
   which solves photodiode positions/normals from sweep observations during arm motion. Output file `auto_<serial>.json` echoes the input config with solved `lighthouse_config` (`channelMap` [22 sensors], `modelPoints` [m, HMD frame], `modelNormals`).
4. The station downloads the device's current config, replaces only `lighthouse_config`, adds a `bscv` version key (`"<total_hits>.<hits_per_sensor>"`, e.g. `800.80`), and re-uploads (`calibrate_headset()` in `calibot/calibrate_diode.py`).
5. IMU calibration support exists (`ImuCalibrationProcess` → `acc_bias`, `acc_scale`, `gyro_bias`) but is commented out in `calibot/main.py::create_imu_calibration_threads` — production template ships zero biases.

So calibot measures: **photodiode geometry per unit** (tracking), optionally IMU bias. It does **not** measure optics.

---

## 3. The on-device config JSON (the calibration artifact)

Canonical template: `calibot/init_json_config.json` (production Beyond 1, `revision: 0.12`). Older: `calibot/init_json_config_o.json` (differs only in serial placeholder, `ipd.default_mm` 63 vs 73, and slightly different nominal `modelPoints`); `valve-tools-wrapper/steamvr/init_json_evt.json` (EVT era); 20 real DVT-unit dumps in `valve-tools-wrapper/steamvr/tools/bin/win64/DVTs/LHR-*.json`; a Beyond-2 test config embedded in `big-notebooks/testConfigFlasher.ipynb` (`BS2_TEST_CONFIG.json`, file itself not in repo).

Top-level keys (all variants):

| key | meaning / production value |
|---|---|
| `manufacturer`/`model_number` | "Bigscreen"/"Beyond" |
| `device` | `eye_target_width/height_in_pixels: 2544×2544` per eye, `first_eye eEYE_LEFT`, `num_windows: 1` |
| `direct_mode_edid_vid/pid` | `"BIG"` / `"1234"` — EDID identity used for direct-mode whitelisting |
| `dsc_version/slices/bppx16` | 17 (0x11), 4, 128 — Display Stream Compression config |
| `display_color_mult` | per-eye RGB white-point multipliers `[0.952545, 0.917645, 1]` (identical both eyes and across all sampled units → design-level) |
| `seconds_from_vsync_to_photons` | 0.002 |
| `seconds_from_photons_to_vblank` | 0.0138888888888889 |
| `parallel_render_cameras` | true |
| `head` | head-origin frame (`plus_x`,`plus_z`,`position`) |
| `imu` | mount orientation `plus_x [-1,0,0]`, `plus_z [0.00067,0.1961,-0.98058]`, `position`, biases/scale (per-unit if IMU cal ran) |
| `ipd` | production B1: `{"default_mm": <unit IPD>}`; EVT/DVT/lhcalib-era: `{"enable": true, "high_mm": 65.0, "low_mm": 65.0, "minstep_mm": 2}` |
| `lighthouse_config` | per-unit: `channelMap` (22 entries), `modelPoints`, `modelNormals` |
| `tracking_to_eye_transform` | array[2] = LEFT, RIGHT — the optics block, below |
| `firmware_config` | `{"imu_rate": 1000}` |
| `expected_bases/controllers` | 2/2 |
| `bscv` | added by calibot: photodiode-cal version stamp |

### 3.1 `tracking_to_eye_transform[eye]` — the distortion model

Per eye:

- `intrinsics`: 3×3 ideal pinhole projection in Valve convention
  `[[fx,0,cx],[0,fy,cy],[0,0,-1]]` with `cx = -center_x`, `cy = ±center_y`. fx,fy ≈ 1.422 (production B1) → per-eye half-FOV = atan(1/1.4224) ≈ 35.1°, i.e. ≈70.3°×70.3° per eye before canting. This is the projection used for the render camera; distortion is defined relative to it.
- `distortion`, `distortion_red`, `distortion_blue`: per-color radial polynomial. Fields: `type`, `center_x`, `center_y` (NDC units, shared across the three channels within an eye), `coeffs`.
- `eye_to_head`: 3×3 **rotation only** (display canting). Translation comes from `ipd.default_mm` (split symmetrically by the driver — inferred; the JSON nowhere stores an eye translation).
- `grow_for_undistort`: 0.6 (production B1; 0.4 EVT/DVT/B2-test) — how much the pre-distortion render target is "grown" so undistorted content covers the panel.
- `undistort_r2_cutoff`: 1.0 — clamp on r² beyond which the polynomial is not extrapolated.

Distortion `type` evolution observed:

| era | type | coeffs |
|---|---|---|
| EVT + DVT (`init_json_evt.json`, `DVTs/LHR-*.json`) | `DISTORT_DPOLY3` | 3 per channel, e.g. green L `[-0.2204, -0.0196, 0.0017]` (negative leading) |
| Production B1 (`calibot/init_json_config.json`) | `DISTORT_POLY3` | 3 per channel, e.g. green L `[0.2390, 0.0549, 0.1362]` (positive leading), red `[0.2159, 0.0887, 0.1174]`, blue `[0.2805, -0.0059, 0.1701]` |
| Beyond 2 test (`big-notebooks/testConfigFlasher.ipynb`) | `DISTORT_DPOLY3_SCALED` | 4: shared base `[k1,k2,k3]` for all channels + 4th coeff = per-channel radial scale: green `0.0`, blue `+0.01`, red `-0.006904` |

Interpretation (the evaluator itself is inside Valve's closed lighthouse driver; none of these repos contain it):

- The DPOLY3 family is the long-known Vive-format model. Public reimplementations (Monado `u_compute_distortion_vive`, OpenHMD) evaluate, **mapping panel/output UV → pre-distortion texture UV**, per channel c:
  - `t = 2·uv − 1` (per-eye NDC), optionally aspect-corrected
  - `t -= (center_x, center_y)`
  - `r² = dot(t,t)`, clamped to `undistort_r2_cutoff`
  - `d = 1 + k1·r² + k2·r⁴ + k3·r⁶`
  - `uv_src = 0.5 + (t·d + center) · 0.5/(1 + grow_for_undistort)` (the grow factor rescales so the grown render target maps onto the panel)
  [external knowledge, marked: matches field names exactly — `grow_for_undistort`, `undistort_r2_cutoff`, `center_x/y`, 3 coeffs — and is what Monado uses to drive real Vive/Index headsets from this same JSON]
- SPECULATION (consistent magnitudes/signs): `DISTORT_POLY3` (positive coeffs) is the same polynomial fitted in the **opposite direction** (undistort vs distort); `1/(1+0.22 r²) ≈ 1−0.22r²+…` matches the observed sign flip between DVT (−0.22) and production (+0.239). Verify empirically against SteamVR output (see §8 validation).
- `DISTORT_DPOLY3_SCALED`: 4th coefficient is a per-channel scale capturing lateral chromatic aberration as pure radial magnification (blue larger, red smaller) on top of a shared base curve. SPECULATION on exact placement: either `d = (1+k4)·(1 + k1r²+k2r⁴+k3r⁶)` or `d = 1+k4 + k1r²+…`; for k4 ≤ 0.01 the two differ negligibly.
- Chromatic aberration in POLY3/DPOLY3 variants is encoded as fully separate per-channel polynomials around a shared center.

### 3.2 Canting (`eye_to_head`) and per-unit alignment

- `check-canting/check_canting.ipynb`: utility that converts pitch/yaw/roll (degrees; +pitch up, +yaw left, +roll CW, user-relative) ↔ `eye_to_head` matrices via scipy `Rotation.from_euler('xyz')`. Used to author/inspect config values.
- Production B1 values: yaw = ±6.17° (outward per eye; `asin(0.10748)`), pitch ≈ +4.5…5.1° in template (per-unit), roll = 0.
- EVT/DVT values: yaw = ±2.0° (`asin(0.034855)`), pitch ≈ +5.0°.
- VAP1 (Beyond 1 launch era, `vertical-alignment-proximity/backup_vap1_alignment.py::align_determine_horizontal_canting`): horizontal cant **depended on IPD SKU** — 2° if IPD ≥ 70 mm else 4.5°. Current `alignment.py`: constants `DOWNWARD_CANT = -5`, `HORIZONTAL_CANT = 6.17`.

**VAP station per-unit measurement** (`vertical-alignment-proximity/README.md`, `alignment.py`, `main.py`):

1. Operator scans HMD serial; it is written into the MCU TLV config (tag 0x08 `HMD_Serial`, `hmd_config.py::hmd_config_write_serial`; the 512-byte TLV store also holds PCB serial 0x01 `XCNL…`, prox cal/threshold/hysteresis 0x06/0x0B/0x0C, brightness 0x0A, etc., read/written via HID feature reports `'U'`/`'W'`/`'V'` with CRC-8 poly 0x07).
2. Proximity sensor calibrated (two-position measure, PASS/FAIL, values into MCU TLV).
3. Optical center of each lens found by displaying a ChArUco board **directly on the panel** (no SteamVR; `DirectModeDisplay`) and detecting corner ID 169/181 with a fixed 640×480 camera under the HMD.
4. A SteamVR-rendered app (DirectX12Seed.exe via named pipe `\\.\pipe\vap_pipe`) draws a horizontal line while sweeping virtual-camera pitch ±7° in 0.1° steps; CV finds the step-edge crossing the optical center; the zero-crossing angle = per-eye pitch error. (Closed-form CV solutions were tried and abandoned as unreliable — README.)
5. Writes per-unit result to SIP flash: `eye_to_head[L] = R_xyz(measured_pitch_L + (−5°), +6.17°, 0)`, `eye_to_head[R] = R_xyz(measured_pitch_R + (−5°), −6.17°, 0)` (`alignment.py::align_write_config_values`), plus `ipd.default_mm` (`align_init` → `steamvr.steamvr.set_ipd_default_mm`). VAP1 additionally solved per-eye roll (`backup_vap1_alignment.py`).
6. Verification step re-measures transition points and PASS/FAILs (≤10 px disparity). `confirm_distortion()` only checks `distortion_red` blocks exist — distortion itself is never measured per unit.

So the complete **per-unit** optical data = `eye_to_head` rotations (pitch±, fixed yaw cant, roll 0) + `ipd.default_mm` + serial. Everything else in the optics block is per-design.

---

## 4. valve-tools-wrapper — Valve tools inventory

`valve-tools-wrapper/steamvr/tools/bin/win64/` ships four Valve factory executables:

- **`lighthouse_console.exe`** — interactive console wrapped by `steamvr/lighthouse_console.py` via stdin/stdout. Commands used: `serial` (list/select device), `deviceinfo` (`DEVICEINFO serial=… device_class=hmd`), `downloadconfig <path>` / `uploadconfig <path>` (SIP flash JSON), `pair`, `reboot`, `poweroff`, `haptic2`, `userdata`. This is the read/write path for **all** calibration data on the device.
- **`lhcalib.exe`** — photodiode + IMU calibration solver (args in §2; also `/imucal <seconds> /imucalautoexit`, `/usedisambiguation synconbeam`, `/outputdir`). Emits `auto_<serial>.json` (solved config) and `calib_observations_<base>_<serial>.json` (raw observations: `file_tracking`, `per_base`, `primary_basestation`, `tracked_device_config`).
- **`imu_calibrator.exe`**, **`vrtrackingcalib.exe`** — present but not referenced by the Python code in these repos.

`steamvr/steamvr.py` extras:

- `update_steam_real_time(config)` → `vrcmd.exe --cmdhmd "set_lenscal <json>"` — **live-reloads lens calibration into a running SteamVR lighthouse driver**. Direct proof the driver consumes exactly this JSON for distortion.
- `load/save_steamvr_vrsettings()` and `SteamVR.set_ipd()` → writes `steamvr.ipdOffset` (meters, relative to 63 mm default) into `C:\Program Files (x86)\Steam\config\steamvr.vrsettings`.
- Config mutators: `set_resolution`, `set_serial_number`, `set_ipd_default_mm`, `swap_eyes`, `swap_xy_distortion_center`, `set_imu_calibration`.
- `steamvr/unbuffered/` — non-blocking variants + `DeviceMonitor` (HID hotplug watcher used by calibot).

---

## 5. big-notebooks — what's relevant

Mostly Shopify/fulfillment ops against the Bigscreen admin cloud (`api.py`: auth/admin/cloud API, `/admin/fabricator/*`, `/admin/shop/*`). Calibration-relevant items:

- **`testConfigFlasher.ipynb`** — flashes `BS2_TEST_CONFIG.json` (Beyond 2 test mule) to a SIP via lighthouse_console; its full output (cell 4) is the complete B2 test config (source of the `DISTORT_DPOLY3_SCALED` model, §3.1).
- **IPD pipeline**: customer face-scan → `scanRequest.ipd` / re-derived "far PD" (`040.RepairTopologyIPDs.ipynb`, `043.…Part2`), clamped to **55–72 mm, 70 mapped to 69** (no 70 mm SKU). HMDs are **built as discrete IPD SKUs**: inventory queried by `serialNumberLike=BS1{ipd}` (`factoryKit.py::loadBs1Stock`) — the IPD is encoded in the HMD serial digits [3:5], which is exactly what VAP1 parsed (`backup_vap1_main.py: input_ipd = int(serial_number[3:5])`). Production quotas per SKU in `022.hmdFulfillment.ipynb`.
- `023.hmdTesting.ipynb` — controller pairing/haptics via lighthouse_console; `startSteamVR.py`.
- No optics-math notebooks here; the optics math lives in `check-canting` and VAP (§3.2).

---

## 6. IPD handling summary (Beyond 1 vs Beyond 2)

- **Beyond 1**: fixed-IPD hardware SKU (lens spacing built-in, 55–72 mm, no 70). Per-unit `ipd.default_mm` written at VAP from the serial-encoded SKU; `eye_to_head` carries no translation (rotation only) — the driver derives eye translation from `ipd.default_mm`. Software trim via `steamvr.vrsettings: ipdOffset`. Early units: horizontal cant varied by SKU (4.5°/2°), later 6.17° uniform.
- **Beyond 2** (test config + current VAP `main.py`): `align_init(serial, 64.0, …)` — IPD hardcoded to 64.0 at the factory. B2 has user-adjustable IPD hardware [public knowledge]; SPECULATION: actual IPD is applied at runtime (user setting / sensor) rather than baked per unit; the config's 64 is just a default. The B2 distortion model moved to shared-base + per-channel scale (`DISTORT_DPOLY3_SCALED`), suggesting one curve valid across the IPD adjustment range with CA as scale.

---

## 7. Where everything lives (per-unit data access paths)

| data | store | access |
|---|---|---|
| Full optical+tracking JSON (distortion, intrinsics, eye_to_head, ipd, lighthouse_config, imu, timings, color mult) | Tundra SIP flash (zlib) | `lighthouse_console.exe` `downloadconfig`/`uploadconfig` (USB HID, VID 0x28de PID 0x2300); SteamVR auto-caches to `Steam\config\lighthouse\<lhr-serial>\config.json` (lowercased serial) |
| Mura/banding correction | device userdata → SteamVR cache `Steam\config\lighthouse\<lhr-serial>\userdata\mura.mc` | PNG-format multiply mask; `extended-mode-display/bars_gen.py` generates column-banding corrections (linear-space multiply, per channel) and overwrites `mura.mc` directly |
| Prox cal/threshold/hysteresis, HMD serial, brightness, PCB serial | Beyond MCU 512-B TLV config | HID feature reports `'U'` read / `'W'` write / `'V'` save (VID 0x35bd PID 0x0101), `vertical-alignment-proximity/hmd_config.py` |
| Order ↔ IPD ↔ face-scan | Bigscreen cloud | `big-notebooks/api.py` admin API (not needed for rendering) |
| PASS/FAIL test records | factory MES (LAN) | `mes_logger.py` |

SteamVR consumption: lighthouse driver reads SIP JSON → implements `ComputeDistortion()` (per-channel poly3), projection from `intrinsics`, eye poses from `eye_to_head`+`ipd`, display timing from the `seconds_*` fields; compositor builds warp mesh by sampling the driver, applies `display_color_mult` and `mura.mc`. Live reload hook: `vrcmd --cmdhmd "set_lenscal <json>"`.

---

## 8. Reimplementing distortion correction WITHOUT SteamVR

Required inputs (all obtainable from the unit itself, offline):

1. The device JSON — either dump with `valve-tools-wrapper/steamvr/tools/bin/win64/lighthouse_console.exe` (`downloadconfig`), or just read SteamVR's cache `Steam\config\lighthouse\<serial>\config.json`. One-time per unit; no cloud, no lighthouses needed.
2. `mura.mc` from the same cache dir (optional quality step).
3. The polynomial evaluator (§3.1) — implement Monado-equivalent `compute_distortion(eye, u, v) → (uv_r, uv_g, uv_b)`.

Render pipeline per eye:

1. Render scene with projection from `intrinsics` (half-FOV_x = atan(1/fx), centers from cx,cy; asymmetric frustum), camera pose = head_pose · eye_to_head_rotation, eye offset = ±ipd.default_mm/2 (sign per eye) along head +X.
2. Oversample render target by `(1 + grow_for_undistort)` relative to 2544×2544 panel area.
3. Full-screen warp pass (mesh or fragment): for each panel pixel, evaluate the per-channel polynomial to get three source UVs; sample R/G/B separately (this is both distortion correction and lateral CA correction).
4. Multiply by `display_color_mult` per eye and the `mura.mc` mask (in linear light — `bars_gen.py` applies corrections in linear space).

**Validation strategy** (resolves the POLY3 direction question empirically): run SteamVR once, use `vrcmd`/OpenVR `GetDistortionMesh`/`ComputeDistortion` to dump Valve's ground-truth UV mapping for this unit, and fit/verify the standalone evaluator against it. Alternatively diff rendered output photographically through the lens. The EVT/DVT-vs-production sign flip (−0.22 vs +0.239) makes blind implementation risky; one dump of ground truth removes all ambiguity.

---

## Relevance to sauna

Concrete recipe for a standalone (no-SteamVR, no-lighthouse) wearable-display renderer for a given Beyond unit:

1. **Extract per-unit calibration once.** Plug in HMD, run `lighthouse_console.exe downloadconfig beyond.json` (admin shell; tool in `valve-tools-wrapper/steamvr/tools/bin/win64/`). Grab `mura.mc` from `Steam\config\lighthouse\<serial>\userdata\` if present. This file contains everything: per-channel distortion polynomials, intrinsics, per-unit eye_to_head, IPD, panel timings, color mult, IMU mount frame + biases.
2. **Get pixels to the panel without SteamVR** — two proven first-party paths in `extended-mode-display`:
   - *Extended mode*: MCU HID feature report `[0,'d',rate]` (`rate` 2=75 Hz tested, 1=90 Hz "at own risk") rewrites the linkbox EDID so the HMD appears as a normal 3840×1920 side-by-side monitor (1920×1920/eye — reduced from native 2560×2560); then any fullscreen window works (`ExtendedModeDisplay`, cv2). Restore with the same command. Also needed: display power `'+'`, panel on `[0,'o',eye,1,0x29]`, duty `'I'`, prox disable `'p'`.
   - *Direct mode at native res*: `resources/direct_mode_dx12.exe` (`-enable/-disable`, `-dsc_present_test 0 1 1 0 0x11 4 128` matching the config's DSC fields; env `DM_VENDOR_ID=0x2709`) and `display_image.exe` present full-res images. Harder but full 2544/2560 px.
3. **Implement the warp** as a single post pass (§8): per-channel poly3 UV remap + grow factor + color mult (+ optional mura multiply). ~50 lines of GLSL/HLSL once coefficients are loaded.
4. **3DOF pose** from the onboard ICM-class IMU (1 kHz, `firmware_config.imu_rate`): orientation via any AHRS filter, using `imu.plus_x/plus_z/position` to rotate IMU frame → head frame, and config gyro/acc biases. The IMU streams over the Tundra SIP HID interface (watchman protocol — parse per OpenHMD/libsurvive's watchman IMU report layout; protocol itself is not in these repos).
5. **6DOF (stretch)**: the 22 photodiodes are useless without base stations, so 6DOF needs an external method (e.g. camera/inside-out or phone-assisted). The `lighthouse_config.modelPoints` do give you the exact sensor constellation geometry if optical fiducials are ever wanted.
6. **Honor per-eye view setup**: render cameras rotated by `eye_to_head` (≈ ±6.17° yaw, −5°+δ pitch per unit) and separated by `ipd.default_mm` — skipping this (not just distortion) is what breaks binocular fusion on canted-display HMDs.
7. **Calibration sanity check**: before cutting SteamVR loose, dump ground-truth warp UVs from OpenVR (`ComputeDistortion` per eye on a grid) on the same unit and unit-test the standalone evaluator against them (settles POLY3-vs-DPOLY3 direction and the `grow_for_undistort` normalization exactly).
