Hi! Five DPC_WATCHDOG_VIOLATION (0x133) bugchecks on a GeForce RTX 4070 Laptop (AD106) under Windows 11 24H2, across two driver versions (610.62 and 616.92, the latter a clean reinstall), always with the laptop in discrete-GPU-only mode. I have three full kernel dumps, and I think I can show why these turn into an unbounded interrupt storm rather than a recoverable display glitch — the relevant logic is in the RM interrupt code you publish in open-gpu-kernel-modules, so this is not guesswork about a closed binary. Below: the code path, what the driver’s own log says during the crash, the measurements, and a suggested change. I have not been able to work out what makes the display engine latch the interrupt in the first place; that part is still open.
The code
src/nvidia/src/kernel/gpu/intr/intr.c, intrServiceStallList_IMPL (line numbers from main at the time of writing; identical in tag 615.78.08):
// 1560
if (stuckIntr[engineIdx].intrCount > pIntr->intrStuckThreshold)
{
NV_PRINTF(LEVEL_ERROR,
"Stuck interrupt detected for mcEngine %u\n",
engineIdx);
bIntrStuck = NV_TRUE;
NV_ASSERT(0);
}
...
// 1583
if (bitVectorTest(pEngines, MC_ENGINE_IDX_DISP) || bitVectorTest(pEngines, MC_ENGINE_IDX_DISP_LOW))
{
intrReenableIntrMask(pGpu, pIntr);
}
// 1588
if (bIntrStuck)
{
NV_PRINTF(LEVEL_ERROR,
"Interrupt is stuck. Bailing after %d iterations.\n",
pIntr->intrStuckThreshold);
return NV_FALSE;
}
bIntrStuck occurs exactly three times in the file: declaration (1506), assignment (1565), and the check above (1588). As far as I can tell there is no code path that masks or disables the interrupt source that was just declared stuck. And intrReenableIntrMask() runs before that check, unconditionally for MC_ENGINE_IDX_DISP / MC_ENGINE_IDX_DISP_LOW.
So when a display interrupt condition cannot be cleared by its service routine, the sequence is: detect → log → assert → re-enable the same interrupt → return NV_FALSE. The interrupt is asserted again immediately and the cycle repeats without bound. “Bailing” only returns from this function; it does not stop the source.
Version history
The unconditional re-enable in this path appears to have been introduced between the 550 and 595 branches:
| Branch | intrReenableIntrMask in intr.c |
DISP/DISP_LOW re-enable in this path | bIntrStuck uses |
|---|---|---|---|
550.163.01 |
absent | absent | 3 (decl / set / check) |
595.104.02 |
1461 | 1607 | 3 |
610.57.04 |
— | 1521 | 3 |
615.78.08 |
1439 | 1585 | 3 |
main |
1439 | 1585 | 3 |
In 550.163.01 I could not find the function or any similarly named one (searched for reenable, re-enable, enableIntr, RestoreIntr), and MC_ENGINE_IDX_DISP does not appear in this function at all. So on that branch a stuck interrupt was left masked after bailing, which is the behaviour I would have expected. The missing mitigation itself (no masking on bIntrStuck) is present in every branch I checked, from 550 through main.
What I observe in the field
Lenovo Legion Pro 7 16IRX8 (82WR), BIOS KWCN54WW, VBIOS 95.6.1e.0.98, internal panel 2560×1600 @ 240 Hz with G-SYNC enabled. Windows 11 24H2 (26100.8117), GeForce RTX 4070 Laptop (AD106), laptop in discrete-GPU-only mode (internal panel driven by the NVIDIA display engine). Five DPC_WATCHDOG_VIOLATION (0x133, Arg1 = 1) bugchecks over four weeks:
| Crash | Driver | Captured | Stuck counter, engine 2 | ISR / DPC count in the window |
|---|---|---|---|---|
| 1 | 610.62 | minidump only | — | — |
| 2 | 616.92 | minidump (after a clean reinstall) | — | — |
| 3 | 616.92 | full kernel dump | 2111 | 149,281 / 1,051 |
| 4 | 616.92 | full kernel dump, 3 h after crash 3 | 850 | 115,003 / 1,056 |
| 5 | 616.92 | full kernel dump, 6 h after crash 4 | 1703 | 171,860 / 808 |
Same bugcheck arguments, same bucket and the same display engine every time, so this is not specific to one driver build. Both affected versions are from the 595-or-later branches, i.e. both contain the unconditional re-enable discussed above. Never once in hybrid mode, including a 79-day uptime there.
From the driver’s own nvlog ring, recovered from the crash dumps, the entire 8 KB print buffer is filled with one repeating triple — 145 times, i.e. the whole ring:
Stuck interrupt detected for mcEngine 2 (MC_ENGINE_IDX_DISP)
Interrupt is stuck. Bailing after 1000 iterations.
The timestamp of the last record matches the moment of the bugcheck. The repeating non-zero service status was 0x1000 in every dump. In two of the three the in-memory stuck counter for engine 2 had run well past the threshold of 1000 — 2111 and 1703 — i.e. the loop kept going for another 700 to 1100 iterations after the driver had already concluded the interrupt was stuck; in the third it stood at 850, caught shortly before the threshold.
One logical CPU is pinned at dispatch level for the full 120 seconds while the rest of the system keeps running; the display freezes and the machine bugchecks about two minutes later.
The three full dumps caught the CPU at three different points of the same loop — once inside the engine service routine, once inside the re-enable path (see below), and once in the timing code that measures how long the service call took. That is what you would expect from a loop running continuously for the whole window, rather than from one call that hangs.
The hang is partly reproducible. Three of the crashes happened at roughly the same point in one game. It is not reliable there — it does not happen on every attempt — but it is enough that I can try on demand instead of waiting days. (I would rather not name the title in a public thread; happy to say privately if it matters.) Practically, this means the machine is available for directed testing: if you want a particular driver build, a registry setting that logs more, or a specific capture attempted, I can try it the same evening rather than waiting for a random crash.
The built-in recovery path does not preempt this. During one hang I pressed Win+Ctrl+Shift+B (the display-driver reset hotkey) while the picture was frozen. Nothing happened: the display stayed frozen and the bugcheck arrived on schedule. It is not that the reset ran and failed — it never started. On this same machine that hotkey normally produces LiveKernelEvent 0x1A8/0x1B8 reports, and on two earlier occasions it did exactly that. For this hang there is no 0x1A8 report, nothing in LiveKernelReports, and no display-related event in the system log for the whole window — only the 0x133 report afterwards. Input was still being processed at the time (the kernel’s idle timer kept resetting for about 43 seconds into the storm), so the keystroke itself was being seen.
That seems worth knowing when deciding what the mitigation should look like: once a core is pinned at dispatch level, the recovery machinery that would normally rescue the display appears to be unreachable. On another affected machine I looked at (same accessor, older driver) the ratio was different — ~50 ISRs against ~80,000–110,000 DPC runs — so the loop can present either way.
0x1000 is the CTRL_DISP bit of the display interrupt summary register 0x611EC0, going by nouveau’s gv100_disp_intr(), which services bits 0–2, 7 and 8 of 0x611C30 for this display generation. Which sub-source latched is something I cannot read from a dump, since the GPU register state is not captured.
I also see five VIDEO_ENGINE_TIMEOUT_DETECTED (0x141) live-dump events over the same period, all in discrete-GPU-only mode and all attributed to the same driver callback.
Where the re-enable actually lands
Following intrReenableIntrMask_IMPL in the published source:
if (pIntr->getProperty(pIntr, PDB_PROP_INTR_USE_INTR_MASK_FOR_LOCKING))
{
oldIrql = rmIntrMaskLockAcquire(pGpu);
intrMaskFlags = intrGetIntrMaskFlags(pIntr);
intrMaskFlags &= ~INTR_MASK_FLAGS_ISR_SKIP_MASK_UPDATE;
intrSetIntrMaskFlags(pIntr, intrMaskFlags);
if (pIntr->getProperty(pIntr, PDB_PROP_INTR_USE_TOP_EN_FOR_VBLANK_HANDLING))
intrSetDisplayInterruptEnable_HAL(pGpu, pIntr, NV_TRUE, NULL);
intrSetIntrMask_HAL(pGpu, pIntr, &pIntr->intrMask.engMaskUnblocked, NULL); // line 1464
intrSetIntrEnInHw_HAL(pGpu, pIntr, intrGetIntrEn(pIntr), NULL);
rmIntrMaskLockRelease(pGpu, oldIrql);
}
On the machine I am looking at, one of the bugchecks was captured with the CPU inside the register write performed by intrSetIntrMask_HAL — i.e. inside intrReenableIntrMask, setting the interrupt-enable bit for the display engine’s vector, immediately after the service routine had failed to clear the condition. So on this chip PDB_PROP_INTR_USE_INTR_MASK_FOR_LOCKING is set and this path is live, not a no-op.
That also points at a clean place to fix it: the mask programmed on line 1464 is pIntr->intrMask.engMaskUnblocked.
Proposed fix
Rather than skipping the re-enable (which would undo whatever the 595-era change was meant to fix), the stuck engine can simply be removed from the set that the re-enable programs. Everything else is restored exactly as today, but a source that was just declared stuck is not re-armed:
if (stuckIntr[engineIdx].intrCount > pIntr->intrStuckThreshold)
{
NV_PRINTF(LEVEL_ERROR,
"Stuck interrupt detected for mcEngine %u\n",
engineIdx);
bIntrStuck = NV_TRUE;
NV_ASSERT(0);
// Do not re-arm a source we have just established we cannot clear.
// intrReenableIntrMask() below programs engMaskUnblocked, so dropping the
// engine here is enough; every other engine is still restored as before.
{
MC_ENGINE_BITVECTOR unblocked;
intrGetIntrMaskUnblocked(pIntr, &unblocked);
bitVectorClr(&unblocked, engineIdx);
intrSetIntrMaskUnblocked(pIntr, &unblocked);
}
}
All three helpers already exist (intrGetIntrMaskUnblocked / intrSetIntrMaskUnblocked at lines 358 / 351, bitVectorClr in nvbitvector.h). This turns an unbounded storm into a degraded display engine, which the OS can then recover from — on Windows a TDR already resets the engine successfully in other situations on this machine.
Known trade-offs, which is why I am asking rather than sending a PR:
- It leaves that engine masked until something restores
engMaskUnblocked. If the intended behaviour is to recover rather than degrade, this is also the natural place to request a GPU/engine reset, but I do not know which helper you would want called. - It only helps where
PDB_PROP_INTR_USE_INTR_MASK_FOR_LOCKINGis set. Where that property is false,intrReenableIntrMask()is a no-op, and if a storm is possible on those chips too, the loop would have to be broken somewhere else. - Masking at engine granularity is coarser than the actual stuck sub-source. If a narrower mask is preferable, that needs knowledge of the display block that I do not have.
To be straight about what I can and cannot contribute: I have no Linux reproduction, so I cannot test a patch to this path in any meaningful way, and I would rather not send untested changes to interrupt handling. What I do have is a machine that reproduces this, with tracing already running and three full kernel dumps on hand. I am glad to run whatever diagnostic you want on it and report back.
Why the service routine cannot clear the condition on this chip
This part comes from looking at the shipped Windows binary with a kernel debugger while analysing my own crash, so treat it as an observation to confirm on your side rather than as authority. I am describing behaviour only; I am not reproducing any code.
The display interrupt service on this build reads the summary register, handles the per-head sources, and when the CTRL_DISP bit is set it reads the sub-status register and hands that value to five pairs of HAL entry points, each pair being “is this yours? → service it”. On this chip (AD106, driver 616.92) only two of those five pairs are populated with real implementations:
| Pair | What it covers |
|---|---|
| 1 | sub-bits 0, 1 and 2 — the modeset supervisor sources |
| 2 | sub-bit 7 — the error source |
| 3, 4, 5 | all three point at the same two no-op stubs: the predicate unconditionally returns false, and the service entry returns immediately |
The final entry point, which is handed the summary value after all of that, is the same no-op stub.
So on this chip every CTRL_DISP sub-source other than 0–2 and 7 is neither serviced, nor acknowledged, nor masked. The driver does contain predicates for other sub-bits elsewhere in the binary — I can see one testing sub-bit 14, and separate code that handles sub-bit 8 (AWAKEN) — but they are not wired into this path on this chip. nouveau services bits 0–2, 7 and 8 of the same register for this display generation, and for sub-bit 3 its comment says it does not know how to acknowledge it and that RM “does not appear to bother” either.
That is consistent with everything above: a sub-source latches, nothing in the path can clear it, the service routine keeps returning the same non-zero summary value, the stuck detector counts past 1000, and then the interrupt is re-enabled anyway.
What I cannot determine is which sub-source latched: the value is only ever held in a stack slot of the service routine, and in all three captured crashes that slot had already been overwritten by the time of the bugcheck. GPU register state is not included in a Windows kernel dump.
I tried nvdebugdump --dumpall as a way to get it. It produces a register snapshot — 343 entries in the graphics range and 191 in the display range, 65 of them in 0x61xxxx — but neither 0x611C30 nor 0x611EC0 is in it, and neither value appears anywhere else in the archive. It does capture close neighbours such as 0x611850, 0x611860 and 0x611A00.
I checked whether that is simply because the machine was healthy when I ran it, and it is not. Every captured address I sampled is present in the driver’s static data, while those two appear only in code, so the captured set looks fixed at build time. I then tested it directly: I triggered a real fault with the display-reset hotkey and captured again. The archive did change — six event records and seven object records were added to error_data.pb, and the nvlog grew by 185 records across 13 new message sites — but not one new value anywhere in the archive falls in the display register range. So running the tool during the hang would not answer this either.
A possible lead I cannot substantiate: multi-plane overlay
I am reporting this for the mechanism, not because I have evidence for it.
Every one of these bugchecks happened with Windows multi-plane overlay active — DWM handing planes straight to the display hardware — and MPO drives the display window channels. If I have the sub-source list right, the channel-related CTRL_DISP sub-sources are precisely the ones not serviced in this path on this chip: AWAKEN (bit 8) and the window semaphore (bit 9) are both outside the serviced set of 0–2 and 7, and bit 8 is one that nouveau does service on this display generation.
The mode correlation points the same way. This machine has never produced a 0x133 in hybrid mode, where the internal panel is driven by the Intel display engine and MPO planes go through that instead — including a 79-day uptime in that mode. All five bugchecks are in discrete-only mode, where the NVIDIA display engine drives the panel.
So I disabled MPO (OverlayTestMode = 5) and tried to reproduce. The result is inconclusive, and I want to be explicit about that: the point in the game where it had happened three times was never reliable, and half an hour of trying with MPO off is not distinguishable from ordinary luck when the baseline interval between crashes runs from three hours to thirteen days. I am not claiming MPO fixed anything, and I would not want this thread to turn into “disable MPO” — even if that did suppress it, it would be a workaround, while the defect is that the storm is unbounded no matter what lights the fuse.
What I would ask instead: if an MPO plane flip can leave a window-channel interrupt pending on AD10x, that would name the stuck sub-source — the one thing I cannot read out myself.
Log sites that fire only during the fault
Having a healthy-system capture to compare against turned out to be useful. The nvlog from a healthy machine covers 225 distinct print sites; during the storm only 24 are active, and exactly five of those do not occur on a healthy system. They are identical in the two dumps I compared:
| File (hash) | Line | Count | Arguments |
|---|---|---|---|
0xA40399 |
1667 | ×145 | 2 — this is “Stuck interrupt detected for mcEngine 2” |
0xA40399 |
1612 | ×146 | none |
0xA40399 |
1614 | ×145 | 1000 — “Interrupt is stuck. Bailing after 1000 iterations.” |
0x9EF0B8 |
842 | ~×250 | 0, an incrementing counter, then a value in 0x48–0x68, then 0x3F |
0x2E4F8C |
8196 | ~×406 | 0 (once 1) |
The last two are the interesting ones: several hundred records each, in both dumps, and absent from a healthy system. I cannot decode them without the message database, but you can. If either of them is emitted from the display interrupt path, it may say more about the stuck source than anything else I have.
Two requests about tooling
- Could
0x611EC0and0x611C30be added to the display register set thatnvdebugdumpcollects? They are the two registers this whole failure mode turns on, and without them the tool cannot diagnose it even when run during the hang. - Failing that, is there a supported way to read them at the moment of the hang — a driver registry setting that logs the stuck value, or a regtrace option? I can see
NV_REG_STR_INTR_STUCK_THRESHOLDin the published source, but that only moves the threshold at which the message is printed; it does not log the value. IfNV_PRINTFin that branch also printed theintrvalue that was compared, the existing nvlog would answer this question by itself, for every user who hits it.
Questions
- Is the absence of any masking on
bIntrStuckintentional? If a stuck interrupt is expected to be survivable, what is supposed to break the loop? - Was
intrReenableIntrMask()meant to run even whenbIntrStuckis already set, given it sits immediately before that check? - Which
CTRL_DISPsources are expected to be serviced on AD10x? On this build it looks like most of them are not, which would explain a status that never clears. - Would the change above be acceptable in principle, and should it degrade (mask and stop) or escalate (mask and request a reset)? Given that the reset hotkey could not even start once the core was pinned, escalation may not be reachable at that point, which would argue for masking at the source first and recovering afterwards.
- Can an MPO plane flip leave a window-channel
CTRL_DISPsub-source (AWAKEN or the window semaphore) pending on AD10x in a way this path would not clear?
Happy to provide more measurements. I am not attaching crash dumps: they are from a personal machine, and a kernel dump contains everything that was in memory at the time. I can share the driver’s own log records, the interrupt counters, or any specific measurement you ask for.
If this looks like a real defect, could I also get an internal bug number for it? I see from other threads that NVIDIA files one per report, and it would help me track whether a later driver addresses it.