Repository navigation
The coverage fold's gain is out by up to 0.9 dB on a directional antenna, against its CPU twin #530
Description
On a real Intel integrated GPU the coverage fold kernel disagrees with its CPU twin by more than the tolerance, so the device fails its own self-check and MeshBench falls back to the processor.
First run of these kernels on real Windows hardware. windows-check.yml's own header says why that matters:
The GPU kernels are proven on Metal (a real Mac) and on Vulkan (Linux), and have never once been run on DX12 … A kernel that is wrong there fails as a silently slow or silently empty link warm, which is exactly the kind of bug that reaches a user.
What happens
Windows 10, Intel UHD Graphics 620, driver 27.20.100.8853. wgpu picks Vulkan here, not DX12 — so this is not the DX12 path that header is about, and DX12 remains untested.
--- FAIL: TestCoverageFoldMatchesCPU
coveragefold_equiv_test.go:95: best: 100 of 9216 cells disagree with the CPU twin
--- FAIL: TestSelfCheckPassesOnARealDevice
selfcheck_test.go:19: a correct device failed its own self-check:
fold best: cell 408's margin disagrees with the processor by 0.07 dB
The tolerance is 0.05 dB; the worst disagreement is 0.07 dB, on about 1% of cells.
The base coverage kernel is fine on the same device, which is what makes this specific rather than "this GPU is bad":
--- PASS: TestCoverageKernelMatchesCPU
coverage_equiv_test.go:45: GPU: 0x3ea0 (vulkan)
coverage_equiv_test.go:87: worst disagreement 0.0263 dB over 9216 cells
0.026 dB for the raster, 0.07 dB once it is folded into best/second per cell. Whatever the fold does beyond the raster is where the difference is.
The product behaves correctly, which is the good news
gpuwarm.go runs SelfCheck before trusting the device and records why when it fails:
if err := d.SelfCheck(); err != nil {
p.present, p.why = false, err.Error()
}So nothing wrong is computed. A user on this hardware gets coverage on the CPU, which is what CLAUDE.md promises — "a machine without a usable GPU loses time, not features" — and the reason is available rather than hidden. This is not a correctness bug in results.
What it costs, and what to decide
Everyone on Intel integrated graphics — a large share of Windows laptops — silently loses GPU coverage. That is the "silently slow" half of the sentence quoted above, and it is worth knowing before a release rather than after.
The question is which of these it is, and I do not think it can be answered from here:
- the fold kernel is wrong, subtly, in a way Metal and Vulkan-on-Linux happen not to expose — in which case the self-check is doing its job and the kernel wants fixing
- 0.05 dB is too tight for float32 accumulation on this hardware, and the fold's arithmetic legitimately lands 0.07 dB away — in which case the tolerance wants widening with a reason, not loosening to make a test pass
The base kernel agreeing to half the tolerance on the same device is evidence for the first. TestCoverageFoldMatchesCPU's own comment already allows a rank swap at a near-tie as float noise, so the shape of the problem was anticipated; this is past that allowance.
One thing possibly relevant: wgpu warns on every device open here.
[wgpu] [Warn] Disabling robustBufferAccess2 and robustImageAccess2: IntegratedGpu Intel
Driver is outdated. Found with version 0x192295, less than the known good version
0x194843 (31.0.101.2115)
Worth ruling in or out with a newer driver before concluding anything about the kernel.
And a process note
windows-check.yml runs go test ./internal/rf/gpu/ with continue-on-error: true, on a hosted runner where wgpu falls back to WARP, DX12's software rasteriser. So this job has never had a real GPU to disagree with, and would not have gone red if it had. That is deliberate — the comment says the GPU step is exploratory — but it does mean the only signal here is somebody running it on their own machine, which is how this was found.
Activity
internal: go test passes on Windows (#526) (#531)
A13xB0 commentedon Sep 2, 2026
Narrowed it down. It is the azimuth gain, on the beam, and it is bigger than the self-check suggests.
I put a diagnostic through the same ADR-0004 harness to separate the kinds of disagreement. The result is much tighter than "0.07 dB, maybe a tolerance":
Four things fall out of that, and together they rule most explanations out.
It is not a ranking problem. Zero cells pick a different station. The fold's ordering is identical to the CPU's; only the margin it records differs. So
TestCoverageFoldMatchesCPU's existing allowance for a rank swap at a near-tie is not what is being exercised.It is entirely the directional antenna. All 100 cells belong to station 1, the only one with a pattern:
Stations 0 and 2 are flat — 2.15 dBi and 5.0 dBi — and contribute nothing. That is the tell: a wrong gain lookup is invisible on a constant table, because every index holds the same number. The harness's own comment says the directional station is there so "the gain table's interpolation is on trial too", and it is the only thing failing.
It is not the near-mast elevation case. Every one of the 100 is more than 5 km out. Close in, elevation is steep and a small distance difference moves it a lot — that would have been the comfortable explanation, and it is not this. Out there the pattern is governed by azimuth.
And it is far too large for f32 rounding. The pattern's steepest slope is
12·(π/360) ≈ 0.105 dBper degree of azimuth, so 0.91 dB is about 8.7 degrees of azimuth disagreement. Single precision on a bearing computed from latitudes and longitudes is nowhere near that. Something systematic differs between how the shader and the twin arrive at the azimuth they look up, not how precisely they round it.What I checked and eliminated
Budgetincoverage.wgsland thebudgetslice incoveragefold.goagree field for field, includingaz_n,el_n,el_min,el_stepand the two pads. A mismatch there would have been exactly this shape — invisible on flat patterns, wrong on a beam — so it was the first thing to look at.gain_dbandGainTable.Sampleare the same arithmetic in the same order, wrapping azimuth and clamping elevation identically.Sample's comment says "interpolates the table exactly as the kernel does", and as written it does.gt.DB, which is[]float32on both sides, so nothing is converted on the way.That leaves the inputs:
bearing_degand the elevation handed togain_db, against whatever the CPU twin computes before callingSample. Eight degrees is the number to explain.Why nothing has caught it
gpu.Openrefuses an adapter that reports itself as CPU, and no CI runner has a real GPU —ci.ymlsays so at length, and it is the right call. SoTestCoverageFoldMatchesCPUskips everywhere in CI, on Linux as well as on the Windows job. It runs on a real Mac, on the maintainer's Linux GPU, and now here. This is not a Windows fault and I should not have implied it was one by tagging it that way: it is the first time this kernel has been checked against its twin on a third device.Whether Metal and the Linux GPU agree with the CPU where this one does not is the next thing worth knowing, and I cannot answer it from here. If they do, the difference is in how this driver compiles the shader. If they do not, the kernel has been out by up to a decibel on directional antennas everywhere, and
CLAUDE.mdsays what that costs: "A wrong FFT does not crash; it produces a plausible waterfall and slightly wrong sensitivity, and nobody notices for months." A beam mispriced by 0.9 dB is the same failure wearing an antenna.The diagnostic
Not committed — it asserts nothing. Dropping this in
internal/rf/gpureproduces the table above; it is the existingTestCoverageFoldMatchesCPUharness with the comparison broken apart.Device here: Intel UHD Graphics 620, driver 27.20.100.8853, wgpu selecting Vulkan (not DX12 — that backend is still untested by anything). wgpu also warns
Disabling robustBufferAccess2 and robustImageAccess2: IntegratedGpu Intel Driver is outdated, which is worth ruling out with a newer driver before concluding it is the kernel.The product still behaves correctly throughout:
SelfCheckrefuses this device and coverage runs on the processor, with the reason recorded.[-]The coverage fold kernel fails its own self-check on a real Intel GPU, so Windows falls back to the CPU[/-][+]The coverage fold's gain is out by up to 0.9 dB on a directional antenna, against its CPU twin[/+]firmware, cmd, package: the Windows pass (#497)
A13xB0 commentedon Sep 2, 2026
The open question here was whether the fold kernel is wrong or 0.05 dB is too tight. I cannot reproduce it - this machine's GPU passes
TestCoverageFoldMatchesCPU- but the mechanism can be measured without the hardware, and it answers the question.What the fold does that the raster does not
Three things: a bearing from
sin/cos/atan2, an elevation fromatan2, and a sampled antenna pattern. The base kernel has none of them, which is exactly the split in the numbers here: 0.026 dB for the raster, 0.07 dB once folded.The interpolation is not the difference.
gain_dbis bilinear and its comment is accurate: the CPU twin'sGainTable.Sampleinterpolates identically.The difference is the arithmetic width.
geo.BearingDegcomputes that formula infloat64. The kernel computes it inf32, and then uses the result to index a pattern that can be steep.Measured
Running the kernel's formula in f32 against
geo.BearingDegin f64, over a 96x96 raster around the fold test's own geometry:To turn 0.0095 deg into the observed 0.07 dB needs a pattern gradient of 7.3 dB/deg. A yagi or a sector near a pattern edge exceeds that comfortably; a 10 dB/deg transition would give 0.10 dB.
And that figure is a best case for the GPU: it uses Go's
float64sin/cos/atan2rounded to f32. A real device's f32 trig carries its own error on top, Vulkan permits several ULP, and this issue's report notes the driver is older than the known-good version.So which is it
Both readings in the issue were half right, and the third option is the true one: the kernel is not wrong, and the tolerance is not merely too tight - the kernel is precision-limited by construction, and the fold is where that becomes visible because only the fold multiplies a direction into a steep function.
That also explains the platform split without appealing to "this GPU is bad". Metal and Vulkan-on-Linux have trig accurate enough that the drift lands under 0.05 dB. Intel with the older driver does not.
What that suggests doing
Still needs the hardware
Whatever is changed has to be confirmed on the Intel machine, because this one cannot fail the test in the first place. The measurement above narrows what to try; it does not close the issue.
gpu: the fold's bearing no longer depends on the driver's trig
gpu: the fold's bearing no longer depends on the driver's trig
GPU coverage fold: compute the bearing without the driver's trig (#779)
A13xB0 commentedon Oct 24, 2026
Reopened: #779 is merged but not yet confirmed on the UHD 620. Run
go test ./internal/rf/gpu/ -run 'TestKernelTwins|TestSelfCheck' -vthere and close this if the fold comparison passes.