Windows backlog archive — 2026-08-26#

Resolved narratives moved out of windows-refactor-backlog.md per the lean-OPEN-only policy (COUNTING NOTE there), on 2026-08-26 as part of the #134 cleanup wave. This tranche is the run-by-run measurement record of four entries whose verdicts and final numbers stay in the live file:

Entry

What it was

Where the verdict lives now

#116

TVM/IREE on the cross lane — runtime-only, compilers ABSENT

live file, #116

#128

GStreamer arm64 webrtc/nice: a build-machine compiler gap, then three meson build-only-subproject defects (runs 11–29)

live file, #128

#131

post-cross-phase refactor wave, four waves in a worktree

live file, #131

#133

the three amd64-only components: rust-std pre-seed, iree.runtime, TVM runtime wheels (runs 29–36 + amd64 run 8)

live file, #133

SUPERSEDED IN PART, same day: the “live file” column above is only still true for #133. Tranche 2 (below) moved #116, #128 and #131 here in full, so their verdicts live in this file now, not in windows-refactor-backlog.md.

Nothing here was edited — this is the same text, in entry order. The measured facts (why a $ anchor fails on CRLF, why tvm-ffi returns early as a subproject, why disabled_subproject poisoned the host glib) are the reason these narratives are kept rather than deleted. Rules that outlived their entry live in ../AGENTS.md, the failure symptoms in failure-modes.md, the invariants in windows-build-invariants.md.

Earlier tranches: windows-backlog-archive-2026-08-11.md, windows-backlog-archive-2026-08-17.md, windows-backlog-archive-2026-08-21.md.

  • #116 — TVM + IREE on arm64. XL · ★ · ✅ DONE 2026-08-24/25 as RUNTIME-ONLY (tvm_runtime + IREE target tools/libs in the bundle, compilers named COMPILER-ABSENT-ON-ARM64; proven arm64 runs 11–19 — the measured findings are the body below) Lowest priority: highest cost, narrowest benefit, medium confidence. First, a retraction: an earlier note claimed TVM’s “codegen cannot emit aarch64 … not fixable in this repository” — false since it was written, because LLVM_TARGETS_TO_BUILD=X86;NVPTX is set by this repo in Build-TvmFromSource.ps1, in an array the script fully controls. Phase 0 is worth doing on its own merits and is amd64-only: adding AArch64 to that list is a one-token edit that lets the x64 image’s TVM emit aarch64 code (tvm.build(target='llvm -mtriple=aarch64-…')) for one extra LLVM target of build time. It does not unblock the arm64 branch — do not conflate the two. The real remaining cross cost is that USE_LLVM=<path> must execute llvm-config.exe, which an x64 host cannot do against a target-arch LLVM — the cross lane needs a host-tools/target-libs split of the minimal-LLVM build. IREE rides this same dropped branch and must be named wherever the absent branches are listed (it was a silent casualty of TVM-only phrasings); upstream already supports the split there via IREE_HOST_BIN_DIR. Everything past Phase 0 (cross-capable minimal LLVM, IREE’s in-tree LLVM and its host tools) is genuinely large. Phase 0 DONE 2026-08-24, measured: the amd64 full regression (media all-branches + merge + final + smoke) built TVM’s LLVM with LLVM_TARGETS_TO_BUILD=X86;AArch64;NVPTX and passed — media-tvm-built green in 21:17 (sccache carried the LLVM rebuild; 9110 compile requests, 8388/8389 targets incl. IREE), then arch gate 1134/0 and smoke 220/0/0. The x64 image’s TVM can now emit aarch64 code. Runtime-only cross IMPLEMENTED 2026-08-24 evening (the scope that needs no host-tools split for TVM at all, and only upstream’s documented one for IREE): the arm64 bundle ships tvm_runtime.dll + tvm_ffi.dll (+ import libs, headers) and IREE’s runtime tools/libs; the compilers and python packages stay amd64-only and are named ABSENT in COMPILER-ABSENT-ON-ARM64.txt. Mechanics: TVM configures USE_LLVM=OFF, python OFF, builds ninja tvm_runtime alone (a -Targets parameter on Invoke-NinjaBuildWithRetry, so tvm_compiler never builds) and stages by hand with an in-stage PE gate; IREE configures the same tree twice — a native runtime-only host build installed to build-host\install (asserted to hold iree-flatcc-cli.exe + iree-c-embed-data.exe (IREE 3.x’s name; the pre-rename generate_embed_data was asserted first and run 5 caught it)), then the target configure with IREE_HOST_BIN_DIR, IREE_BUILD_COMPILER=OFF, python OFF — with a static PE gate replacing the compile+run gate. media-tvm left $crossBlockedBranches (the driver-level refusal list, removed altogether on 2026-08-25 in #131 once empty), all three branches are merge-required on both lanes, and the media-branch-absent stand-in stage is retired. Full description in docs/windows-cross-builds.md § “TVM and IREE cross-build runtime-only”. Measured on the way (arm64 runs 3–4): (1) TVM runtime cross-builds in under a minute — 4 runtime binaries, all 0xAA64 — but the header copy hit 0.26’s layout: dmlc-core is gone and dlpack lives inside the tvm-ffi submodule’s own 3rdparty; the copy is now layout-searched and asserts tvm\runtime\c_backend_api.h, tvm\runtime\device_api.h, tvm\ffi\c_api.h, dlpack\dlpack.h landed (the first anchor tried, c_runtime_api.h, is itself gone with the FFI split — run 4 caught that, which is what the assert is for). (2) The IREE host pass died in its first try-compile: msvcrtd.lib(exe_main.obj): machine type arm64 conflicts with x64 — VsDevCmd -arch=arm64 leaves LIB on the ARM64 CRT and lld-link reads only LIB (CMake calls it directly, no clang-driver auto-detection). Exactly the “necessary but not sufficient” the choke point’s comment warned about. Invoke-WithHostArchLibraryEnvironment now rewrites the arch segment of every LIB/LIBPATH entry (\arm64 → \x64) for the duration of the host pass and restores it (SourceBuild.HostArchLibEnv.Tests.ps1), without re-entering VsDevCmd (a second invocation appends rather than resets). (3) With the host tools built, the target ninja died on 'build-host/install/bin/iree-flatcc-cli', needed by '…/dummy_reader.h', missing and no known rule to make it — IREE composes ${IREE_HOST_BIN_DIR}/<tool> without the .exe suffix (Linux-shaped), and ninja wants that exact file as a dependency. An upstream gap for Windows hosts (draft: out/upstream-issue-iree-host-bin-dir-exe.md); the script stages a suffix-less twin of each host tool next to the .exe — ninja sees the file, CreateProcess appends .exe when launching an extension-less full path, same bytes either way. Also caught on the way: the host tool is iree-c-embed-data.exe in IREE 3.x, not the pre-rename generate_embed_data. (4) With the host tools resolved, the target build reached IREE’s arm_64 ukernels and died the MLAS/XNNPACK way: always_inline function 'vfmaq_f16' requires target feature 'fullfp16' — upstream hands each feature kernel its -march=armv8.2-a+<feat> through iree_select_compiler_opts(CLANG_OR_GCC …), and clang-cl is classified as MSVC there, so the flags are dropped; plus bare asm(...) (GNU keyword, off under MS compat) in mmt4d_arm_64_fp16fml.c. Same remedy, same discipline as the other two: post-configure per-TU tagging of build.ninja (/clang:-march=armv8.2-a+{fp16,fp16fml,bf16,dotprod,i8mm} on the five feature kernels, floor 5 — the pre-fix state tags 0 — and -Dasm=__asm__ on every arm_64 ukernel TU). (5) Past the ukernels, iree_hal_local_elf_arch.lib failed to archive: x86_64_msvc.obj: file machine type x64 conflicts with library machine type arm64 — upstream bug, hal/local/elf/CMakeLists.txt adds the x86-64 MASM trampoline whenever MSVC_C_ARCHITECTURE_ID MATCHES 64, and ARM64 matches 64 (draft: out/upstream-issue-iree-elf-arch-arm64-msvc.md). Inline-patched to an exact x64 match on both lanes (on amd64 the id is x64; nothing changes there), verified post-patch. (6) With that, the entire ARM64 runtime compiled (608/659) and the tools failed only to link: undefined symbol: iree_uk_mmt4d_tile_s8s4s32_1x8x16_arm_64_i8mm. Checked file by file: it is upstream’s one non-static C inline definition in the arm_64 kernel set, and the entry point takes its address — under C99 inline semantics (clang’s default in C mode) an inline definition without extern emits no external symbol. Per-TU /clang:-fgnu89-inline was tried first (run 10) and did not produce the symbol under clang-cl, so the definition itself is inline-patched to a plain void …( — inline buys nothing for a function used by address; verified after applying, inert on amd64 (the arm_64 dir is not compiled there). How upstream’s own Linux/clang builds link this is not verified here; flagged as a question, not asserted. ✅ DONE 2026-08-24 evening, measured (arm64 run 11): media-tvm-built green in 7:59 — TVM runtime 4 binaries, IREE 14 target binaries under bin\, every one PE 0xAA64 in-stage; merge 34:50 with the arch gate at 950 inspected / 0 violations (up from 931: the TVM/IREE runtimes are now in the bundle); final + smoke 97/0/15. Runtime-only is the shipped scope; the compilers and python packages stay amd64-only and are named ABSENT in COMPILER-ABSENT-ON-ARM64.txt. Execution proof, as for everything on this lane, is owed to a native host.

  • #128 — GStreamer arm64 lacks webrtc/nice and gst-ptp-helper; two lane-identical Meson bugs found on the way. M · ★★ (opened 2026-08-25, from a log diff of the two merge stages) Measured (amd64 bk-…-merge… vs arm64 run 11): plugin lists identical except webrtc only on amd64 (104 vs 105 in gst-plugins-bad), gstwebrtcnice-1.0-0.dll + libnice + gst-examples only on amd64. Cause: the cross file defines host [binaries] only, meson finds no build-machine C compiler (Compiler for language c for the build machine not found, 93×), the build-machine glib fallback dies, and libnice’s by-name subprojects/glib lookup then fails although the host glib configured fine (glib-2.0 for host machine found: YES 2.86.3 (overridden)). gst-ptp-helper is Rust and has no rustc --target=aarch64-pc-windows-msvc in the cross file. Fix: a native file with the x64 compilers for the build machine (and the rust target), then add nice/webrtc to the plugin contract or a lane plugin-inventory diff gate. Both lanes, not parity: -Dcairo:win32=disabled and -Dgst-devtools:dots-viewer=disabled are unknown options in these versions and disable cairo, pango and gst-devtools everywhere; amd64 builds 562 glib test targets (tests: true) that ship nothing. Implemented 2026-08-25, proof pending arm64 run 20 + the next amd64 regression: (1) the cross branch now also writes a meson native file (meson-native-amd64.ini: the same clang-cl/llvm-lib/llvm-rc without --target, i.e. what amd64 runs) so the build machine has a C compiler; (2) rust for the target is probed, not assumed — rustup target add aarch64-pc-windows-msvc (idempotent, the container has network) then a one-line staticlib compile; only a passing probe puts rust = [rustc, --target=…] into the cross file (a declared but broken rust compiler fails meson setup, an absent one merely skips gst-ptp-helper, and either outcome is logged); (3) webrtc and nice joined the plugin contract on BOTH lanes as Detection = 'meson' entries carrying their meson option (gst-plugins-bad:webrtc, libnice:gstreamer), which the build passes as =enabled and the DLL + gst-inspect gate proves like every other entry; (4) -Dglib:tests=false. Correction to the finding above: the two “unknown options” are deliberate — -Dcairo:win32=disabled is the documented way to keep cairo (and so pango) out because its win32 backend crashes LLVM 22’s clang-cl (mmintrin.h __builtin_shufflevector), and -Dgst-devtools:dots-viewer=disabled keeps a Rust dev tool whose crates.io fetch fails offline out; both stay as they are, now with that reason recorded here. Measured on arm64 run 23: (a) the native file alone was not enough — meson detected the build-machine clang-cl, but its sanity check LINKED against the target’s CRT (msvcrt.lib (exe_main.obj): machine type arm64 conflicts with x64; the RUN’s LIB names lib\arm64 / um\arm64 and lld-link reads only LIB), so the build-machine C compiler stayed “not found”, glib’s build-machine configure died, and that failure poisoned the by-name glib lookup that libnice’s anonymous dependency('', fallback: ['glib', …]) needs (the gio-2.0 lookup works via override, the anonymous one cannot). First fix attempt (run 24): [built-in options] c_link_args = ['/LIBPATH:<x64 dir>', …] in the native file — measured wrong: meson hands the build machine’s link args to the clang-cl DRIVER also before /link (the sanity check command carries them twice), and clang-cl reads a path-shaped /LIBPATH:C:/… there as an input file (no such file or directory) — /LIBPATH can never ride c_link_args with clang-cl. Fix: c_link_args/cpp_link_args = ['/vctoolsdir:<VC\Tools\MSVC\<ver>>', '/winsdkdir:<Windows Kits\10>', '/winsdkversion:<ver>'] — options BOTH the driver and lld-link understand; lld-link adds their lib\<machine> dirs to the search path AHEAD of the LIB entries and picks the arch from /machine:, so the build machine links x64 while the host (arm64) compiles of the same meson run keep LIB as it is. Both roots are derived from the RUN’s LIB entries, not assumed. (b) The rust probe failed as designed and stayed out of the cross file: rustup target add aarch64-pc-windows-msvc pulls from the image’s OFFLINE dist mirror (file:///…/rustup-dist/…), which carries the x64 std only — gst-ptp-helper stays absent on arm64 until the base image preseeds the aarch64 rust-std (a base rebuild; owner’s call). Measured on arm64 run 25 (2026-08-26): /vctoolsdir+/winsdkdir work — every subproject logs C compiler for the build machine: clang-cl (clang-cl 22.1.8) / C linker … lld-link, the C and C++ sanity checks link x64, and glib’s build-machine configure runs to its LAST statement. There it dies on a meson bug, not a toolchain one: glib-2.86.3/meson.build:2777:0: Exception: Summary section 'Build environment' already have key 'host cpu'. meson 1.12.0 (and master, checked the same day) keys Interpreter.summary by subproject name and shares it across nested interpreters, while Interpreter.subprojects is keyed [machine][name] — so the build-machine run of a subproject that the host already configured re-adds the same summary keys and throws; _print_summary would then KeyError on a build-only name anyway. Meson marks glib(build) buildable: NO, the same by-name libnice lookup fails as in run 23, and webrtc/nice take gst-plugins-bad down (gst-libs/gst/webrtc/nice/meson.build:16:14: ERROR: Subproject "subprojects/libnice" required but not found). Fix: Invoke-MesonBuildSubprojectSummaryPatch (defined in Build-GstreamerFromSource.ps1 — deliberately NOT in a module. Correction 2026-08-26: this was first recorded here as “/bkmods is bind-mounted whole”; it is not — buildmods is a curated 6-module stage. The real reason is that those six ARE the import closure, they are mounted into all 11 media/merge RUNs, and the classic lane COPYs the same six into common, the ancestor of every BK compile stage, so any module edit re-keys every branch on both lanes twice over; a stage script is mounted per file, this one only into the merge RUN. #134 splits that) rewrites the pip-installed mesonbuild/interpreter/interpreter.py before meson setup — summary_impl returns early when self.build.for_machine is MachineChoice.BUILD (set only by Build.copy_for_build_machine() for native: true subprojects; summaries are cosmetic; non-cross lanes never create such an interpreter, so amd64 is untouched). Located via python -c "import mesonbuild.interpreter.interpreter as m; print(m.__file__)", idempotent by marker, throws on layout drift (load-bearing on the cross lane). Fixture test SourceBuild.MesonSummaryPatch.Tests.ps1 pins the 1.12.0 layout + indentation; upstream draft out/upstream-issue-meson-summary-build-subproject.md (not filed — owner’s call). Proof: run 26. Side fix from the same run: a failed meson setup used to stream the whole meson-log.txt through log — 425k lines on runs 23/24, 800k+ on run 25, i.e. 30–60 min per failed attempt, longer than the configure itself. Select-MesonLogExcerpt (gst script, fixture-tested) now logs the diagnostic lines with numbers (ERROR/Exception/”required but not found”/”conflicts with”/”buildable: NO”), the block after each “Sanity check” header, and the last 300 lines; the full file stays in the preserved container and the retry classification still scans it all for the hard error. Second side fix, same run: the transient-vs-deterministic classifier matched SSLError case-insensitively and without word boundaries, and the SDK constant BINDINFO_OPTIONS_IGNORE_SSLERRORS_ONCE (urlmon.h, inlined from a probe source into meson-log.txt) turned run 25’s deterministic failure into a “transient” retry — full wrap re-download, identical failure, second dump, ~1 h for nothing. Get-MesonSetupFailureClass (gst script, fixture-tested against exactly that line and the two measured real network shapes) now scans meson’s stdout plus only the log’s last 400 lines for network signatures, with \b around the exception names; a fatal download error always ends the log. Measured on arm64 run 26 (2026-08-26): the meson patch holds — Patched meson build-subproject summary fix at 15 s, glib(build) configures (gstreamer core 219 targets vs 124 on run 25; glib 198), libnice passes its lines 214–219 with warnings only, meson setup completed at 550 s with 5772 ninja targets. The compile then died at target 694 in the build-machine libffi: the libffi meson port preprocesses x86_win64_intel.S with cl /EP and assembles with ml64, both find_program’d, and on the build-machine subproject both resolved through PATH — which under VsDevCmd -arch=arm64 leads with bin\HostX64\ARM64. An ARM64-targeting cl.exe defines _M_ARM64, ffitarget.h never enters its X86_WIN64 branch, and ml64 stops on A2006: undefined symbol : FFI_TYPE_SMALL_STRUCT_4B (the host libffi is fine: it wants the ARM64 cl + armasm64). Fix: the native file’s [binaries] now names cl and ml64 explicitly as <VC tools root>/bin/HostX64/x64/… (meson consults the machine file before PATH); Resolve-BuildMachineMsvcTool (gst script, fixture-tested) throws when the x64 pair is missing instead of failing 40 min later in ninja. Measured on arm64 run 27 (2026-08-26): cl/ml64 resolved from the native file, libffi(build) fine — and the build-machine glib compile died next: 'glibconfig.h' file not found for every build.subprojects/glib-2.86.3/glib/*.obj. Third meson bug of the family: targets and include dirs of a build-only subproject live under build.<subdir> (BuildProject.prefix), but configure_file writes to the unprefixed self.subdir — the build machine’s glibconfig.h/config.h/fficonfig.h land in, and overwrite, the host subproject’s build dir. Reading meson for that surfaced the actual poison behind run 25 too: do_subproject’s failure paths call disabled_subproject(subp_name, exception=e) without for_machine (default HOST), so a failed glib(build) overwrote the healthy host glib holder — hence libnice’s by-name failure and the 30+ re-configures. And nothing ever needs a build-machine glib: the only requester is meson’s gnome module (mkenums_simple → find_tool → dependency('glib-2.0', native: true, required: false)), which falls through to the host override (python scripts) when it is absent. Strategy change: the summary patch is withdrawn (a glib(build) that configures is one that compiles, into meson’s unprefixed current_build_dir() next); Invoke-MesonBuildSubprojectPatch now applies two fixes — for_machine=for_machine at both disabled_subproject failure sites, and the project prefix on configure_file’s output path + returned File — so glib(build) still fails at the summary bug, is recorded under BUILD only, clobbers nothing, and libnice/webrtc configure against the host glib. Fixture test SourceBuild.MesonBuildSubprojectPatch.Tests.ps1 (5 tests). Proof: run 28. ✅ DONE 2026-08-26 (arm64 run 28, [bk] Done in 00:29:32). glib(build) attempted exactly once per configure (run 25: 30+), required but not found: 0, libnice’s anonymous host fallback found: YES 2.86.3, libnice 340 targets, gst-plugins-bad 603; the plugin gate passed all six contract entries — gstwebrtc.dll and gstnice.dll with gst_plugin_*_get_desc exported and their imports resolving — All 6 mandatory GStreamer plugins verified present; arch gate 980 inspected, 0 violations (run 19: 970 — the ten new PEs are libnice and the two plugins), import walk 577 walked, 0 unresolved (571), deps 7/0, smoke 97/0/15. Plugin inventory diff (ninja “Linking target” DLLs, run 28 vs amd64 run 7): 200 = 200, no name on one side only. gdkpixbuf is absent on BOTH lanes — arm64 for glib-compile-resources (a build-machine glib C tool), amd64 for a missing rst2man — so it is not a lane difference. (gst-ptp-helper: first recorded here as “linked on neither lane” — wrong, a grep for “Linking target” misses Rust links; amd64 run 7 installs gst-ptp-helper.exe, arm64 got it on run 29 via #133 (a). Corrected 2026-08-26.) amd64 regression (run 7, 2026-08-26, [bk] Done 02:12:20): the same script and contract on the native lane — the meson patch applies and is inert (no build-only subprojects without a cross file), meson setup completed at 465 s, the gate loads all six plugins through gst-inspect (webrtcbin, nicesrc/nicesink among them), manifest 6 DLL homes / image CPython / 6 wheels / 0 absent, arch gate 1134/0 (unchanged), smoke 222/0/0 (220 before — the two new contract entries).

  • #131 — post-cross-phase cleanup (refactoring). M · ★★ (opened 2026-08-25, owner’s request) · ✅ DONE 2026-08-25 in four waves, developed in an isolated worktree, 662/662 tests, proof = the bundled amd64 + arm64 regression that followed. What landed: helpers Add-NinjaPerTuFlags (one per-TU tagging pass with floor + idempotency marker, replacing the MLAS/XNNPACK/IREE copies), Assert-PeTargetMachine / Assert-DirectoryTargetArch / Assert-PythonExtensionTag (the static PE gates, in the dependency-free arch module), Write-AbsentOnCrossMarker (one marker convention), Get-PythonCMakeHintArgs (the FindPython trio per prefix), Invoke-HostToolCmakeBuild (host target + host LIB + retry ladder — LiteRT’s flatc pass gained the LIB swap and a persistent log), Invoke-PythonWheelBuild -CrossStage (ORT/GenAI/PyAV wheels through one call), Invoke-InlineRegexPatch -SkipIfMatch/-AssertGone (the six hand-rolled verify pairs), Resolve-QnnSdk / Copy-QnnRuntime (the QNN block, now fixture-tested with a fake SDK zip), Invoke-OnnxDmlClangClPatch moved into the Patches module (80 lines of embedded C++ out of the build script). Deleted: the dead Get-WindowsX86SimdFlags/Get-WindowsX86Avx512Flags, the false “no arm64 CPython” warning, the unreachable $crossBlockedBranches mechanism, the cpython script’s private PE reader and the two inline reads in the smoke test, ~120 lines of HISTORY comments now pointing at the docs, the stale nvcc-launcher comment. Structure: named Gpu/Cpu/Arm64 smoke-floor columns (the calibration test parses the new shape), $onnxCross resolved once at the top of the ORT script. Tests added: SourceBuild.CrossHelpers (tagging, marker, hints, target python fixture, patch guards, PE asserts), SourceBuild.NinjaTargets, SourceBuild.Qnn, TargetArch.CrossArgs, TargetArch.PeInspection; FindPythonPrefix rewritten against the helper; the test runner now prints the assertion message under a red FAIL line. Original findings, kept for the record: duplication — per-TU build.ninja FLAGS tagging with a floor exists three times (MLAS build-onnx, XNNPACK build-litert, IREE ukernels build-iree) → one Add-NinjaPerTuFlags -NinjaFile -Select -Floor -Label helper; static PE gates over directories/lists exist in tvm, iree, cv2, Assert-WheelTargetArch, cpython (a private Get-StagedPeMachine clone of Get-PeFileMachine) and two inline reads in the smoke test → Assert-PeTargetMachine / Assert-DirectoryTargetArch in the dependency-free arch module; ABSENT markers written three ways → Write-AbsentOnCrossMarker; Invoke-InlineRegexPatch

    • hand-rolled “verify gone or throw” pairs (six sites) → -AssertGone/-SkipIfMatch; the Python CMake hint trio composed three ways → Get-PythonCMakeHintArgs -Prefix; the cross wheel build/stage/assert shape (ORT, GenAI, hand-rolled PyAV) → Invoke-PythonWheelBuild -CrossStage; the host-tool pass (IREE with the LIB swap, LiteRT’s flatc without it — safe today only because that script never enters VsDevCmd) → Invoke-HostToolCmakeBuild. Dead/false: Get-WindowsX86SimdFlags/Get-WindowsX86Avx512Flags have no callers (comment mentions only); Get-SourceBuildPython’s “no arm64 CPython is built on this lane” warning is now false and fires twice per green arm64 run (via Get-TargetBuildPython); WindowsTargetArch.Common.psm1 claims the inline PE reads were removed — they were not; $crossBlockedBranches = @() is an unreachable mechanism contradicting the “ship your own marker” convention (drop it); the LiteRT-LM Bazel blocker paragraph lives in five places; 36-line HISTORY comments in the ORT/GenAI/OpenCV scripts restate docs/windows-cross-builds.md. Hot spots: Build-OnnxFromSource.ps1 (811 lines: DML patch fn, QNN block, python args, MLAS tagging → move the DML patch into the Patches module and QNN into a WindowsSourceBuild.Qnn module with a fake-zip test); the IREE cross branch; the TVM cross branch; the positional three-column smoke floor table (→ named Gpu/Cpu/Arm64 keys). Test gaps: Get-PeFileMachine, Assert-WheelTargetArch, Get-TargetBuildPython, -Targets, Get-QnnSdkLibDirName, the QNN block, the tagging helper, Get-CMakeCrossArgs’ ASM pair — all cheaply fixture-testable (synthetic 0x46-byte PE header, fake build.ninja, fake SDK zip, stub ninja). Order: wave 0 deletions → wave 1 helpers + fixture tests (no call-site change) → wave 2 call-site migration proven by one bundled amd64 + arm64 regression against today’s recorded counts (MLAS 11/25, XNNPACK 569 + 335, IREE 5; gates 1134/0 and 950/0; smoke 220/0/0 and 97/0/15) → wave 3 structural extractions. Proof, after landing on main (2026-08-25): amd64 regression run 5 green ([bk] Done in 02:01:31, arch gate 1134/0, smoke 220/0/0, MLAS 11 through the new helper, 158 x86asm object lines, GenAI setup.py patch, Windows-Update guard in every RUN). The arm64 regression found two defects the worktree’s 662 tests and the parse gate could not see, both fixed and pinned: (1) four Get-PeFileMachine -Path$x calls (no space — a parameter literally named Path$x; now the third AST lint trap, Native.ArgQuoting.Tests.ps1); (2) Invoke-WithHostArchLibraryEnvironment “restoring” a captured $null with SetEnvironmentVariable left LIB= defined-empty, so lld-link skipped its MSVC/SDK auto-detection in the one cross script that never enters VsDevCmd (LiteRT: “could not open ‘kernel32.lib’” right after the host flatc pass; runs 16/17, probed with the env dump that now stays in the script) — Remove-Item Env: on a null snapshot, same fix applied to the LiteRT-LM snapshot restore, its bazel NDK unset and the agentic-loop sanitizer restore; pin SourceBuild.HostArchLibEnv.Tests.ps1, invariant (d) in docs/windows-build-invariants.md. Run 18 then passed LiteRT (XNNPACK 569 + 335, MLAS 25 through the helpers) and exposed (3): Invoke-HostToolCmakeBuild returned its block’s whole pipeline output — every teed ninja line, path last — so IREE’s Join-Path $ireeHostBinDir $tool died with “A drive with the name ‘ninja’ does not exist” (LiteRT never noticed: it [void]s the call). | Out-Host inside the helper keeps the lines in the log and off the pipeline; pin: the chatty-ninja case in SourceBuild.NinjaTargets.Tests.ps1. arm64 run 19 is the proof ([bk] Done in 01:10:51): MLAS 25, XNNPACK 569 + 335, IREE 15 + 5 through the helpers; OpenSSL 2 runtime DLLs staged (deduplicated from 8 package copies); deps 7 wheels / 0 unresolved; arch gate 970/0; import walk 571 / 0 unresolved / 3 external / 6 device-OS; smoke 97/0/15 — every number identical to the pre-refactor run 14. With amd64 run 5 (1134/0, 220/0/0) the refactor is proven on both lanes; the worktree is removed.

  • VERIFY RIDE — MOSTLY CLOSED 2026-08-24 by the amd64 full regression (media all branches + merge + final + smoke: arch gate 1134/0, smoke 220/0/0, [bk] Done), and re-confirmed by a second full amd64 regression the same night (2026-08-25 01:2x, [bk] Done in 02:15:51, arch gate 1134/0, smoke 220/0/0) after the evening’s changes — the Python_* FindPython fix, #119 x86asm, the QNN off-path, the IREE inline patches, the -Targets and host-arch-LIB module additions. That ride covered, on the BuildKit lane, every risk surface the 2026-08-20/21 landings listed: ffmpeg/onnx trap-phase tables, litert-lm phases 5a-5e, the chain Invoke-stage shape (build-litert-all), Find-TensorRtZipIn newest-by-version (zip-less skip path), the checked-in cpython Directory.Build.props COPY, the unified SHELL guard lines, merge sccache ARG parity. Still open, exactly one surface: the classic-lane smoke gate (build.ps1, docker-run form with the directory mount) — only a classic ride proves it, and none has run since 2026-08-21. Assert-Elevated at the host sites is exercised by host scripts, not by a ride. Follow-ups that were chained to this: re-measure the at-scale sccache hit rate (the 2026-08-24 runs show warm hits on every rebuild — e.g. TVM’s LLVM rebuild in 21 min), and the log forensics against the first fully-captured chain (details under Pending).

  • #133 — the three amd64-only components, re-verified 2026-08-26 (owner asked “are you sure?”). M–L · ★ (opened 2026-08-26, not started — owner’s call). What the arm64 bundle names ABSENT is three things, and only one of them is a real upstream wall: (1) LiteRT-LM — the wall is the active Bazel path (no windows-arm64 config in .bazelrc, the x86_64-only libGemmaModelConstraintProvider prebuilt in the default Windows graph; upstream v0.16.1 ships prebuilts for windows_x86_64, linux_arm64, macos_arm64, android_arm64 — no windows_arm64, verified against the tree). It is NOT “not fixable downstream”: upstream’s own CMake path (root CMakeLists.txt + CMakePresets.json + cmake/, present at v0.16.1) carries cmake/patches/stubs/gemma_model_constraint_provider.cc, a no-op provider that prints “Gemma Constraint Provider is STUBBED/DISABLED” — i.e. upstream itself builds without the prebuilt at the price of grammar-constrained decoding. A cross-build of litert_lm_main for arm64 through that path (clang-cl, our LiteRT core from #115, WebGPU/dawn prebuilts absent → CPU only) is a work item, unproven, with the repo’s retired CMake builder (Build-LitertLmFromSource.ps1) as the starting point. (2) TVM compiler + tvm python — needs an LLVM built FOR aarch64-windows (cross-building LLVM with clang-cl is feasible, ~hours) plus a host llvm-config; the python package is pure python + DLLs, so a win_arm64 wheel can be assembled on the host exactly like the ORT/av wheels (#120) — the ABSENT marker’s “needs the target interpreter” is imprecise: it needs the target’s link inputs, which the aarch64 CPython provides. Not attempted; PyPI apache-tvm 0.26.0 has win_amd64 only (verified). (3) IREE compiler + iree.compiler / iree.runtime python — the compiler is LLVM/MLIR for the target (same cross-LLVM cost as (2), no upstream precedent for win_arm64: PyPI iree-base-compiler/iree-base-runtime 3.11.0 ship win_amd64 only, verified); the runtime python package is a pybind extension against the runtime we already cross-build, so it is the #120 pattern again. Not attempted. Everything else in the bundle is at parity with amd64 (GStreamer plugin-DLL sets 200 = 200). The permanent walls stay: CUDA/cuDNN/TensorRT/NVENC (no Windows-on-ARM CUDA), the torch app stage (uv sync must run the target interpreter), and — orthogonal to all of this — no arm64 binary has ever been executed; the windows-11-arm runner is the only path to that proof. In progress 2026-08-26 (“solve them, as easy as possible”), smallest viable path per item: (a) Rust for the target — the actual blocker under both gst-ptp-helper and LiteRT-LM: Install-RustTargetStdFromPinnedManifest (gst script) fetches the aarch64 rust-std tarball the image’s pinned channel manifest already names (with upstream’s sha256) from static.rust-lang.org into the file:///…rustup-dist/… path the manifest points at, so rustup target add verifies and installs it; proof = gst-ptp-helper on arm64 (run 29). ✅ (a) DONE 2026-08-26 (arm64 run 29, [bk] Done 01:24:55): rust-std aarch64-pc-windows-msvc: fetched …/dist/2026-08-20/rust-std-1.98.0-aarch64-pc-windows-msvc.tar.xz (22 MB) → rustup: downloading component rust-std (hash-verified against the pin) → staticlib probe OK → meson Rust compiler for the host machine: rustc --target=aarch64-pc-windows-msvc -C linker=link → gst-ptp-helper.exe compiled (5056 targets, 5051 before) and installed to C:\runtime\libexec\gstreamer-1.0; arch gate 981/0 (+1 = the helper), walk 578/0, plugins 6/6, deps 7/0, smoke 97/0/15. amd64 had the helper all along (run 7 installs it) — both lanes now ship it. Rust for the target is thereby available to any later stage (LiteRT-LM). (b)/(c) did NOT run on run 29: the tvm branch starts from the media-core fan-in, not from the ONNX layer that builds the target CPython, so Get-TargetBuildPython.Available was false in both scripts — Build-MediaTvmAll.ps1 now runs Build-TargetCpython.ps1 first (~90 s, same contract as media-core; BK mounts + classic COPY carry it, parity test B6). Proof: run 30. Run 30 (2026-08-26): the target CPython now builds in the branch (91 s, python.exe 0xAA64) and the wheel path switched on — then ninja: unknown target 'tvm_ffi_cython': tvm-ffi’s CMakeLists return()s as soon as it is a subproject (“only triggered when the project is the root”), so TVM_FFI_BUILD_PYTHON_MODULE on TVM’s configure was never read (CMake: “Manually-specified variables were not used”). Fix: the cross python block configures 3rdparty/tvm-ffi as its own root project (cross args from the choke point, Python_* hints, tests off), builds tvm_ffi_cython, and ships that build’s tvm_ffi.dll/.lib beside core.<target>.pyd; TVM’s own configure carries no python knobs again. Proof: run 31. Run 31: the root configure works and compiles tvm-ffi — every TU with cannot use 'throw' with exceptions disabled: an explicit CMAKE_CXX_FLAGS replaces CMake’s MSVC init flags (/EHsc among them) and, unlike TVM’s own CMake, tvm-ffi as a root project does not add it back. /EHsc is now explicit on that configure. Proof: run 32. Run 32: tvm-ffi compiles, Cython transpiles core.pyx, the module links — as a bare core.pyd: FindPython reports no SOABI for CPython on Windows, so python_add_library(... WITH_SOABI) appends nothing (amd64 produces the same name). That is a valid import name the target interpreter loads; only my name check was over-strict (it demanded the target tag). It now rejects a HOST-tagged name only; the PE machine check is the arch gate. Proof: run 33. Run 33: the tvm branch is green — apache_tvm_ffi-0.1.13.post2-cp314-cp314-win_arm64.whl (tvm_ffi\core.pyd + tvm_ffi\lib\tvm_ffi.dll, 0xAA64), apache_tvm-0.26.0-cp314-cp314- win_arm64.whl (tvm\lib\tvm_runtime.dll), iree_base_runtime-3.11.0.dev0+…-cp312-abi3- win_arm64.whl (11 native members, _runtime.pyd + the runtime tools); manifest 6 wheels. The merge’s deps gate then refused the tvm-ffi wheel’s metadata — and rightly so: Get-PyprojectDependencies captured past tvm-ffi’s ONE-LINE dependencies = ["typing- extensions>=4.5"] to the next ] at a line start, i.e. through [project.urls] and the optional-dependencies, and the wheel declared its Homepage URL, ninja, torch and setuptools as requirements (pip download fails on the URL). The capture now closes at the first ] that ends a line; fixture test covers the one-line shape and an extras marker. Proof: run 34. Run 34 — two more, both found by gates, not by luck: (i) the corrected regex forbade any [ between [project] and dependencies, but classifiers = [ / authors = [{…}] / keywords = [ precede the list in BOTH pyprojects, so it matched nothing and the two wheels shipped with no Requires-Dist at all — a defect the deps gate structurally cannot catch (fewer requirements only make it greener; it showed up as 7 first-touch requirement(s) where run 33 had 14). The parse is now two steps: isolate the [project] table body (up to the next ^[ header), then find the list inside it. (ii) With the metadata right, the arch gate caught the real hole: 1 unresolved import — core.pyd imports tvm_ffi_testing.dll, which tvm-ffi links the Cython module against and upstream’s own wheel ships beside tvm_ffi.dll; the assembled wheel now stages it too (both DLLs asserted present). Deps gate itself passed: store holds 11 wheel(s); 0 requirement edge(s) unresolved. Proof: run 35. ✅ (b)+(c) DONE 2026-08-26 (arm64 run 35, [bk] Done 00:41:22). The bundle now ships apache_tvm_ffi-0.1.13.post2-cp314-cp314-win_arm64.whl (3 native members: core.pyd, tvm_ffi.dll, tvm_ffi_testing.dll), apache_tvm-0.26.0-cp314-cp314-win_arm64.whl (tvm_runtime.dll) and iree_base_runtime-3.11.0.dev0+…-cp312-abi3-win_arm64.whl (11 members, _runtime.pyd + the runtime tools) — every member 0xAA64. Manifest 6 wheels (= amd64), deps store 11 wheels / 0 unresolved edges, arch gate 992 / 0 with 606 walked, 0 unresolved import(s), smoke 97/0/15. One metadata defect survived to be fixed after the fact: the two assembled wheels’ Requires-Dist were EMPTY because .NET’s multiline $ matches only before \n and the container’s git checkout is CRLF (the same text parsed correctly on the host, and the deps gate cannot see missing requirements — fewer edges only make it greener). Every anchor is \r?$ now and the fixture asserts the CRLF variant. Proven on run 36: the deps gate reads 10 first-touch requirements (7 before), apache-tvm-ffi>=0.1.13.post2 is resolved from the bundle’s own wheel and typing_extensions is downloaded for the target (store 12 wheels), 0 requirement edge(s) unresolved; arch gate 992/0, walk 606/0. amd64 regression (run 8, 2026-08-26): the native paths are untouched by all of this — iree native gate OK (llvm-cpu compile + local-task run, abs(-5)=5), iree python gate OK: abs(-5) = 5.0 (both wheels installed and interoperating), import tvm green, tvm branch 23:45, plugin gate 6/6, manifest 6 wheels / 0 markers, arch gate 1134/0, smoke 222/0/0 — every number identical to run 7. (b) IREE runtime python — iree.runtime is a nanobind extension over the runtime the tvm branch already cross-builds: IREE_BUILD_PYTHON_BINDINGS=ON on the target configure with Python3_*+Python_* hints (host exe / neutral include / TARGET python314.lib / host-probed numpy headers), then the build tree’s synthesized runtime/setup.py bdist_wheel --plat-name win_arm64 via Invoke-PythonWheelBuild -CrossStage (staged + PE/name-checked). The compiler package stays ABSENT (proof: run 29). (c) TVM runtime python — next: TVM 0.26 supports a runtime-only mode natively (_RUNTIME_ONLY falls back when tvm_compiler is absent); tvm_ffi’s Cython core module cross-builds through the tvm-ffi CMake target (TVM_FFI_BUILD_PYTHON_MODULE, python_add_library WITH_SOABI → target EXT_SUFFIX), and both apache-tvm-ffi and apache-tvm wheels are assembled from the package sources + the cross-built binaries (scikit-build-core would rebuild the compiler and cannot tag win_arm64 from a host interpreter). (d) LiteRT-LM — stays a port, not a switch, after reading upstream v0.16.1: its CMake path is an ExternalProject orchestrator with a host prebuild phase, Unix-flavoured link flags plus an MSVC branch, and a Rust crate (tokenizers+onig, llguidance, antlr4rust, minijinja, cxx) that must be built for the target through corrosion; (a) is its prerequisite, the rest is days of iteration at ~1 h per attempt — deferred, not declared impossible. The compilers (TVM, IREE) stay out: a cross-built LLVM for aarch64-windows, no upstream precedent.

