Skip to content

Windows x64 port - #8

Draft
lfnoise wants to merge 30 commits into
mainfrom
windows-port
Draft

lfnoise wants to merge 30 commits into
mainfrom
windows-port

Conversation

@lfnoise

@lfnoise lfnoise commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Windows (x64) support for all components: clang-cl host binaries, a bundled llvm-mingw toolchain for runtime-compiled synthdef plugins, WASAPI/DirectSound audio, OSC, and the JUCE app. See docs/WINDOWS.md for the design and dev/windows/README.md for the first-build checklist.

Draft: opened to run the new windows-core CI job (continue-on-error). macOS and Linux suites are green; every Windows code path was cross-checked with the llvm-mingw cross compiler from macOS, but nothing has run on a Windows machine yet.

Also in this branch, on every platform: platform-independent value hashing and sinpi family (one golden set), plugin headers shipped in the distribution folder (fixes runtime synthdef compilation on machines without the source tree), and argv-based plugin compile/link (paths with spaces).

🤖 Generated with Claude Code

https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6

lfnoise and others added 30 commits September 5, 2026 20:00
Golden tests byte-compare LF interpreter output against .expected files;
without an attributes file, Git for Windows' default autocrlf would check
every golden out as CRLF. Renormalizes the two vendored CRLF files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
…`long`

On Windows `long` is 32 bits. lang's Word::i, the engine's i64/u64, the
ext_vector_type lane types in shared/tzpl_simd.hpp, the voicer header's
standalone alias, and the bridge/app node/control/tap IDs were all `long`
and would silently truncate. All are now std::int64_t / std::uint64_t (the
same type in every namespace, which also removes the old long-vs-long-long
split between engine and lang). isize is std::ptrdiff_t (no ssize_t in the
MSVC CRT). The non-Apple SIMD branch gains the u32xN types the Apple branch
already had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
…e golden set

Map/Set iteration order (and so printed output) hashed through std::hash,
which differs between libc++, libstdc++ and the MSVC STL, and sinpi/cospi/
tanpi/exp10 mapped to Apple libm extensions on macOS but sin(x*pi)
elsewhere. Windows would have added a third set of golden overrides.

