leak_matrix_logs.zip (21.3 KB)
leak_probe.zip (43.6 KB)
Build: TouchDesigner 2025.33070 (Windows 11 Pro 24H2, 10.0.26100) Hardware: 3× NVIDIA T1000 8 GB, driver 596.86 (R595 U7); also reproduced on 596.72. Nine monitors: eight 2560×1080 on two of the cards (four each), one 1280×1024 on the third (Windows primary). Launch: TouchDesigner.exe -gpubusid 0:97:0:0 project.toe (render GPU = card B); the leaking window is placed on a monitor attached to card A.
Summary
When a Window COMP is repeatedly opened and closed (Open/Close pulses, window created and destroyed each time) on a monitor that is attached to a different GPU than the one TouchDesigner renders on, the process’s WDDM dedicated GPU memory (“GPU Process Memory / Dedicated Usage” performance counter for the TouchDesigner process) grows in steps of exactly one window surface — 11.3 MiB for a 2560×1080 window, 22.6 MiB for a 5120-wide window spanning two monitors — and never comes back while the process lives. With the same window on a monitor attached to the render GPU, nothing is retained. Content does not matter (an animating Noise TOP is enough); the probability per cycle goes up with 16-bit float content, with a panel (container COMP) as the window operator, and with several windows opened in the same frame. In our installation (three cross-adapter windows opened/closed per visitor interaction) this ratcheted at +30–40 MB per interaction and reached the 8 GB card within a day, until we stopped destroying the windows.
Minimal reproduction (attached leak_probe.toe, ~16 kB, headless, driven over OSC; a plain Window COMP showing a Noise TOP does the same)
- Start TD with
-gpubusidpointing at card B. - Window COMP: 2560×1080,
justifyoffsetto alldisplays,winoffsetx/yon a monitor of card A, Borders off, Always On Top on (so the DWM composes it; it is “Composed: Flip” in PresentMon), content = Noise TOP animating at 60 fps. - Loop: pulse
winopen, wait 8 s, pulsewinclose, wait 30 s (lazy release), read the process’s WDDM dedicated usage. Repeat 10×.
Results (10 cycles per row, “unit” = 11.3 MiB = 2560×1080×4 B; retained memory never returns):
| window(s) on card A (cross-adapter) | units retained in 10 cycles (run 1, evening / run 2, daytime) |
|---|---|
| 1 window 2560×1080, Noise TOP (8-bit) | 2 / 10 (run 2: one unit in every cycle, two in the last) |
| 1 window 5120×1080 (two monitors), Noise TOP | 4 (two events of 2 units) |
1 window 2560×1080, Render TOP rgba16float |
7 / 8 |
| 1 window 2560×1080, Noise via a container COMP as window operator | 7 |
| 3 windows 2560×1080 (three monitors), Noise, opened/closed in the same frame | 15 / 28 (2–4 units in every cycle) |
| 3 windows 5120×1080, Render TOP 8-bit, Always On Top | 38 |
| same windows on a monitor of card B (render GPU) | 0–1 (three controls: bare, 16-bit float, 3 wide windows) |
Rows with two numbers were run twice (evening 2026-09-11 and daytime 2026-09-12, identical parameters as logged in the probe’s START line, identical placement as logged by /probe/where); the rate clearly depends on machine state we did not control (a bare window leaked in 2 of 10 cycles in the evening and in 10 of 10 the next day), while the block size was always a whole number of window surfaces and the card-B controls were always 0–1. Single-number rows were run once. The same class of ratchet (+30–40 MB per interaction, in multiples of 11.3 MiB) was measured in our production project on three evenings before the probe existed. Driver 596.72 → 596.86 made no difference. Vsync on, SyncInterval 1, no tearing; PresentMon shows “Composed: Flip” for these windows in every variant.
What it is not
- Not the content: a Noise TOP leaks; 16-bit float content or a panel as window operator only raised the per-cycle probability within the same session (evening runs: ~0.2 → ~0.7; in the daytime run the bare window already leaked every cycle).
- Not the amount of pixels per se: the retained block equals the window surface (one unit per monitor covered), the rate depends on the number of windows and the content class.
- Not the driver version (two R595 branches). Not present on the render GPU’s own monitors.
- Per-frame cost is unaffected: PresentMon and the DWM’s GPU-engine counters are identical for panel vs TOP and for cross vs same card (60 fps, ~31 ms to display, same DWM load).
Workaround we use: never destroy the cross-adapter windows; keep them open and hide them behind other windows / stop drawing (drawwindow) instead. With that, the process’s GPU memory is flat over a full day.
Questions: Is a cross-adapter swapchain surface (or its DWM redirection surface) leaked on window destruction in TouchDesigner, or should this go to NVIDIA/Microsoft? Is there anything in the Window COMP teardown we can influence (e.g. closing the window before the last present, or forcing a different present mode) that avoids the retained surface?
Attachments
leak_probe.toe+leak_probe_exec.py+ README (OSC-driven probe;/probe/set,/probe/start N,/probe/whereprints the monitor table and the Win32 rect of each window).- Logs of the runs in the table (
leak_matrix_0/2/3b/5/9/13/14/15/ctl/3bctl/9ctl_2026-09-11.txt,leak_matrix_0/3b/5_2026-09-12.txt): per cycle the WDDM dedicated usage 2 s after close, plus the START line with every parameter and the WHERE lines proving the placement. dxdiag/ monitor topology on request.