Tranche 2 — nineteen fully resolved entries (moved 2026-08-26)#

A status census on 2026-08-26 found 19 of the live file’s 24 entries fully resolved by their own text and still sitting under ## OPEN — about 54% of the file — against the lean-OPEN-only policy stated there. Each was spot-checked against the tree before moving: #124-#127, #129, #130, #132 and #133(a-c) are all implemented and wired into Dockerfile.media-merge-builder, so they read as open work that no longer exists.

Moved whole, unedited, in entry order: #112-#120 and #123-#132. What stayed live: #121 (QNN scaffold, no SDK ever staged), #122 (CUDA probe never run), #133 (only (d) LiteRT-LM is open), #134 (the paid cleanup wave) and #31 (owner decision). Note this supersedes the tranche-1 table above for #116, #128 and #131 — their verdicts now live HERE, not in the live file; only #133’s does.

  • #112 — FFmpeg aarch64 assembly. ✅ DONE 2026-08-24 (Route A, built and verified). Route A needed no new tooling: $confFlags += "--as=clang$ffCcTargetFlag" replaced --disable-asm, letting clang’s integrated assembler handle the GAS syntax that gas-preprocessor.pl + armasm64 (Route B) would otherwise have been needed for. The precondition held: --enable-cross-compile disables configure’s runtime probes, so configure only assembles its test fragments and never runs a produced binary — which is the only reason this is tractable on a host that cannot execute aarch64 at all. Evidence, not inference: the build log reports NEON enabled yes, config.mak: ARCH=aarch64 and config.mak: AS=clang --target=aarch64-pc-windows-msvc, and of the 99 distinct aarch64/*.o objects, 56 went through real AS steps — swscale_unscaled_neon.o, vf_bwdif_neon.o, aacencdsp_neon.o, ac3dsp_neon.o and so on — while the other 43 are C-compiled *_init*/dispatch TUs. (Two earlier notes said 87 and then “99 assembled”; both were mis-counts — 99 is the total, 56 is the assembled subset, measured 2026-08-24 with AS-line extraction rather than filename counting.) The amd64 lane is untouched because the flag sits inside the cross-only branch and is composed solely from the cross-lane target flag. At the time this landed amd64 had no nasm path to keep — --disable-x86asm had been appended unconditionally since bd6adca4 (2026-06-25, it entered with the file itself), so FFmpeg built no external x86 assembly on either lane. That asymmetry (arm64 with NEON assembly, amd64 with none) was filed as #119 and closed the same day by enabling x86asm on the amd64 lane (see #119 below).

  • #113 — DirectML on arm64. ✅ DONE 2026-08-24 (ORT side; built and gate-verified). Confirmed by byte inspection, not inference: Microsoft.AI.DirectML 1.15.4 does ship bin/arm64-win/DirectML.lib (COFF import archive, machine 0xAA64). This was never a packaging gap, and the “no arm64 import library” verdict recorded until 2026-08-23 is retracted. cmake/external/dml.cmake declares the download’s outputs lower-case as bin/arm64-win, while cmake/onnxruntime_providers_dml.cmake composes its consumer paths as bin/${onnxruntime_target_platform}-win — and onnxruntime_target_platform holds the verbatim, upper-case ARM64. The two spellings never meet, so the lane failed with bin/ARM64-win/DirectML.lib ... missing and no known rule to make it. That the failing message itself said ARM64 is what proves the variable’s casing, and therefore that TOLOWER lands on the directory that exists. Fix: two Invoke-InlineRegexPatch edits in Build-OnnxFromSource.ps1 — one inserts string(TOLOWER "${onnxruntime_target_platform}" onnxruntime_dml_redist_platform) above the if (NOT onnxruntime_USE_CUSTOM_DIRECTML) block, the other reroutes both consumers through it; a re-read then throws if either edit misses, so upstream drift fails loudly instead of silently reverting to the broken path. Both were simulated against the real upstream file before building (TOLOWER inserted once at module scope, zero upper-case consumers left, two lower-case). A static .patch was written first and deleted — its hunk line counts were hand-computed, and the inline patcher is already this repo’s drift-tolerant mechanism. USE_DML is now ON unconditionally in ORT. Evidence it worked: both patch lines logged at 33.5s; ONNX: DirectML EP ON; [1116/1118] Linking CXX static library onnxruntime_providers_dml.lib; DirectML.dll staged to C:\runtime\lib\onnxruntime-source\bin; 117 of 994 sccache requests missed — the DML translation units that had never compiled on this lane. The merge-stage arch gate then reported 390 binaries inspected, 0 violations (389 before DirectML joined), with no allowlist skips. Since the gate’s root is C:\runtime, .dll is in its extension set, and Dockerfile.media-merge-builder:155 copies the whole C:\runtime tree, DirectML.dll was necessarily among the 390 — so the shipped DirectML is arm64. Honest limit: this proves the right bytes ship, not that the DML EP runs. Nothing arm64 executes on this x64 host; only the windows-11-arm CI job can show that. Follow-up — closed by #118 (2026-08-24): GenAI (-DUSE_DML) and OpenCV (WITH_DIRECTML) were OFF when this entry was written; both are ON on both lanes since #118 landed the same day. Worth recording so the next reader does not re-investigate it: GenAI’s D3D12Core.dll staging is already correct for arm64 — $d3d12ArchDir = (Get-WindowsRuntimeIdentifier) -replace '^win-', '' resolves through Get-WindowsTargetArch, i.e. the target, giving arm64 on this lane, and the nuget’s directory names are exactly those RID arch components. That was made target-derived deliberately (see the comment at Build-OnnxGenaiFromSource.ps1:286) precisely for this eventuality.

  • #114 — aarch64 CPython, and the Python bindings it unblocks. L · ★★★ · SUPERSEDED-BY #120 (its Phase-0 questions still apply — answer them there first) · ✅ CLOSED 2026-08-25 through #120 (target CPython from source, all four bindings staged, proven by arm64 run 14) The highest-leverage item: it is what keeps cv2, the ONNX Runtime wheel, ONNX GenAI’s bindings and PyAV off the lane. Do Phase 0 first — a ~20 min probe (no chain rebuild) answering exactly three questions: does the image’s VS ship Platforms\ARM64\PlatformToolsets\ClangCL, does Hostx86\arm64\cl.exe exist (setuptools’ x86_arm64 spec needs it for PyAV), and does PCbuild\build.bat -p ARM64 actually run to completion. Could not be answered up front because ctr/docker need elevation on this host. The decisive distinction is between compiling and linking a .pyd against the target’s headers and import lib (no execution, feasible) and setuptools/pip wheel packaging, which normally runs the interpreter (not feasible here). Get-SourceBuildPython must stay HOST-pinned — the cross lane runs builds with the host interpreter while linking against the target one. Those two must never be conflated again.

  • #115 — plain LiteRT without LiteRT-LM (would also restore the tflite GStreamer plugin). L · ★★ · ✅ DONE 2026-08-24 (built, merged, gate- and smoke-verified). media-litert runs on arm64 — 146 libs staged incl. the aarch64 tensorflowlite_c.lib; the LiteRT-LM stage self-skips (citing its two real Bazel blockers, below) and stages the empty litert-lm stand-in tree so the merge COPY keeps working. Cross needs exactly TWO host tools, both from pinned sources: flatc built natively from the SAME tree (the flatbuffers-flatc target, a per-call -TargetArch host override on that one choke point) and protoc 21.9 (github release zip — the version derives from the VENDORED protobuf commit 90b73ac3 = C++ runtime 3.21.9, NOT the LM lane’s PROTOC_VERSION=31.1, whose gencode needs a google/protobuf/runtime_version.h that 3.21.9 does not ship). XNNPACK needed the MLAS-class per-TU treatment, now in Build-LitertFromSource.ps1: 569 C microkernel TUs tagged per-FAMILY in build.ninja post-configure (families completed against upstream’s PROD_* list; SME skipped; floor 100), while the 335 hand-written aarch64 .S kernels get a FULL-UNION in-source .arch armv8.2-a+fp16+dotprod+i8mm+bf16 directive — an assembler validates but never emits, so the union is byte-neutral for asm, while C stays per-family because a compiler may auto-vectorize (floor 10). The green took 8 iterations: two self-inflicted (a PowerShell plus-sign-outside-parameter binding bug; the legacy export-symbol assumption, next paragraph), the rest genuine cross gaps — the per-TU features, the chain-wide ASM triple (Get-CMakeCrossArgs now also sets CMAKE_ASM_COMPILER_TARGET / CMAKE_ASM_FLAGS_INIT; before that, any project enabling the ASM language assembled with the X64 default target, and an aarch64 -march handed to that x86 driver was misread as a CPU name — amd64 untouched, its cross args stay empty), host flatc, host protoc + its version family, and the vcruntime redist copy (#120). Durable measurement (2026-08-24), so no future gate re-asserts the wrong symbol: the plugin contract demands all four mandatory plugins on BOTH lanes again (Get-RequiredGstPlugin’s UnavailableOn.arm64 deleted), the tflite switch is presence-driven (-Dgst-plugins-bad:tflite=enabled, never auto), and the hardened cross plugin gate walks the dependency tree (dumpbin) AND asserts the per-plugin export marker — MEASURED: modern GStreamer (per-plugin registration since 1.14) exports gst_plugin_<name>_get_desc + _register, NOT the legacy gst_plugin_desc the gate’s first version asserted; that version failed all four plugins incl. three amd64-proven ones and was recalibrated from dumped export tables. History — the pre-build analysis below is kept as written (it dates its own corrections): Corrected twice — the 2026-08-23 correction that stood here was itself half-wrong (fixed 2026-08-24): the original note blamed ONLY the prebuilt libGemmaModelConstraintProvider.lib; the 2026-08-23 rewrite swung to blaming ONLY the .bazelrc. Both halves are real on LiteRT-LM’s active Bazel path: (a) .bazelrc has no windows-arm64 config (only android/macos/ios arm64 ones), and (b) the prebuilt x86_64-only libGemmaModelConstraintProvider is in the default Windows dependency graph via gemma3_data_processor — severable with the litert_lm_fst_constraints_disabled config_setting (model_data_processor/BUILD:26-33), so a removable blocker, not a wall. (The earlier “upstream’s CMake path compiles the gemma_model_constraint_provider.cc stub instead” observation described the frozen CMake fallback, not the active Bazel path.) Neither half applies to plain LiteRT: pure CMake, no Bazel, no prebuilt — its only cross obstacle is upstream’s TFLITE_HOST_TOOLS_DIR requirement for a host flatc, and flatc is already named on the merge gate’s host-tool allowlist. That plain-LiteRT cross build is exactly this item. If it lands, the media-branch-absent stand-in shrinks and Get-RequiredGstPlugin -Arch can stop dropping tflite.

  • #116 — TVM and IREE on the cross lane. L · ★★ (opened 2026-08-24) · ✅ DONE 2026-08-24, extended 2026-08-26 by #133. Runtime-only by design: tvm_runtime.dll + tvm_ffi.dll + headers, and 14 IREE target tools/libs under C:\runtime\iree\bin, built through upstream’s documented host-tools/IREE_HOST_BIN_DIR split. The compilers need TARGET-arch LLVM libraries plus a HOST llvm-config at configure time — a split this repo does not build — so they are named ABSENT in the bundle rather than silently missing. Since #133 the two runtimes also ship their python packages. Narrative: windows-backlog-archive-2026-08-26.md.

  • #117 — the arch gate covers C:\runtime only; the CPython tree is outside it. S · ★★ · ✅ RESOLVED 2026-08-24 (see the resolution below; the question stands as history). Dockerfile.media-merge-builder:172 fans in C:\temp\cpython\Lib\site-packages, and the gate runs -Path 'C:\runtime'. So the arm64 image carries the host x64 CPython and its site-packages, and the binaries the gate reported were all genuinely arm64 — they just were not everything. Not obviously a defect, which is why this is a question and not a fix: that interpreter is the HOST tool that runs the builds and is legitimately x64, so simply adding C:\temp\cpython to -Path turns a correct build red. Decide first what C:\temp is — build residue or shipped payload — then either exclude it from the image on the cross lane or gate it with the host-tool allowlist. A first attempt at “just gate it too” was written and reverted on 2026-08-23 for exactly this reason. Resolve together with #114, which changes the answer. Resolution (2026-08-24): exactly as this entry predicted, #114’s successor — #120 — changed the answer. The merge arch gate now scans C:\runtime AND C:\temp\cpython\Lib\site-packages, with -IncludeArchives; the 58 host x64 .pyds in site-packages appear as REPORTED allowlist skips, and the green figure — 931 binaries, 0 violations — is the whole-image statement this item asked for. The “is C:\temp payload?” half is settled by #120: the SHIPPED interpreter on arm64 is C:\runtime\python; the host CPython stays build tooling.

  • #118 — flip DirectML ON for GenAI and OpenCV now that ORT’s arm64 DML is verified. S · ★★ · ✅ DONE 2026-08-24 (both flips built and gate-verified). Unblocked by #113. Both are OFF purely as sequencing, not for any platform reason: GenAI links ORT, so a half-enabled DML there produces link errors that read like a GenAI bug, and OpenCV’s cv::dnn would advertise a backend whose runtime half was unproven. ORT’s side is now built, linked and gate-verified, so that reason has expired. Two things already checked, so the next reader does not redo them: (1) GenAI’s D3D12Core.dll staging is already target-derived — (Get-WindowsRuntimeIdentifier) -replace '^win-', '' resolves through Get-WindowsTargetArch, yielding arm64 here, and the Microsoft.Direct3D.D3D12 nuget’s directory names are exactly those RID arch components. (2) The Agility SDK does ship an arm64 D3D12Core.dll. So this is close to two one-line flips plus a rebuild. What to watch: the arch gate count must rise again (each newly staged DLL is one more inspected binary, and D3D12Core.dll is not on the host-tool allowlist), and OpenCV’s WITH_DIRECTML=ON pulls DirectML headers into cv::dnn TUs that have never seen them on this lane. Do GenAI first and OpenCV second — same reasoning as #113’s ordering. The limit stays the same as #113: a green build proves the right bytes ship, never that the EP runs. Only the windows-11-arm CI job can show that. Done (2026-08-24): GenAI builds with USE_DML=ON on both lanes and stages the arm64 D3D12Core.dll from the nuget’s bin/arm64 — the target-derived filter above resolved exactly as recorded, no re-investigation needed; OpenCV builds with WITH_DIRECTML=ON, consumed via the G-API EP. The arch gate count rose as predicted and the whole-image figure stands at 931 binaries, 0 violations (see #117). The runtime limit above is unchanged: bytes proven, execution still owed to the windows-11-arm CI job.

  • #119 — amd64 FFmpeg ships with NO external x86 assembly, and arm64 now has more SIMD than it. M · ★★ ✅ DONE 2026-08-24, proven the same evening on the amd64 regression: configure reports x86 assembler  …/nasm.exe, the build assembled 154 X86ASM objects (libavcodec/x86, libavfilter/x86, libswscale/x86, …) and linked them under lld-link into ffmpeg.exe + the 7 DLLs; the stage went green in 6:15 including the PyAV wheel and its import av gate. The lld-link-vs-nasm-object question the original entry raised is answered by that link step. Found 2026-08-24 while checking whether #112 could regress amd64. It cannot — but the check turned up something else: Build-FfmpegFromSource.ps1 appended --disable-x86asm unconditionally (present since bd6adca4, 2026-06-25 – it entered with the file itself; an earlier note here blamed 8c5c50e7, which is only the relicense commit that MOVED the file), so FFmpeg built none of its hand-written x86 SIMD on either lane. After #112 the cross lane assembled 99 NEON objects while amd64 assembled zero — the arm64 bundle was, in this one respect, ahead of the shipped amd64 image. The archaeology was done before the flip, as this entry demanded. git show bd6adca4 is a bare “fix” commit (5 files, 347 insertions) that added the whole FFmpeg script; the flag sits between --toolchain=msvc and the CUDA comment with no rationale, no failing configure, no nasm/lld-link note — a first-bring-up simplification that later got documented as a premise. The script now enables x86asm on the amd64 lane (--x86asmexe=<pinned nasm>, asserting nasm is on PATH — Test-Toolchain.ps1 already pins it) and keeps --disable-x86asm explicitly on the cross lane, where the knob is meaningless for aarch64. The comment at Build-FfmpegFromSource.ps1:410 no longer calls the disabled state a premise of the toolchain choice; it was never load-bearing for the msvc-preset-plus-compiler-override approach (configure assembles nasm fragments independently of --cc). What “proof” meant here, and what delivered it: configure names the x86 assembler only when nasm assembled its test fragment, and the build log then carries X86ASM lines for the .asm kernels. The deciding artifact was the amd64 media-core-built-ffmpeg solve of 2026-08-24 ~23:10 (see the DONE line above) — nasm invoked through its scoop shim, 154 objects, clean link. The misattribution this uncovered (three places claimed nasm was FFmpeg’s assembler: the pins table, Test-Toolchain.ps1, the old #112 text) was corrected the same morning — and then re-corrected the same evening, because the flip made the original attribution true again: nasm now shapes both GStreamer’s openh264 (Build-GstreamerFromSource.ps1:482) and FFmpeg’s amd64 kernels.

  • #120 — target aarch64 CPython built from source (PCbuild -p ARM64, ClangCL), and the bindings it unblocks. L · ★★★ · ✅ DONE 2026-08-24 — step 1 (the interpreter) in the morning, step 2 (all four binding consumers) in the evening (opened 2026-08-24; supersedes #114) The gap it closes: the arm64 image advertises PYTHON_WHEELS while the wheel store C:\runtime\wheels is empty on that lane — no cv2, no ORT wheel, no GenAI bindings, no PyAV. Decided 2026-08-24: build the target CPython from source via PCbuild\build.bat -p ARM64 under ClangCL, the same route the host CPython already takes; the alternative — fetching the upstream nuget arm64 CPython — was considered and rejected by the owner (2026-08-24), so do not resurrect it. #114’s Phase-0 questions (ClangCL ARM64 platform toolset present? Hostx86\arm64\cl.exe present? does PCbuild\build.bat -p ARM64 complete?) still apply and come first. #114’s decisive distinction also carries over unchanged: Get-SourceBuildPython stays HOST-pinned — the cross lane runs builds with the host interpreter while linking against the target one; the two must never be conflated again. Step 1 DONE (2026-08-24), measured: PCbuild\build.bat -e -p ARM64 with the repo’s ClangCL props + /p:PreferredToolArchitecture=x64 completes in ~91 s incl. the externals fetch; python.exe is PE 0xAA64, verified IN-STAGE; 2864 files staged to C:\runtime\python (interpreter, python314.lib, headers, stdlib). This answers #114’s Phase-0 Q1 POSITIVELY: VS 18 ships the ClangCL PlatformToolset for ARM64 — pythoncore’s own “Toolset ClangCL is not used for official builds” warning fired, which proves the toolset resolved. Durable fact (2026-08-24), so nobody hunts for a “missing” DLL: vcruntime140_1.dll has NO ARM64 edition by design — it exists only to carry x64 FH4 exception helpers; ARM64 keeps everything in vcruntime140.dll. MSBuild’s host-blind redist copy put the x64 one into the ARM64 build output, the extended arch gate caught it (the single violation among 932 scanned), and the stage now self-polices every staged binary’s PE machine and drops exactly that file under a tightly-guarded rule. Step 2 DONE (2026-08-24 evening, arm64 run 3), measured — all four consumers build for the target: onnxruntime-1.29.0-cp314-cp314-win_arm64.whl (4 native members, all 0xAA64), onnxruntime_genai_directml-0.15.2-cp314-cp314-win_arm64.whl (3), av-18.1.0-cp314-cp314-win_arm64.whl (49), and cv2.cp314-win_arm64.pyd installed into the target interpreter’s site-packages (C:\runtime\python\Lib\site-packages, inside the arch gate’s scan root). The design that made it work, in one line each — full detail in docs/windows-cross-builds.md § “#120 step 2”: the HOST interpreter runs every build (Get-TargetBuildPython .Exe), the TARGET import lib is what gets linked (.Lib/.LibDir); wheels are built with an explicit --plat-name win_arm64 and staged, never installed or imported here (Invoke-PythonWheelBuild -StageOnly, since #131 the one-call -CrossStage → Assert-WheelTargetArch, which opens the wheel and PE-checks every native member and its EXT_SUFFIX name tag); cv2 gets the static equivalent of its import gate. Three findings the runs produced, each now pinned by code or test: (1) ORT’s CMake reads Python_*, not Python3_* — the names passed for months were silently ignored on every lane (“Manually- specified variables were not used”), amd64 only worked by auto-detection; GenAI needs both Python_* (its own find_package) and the legacy PYTHON_* (its vendored pybind11 in classic mode) — SourceBuild.FindPythonPrefix.Tests.ps1. (2) if (Test-WindowsCrossTarget -and -not …) is parsed in command mode — -and/-not become arguments, the branch fires regardless — which is how run 2 skipped the ORT wheel with “python wheel 0s”. (3) The host-pinned sitecustomize shim stamped the host EXT_SUFFIX on target modules (cv2.cp314-win_amd64.pyd, machine 0xAA64: right bytes, unloadable name — an arm64 interpreter loads only .cp314-win_arm64.pyd or bare .pyd); the shim now pins EXT_SUFFIX to the target on the cross lane while get_platform() stays host (pip resolves downloads with it), verified standalone under a host python before the run. The C:\runtime\wheels store is no longer empty on arm64. What still cannot happen here: importing any of it — every arm64 signal stays static.

  • #123 — llvm-ml for the MASM-syntax assembly on amd64 (MLAS x64 kernels, IREE’s x86_64_msvc.asm). S–M · ★ (opened 2026-08-24 evening, owner’s request) The “clang-cl everywhere” rule holds for compilers and linkers on both lanes and for every assembly path on arm64 (clang’s integrated assembler: FFmpeg --as=clang, XNNPACK/MLAS .S, IREE inline asm). On amd64 two other assemblers are in the build: nasm for NASM-syntax kernels (FFmpeg since #119 — 154 objects, libjpeg-turbo in OpenCV, openh264 in GStreamer) and MSVC’s ml64.exe for MASM-syntax sources — ONNX Runtime’s mlas/lib/amd64/*.asm (Building ASM_MASM object in every ORT log; the assembler is CMake’s default ASM_MASM search, ml64 before ml, no CMAKE_ASM_MASM_COMPILER is set anywhere in this repo) and IREE’s ELF trampoline (add_custom_command COMMAND ml64 upstream). nasm has no LLVM replacement (LLVM ships no NASM-syntax assembler; dropping it means dropping the kernels). ml64 does: llvm-ml is LLVM’s MASM-compatible assembler and ships with the pinned LLVM. The work: -DCMAKE_ASM_MASM_COMPILER=<llvm-ml.exe> in Invoke-CmakeConfigure (same shape as the CMAKE_AR=llvm-lib archiver arg), an assert that the ORT configure reports llvm-ml as the ASM_MASM compiler, an amd64 ORT build proving MLAS’s macro-heavy .asm files assemble and link under it, and for IREE a small patch of the ml64 custom command (or leave that one file on ml64 and say so). Unverified until built: whether MLAS’s MASM dialect is fully within llvm-ml’s compatibility — that is the whole question this item answers. Ordering: after the current amd64 regression; it touches the ~50-min ORT stage. Implemented 2026-08-25, proof pending the next amd64 regression: Get-LlvmMasmCmakeArg (WindowsSourceBuild.Common.psm1, throws when llvm-ml is absent — a silent ml64 fallback is the exception this removes) → -DCMAKE_ASM_MASM_COMPILER:FILEPATH=<llvm-ml> on the native ORT configure, whose output is now tee’d to onnxruntime-configure.log and asserted to name llvm-ml in its ASM_MASM/assembler lines; IREE’s COMMAND ml64 custom command is patched to ${IREE_MASM_COMPILER} (fail-loud when neither form is present) and both its configures (host tools and target) pass -DIREE_MASM_COMPILER=<llvm-ml> — on the cross lane the HOST tools pass assembles the x86_64 trampoline through it, so the arm64 run is the first proof of the IREE half; the MLAS half needs the amd64 lane. First measurement (arm64 run 22, host pass): llvm-ml /nologo /Zi /c /Fo … x86_64_msvc.asm → .seh_* directives are not supported on this target — not a dialect gap: llvm-ml assembles for i386 unless told -m64, and the file’s x64 frame directives (.PUSHREG/.SETFRAME, which llvm-ml lowers to .seh_*) need the x86_64 target. -m64 is now part of the IREE command and of ORT’s CMAKE_ASM_MASM_FLAGS. ✅ DONE 2026-08-26 as a split, not a sweep — IREE on llvm-ml (arm64 run 23), MLAS stays on ml64.exe by measurement (amd64 run 6): with -DCMAKE_ASM_MASM_COMPILER=llvm-ml -m64 ORT’s configure did report Found assembler: …/llvm-ml.exe, and then all four MLAS .asm kernels ninja reached failed on their first line — .xlist → expected section directive before assembly directive — followed by INCLUDE mlasi.inc → Could not find include file and ~6400 cascaded errors. Root causes read from LLVM 22’s MasmParser.cpp (release/22.x): it implements no listing directives (.list/.xlist/.nolist are absent from the directive map), and INCLUDE resolves through SourceMgr.AddIncludeFile with the -I dirs only — ml64 also searches the includer’s directory, which is how mlas/lib/amd64/*.asm finds mlasi.inc without any /I. Behind that include sits the Windows SDK’s MASM macro layer (macamd64.inc: NESTED_ENTRY, rex_push_reg, …). That is missing MASM coverage, not a flag, and rewriting ~40 upstream kernels plus SDK includes is not this repo’s job. Reverted in Build-OnnxFromSource.ps1: no ASM_MASM override on the native configure, the tee’d configure log now asserts ml64 (a drift stops at configure, not 40 min into ninja), the reason sits beside the check. Resolve-LlvmMasm/Get-LlvmMasmCmakeArg stay in the module for IREE. AGENTS’ assembler paragraph names the split. amd64 run 7 (2026-08-26, [bk] Done 02:12:20): the configure log reports The ASM_MASM compiler identification is MSVC … ml64.exe, the ORT stage passes in 4:58 (sccache warm), arch gate 1134/0, smoke 222/0/0.

  • #124 — the target CPython cannot start on a clean Windows-on-ARM machine: vcruntime140.dll is staged into DLLs\, not beside python.exe. S · ★★★ (opened 2026-08-25, consumer-side audit) Build-TargetCpython.ps1:133-138 copies python*.exe/python*.dll to the root and every other DLL — the CRT included — into DLLs\. python314.dll imports vcruntime140.dll through the normal loader search (exe dir, System32, PATH); DLLs\ is a Python search path, not a loader one. On a box with the ARM64 VC redist it works by accident; on a clean box the interpreter dies with 0xC0000135 before any Python runs. python.org’s own layout keeps the CRT next to the exe. Fix: stage vcruntime140.dll (+ msvcp140*.dll when present) beside python.exe, keep the copy in DLLs\ for the .pyds, and extend the stage’s self-check; the whole-tree import walk of #127 is what would have caught it. DONE 2026-08-25 (arm64 run 14): Build-TargetCpython.ps1 stages the target-arch CRT set — vcruntime140.dll, vcruntime140_threads.dll (added after run 13’s import walk), msvcp140*.dll, concrt140.dll, vccorlib140.dll — from the build output or the VS ARM64 redist, PE-checked, beside python.exe and into C:\runtime\bin; throws if vcruntime140.dll cannot be staged. Measured: staged 9 CRT DLL(s); the #127 walk resolves every CRT import inside the bundle.

  • #125 — no sitecustomize for the TARGET interpreter: every first import of cv2/av/ORT on the device fails to find its DLLs. S · ★★★ (opened 2026-08-25) Initialize-PythonPlatformTag writes the shim into the host tree only (WindowsSourceBuild.Common.psm1, $CpythonDir\Lib\site-packages = C:\temp\cpython); nothing writes one into C:\runtime\python\Lib\site-packages. Python ≥ 3.8 ignores PATH for extension-module dependencies, so cv2.pyd → opencv_videoio500.dll → avcodec-63.dll and opencv_gapi → onnxruntime.dll cannot resolve, and the PyAV wheel (49 .pyd, 0 bundled DLLs) is built on the same assumption (Build-FfmpegFromSource.ps1 says its DLLs “resolve via the sitecustomize shim”). Fix: emit a second shim — DLL directories only, no get_platform override (the target reports win-arm64 itself) — into the target site-packages at the target-cpython stage (arch-aware paths from the same table), and assert its presence in the merge. DONE 2026-08-25 (arm64 run 14): the shim writer is one function (Write-PythonDllDirectoryShim, used by Initialize-PythonPlatformTag for the host tree); Build-TargetCpython.ps1 calls it for C:\runtime\python\Lib\site-packages with the arch-aware DLL homes and no platform/EXT_SUFFIX override, after emptying the target site-packages of the host tree’s pip/setuptools. Measured in-stage: wrote the DLL-directory sitecustomize shim for the target interpreter.

  • #126 — no runtime deps and no pip for the target: numpy (ORT import_array, cv2), packaging, flatbuffers, protobuf, sympy, coloredlogs are absent; C:\runtime\wheels holds only our three wheels. M · ★★★ (opened 2026-08-25) Every pip call in the chain runs the host interpreter; the cross wheel staging (-CrossStage) never resolves dependencies; no pip download --platform win_arm64 exists anywhere. Fix: a requirements-winarm64.txt resolved on the host with pip download --only-binary=:all: --platform win_arm64 --python-version 3.14 -d C:\runtime\wheels (pure wheels + the win_arm64 numpy/protobuf — verify each pin publishes one for cp314 before relying on it), a bundle install note (python -m ensurepip works offline — Lib\ensurepip\_bundled ships with the stdlib copy; install ours with --no-deps so PyPI’s onnxruntime never shadows the staged one), and a static gate that every Requires-Dist of the staged wheels resolves inside the wheel store. Implemented 2026-08-25 as windows/scripts/build/Copy-TargetPythonDeps.ps1, a cross-only merge step before the arch gate: it reads Requires-Dist from every wheel in C:\runtime\wheels (extras dropped, numpy added for cv2), downloads with the HOST pip (--only-binary=:all: --platform win_arm64 --python-version 3.14 --abi cp314/none/abi3) only what the bundle does not provide itself, then gates that every wheel is pure or win_arm64 (PE-checked) and that every requirement edge resolves inside the store. Measured on arm64 run 12: the first pip download died on onnxruntime-directml>=v1.29.0 (“from versions: none”) — onnxruntime-genai’s setup.py.in derives its ORT requirement from the package name (onnxruntime-genai-directml → onnxruntime-directml, -cuda → onnxruntime-gpu), but this bundle ships its combined CPU+DML(+CUDA) ORT wheel as plain onnxruntime (build-onnx passes no --wheel_name_suffix on purpose). Microsoft publishes no win_arm64 onnxruntime-directml, and on amd64 the same edge makes a consumer’s pip install pull a second onnxruntime over ours (the 2026-07-13 DmlExecutionProvider loss that the build’s -NoDeps only papers over). Fix, both lanes: Build-OnnxGenaiFromSource.ps1 rewrites the configured build\wheel\setup.py mapping to dependency = "onnxruntime" before packing (fail-loud when the pattern is gone), so the wheel declares onnxruntime>=v1.29.0 — the package the store actually holds. numpy 2.5.2 publishes a cp314-win_arm64 wheel; flatbuffers, packaging, protobuf resolve as pure wheels. DONE 2026-08-25 (arm64 run 14): store holds 7 wheel(s); 0 requirement edge(s) unresolved — the three bundle wheels plus numpy-2.5.2-cp314-cp314-win_arm64 (PE-checked), flatbuffers-25.12.19, packaging-26.3, protobuf-7.36.0 (pure); pip itself comes from the target stdlib’s ensurepip\_bundled (asserted in-stage). Install on the device with python -m ensurepip then pip install --no-index --find-links C:\runtime\wheels <name>.

  • #127 — whole-tree static import walk for the arm64 bundle. M · ★★★ (opened 2026-08-25) Today Get-UnresolvedDeps runs for the four mandatory GStreamer plugins only; every other shipped .dll/.exe/.pyd and every wheel member gets a PE-machine check and nothing else. #124 is exactly the class that a dependency walk catches. Fix: run the same dumpbin /dependents (or llvm-readobj --coff-imports) walk over everything under C:\runtime plus extracted wheel members, resolving against the bundle and a Windows 11 ARM64 System32 name list; report unresolved imports per file, floor on the file count, fail on any unresolved non-system import. Implemented 2026-08-25 as Test-TargetArch.ps1 -ImportWalk (merge arch gate, cross lane): a dependency-free PE import-table parser (Get-PeImportNames, import + delay-load directories, PE32/PE32+) walks every inspected PE plus the native members of every wheel and resolves each name against the bundle, the loader’s api-ms-/ext-ms- API sets, this container’s System32 list (CRT family excluded from it on cross — a clean device has no redist), a driver/toolkit allowlist (-ImportAllowlist: nvcuda, vulkan-1, opengl32, d3d12core, Qnn*) and a client-OS list (-ClientOsPattern: dsound, mf/mfplat/mfreadwrite/mfcore, winspool.drv — DLLs every Windows client SKU ships but the Server Core reference does not). Measured, arm64 run 13 — the first walk over 567 files found 13 unresolved imports in three classes, all real: (1) 6× libcrypto-4-arm64.dll/libssl-4-arm64.dll from gsthls/gstdtls/gstaes and gio’s openssl TLS module — linked against C:\opt\openssl-arm64’s import libs, but the DLLs were never installed (amd64 gets scoop’s x64 OpenSSL from the image PATH, which a bundle cannot rely on); fix: the GStreamer cross branch now stages them, PE-checked, into C:\runtime\bin. (2) 6× client-OS names (DSOUND.dll ← gstdirectsound*, MF.dll/MFPlat/MFReadWrite ← gstmediafoundation, WINSPOOL.DRV ← tcl9tk90.dll) — present on the device, absent on Server Core; classified, reported, not counted. (3) 1× VCRUNTIME140_THREADS.dll ← LiteRT’s tensorflowlite_c.dll — the one CRT member missing from #124’s staging list; added. DONE 2026-08-25 (arm64 run 14): inspected 970, violations 0; import walk: 571 file(s) walked, 0 unresolved import(s), 3 allowlisted external(s), 6 device-OS (client SKU) import(s) — the walk is a hard merge gate on the cross lane (Dockerfile.media-merge-builder, -ImportWalk). On the native lane the same walk runs report-only (measured amd64 run 4: 203 edges, all image facts — 186× python314.dll + 8× python3.dll because the host interpreter lives in C:\temp\cpython\PCbuild\amd64, outside the roots, and 6× scoop’s libcrypto/libssl-4-x64.dll from the image PATH); the first native run threw on them and failed the amd64 regression, so the throw is now cross-only and the report groups edges by DLL name first.

  • #128 — GStreamer arm64 lacked webrtc/nice. M · ★★ (opened 2026-08-25 from a lane log diff) · ✅ DONE 2026-08-26 (arm64 run 28, [bk] Done 00:29:32; amd64 run 7 the same day). Five layers, each measured: no build-machine C compiler in the cross file (→ a meson native file), the build machine linking the TARGET CRT (→ /vctoolsdir + /winsdkdir, because a path-shaped /LIBPATH: is read as an input FILE by the clang-cl driver), the build machine’s cl/ml64 coming from PATH which leads with bin\HostX64\ARM64 (→ named explicitly in [binaries]), and three meson 1.12.0 defects around build-only subprojects — a failed native: true subproject is recorded under the HOST key and overwrites the healthy host holder (the actual poison behind libnice’s by-name lookup), its configure_file outputs land in the host’s build dir, and its summary() collides. The first two are patched before meson setup (Invoke-MesonBuildSubprojectPatch); the third is left to fail glib(build) cleanly, since nothing consumes it — only meson’s gnome module ever asks for it. Result: gstwebrtc.dll + gstnice.dll on both lanes, All 6 mandatory GStreamer plugins verified present, arch gate 980/0, walk 577/0, smoke 97/0/15 (amd64: 1134/0, 222/0/0). Upstream draft for all three meson defects: out/upstream-issue-meson-summary-build-subproject.md (not filed — owner’s call). Two side fixes the runs forced: a failed meson setup logs a numbered EXCERPT instead of 400k–800k lines (30–60 min per failure before), and the retry classifier scans stdout + the log tail with word-bounded signatures (an SDK constant …_SSLERRORS_ONCE had turned a deterministic failure into a “transient” retry). Run-by-run record: windows-backlog-archive-2026-08-26.md.

  • #129 — OpenCV arm64 ships zero dispatched NEON kernels. M · ★★ (opened 2026-08-25) The arm64 configure log (run of 2026-08-24 20:50) prints Baseline: NEON and an empty Dispatched code generation: — HAVE_CPU_NEON_FP16_SUPPORT / _DOTPROD_ / _BF16_ - Failed, “NEON_FP16 is not supported by C++ compiler”: OpenCV’s feature probe hands clang-cl GCC-style flags it rejects. amd64 gets SSE4_1 SSE4_2 AVX FP16 AVX2 AVX512_SKX. Same failure class as the MLAS/XNNPACK/IREE per-TU fixes, degrading silently (fp16/dotprod paths fall back to baseline NEON + carotene). Fix: on cross pass CPU_DISPATCH=NEON_FP16;NEON_DOTPROD;NEON_BF16 with the /clang:-march=armv8.2-a+… spellings (OpenCV’s OPENCV_CPU_* flag overrides or an inline patch of OpenCVCompilerOptimizations.cmake) and gate on a non-empty dispatch line. Implemented 2026-08-25, proof pending arm64 run 20: no source patch needed — upstream sets the per-feature flag variables with ocv_update (set-if-unset) and its if(MSVC) branch blanks them to "" under clang-cl, so three cache definitions on the cross configure win: -DCPU_NEON_FP16_FLAGS_ON=/clang:-march=armv8.2-a+fp16 (+ _DOTPROD_, _BF16_); the dispatch SET stays upstream’s AArch64 default (NEON_FP16;NEON_BF16;NEON_DOTPROD). The gate parses opencv-configure.log: an empty Dispatched code generation: line fails BOTH lanes, and the cross lane must additionally list NEON_FP16. ✅ DONE 2026-08-25 (arm64 run 22). The flags alone were not enough: runs 20/21 dispatched NEON_BF16 only (0 files), and the gate’s CMakeError.log excerpt showed why — the probes cmake/checks/cpu_neon_fp16.cpp / cpu_neon_dotprod.cpp compile only under __GNUC__ && (__arm__ || __aarch64__); the _MSC_VER && _M_ARM64 alternative is commented out upstream (opencv/opencv#25052: MSVC’s arm_neon.h), so under clang-cl every probe ended in #error "... is not supported" whatever the flags. clang-cl ships clang’s own arm_neon.h, so the cross branch now patches the probes to also accept __clang__ && _M_ARM64 (search over cmake/checks, floor 2 files). Result: Dispatched code generation: NEON_DOTPROD NEON_FP16 NEON_BF16, the OpenCV stage green in 4:54 with the dispatched kernels compiled. Still baseline-only: the scalar FP16 probe (cpu_fp16.cpp, a different guard shape) — the fp16 conversions ride the NEON_FP16 dispatch.

  • #130 — bundle contract and small consumer-facing gaps. S · ★ (opened 2026-08-25) (a) No file in the bundle names its own layout: all pointers are Dockerfile ENV of a windows/amd64 image, PATH’s python is the host x64 one, C:\runtime\python appears in no ENV — emit C:\runtime\BUNDLE-ENV.cmd/.ps1 from the same table plus a bundle README. (b) GIO module cache absent (gio-querymodules skipped under DESTDIR) — document the one-time on-device run. (c) Assert-WheelTargetArch logs only a member count — log the names, so it is known whether the GenAI wheel embeds onnxruntime.dll. (d) Stale labels/comments: windows\Dockerfile LABEL and header still call TVM/IREE/LiteRT “empty markers on arm64”, the merge Dockerfile says the arm64 site-packages “wait on #120”, WindowsGstPlugins.Common.psm1 says LiteRT “cannot be built for Windows-on-ARM”, Test-Health.ps1 says the arm64 contract drops tflite. Implemented 2026-08-25, proof pending arm64 run 20: (a)+(b) Write-BundleManifest.ps1 runs in the merge stage on both lanes and writes C:\runtime\BUNDLE-ENV.cmd, BUNDLE-ENV.ps1 (every existing DLL home from the merge ENV plus C:\runtime\bin, the target python and the wheel store) and BUNDLE-README.md (the arch facts, the --no-index --find-links install line, the one-time gio-querymodules run on a device, the plugin contract, and every ABSENT-ON-* / COMPILER-ABSENT-* marker in the branches’ own words); (c) Assert-WheelTargetArch now logs the native member NAMES; (d) all four stale texts rewritten (the final Dockerfile header + LABEL, the merge fan-in comment, the plugin module header, the healthcheck fallback comment). ✅ DONE 2026-08-26 (arm64 run 28). In-container proof: Bundle manifest (arm64): 6 DLL home(s), python=C:\runtime\python, 3 wheel(s), 3 absent marker(s), plugins [libav opencv onnx webrtc nice tflite] -> C:\runtime\BUNDLE-ENV.cmd, BUNDLE-ENV.ps1, BUNDLE-README.md, followed by the deps store (7/0), the arch gate (980/0, walk 577/0) and the smoke gate (97/0/15) over the same tree. amd64 side (run 7, 2026-08-26): Bundle manifest (amd64): 6 DLL home(s), python=image CPython, 6 wheel(s), 0 absent marker(s), plugins [libav opencv onnx webrtc nice tflite].

  • #131 — post-cross-phase cleanup (refactoring). M · ★★ (opened 2026-08-25, owner’s request) · ✅ DONE 2026-08-25 in four waves, developed in an isolated worktree, 662/662 tests, proof = the bundled both-lane ride. Deletions first, then shared helpers with fixture tests, then the call-site migration. The ride is the lesson this repo keeps quoting: the worktree passed every host test and the parse gate, and the arm64 run still found three defects the host suites structurally could not see (-Path$x binding as a parameter named Path$x, a null-restore leaving LIB= defined-empty, a helper leaking its pipeline into the return value). Narrative: windows-backlog-archive-2026-08-26.md.

  • #132 — Windows Update inside the build container poisons a layer. S · ★★★ (opened and DONE 2026-08-25, measured on the amd64 regression after #131) The media-core-onnx stage finished its 150 s RUN and then died in finalize, byte-identical on both retries: failed to reimport snapshot: Files/Windows/SoftwareDistribution/Download/<id>/ Windows11.0-KB5120233-x64.msu: unknown stream ID 9. servercore ships wuauserv and the Update Orchestrator (UsoSvc), both trigger-started; the container has network; the client downloaded a cumulative update into the spool during the RUN, and BuildKit’s Windows layer writer cannot carry that file’s alternate stream. Retries cannot help — the RUN result is cached, only the finalize re-runs. Fix: Disable-ContainerWindowsUpdate (WindowsSourceBuild.Common.psm1) stops and disables both services and sets NoAutoUpdate=1 as the first step of every build script (Initialize-SourceBuildEnvironment) and reports the spool count — an entry inherited from the parent image (1 item at RUN start on every media stage) is harmless, only a file written during the RUN lands in the diff. Deliberately prevention only: no script deletes under C:\Windows (protected-root rule); a poisoned layer is fixed by re-running its RUN with the guard in place (the module edit re-keys it). Proven on the amd64 regression run 4: the same media-core-onnx RUN finalized in 5:32 with the guard active at 4.7 s.