- lang/src/stable_hash.hpp: ints hash to themselves and floats to their
  bits (libc++'s rules, so existing macOS output is preserved); strings
  use FNV-1a with a splitmix64 finalizer. Symbols hash by name, which also
  makes the hash(Symbol) builtin deterministic across runs.
- shared/tzpl_sinpi.hpp: exact quadrant reduction, then one std::sin, so
  integer/half-integer arguments give exact 0/+-1 on every platform. Used
  by the interpreter, the synthdef constant folder, generated plugin code,
  and the 2-wide SIMD fallbacks. M_PI/M_E users switch to <numbers>
  (MSVC has no M_PI without _USE_MATH_DEFINES).
- shared/tzpl_fft.hpp: the portable radix-2 path is now the only path;
  the vDSP branches and the three Accelerate links are gone.
- Golden files: 8 macOS goldens regenerated (map order, FFT signed zeros);
  the 14 .expected.linux overrides shrink to 3 residual libm-drift cases.
  run_tests.sh now selects .expected.<os> from uname (linux / windows).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
…known paths

New shared/tzpl_platform static library (linked through the shared
INTERFACE target):

- tzpl_dynlib.hpp: dlopen/dlsym/dlclose/dladdr on POSIX, LoadLibraryExW/
  GetProcAddress/FreeLibrary/GetModuleHandleExW on Windows. Replaces the
  raw dl* calls in the engine loader, the synthdef compiler, and the FFI
  integration test.
- tzpl_process.hpp: runProcess(argv, onLine) via posix_spawnp or
  CreateProcessW, no shell. The plugin compile/link steps now pass argv
  vectors, which also fixes paths with spaces on every platform (the old
  string concatenation quoted nothing), and works from a GUI-subsystem
  process where _popen does not.
- tzpl_paths.hpp: executablePath, distRoot (the folder holding modules/),
  homeDir, userConfigDir, defaultBuildDir, and kPathListSep (';' on
  Windows, since drive letters contain ':'). module_paths.cpp, both
  main.cpp REPL history paths, -I / $TZPL_PATH splitting, and the app's
  user config location use these.

synthdef_compile_link.cpp: plugin compiler resolution order is $TZPL_CC,
the toolchain bundled in the distribution folder (Windows), the baked-in
compiler if it still exists, then PATH; headers are staged from
$TZPL_SHARED_INCLUDE, the distribution's include/, or the source tree
(previously only the source tree, which does not exist on a user's
machine); Sleef staging also looks in the distribution folder; a
once-per-process sweep prunes revisions an earlier session could not
delete; .dll is the plugin extension on Windows. The --test stage macros
are #ifndef-overridable for staged bring-up.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
…ders in the distribution

- lang/src/builtins_io.cpp: fileExists/isDirectory/fileSize/listDir/
  makeDir/removeFile/renameFile/currentDir are std::filesystem now
  (dirent/stat/mkdir/unlink/getcwd do not exist in the MSVC CRT). Paths
  cross the boundary as UTF-8 via tzpl::pathFromUtf8/pathToUtf8, and
  currentDir stays '/'-separated on every platform, the form std.path
  expects. New tempDir() builtin; the four scratch-dir tests use it
  instead of a literal /tmp and mask the directory in printed errors.
- shared/tzpl_plugin_abi.h: TZPL_PLUGIN_EXPORT (__declspec(dllexport) on
  Windows, default visibility elsewhere), emitted on all seven exported
  symbols by both the C++ and the synthc code generators. A Windows DLL
  exports nothing unless told to.
- The dist component installs shared/*.h* as include/ (and Sleef on
  Linux) beside modules/. Runtime synthdef compilation copied headers
  from the absolute source path baked in at configure time, so a shipped
  DMG only worked on machines that had the source tree at that path.
  make_dist_dmg.sh now refuses a stage without the headers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
CMake: a WIN32 block in the root (clang-cl host toolchain, /MD runtime,
/utf-8 /bigobj /EHsc, NOMINMAX etc., 16 MB stack, TZPL_PLUGIN_TOOLCHAIN_DIR
and TZPL_BUNDLE_TOOLCHAIN_DIR cache vars, a dist target that zips via
packaging/make_dist_win.ps1); RtAudio with WASAPI + DirectSound; Sleef on
every non-Apple platform (static for the clang-cl host) plus an
ExternalProject build of Sleef with the llvm-mingw plugin toolchain,
installed as lib/libsleef.a + include/sleef.h in the dist; libsndfile via
a CMake config package or a static FetchContent build; oscpack's vendored
win32 sockets; GNU-style -O/-W/-ffast-math options guarded on
CMAKE_CXX_COMPILER_FRONTEND_VARIANT (clang-cl gets /clang:-ffast-math);
tzpl.exe and Tzopilotl.exe install at the distribution root beside
modules/; the headless app tests build with either GUI app.

Sources (all under _WIN32, POSIX paths unchanged): MMCSS "Pro Audio" +
TIME_CRITICAL for worker silos; WASAPI backend selection; rand_s seeding
(no extra library, the header is compiled into plugins); CLI console
setup (binary LF stdout, UTF-8 code page, VT processing for colours) and
an application manifest (UTF-8 active code page, DPI awareness);
steady_clock for the GC deadline clock; _create_locale/_strtod_l; a
linenoise stand-in over fgets; Winsock in the OSC server; the app's print
capture on _pipe + PeekNamedPipe and its 8 MB evaluation thread on
_beginthreadex; sleep_for instead of usleep.

shared/tzpl_abi_layout_check.c prints sizeof/alignof of every ABI struct
so the clang-cl host and mingw plugin builds can be diffed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
- packaging/make_toolchain_subset.ps1 trims llvm-mingw
  (llvm-mingw-20260826-ucrt-x86_64, LLVM 23.1) to what plugin compilation
  needs and self-checks by building a DLL with PATH cleared;
  packaging/make_dist_win.ps1 assembles Tzopilotl-<ver>-win64.zip from the
  dist stage: bundled toolchain/, app-local VC++ runtime DLLs, an import
  check mirroring the DMG script's otool check. Unsigned (no certificate).
- .github/workflows/tests.yml: windows-core job (clang-cl + Ninja on
  windows-latest, cached llvm-mingw, ABI layout diff between the two
  compilers, lang suite under Git Bash, synthdef --test, doc/config tests,
  JUCE self-test), continue-on-error until it has a green streak.
- docs/WINDOWS.md (requirements, build, tests, audio backends, runtime
  plugin compilation, distribution, known gaps), dev/windows/README.md
  (first-build checklist), and platform statements in README, CLAUDE.md
  files, the site pages (which still claimed macOS only), Getting Started,
  the shortcut table (Ctrl for Cmd), packaging/README.txt,
  THIRD_PARTY_NOTICES.md, NEWS.md, and the release skill.
- toolchainCommand prefers llvm-mingw's target-prefixed driver.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
Syntax-checking all non-JUCE sources for x86_64-w64-windows-gnu (real
Windows/UCRT headers) and building a generated plugin into a DLL found:

- synthdef_hash.hpp: two constrained hash_combine overloads became
  functionally identical under LLP64 (size_t == uint64_t == unsigned long
  long) and Clang rejected the redefinition. One std::integral template
  replaces the per-type set.
- tzpl_random.hpp (both copies): _CRT_RAND_S must precede the first
  <stdlib.h> in the translation unit, which generated plugin code (starting
  with tzpl_plugin_abi.h) cannot guarantee; rand_s is declared directly.
- tzpl_client_interface.cpp: std::filesystem::path::c_str() is wide on
  Windows; the loader now passes UTF-8 to loadOneDef.
- plugin_tags.cpp: fnmatch(3) does not exist on Windows; a small glob
  matcher (checked against fnmatch on a test matrix) replaces it, and the
  store's default location goes through tzpl::userConfigDir.

Sleef 3.7 cross-builds for the mingw target, and the resulting plugin DLL
exports load/tzpl_abi_version and imports only KERNEL32 and the UCRT
api-sets. dev/windows/README.md records the procedure and what remains
for a real Windows machine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
…tent)

Its cmake_minimum_required(3.1.2) is rejected by CMake 4.x, which the
windows-latest runner has; CMAKE_POLICY_VERSION_MINIMUM=3.5 scoped to the
sub-build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
With the MSVC toolchain the header pulls in headers that open namespace
std; nested inside simd_compat that became simd_compat::std and shadowed
::std for every std:: call in the SIMD wrappers (first clang-cl build
error on the windows-core job).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
CMake already embeds a default manifest for MSVC-style links; the .rc
copy produced a duplicate RT_MANIFEST resource at link time (windows-core
run 3). Listing the .manifest as a source merges it instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
open_memstream and setenv are POSIX-only (windows-core run 4 stopped on
them at step 394/404, after every library and the tzpl/engine/compiler
executables had linked). TestPrintCapture wraps a memstream on POSIX and a
delete-on-close temp file on Windows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
juce_add_gui_app links with /SUBSYSTEM:WINDOWS, so the CRT wants WinMain
(windows-core run 5: the one remaining link error at step 403/404). It
attaches the parent console when stdio was not redirected, converts the
wide command line to UTF-8, and forwards to the shared entry point.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
long double is 16 bytes under mingw and 8 under MSVC; it was an
informational row, not an ABI type, but it failed the comparison (run 6,
the first run in which the whole tree built on Windows).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
Run 7 built everything and passed the ABI check; every golden test then
failed with exit code 126 (cannot execute) and the runner discards stderr.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
Run 8: tzpl.exe would not start ('side-by-side configuration is
incorrect'). The fragment CMake merges into its default manifest now
carries only the activeCodePage and dpiAwareness settings, the form
Microsoft documents; the diagnostic step dumps the embedded manifest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
First Windows golden run (run 9): 437 of 475 pass. The errors/ goldens
compare stderr with the repo root stripped; the interpreter printed native
paths (backslashes, drive letter) where bash knows /d/a/.... Display paths
are generic_string() now and the runner normalizes/strips the native root
on Windows. The suite runs --verbose in CI until the remaining diffs are
understood.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
- parseInt: writeOptionIntResult(vm, dst, parseIntStrict(..., v), v) read
  v in the same argument list that filled it. Argument evaluation order is
  unspecified; Clang goes left-to-right on Itanium targets and right-to-
  left on the MSVC ABI, so every parseInt (and every `try` test built on
  it) returned garbage on Windows. Sequenced into two statements.
- formatFloat: every NaN prints as "nan". to_chars spells the sign and
  MSVC's adds "(ind)", and whether 0.0/0.0 sets the sign bit differs
  between x86 and ARM, so output was platform-dependent.
- Integer / and %: defined results for x/0, x%0 and INT64_MIN/-1 (the
  ARM64 hardware results the language has always shipped: 0, x, wrap)
  instead of C undefined behaviour, which traps on x86 (the
  qa/bug_int_div_zero test crashed on Windows and would on x86 Linux).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
Run 11: 474 of 475 golden tests pass on Windows; the last one is the
complex exp() libm difference that Linux already overrides, same output.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
Run 12: all 475 golden tests pass on Windows; the plugin pipeline then
compiled but failed to link (undefined __declspec(dllimport)
Sleef_powf4_u10): Sleef's header declares dllimport on Windows unless
SLEEF_STATIC_LIBS is defined, and both sides link it statically. Verified
by linking bank_voicer (which calls Sleef pow) with the cross toolchain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
windows-core is green: full clang-cl build, ABI layout diff, 475/475
golden tests, synthdef-compiler --test through the llvm-mingw plugin
pipeline, doc/config tests, JUCE self-test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
Builds the llvm-mingw subset with packaging/make_toolchain_subset.ps1,
runs the dist target (packaging/make_dist_win.ps1), and uploads
Tzopilotl-<version>-win64.zip as an artifact (14-day retention) after the
test steps, so testers can try audio on real hardware.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
…toolchain

The first CI run of make_toolchain_subset.ps1 hit a PowerShell parse error
(a bare comma is the array separator) and the step did not check its
exit code, so the zip shipped without toolchain/. The dist script now
errors when a toolchain dir is configured but missing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
No lld.exe (only ld.lld.exe); the drivers are launchers for clang-<N>.exe;
copy only the DLLs clang and lld import instead of all of bin\ (which
holds lldb, python, OpenMP and ASan libraries nothing here needs).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
…lvm-mingw

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
They select libc++, compiler-rt, libunwind and lld; without them the
trimmed driver searched for libstdc++ headers and the self-check's probe
could not find <cmath>.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpZrs6vKijjsnpWRKtBJ6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant