DPC_WATCHDOG_VIOLATION 0x133, RTX 4070 Laptop (610.62/616.92): stuck display interrupt never masked in intrServiceStallList_IMPL — proposed fix

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_LOCKING is 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 0x611EC0 and 0x611C30 be added to the display register set that nvdebugdump collects? 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_THRESHOLD in the published source, but that only moves the threshold at which the message is printed; it does not log the value. If NV_PRINTF in that branch also printed the intr value that was compared, the existing nvlog would answer this question by itself, for every user who hits it.

Questions

  1. Is the absence of any masking on bIntrStuck intentional? If a stuck interrupt is expected to be survivable, what is supposed to break the loop?
  2. Was intrReenableIntrMask() meant to run even when bIntrStuck is already set, given it sits immediately before that check?
  3. Which CTRL_DISP sources 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.
  4. 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.
  5. Can an MPO plane flip leave a window-channel CTRL_DISP sub-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.

Update: sixth bugcheck, and I need to withdraw the MPO lead from my first post.

It happened again on 15 Oct, 6 days 10 hours after the previous one. Same signature as all the others: 0x133 with Arg1 = 1 and the 0x1e00 watchdog period, nvlddmkm, same bucket, engine 2, and the repeating service status is 0x1000 again. Measurements from the new kernel dump:

Crash 3 Crash 4 Crash 5 Crash 6 (new)
Stuck counter, engine 2 2111 850 1703 1156
Repeating status 0x1000 0x1000 0x1000 0x1000
ISR count in the window 149,281 115,003 171,860 76,921
DPC count in the window 1,051 1,056 808 796

The threshold of 1000 was exceeded again, and time spent in DPC exceeds time in ISR by roughly 22x, so the core really is sitting in the DPC rather than being re-entered from the ISR.

Please disregard the MPO question (question 5)

In my first post I floated multi-plane overlay as a possible way the display engine ends up with a latched sub-source, on the reasoning that MPO drives the window channels and the channel-related CTRL_DISP sub-sources are the ones not serviced in this path on this chip.

That lead is dead, and I would rather say so than leave it sitting there. I had disabled MPO on 9 Oct and it stayed disabled continuously — I verified the compositor process was never restarted between then and this crash. In that window the machine produced both a 0x141 engine-timeout event (14 Oct) and this 0x133 (15 Oct). So the failure reproduces with MPO off, and whatever latches the sub-source is not MPO.

Two consequences:

  • The candidate list for the stuck CTRL_DISP sub-source is wide open again. That makes question 3 the important one — which sub-sources are expected to be serviced on AD10x. If most of them are genuinely unhandled on this chip, as it looks from here, then almost anything that can set one of them will produce this failure.
  • My earlier “quiet period” observation was worthless, and I should flag that too: the gap before this crash was six days, and my previous gaps were 13 days, 13 days, 3 hours and 6 hours. With that spread, a few quiet days means nothing at all. I had already said the MPO result was inconclusive; it is now simply wrong.

One new piece of evidence for the loop being continuous

This dump caught the CPU at a fourth distinct point: inside a register-read accessor, on the instruction immediately after the read, reached through a longer call chain than any previous capture.

So across four full dumps the CPU was caught in four different places in the same cycle — inside the engine service routine, inside the re-enable path, in the timing code that measures the service call, and now in a register read. Random captures landing in different parts of the loop is what you would expect if it genuinely runs continuously for the whole 120-second window, and it is hard to explain any other way.

I still cannot read out which sub-source latched. In this capture the register offset is held in a register that a debugger cannot reliably recover for a non-topmost frame, and there is no constant offset in the calling code to read instead — it is computed and passed in. I had a 1-in-5 expectation of catching the value in a surviving stack frame; six crashes in, it has not survived once, so I am treating that route as closed unless you know of a better one.

What would actually move this forward

My standing offer, now with better numbers behind it: this machine produces the failure every few days, with tracing already running and four full kernel dumps retained. If you can supply anything that logs the sub-status value at the moment the stuck interrupt is detected — a debug driver, a registry knob, a regtrace option — I will run it and report the value back. That one number would answer question 3 directly.

As noted in the first post, the cheapest version of this for everyone would be printing the compared intr value in the existing NV_PRINTF on the stuck-interrupt branch. The driver already logs that it is stuck, 145 times over, and the one thing it does not log is what it is stuck on.

Still happy to get an internal bug number for this if it is tracked.