#122 — CUDA on arm64: Phase-0 probe (DONE 2026-08-28, CLOSED by owner decision)#

Moved here from windows-refactor-backlog.md on 2026-08-28 after the owner confirmed no near-term plan to integrate CUDA into the Windows container build. The probe ran on 2026-08-28 (no chain rebuild); the verdict closed the entry. Kept for the measured facts, which correct two prior records.

Verdict: cuDNN arm64 ships today; CUDA itself does not — not at 13.4, not at any 13.3.x or 12.x. Wiring is blocked on NVIDIA publishing a windows-arm64 CUDA redist, not on this repo.

(1) The hardcoded windows-x86_64 literal in Install-Cuda.ps1 — CONFIRMED, one site. Install-Cuda.ps1:138 builds the cuDNN URL with a literal windows-x86_64 segment: …/cudnn/redist/cudnn/windows-x86_64/cudnn-windows-x86_64-{0}_cuda{1}-archive.zip. The CUDA installer URL (:40) carries no arch — it is cuda_$CudaVersion_windows.exe, a single x86_64 .exe. So arch-parameterising means the cuDNN URL ONLY: swap windows-x86_64 -> windows-arm64 (and the matching filename prefix) on the arm64 lane. No CUDA installer URL change is possible because there is no arm64 installer.

