Hi,
in an earlier issue (#41) I pointed out that a try/finally block was missing in the Rasterize
procedure of Img32.Draw.pas
, because there are situations where the rounding mode (altered with SetRoundMode
) is not reset properly.
Even with the fix apply, I kept seeing incorrect rounding modes, in particular in background threads, so I did a bit more research.
TL;DR: SetRoundMode
should not be used in a multi-threaded application, because it is not thread-safe at all.
The answer to this SO question by David Heffernan is a good starting point to understand the issue: https://stackoverflow.com/questions/39684161/why-an-application-starts-with-fpu-control-word-different-than-default8087cw
(The SO question deals with floating point exceptions, but these are also controlled by bits/flags in the 8087 control word, just like the current rounding mode.)
In a nutshell, using SetRoundMode
(or more generally, any of Delphi's function to change the FPU flags, like Set8087CW
) will store the current state in Default8087CW
, which is a global variable in System.pas
. And which is also used to initialize the FPU for any new thread being started (in _FpuInit
in System.pas
).
So when you temporarily change the rounding mode to rmDown
via SetRoundMode
, and a thread is started before you change it back to rmNearest
, the thread will be initialized and run in rmDown
mode. Here is a small console application that demostrates the problem: https://github.com/tweibert/RoundModeNotThreadSafe
Img32.Draw.pas
does not use any threading, obviously, but it could be the case that there is an existing background thread that spawns another background thread, while the main thread is in the middle of executing the Rasterize
function. Then the second background thread would suffer from this effect.
Now what is the solution? The bottom line is, the Delphi version of SetRoundMode
should not be used. Instead, one could either:
or
- Do not use
Round()
at all but rather Trunc()
. I saw comments in the code saying that Trunc()
was rather slow, and also some (commented out) more efficient __Trunc()
function that was used in earlier versions of Image32. As a quick fix, I re-enabled that function now, and changed all calls from Round()
to __Trunc()
in Img32.Draw.pas
.
Thanks for looking into this.
Torben
EDIT: Regarding the former possible solution (thread-safe version of SetRoundMode
), the following code could be used:
procedure Set8087CWThreadSafe(ANewCW: Word);
var
L8087CW: Word;
asm
mov L8087CW, ANewCW
fnclex
fldcw L8087CW
end;
function SetRoundMode(const RoundMode: TRoundingMode): TRoundingMode;
var
CtlWord: Word;
begin
CtlWord := Get8087CW;
Set8087CWThreadSafe((CtlWord and $F3FF) or (Ord(RoundMode) shl 10));
Result := TFPURoundingMode((CtlWord shr 10) and 3);
end;
This is for 32-bit only. For x64, Delphi uses the MMX extensions instead. And other platforms? No idea, honestly.