v0.3.19 - Frame Generation is refused on a Radeon instead of crashing the worker
v0.3.19
Sensitive to flashing images? See the note in v0.3.13 - it still applies.
Frame Generation is refused on a Radeon, instead of crashing the worker
A reporter on a 9070 XT turned the FG switch on and the worker died 21 ms later:
04:02:01.745 [fg] UI: on, 2x
04:02:01.766 [crash] ACCESS_VIOLATION (0xC0000005) at nvngx.dll + 0xCA05,
tried to read address 0x0, amd active=1 frames=1293
[main] the worker CRASHED (exit code 3221225477)
nvngx.dll is our own worker, so that fault is our dereference of a null pointer - not the runtime declining politely. Frame generation is an NGX feature end to end (NVSDK_NGX_D3D12_*, served by nvngx_dlssg.dll); a Radeon has no NGX, and the path was entered anyway. The switch was offered, unchecked, on the one card where it can only take the session down - overlay, recording and pass with it.
The refusal is decided in one place, FgRequested, the single gate every FG path passes through (PresentFrame and the HDR presenter both ask it). It returns false on a Radeon before it consults NS_FRAMEGEN or the menu switch: a remembered preference cannot outrank a card with no runtime. The guard reads g_radeon_present - the adapter actually picked - not "is the AMD pass live": a pass that failed is exactly when someone starts looking at the other switches.
The log says so when the switch goes on, and the menu draws the row as unavailable with the reason under it instead of offering a live switch. The hotkey refuses the same way.
The probe now reads the one link it never read
Every other surface in the frame had a number before this one did: the captured frame, the conversion's output, the network's surface, and the surface the present copies from. The FSR upscale's output - up_out, between the network and the composite - had none, and that is exactly where a reporter's own bisect pointed.
The bisect: Work Scale to 1:1, no rebuild. The alternation stopped. Measured from his recording rather than taken on his word:
| work buffer | output | frames alternating | |
|---|---|---|---|
| before | 1102x756 | 1698x1164 | 231 of 231 |
| at 1:1 | 1816x1221 | 1816x1221 | 2 of 329 |
That slider moves two things at once, and it is worth saying so plainly: at 1:1 dispatch B is skipped, and the network also runs at a larger size (the work buffer is the source frame). A second reporter's run separates them by sliding in three steps - 0.65, 0.80, 1:1 - and the network's own residual is the same at all three (0.042 / 0.034 / 0.037, measured per frame), while the presented surface swings from 3.5x its anchor to 0.93x. The network is not what moves. What is left between the network and the screen is this one surface.
NS_AMD_PROBE_EACH=1 on a frame that flickers now answers it: the upscale's mean is printed on its own line, per frame, beside the network's and the presented surface's. A value that alternates there puts the fault in the upscale pass; a steady value with an alternating presented surface puts it in the composite. At 1:1 the line is absent, because the surface is.
The probe still runs every 300th frame by default - a readback is a full GPU-to-CPU sync, and this is a diagnosis run, not for playing.
The menu is baked where the user saw it, also when the mode is chosen from the menu
_window_layer is the origin the saved frame's panel is shifted by, and it was written in exactly two places: when the captured window moves, skipped while the menu is open because the user may be dragging the panel; and when the menu closes.
Choosing a window mode from an open menu ran neither. The panel was laid out against the screen and then blitted as if the frame still sat at the previous window's origin - the reporter saw it in his recording as "transparent / incorrectly composited". Measured on the pre-fix code: the baked panel lands 349 px from where he saw it, exactly the window's origin x.
switch_window now writes the geometry it just measured, before the rebuild that consumes it, and clearing it is its own call for the way back to the desktop.
Nothing else changed
The pass itself, the exposure curve, the runtime selection and the open questions are all as they were in v0.3.18.
Suite: all green, on an RTX 5070 Ti. Nothing here was run on a Radeon. Every new check was written to fail on the code as it was: the upscale probe on five separate properties (state, existence guard, spec size, and both log lines), the baked panel on the static write and on the end-to-end sequence, and the FG refusal on ten - the guard's presence, the flag it reads, the include order, the log line, the menu flag, the dead row, the click that must send nothing, and the NGX card that must stay untouched. The behavior is exercised, not only grepped: the menu is built, the row is clicked, and the refusal watcher is run against a real log line.