(2) cuDNN arm64 archive at the exact pin — CONFIRMED HTTP 200, but the size previously recorded was WRONG. https://developer.download.nvidia.com/compute/cudnn/redist/cudnn/windows-arm64/cudnn-windows-arm64-9.25.0.15_cuda13-archive.zip returns HTTP 200, 90,177,536 bytes (~90 MB) — NOT the 421 MB recorded on 2026-08-24. The x86_64 counterpart at the same pin is HTTP 200, 632,936,226 bytes (~633 MB). The 421 MB figure was never re-measured after it was first recorded; the archive may have been re-cut, or the figure was always wrong (the 2026-08-24 note says “421 MB” with no fetch). The cuDNN redist manifest JSON (redistrib_9.25.json and every variant tried) 404s, so the SHA256 pin in versions.env (CUDNN_ZIP_SHA256) cannot be re-derived from the manifest — it must be computed from a download, which is what versions.env:779 already says. The archive’s own lib\arm64 contents were not inspected here (extraction needs 7-Zip, not on this host’s PATH); the 2026-08-24 note asserts lib/arm64 inside and the 90 MB size is consistent with an arm64-only slice of the 633 MB x86_64 zip.

(3) CUDA 13.4 arm64 device .libs — NOT PUBLISHED. CUDA 13.4 does not exist on NVIDIA’s servers. https://developer.download.nvidia.com/compute/cuda/13.4/local_installers/cuda_13.4_windows.exe returns HTTP 404. The same path at 13.4.0 and 13.4.1 also 404. The /cuda/13.4/ directory page returns NVIDIA’s “Page Not Found” HTML. There is no CUDA 13.4 installer to pull, preview or otherwise. The CUDA redist manifest for 13.3.1 (HTTP 200, 47 KB) lists only windows-x86_64 for Windows — zero windows-arm64 entries. Same for 13.3.0, and for every 12.x checked (12.5.1, 12.6.0, 12.6.3, 12.8.1): 0 windows-arm64, 62 windows-x86_64 each. The only arm64 CUDA target NVIDIA publishes anywhere is linux-sbsa (Linux ARM64 server). The staged windows/downloads/cuda_13.3.0_windows.exe is an i386 InstallShield launcher (PE machine 0x014C) — the wrapper, not a payload indicator; extracting its 2.5 GB embedded archive to look for arm64 .libs needs 7-Zip (not on PATH here), but the redist manifest is the authoritative publication list and it has no arm64 row.

