https://bugs.winehq.org/show_bug.cgi?id=54659
--- Comment #1 from François Gouget fgouget@codeweavers.com --- This is nicely reproducible. The high-level recipe is as follows: * Pick a platform where xvidmode.c relies on XF86VidMode{Get|Set}Gamma() such as a QEmu VM. * Run d3d8:device or d3d9:device enough times. More precisely: - Run d3d8:device or d3d9:device once to get the gamma unmoored. - Create and destroy a thousand swapchains or so (e.g. by running Direct3D tests) so the gamma creeps down to the fatal 0.000300 value. - Run d3d8:device or d3d9:device once more to get the crash.
More details about what happens:
1. Gamma ramp vs. gamma -----------------------
When using the standard QXL emulated GPU, XF86VidModeGetGammaRampSize() returns a zero ramp size (no error) which causes xvidmode.c to use XF86VidMode{Get|Set}Gamma().
2. Swapchains vs gamma ramp ---------------------------
The gamma setting is saved whenever a swapchain is created, and restored when it is destroyed. But struct wined3d_swapchain only accounts for gamma ramps so the RGB gamma values returned by XF86VidModeGetGamma() are first converted to a gamma ramp, and then converted back to gamma values for XF86VidModeSetGamma() when the swapchain is destroyed.
When the gamma is 1.0 this round-trip works flawlessly. But as soon as the gamma is slightly different, rounding errors cause it to creep down with each round-trip:
0024:fixme:xvidmode:GenerateRampFromGamma gamma 0.09960000217 -> r_gamma 10.04016018 0024:fixme:xvidmode:ComputeGammaFromRamp gamma 0.09929743409 <- g_avg 10.07075405 0024:fixme:xvidmode:GenerateRampFromGamma gamma 0.09920000285 -> r_gamma 10.08064461 0024:fixme:xvidmode:ComputeGammaFromRamp gamma 0.09893912077 <- g_avg 10.10722542 ...
3. Direct3D vs. FPU rounding mode ---------------------------------
As said above, as long as the gamma is 1.0 all is fine.
So one way to get the ball rolling is to use SetDeviceGammaRamp() to set it to some other value. But that's not what d3d8:device and d3d9:device do.
Instead they play with the FPU rounding mode: * set_fpu_cw(0xf60); Where 0xc00 in the above mask changes the FPU rounding mode to 'truncate'.
* device_desc.flags = CREATE_DEVICE_FPU_PRESERVE; Which tells Direct3D to use our FPU settings instead of overriding them.
This causes the GenerateRampFromGamma() to generate a gamma ramp where values are off by one: 1111 -> 1110 ... eeee -> eeed. Then the conversion back to a gamma value gives 0.9998839036 instead of 1.0.
4. Unmasking underflow exceptions ---------------------------------
When the gamma is low enough, around 0.000300, generating the gamma ramp will generate underflows. This is usually fine.
However the same 0xf60 FPU test mentioned above also enables underflow exceptions (0x10 is cleared) and tells Direct3D to use that setting as before, thus causing the exception when the conditions are right.