What this means for the wiring plan. The entry’s “only if all three hold” gate FAILS on (3): there is no CUDA arm64 installer and no windows-arm64 CUDA redist, so there are no arm64 device .libs to point nvcc at and no CUDA root for the ORT CUDA branch to key on. Re-keying ORT on “an arm64 CUDA root exists” is correct in principle but vacuous until NVIDIA ships one. Do NOT relax the -Gpu driver refusal — the image-state hazard it guards is real and the prerequisite (a CUDA arm64 publish) does not exist. Re-open this entry ONLY when NVIDIA publishes a windows-arm64 CUDA redist or installer; check the redist manifest, not the marketing page. The 2026-08-24 retraction of “CUDA does not exist at all” was itself half-wrong: cuDNN exists, CUDA (toolkit/nvcc/device libs) does not, and the distinction matters because ORT’s CUDA EP needs the toolkit, not just cuDNN.

One free, build-free fix that fell out of this probe (PARKED, not done): arch-parameterise the cuDNN URL in Install-Cuda.ps1:138 so the arm64 GPU lane fetches the 90 MB arm64 archive instead of the 633 MB x86_64 one. That is a correctness fix for the arm64 GPU lane’s cuDNN layer (it currently downloads the wrong arch), independent of whether CUDA itself ever ships for arm64. Do it in a base-layer window, not folded into #134/#135.


#136 — VS RUN never cached across runs (SOLVED + DEPLOYED 2026-08-26)#

Moved here from windows-refactor-backlog.md on 2026-08-28. The fix landed and was proven on 2026-08-26; the entry was kept OPEN only because the narrative had not been archived. Kept for the 0B = “already pruned to floor” inversion lesson, which is the general rule worth remembering.

Every launch replays #9 RUN Install-Vs.ps1 while #6 (a RUN with a bind mount), #7 and #8 (the COPY of that very script) all report CACHED. Reproducible across five consecutive launches on 2026-08-26.

The cost is NOT constant, and the first version of this entry overstated it. Cold it was ~22 min of build plus ~7 min of export; on the very next launch the same step reported #9 DONE 363.2s — six minutes — because the VS installer finds its downloads already present. So the tax per iteration is roughly 6–10 min warm and ~30 min cold, not a flat 30. Still worth fixing (it multiplies every Windows experiment), but do not plan around the cold figure.

Ruled out by measurement, not by reasoning: GC eviction (buildctl du reports Reclaimable: 4.27MB of a 499.9 GB store, and the two ~37.6/37.8 GB VS-class records are present); a build-arg that varies per run (the driver passes only version pins — no timestamp, VCS ref or GUID reaches this stage); -NoCache/-NoCacheStage (not passed); and a changed COPY input, since #8 — the COPY of Install-Vs.ps1 itself — is CACHED, so its parent chain and that file are byte-identical.

SOLVED 2026-08-26 (916c91f0) — it was the GC reserve, and the answer was inside buildkitd.toml itself. Its sizing note concludes “150GB is the floor this file’s own sizing note gives” and “keep reservedSpace >= 150 GB regardless”; the value directly beneath it read 40GB — below the single ~37 GB VS-class layer, so the spine could not survive between runs. maxUsedSpace compounded it: the store had grown to 545 GB, above BOTH ceilings (400/450 GB), so GC was evicting on every run no matter what the reserve said. My earlier “GC ruled out because du reports Reclaimable: 0B” was the wrong reading — 0B is what a store already pruned to its floor looks like, not a store that is never pruned. That inversion is the lesson worth keeping.

Fixed: reservedSpace 40→150 GB, maxUsedSpace 400→650 GB (tier 1) and 450→700 GB (tier 2). Arithmetic at edit time: C: 824 GB free of 1861 GB, store 545 GB, other content ~492 GB — so 150 GB is comfortably satisfiable, unlike the 2026-08-08 deadlock at 214.75 GB when only ~294 GB was available to buildkit. Three docs claimed a third number (200 GB) matching neither the config nor windows-build-lanes.md’s own “150GB now”; all aligned.

This was the second half of a fix only half-made on 2026-08-11, which raised maxUsedSpace after GC evicted “the base/sdk/toolchain spine between driver runs → every run re-solved the prefix” — verbatim this symptom — and left the reserve alone.

DEPLOYED AND PROVEN 2026-08-26 23:10 (owner ran the elevated apply). Verified by effect, not exit code, as that script has reported success while leaving the old value before (2026-08-08): the live daemon reports Reserved space: 161.0612736GB on rules 1 and 2 with Maximum used space 697.93/751.62GB. The proof is the next build, not the config dump — the very first launch afterwards reported #6 #7 #8 #9 #10 CACHED, the first time #9 had cached in six attempts that day. Base went from ~4–7 min of VS install plus ~7 min of export to under a minute. Six attempts on 2026-08-26 would have cost ~2.5 h in cold prefix alone.

My “this run probably will not benefit yet, the records were already evicted” caveat was wrong: the previous run’s records were still present, and the raised ceiling simply stopped GC from taking them.

One doc trap this exposed, now fixed: buildctl debug workers -v prints GB while the toml takes GiB, and uses different labels (Reserved space: vs reservedSpace). A 150 GiB pin shows as 161.0612736GB, so the verification instruction “must read 150GB” — which I had just written into two pages — would make a correct deploy look failed.

  • #135 — LLVM 23.1.0 AArch64 codegen: both failures fixed; the second one’s earlier diagnosis was wrong from end to end. M · ★★ (opened 2026-08-26, closed 2026-08-27) The forced clang-cl bump to 23.1.0 broke the cross build of OpenCV in two places. They share one root signature — LLVM lays a function out a few bytes SHORT of what it then emits, so a pass picks an encoding the assembler then rejects — and docs/failure-modes.md § AArch64 cross compile aborts carries the symptom-first write-up, the four “do NOT”s and the local-repro recipe. (1) Jump-table entry width — FIXED (eb13d2a2). AArch64CompressJumpTables picks 1-byte entries from an estimate; every N measured sat just past the 1020-byte ceiling (256 = 1024 B, 258, 259, 260, 262, 272, 281, 284). -Xclang -target-feature -Xclang +force-32bit-jump-tables keeps every table on 4-byte entries. value evaluated as went 4 → 0 in the cross build. Cost 4522 → 4650 bytes of object, ~2.8 %, all jump-table DATA; full /O2 retained. (2) tbnz just out of branch range — FIXED (2026-08-27, per-TU /Ob1 in Build-OpencvFromSource.ps1). At /O2 the whole baseline median filter collapses into ONE function, cv::cpu_baseline::medianBlur, 8,465 instructions ≈ 33,860 bytes, and tbnz w9, #31, .LBB546_847 inside it must reach a block ~32,916 bytes away against the ±32,768 a 14-bit displacement encodes. BranchRelaxation exists to catch that and its estimate came out short. /Ob1 on the offending TUs (via Add-NinjaPerTuFlags) stops the inliner gluing file-static helpers into one oversized function — no optimisation level is lowered; every kernel keeps /O2, vectorisation and unrolling, at the cost of one call per medianBlur(). The largest function in that TU drops 33,860 → 10,620 bytes: 3.1× headroom, not a 148-byte miss. The list is a census: NINJA_KEEP_GOING=1 (→ ninja -k 0) compiled all 1,870 objects in one run and named every offender — median_blur.dispatch.cpp and multiview_calibration.cpp, and no third. Re-run it that way after an OpenCV bump. WHAT THE EARLIER ENTRY GOT WRONG — recorded so nobody re-derives it. It described (2) as an adr reach problem “in a function larger than ~1 MB”, blamed it on -aarch64-enable-compress-jump-tables=false having removed the pass’s adr check, and credited +force-32bit-jump-tables with “keeping the pass enabled”. Every part of that is false: the feature makes the pass return false before it scans anything (source, 23.1.0) and emits byte-identical asm to =false (measured); with the pass off the adr is self-relative and cannot overflow; the largest function in the TU is 33 KB, not >1 MB; and the failing instruction is a tbnz, with no jump table involved at all (-fno-jump-tables fails identically). Ruled out by measurement on the real TU, keep them ruled out: -fno-jump-tables, dropping +force-32bit-jump-tables, -mllvm -inline-threshold=100 and =25 (single-call-site statics are inlined regardless of threshold — this is why /Ob1 works where a threshold does not); -max-jump-table-size (caps entry COUNT, the ceiling is a byte SPAN); -align-all-* (kills clang-cl through the SEH unwind writer, llvm#122707 / llvm#47432); /Od and /O1 (move the failure to the next TU, three times). Upstream: two reportable LLVM 23.1.0 bugs, neither a prerequisite for this repo. (a) The layout estimate that comes out short, which drives BOTH failures above — reportable on the evidence in hand (a tbnz left ~150 bytes out of reach is a BranchRelaxation miss, and the jump-table N values are the same defect one pass over). (b) New, found 2026-08-27: the asm printer emits a catch funclet’s block address as add x0, x0, .LBB0_903 — the :lo12: specifier is MISSING — so /FA output does not round-trip through LLVM’s own assembler (expected compatible register, symbol or integer in range [0, 4095]). Object emission is unaffected. Reproduced locally in 15 lines: one try/catch whose body pushes the continuation block past 4 KB. This one matters beyond cosmetics: it blocks the /FA diagnostic route that failure-modes.md prescribes, until you repair the listing (the recipe there does). Check upstream HEAD before filing either — out/ holds the drafts of previous reports. (b) FILED 2026-08-27: llvm/llvm-project#219200, from Kataglyphis/llvm-project branch aarch64-catchret-lo12 — one commit ahead of llvm/main (the fork’s main is 0 ahead / 12 behind, and that branch is its only non-upstream work). It does NOT retire either workaround, and the tempting inference that it does is wrong. The commit’s own verification says so: the relocations were always correct (PAGEBASE_REL21 + PAGEOFFSET_12A, checked by assembling the fixed output against -filetype=obj) and the change is -S / /FA only. Both #135 aborts happen during OBJECT emission, which that patch leaves byte-identical. What it buys is the diagnostic route: failure-modes.md prescribes /FA for exactly these no-source-location errors, and on Windows-on-ARM with C++ exceptions that route was closed. (a) turned out to be TWO independent defects, not one — see the root-cause block below. Settling any future candidate: don’t reason about it, run windows\scripts\diagnostics\Invoke-LlvmAarch64Layout.ps1 — the five real offenders frozen as preprocessed .i, compiled with the workaround OFF, with the stock compiler’s reproduce-and- suppress arms gating the verdict so a stale corpus reports INVALID instead of a false fix. A FIXED verdict licenses the NINJA_KEEP_GOING=1 census; it does not replace it.

    2026-08-27 root cause: (a) is TWO defects, and this entry’s “one signature at two sites” framing was wrong

    They do not share a cause. One is a known upstream bug with an existing fix; the other is still open but is now narrowed to a single measurable quantity.

    (a1) tbnz / fixup value out of range — SAME root cause as (a2), retired by the SAME patch. BranchRelaxation consumes the same getInstSizeInBytes; median_blur.dispatch.cpp and multiview_calibration.cpp are also compiled /EHa; and the ~150-byte miss is 37 EH_LABEL nops counted as zero. CLAIMED BUT UNLOGGED — do not treat this as measured. A NINJA_KEEP_GOING=1 census is recorded as having built all 1,869 objects green with BOTH OPENCV_NO_JUMPTABLE_WORKAROUND=1 and OPENCV_NO_OB1_WORKAROUND=1, on pinned llvmorg-23.1.0 plus only the two getInstSizeInBytes patches (llvm#219275, llvm#219276). It was run by hand inside the container, so it left no bk- log: no artifact in out/windows-build-logs/ mentions either knob or llvm-patched, and the newest log there is bk-20260827-083658. Re-run it through the driver and keep the log before deleting either workaround.

    THIRD INSTANCE OF THE SAME INFERENCE ERROR — read this before the next attribution. This block previously read “ROOT CAUSE FOUND, FIX EXISTS UPSTREAM: c6e184686cd7 (trampoline blocks created at offset zero), so /Ob1 stays until the toolchain moves to LLVM main”. The census compiler contains no such commit — it is the pinned 23.1.0 release plus two patches that touch only getInstSizeInBytes — so llvm#202716 cannot be a necessary cause here. It is a real upstream defect, kept for the record, and nothing more. What the branch-relax-tbz.mir revert actually proved was that the commit is load-bearing for upstream’s own test, never anything about median_blur.dispatch.cpp. That is the same “evidence about X read as evidence about Y” mistake this entry already records twice above. The 2026-08-27 decision to move the toolchain to LLVM main is WITHDRAWN — superseded by the shipped path (pinned 23.1.0 + the two patches). No backport request is needed from anyone.

    Both workarounds are now removable — but they STAY until the patched toolchain is the DEFAULT. windows/Dockerfile.toolchain-builder still carries ARG BUILD_PATCHED_LLVM=0, so a stock image ships unpatched clang-cl 23.1.0 and still needs both settings. Delete the flags from Build-OpencvFromSource.ps1 in the SAME change that flips that default, never before.

    The stage is reachable since 2026-08-28: build-buildkit.ps1 -PatchedLlvm. Until then it was not — the driver always targeted built and BUILD_PATCHED_LLVM appeared in no .ps1 at all, so the plan above would have been a no-op found mid-build. Pinned by windows/scripts/tests/BuildKit.PatchedLlvm.Tests.ps1. The switch is opt-in because the extra RUN + ENV PATH re-key every media stage below the toolchain image.

    (a2) Jump-table entry width — NOT fixed on main, and now narrowed to one thing. AArch64CompressJumpTables.cpp is byte-identical between llvmorg-23.1.0 and main; the AsmPrinter’s jump-table emission is untouched (its one commit since 23.1.0 is PAC-only); getInstSizeInBytes’s body is unchanged and no pseudo’s .td Size moved. Moving to main will not retire +force-32bit-jump-tables. The pass is sound given correct instruction sizes: offsets are upper bounds, that inflation accumulates monotonically in layout order, so Span = MaxOffset - MinOffset is over-estimated, which selects a LARGER entry. It can therefore only fail if some instruction reports fewer bytes than it emits. The measured values (256, 258, 259, 260, 262, 272, 281, 284) put that under-count at 4–116 bytes, between the table’s lowest and highest target block.

    The tool that will name it. LLVM’s AsmPrinter already verifies reported size against emitted bytes and aborts printing the function, the MachineInstr and both sizes — but the check is inert on AArch64: TargetInstrInfo::getInstSizeVerifyMode() defaults to NoVerify and AArch64 never overrides it (only AMDGPU and PowerPC do). The opt-in is written and committed: D:\GitHub\llvm-project, branch aarch64-instsize-verify, commit 65a5bd5601fe — local only, unpushed. Build clang with it and compile 3rdparty/protobuf/.../descriptor.cc in the container; the abort names the instruction and the fix is then a one-line Size. Two scans, both at aarch64-pc-windows-msvc, and keep them distinct: the size verifier over the 2,925 .ll files (2,049 compiled) reported zero under-counts; a separate signature scan over 3,347 files (.ll + .mir) reported zero genuine reproductions of either abort. So the offending construct is one LLVM’s own tests never build. The single apparent fixup value out of range hit was an artifact of forcing a Linux/PIC test onto a Windows triple (it produces an :abs_g3: relocation COFF cannot encode); under its own triple it compiles clean. Ruled out by reading the code, keep them ruled out: JumpTableDest8/16/32 (declares Size = 12, pessimistic BY DESIGN — the .td comment says “optimization occurs after branch relaxation so be pessimistic”); block alignment (modelled by BasicBlockInfo::postOffset); EH-funclet alignment (beginCodeAlignment is implemented only by DwarfDebug, emits no code bytes, unused on COFF); CATCHRET (lowers to a single 4-byte RET, and its ADRP/ADD are already real instructions before the estimating passes run).

    A third LLVM bug, found on the way, and NOT the cause of either failure — now FIXED. Every SEH_* pseudo reported 4 bytes and emitted 0 (538 of the 2,925 .ll files). That is the over-estimate direction, so it is conservative for both consumers — but it inflated every Windows-AArch64 size estimate by 4 bytes per SEH directive, and a prologue emits one per saved register. Filed upstream 2026-08-27 as llvm#219276 (40f1b36c642e on origin/aarch64-seh-size). The verifier opt-in that found it is NOT upstreamed — it waits on llvm#219275 and llvm#219276 landing, and lives on the local branch aarch64-instsize-verify. (The local development SHAs 1e6148bc4b9c / f533c8e88038 are rebase ancestors of the pushed commits; their patch-ids differ, so chase the PR numbers, not these.) Mismatches 558 → 45, all 45 remaining being over-estimates in unrelated pseudos; check-llvm-codegen-aarch64 4197 passed / 0 failed. The .td route does not work — the default case tests if (Desc.getSize()), so a declared Size = 0 is indistinguishable from “unset” and still yields 4; it must be a C++ early return on isSEHInstruction. Full PR handover, including the two build-environment traps that each cost a run: out/upstream-llvm-aarch64-seh-instsize.md (ephemeral handover document, not in repo). It does not retire +force-32bit-jump-tables: removing an over-count moves the estimate toward the true value but never below it.

    2026-08-27, second attempt at (a2): a sensitive detector, and it still did not fire

    The error-signature scans could only ever catch the defect if a span landed on the 1020-byte knife edge. The defect itself is Span_est < Span_true at ANY magnitude, so the pass was instrumented to dump its estimate and a checker computes the TRUE span from the emitted assembly — making every jump table a test. Instrumentation patch (not upstreamable, keep it): D:\llvm-patches\jtspan-instrumentation.patch.

    Measured, all at aarch64-pc-windows-msvc, all with a validity gate that fails the run when nothing was actually measured:

    corpus

    jump tables measured

    Span_est < Span_true

    598 real C++ modules (LLVM’s own sources)

    1,902

    0

    400 synthetic MSVC-EH funclet + jump-table modules

    400

    0

    the three real offenders (descriptor.cc, generated_message_reflection.cc, wire_format.cc, from OpenCV’s own bundled protobuf)

    28

    0

    So the estimate held as an upper bound on ~2,350 real jump tables, including the exact source files that fail in the lane. The one remaining difference is the one that cannot be closed on a host without MSVC: that IR was produced by clang targeting x86_64-w64-windows-gnu (libstdc++, Itanium EH), because the Windows SDK is not present. The lane compiles it with clang-cl for aarch64-pc-windows-msvc under /EHa — MSVC STL, MSVC EH, different functions, different jump tables. That gap is the whole remaining hypothesis space, and closing it needs the container.

    2026-08-27 — (a2) SOLVED. EH_LABEL under /EHa emits a 4-byte nop counted as zero

    Root cause. AsmPrinter::emitFunctionBody(), in the EH_LABEL case, emits a NOP when the module flag eh-asynch is set and the next instruction may load/store or raise an FP exception — so an async fault lands in the right EH region. EH_LABEL is a meta-instruction, so getInstSizeInBytes returns 0 for it while 4 bytes are emitted. Every consumer of MIR-level block sizes is then short by 4 bytes per such label. That is the under-count this entry predicted, and its magnitude (4–116 bytes) is exactly 1–29 labels.

    How it was isolated, and where the earlier corpora went wrong. The whole hunt over LLVM’s tests, synthetic IR and 598 real modules missed it for one reason: that IR was produced by clang targeting x86_64-w64-windows-gnu, which never sets eh-asynch. The lane compiles with /EHa. Flag bisection on the real TUs shows /EHa is the sole trigger — /EHsc plus any combination of /Gy /Oi /bigobj /fp:precise -TP compiles clean.

    Measured A/B, one binary, same bitcode, toggling only the fix:

    TU

    fix OFF

    fix ON

    descriptor.cc

    value evaluated as 258, 281

    clean, 1,457,567-byte object

    generated_message_reflection.cc

    284

    clean, 335,461-byte object

    wire_format.cc

    260

    clean, 233,801-byte object

    Those are this entry’s own recorded values. Filed upstream 2026-08-27 as llvm#219275 (c03f827dc49f on origin/aarch64-ehlabel-size); check-llvm-codegen-aarch64 4197 passed / 0 failed. The local development SHA was f072f90a9e37 — a rebase ancestor, not findable upstream.

    Reproducing it costs ~10 seconds, not a lane run. The bk-windows-media-core-opencv-arm64 image already carries clang-cl 23.1.0, MSVC and the ARM64 SDK; buildctl reaches buildkitd over npipe:////./pipe/buildkitd without elevation. Copy OpenCV’s 3rdparty/protobuf/src into a build context and compile one TU with --target=aarch64-pc-windows-msvc /O2 /Ob2 /EHa. Dockerfiles kept in D:\pb-probe.

    2026-08-27 — a way to get the fixes into the lane without waiting for a release

    Both fixes apply cleanly to the pinned llvmorg-23.1.0 (verified with git apply --check), which is what makes this cheap. Building the pinned release plus the two patches keeps the banner at clang version 23.1.0, so Test-Toolchain.ps1’s provenance gate passes with LLVM_WINDOWS_VERSION untouched and every clang-cl-shaped source patch (#129 probes, mlasi.h, softfloat) stays valid. Moving to LLVM main would disturb all of that for no extra benefit — the fixes are the only delta that matters.

    • windows/scripts/patches/llvm/001-aarch64-ehlabel-size.patch and 002-aarch64-seh-pseudo-size.patch — the two upstream commits (llvm#219275, llvm#219276).

    • windows/scripts/build/Build-LlvmFromSource.ps1 — downloads the SAME pinned tarball and SHA256 the TVM stage already uses (so the no-unpinned-download rule, #47, is satisfied by a pin already in the repo), applies both patches through Invoke-SourcePatch, builds clang+lld, and throws if the patched source does not carry eh-asynch — a silently unpatched compiler would rebuild the exact bug this stage removes and only resurface hours later in the OpenCV stage.

    • Dockerfile.toolchain-builder gains an opt-in patched-llvm stage (BUILD_PATCHED_LLVM=1, off by default) and prepends C:\llvm-patched\bin to PATH. That is the entire integration: every build script resolves the compiler by bare name ($env:CC = 'clang-cl', --cc=clang-cl), so shadowing the scoop shim needs no build-script change.

    2026-08-28 — BUILT AND MEASURED IN THE CONTAINER. The fork fixes it.

    The shim patch was redeployed (gate hash MATCHES, 25,937,920 bytes) and the whole path was run for real. Build-LlvmFromSource.ps1 works end to end: pinned tarball, SHA256-checked, both patches applied, the eh-asynch drift assertion passed, install in ~9.5 min, and the banner reads clang version 23.1.0 — so Test-Toolchain.ps1’s provenance gate accepts it. Image tagged docker.io/local/kataglyphis:bk-llvm-patched.

    Stock clang-cl 23.1.0 vs the same 23.1.0 + the two fork patches. Same sources, same flags, same MSVC headers, same container, NO workaround flags:

    stock

    patched

    protobuf descriptor.cc

    258, 281 out of range

    clean

    protobuf generated_message_reflection.cc

    284 out of range

    clean

    protobuf wire_format.cc

    260 out of range

    clean

    protobuf, objects produced

    0 / 3

    3 / 3

    OpenCV imgproc

    2 range errors (277, 258), 150 objects

    0 errors, 151 objects

    Every value matches the ones this entry recorded from the lane logs. Objects are counted deliberately: zero errors with zero output would prove nothing.

    Still NOT established, and the workarounds stay until it is. This is BUILD_LIST=imgproc in a minimal configure — 151 objects against the lane’s ~1,870 — and median_blur.dispatch.cpp never compiled here (OpenCL kernel-header generation), so /Ob1 and the whole BranchRelaxation half remain untested. The rule this entry set still holds: this licenses the NINJA_KEEP_GOING=1 census with both settings removed, it does not replace it.

    Three environment problems solved on the way, each documented in failure-modes.md:

    • patch.exe silently skipping every patch on a non-git source tree — a latent bug in Invoke-SourcePatch, found only because the drift assertion fired.

    • atlbase.h missing (ATL is not in the container’s VS Build Tools) — fixed with -DLLVM_ENABLE_DIA_SDK=OFF.

    • ImportLayer 0xb7 on image export — snapshot debris, cleared by --no-cache exactly as documented. Ruled out first: the shim (gate hash MATCHES) and the RDNA4 dGPU (ConfigManagerErrorCode=22, i.e. disabled). docker/for-win#14977 is still open and still needs-triage as of 2026-08-28, so the Set-Rdna4Gpu.ps1 -Disable workflow stays.

    Strong open hypothesis — this may also be (a1), i.e. /Ob1. BranchRelaxation consumes the same size function, median_blur.dispatch.cpp and multiview_calibration.cpp are also compiled with /EHa, and the miss there was ~150 bytes ≈ 37 labels. If so this single fix retires both workarounds and llvm#202716 is a separate, additional defect rather than the explanation. Test it the same way before assuming either.

    Local build assets (not in this repo): D:\GitHub\llvm-project (blobless clone) and D:\llvm-build (llc/llvm-objdump/llvm-nm, Release+assertions, AArch64-only, built with the Strawberry MinGW g++ 13.2 that ships with this host — there is no MSVC here, which is also why a Windows-ARM64 object cannot be produced outside the container).

  • #136 — VS RUN never cached across runs: SOLVED + DEPLOYED 2026-08-26, archived. The