[Lazarus] Hi-DPI tweak of components

Ondrej Pokorny lazarus at kluug.net
Thu Jun 8 18:40:09 CEST 2023


And because it is so much fun, I wrote a completely general program to 
test it.

You can define how many resolution combinations you want to go through 
and what maximum value you want to start with and it goes through all 
possible combinations and gives you results after how many cycles a 
start-end pair has settled down and what was the maximum difference.

E.g. for the most typical use-case of 2 different resolutions and max 
scaling of 200% and max starting value of 2000, the results are:

-- Input --
Possible resolutions: 100%, 125%, 150%, 175%, 200%
UniqueCombinations: True
ResolutionSteps: 2
MaxValue: 2000
-- Results --
Count: 40020
Error count after the cycle #1: 5615 (14,03%)
No errors after cycle #2
Maximal error value (difference between start and end value): 1 (5,88% 
from value 17 to 18 for combination 10)

So not really bad, after the 1st cycle the value always settles down and 
the maximal error is 1px, which is ~6% of the start value.

---------

For 3 different resolutions (100%, 125%, 200%), the maximum error value 
is 2px after max 10 resolution changes:

-- Input --
Possible resolutions: 100%, 125%, 200%
UniqueCombinations: False
ResolutionSteps: 10
MaxValue: 500
-- Results --
Count: 29583048
Error count after the cycle #1: 6805918 (23,01%)
No errors after cycle #2
Maximal error value (difference between start and end value): 2 (9,52% 
from value 21 to 19 for combination 2000000001)

----------

But even after 8 resolution steps (using 5 different resolutions), the 
error is not that bad:

-- Input --
Possible resolutions: 100%, 125%, 150%, 175%, 200%
UniqueCombinations: False
ResolutionSteps: 8
MaxValue: 500
-- Results --
Count: 195702624
Error count after the cycle #1: 82854224 (42,34%)
Error count after the cycle #2: 9545914 (4,88%)
Error count after the cycle #3: 184165 (0,09%)
No errors after cycle #4
Maximal error value (difference between start and end value): 5 (26,32% 
from value 19 to 14 for combination 32401201)

---------

To sum it up, for 3 and less monitor DPI resolutions, the maximum error 
is 2px, which is marginal and it is there only for the very small value 
of 19px.

When the number of different resolutions increases (in the above example 
5 different resolutions), the error gets higher, but still is acceptable 
and the higher the starting value, the lower the rounding error. And 
honestly, who has 5 monitors and every one with a different resolution 
and moves a window between all of them, so that it bothers him?

My conclusion is that it really is not worth it to introduce a 
floating-point precision sizes within the LCL. Yes, there would be some 
gain in scaling precision but only for a lot of different DPI values 
(more than 3) and the gain would not be striking. On the other hand, the 
effort needed would be very high, especially due to forwards 
compatibility of the LFM format.

Ondrej

Program code:

program TestScaling;
uses Math, SysUtils;
const
   //DefResolutions: array[0..2] of Double = (1.00, 1.25, 2.00);
   DefResolutions: array[0..4] of Double = (1.00, 1.25, 1.50, 1.75, 2.00);
   //DefResolutions: array[0..8] of Double = (1.00, 1.25, 1.50, 1.75, 
2.00, 2.50, 3.00, 3.50, 4.00); // test even more resolutions - up to 400%
   MaxDiffFromValue = 16; // do not test values smaller than this for 
MaxDiff*
function Scale(const aValue: Integer; const aFromResolution, 
aToResolution: Double): Integer;
begin
   Result := Round(aValue / aFromResolution * aToResolution);
end;
function ScaleCycle(const V: Integer; R: TArray<Integer>): Integer;
var
   I: Integer;
begin
   Result := V;
   for I := 0 to High(R)-1 do
     Result := Scale(Result, DefResolutions[R[I]], DefResolutions[R[I+1]]);
   Result := Scale(Result, DefResolutions[R[High(R)]], 
DefResolutions[R[0]]);
end;
function NextCombination(var R: TArray<Integer>): Boolean;
var
   I: Integer;
begin
   for I := High(R) downto 0 do
   begin
     if R[I]<High(DefResolutions) then
     begin
       Inc(R[I]);
       Exit(True);
     end;
     R[I] := 0;
   end;
   Result := False;
end;
function CombinationIsUnique(const R: TArray<Integer>): Boolean;
var
   I, L: Integer;
begin
   for I := 0 to High(R) do
     for L := I+1 to High(R) do
       if R[I]=R[L] then
         Exit(False);
   Result := True;
end;
function WriteCombination(const R: TArray<Integer>): string;
var
   I: Integer;
begin
   Result := '';
   for I := 0 to High(R) do
     Result := Result + IntToStr(R[I]);
end;
function WriteResolutions(const R: array of Double): string;
var
   I: Integer;
begin
   Result := '';
   for I := 0 to High(R) do
   begin
     if Result<>'' then
       Result := Result + ', ';
     Result := Result + IntToStr(Round(R[I]*100))+'%';
   end;
end;
var
   Resolutions: array of Integer;
   I, F, RefValue, Cycle, ResolutionSteps, MaxValue, StartValue, 
MaxDiff, MaxDiffStartValue, MaxDiffEndValue: Integer;
   Count: Int64;
   MaxDiffRatio: Double;
   Errors: array of Int64;
   MaxDiffCombination: string;
   UniqueCombinationsC: Char;
   UniqueCombinations: Boolean;
begin
   try
     Write('Do you want to test only unique combinations? (y/n): ');
     ReadLn(UniqueCombinationsC);
     UniqueCombinations := LowerCase(UniqueCombinationsC)='y';
     if UniqueCombinations then
       Write(Format('Input how many resolutions do you want to cycle 
through (min = 2, max = %d): ', [Length(DefResolutions)]))
     else
       Write('Input how many resolutions do you want to cycle through 
(min = 2): ');
     ReadLn(ResolutionSteps);
     if ResolutionSteps<2 then
       raise EInOutError.Create('Invalid resolution count');
     if UniqueCombinations and (ResolutionSteps>Length(DefResolutions)) then
       raise EInOutError.CreateFmt('Invalid resolution count (in case of 
unique combintations, the resolution count must be max "%d"', 
[Length(DefResolutions)]);

     Write('Input the maximum value you want to test (e.g. 1000): ');
     ReadLn(MaxValue);
     if MaxValue<1 then
       raise EInOutError.Create('Invalid maximum value');
     Resolutions := nil;
     SetLength(Resolutions, ResolutionSteps);
     for I := 0 to High(Resolutions) do
       Resolutions[I] := 0;

     Count := 0;
     MaxDiff := 0;
     MaxDiffRatio := 0;
     MaxDiffStartValue := 0;
     MaxDiffEndValue := 0;
     MaxDiffCombination := '';
     Errors := nil;
     while NextCombination(Resolutions) do
     begin
       if UniqueCombinations and not CombinationIsUnique(Resolutions) then
         continue;
       Write('Combination: ');
       Writeln(WriteCombination(Resolutions));
       for I := 0 to MaxValue do
       begin
         Inc(Count);
         StartValue := I;
         RefValue := I;
         F := ScaleCycle(RefValue, Resolutions);
         Cycle := 0;
         while not SameValue(F, RefValue) do
         begin
           // compute MaxDiff - but only for values above MaxDiffFromValue
           if (StartValue>=MaxDiffFromValue)
           and (MaxDiff<Abs(F-StartValue)) then
           begin
             MaxDiff := Abs(F-StartValue);
             MaxDiffRatio := Abs(F-StartValue) / StartValue;
             MaxDiffStartValue := StartValue;
             MaxDiffEndValue := F;
             MaxDiffCombination := WriteCombination(Resolutions);
           end;
           RefValue := F;
           if High(Errors)<Cycle then
           begin
             SetLength(Errors, Cycle+1);
             Errors[Cycle] := 0;
           end;
           Inc(Errors[Cycle]);
           Inc(Cycle);
           F := ScaleCycle(F, Resolutions);
         end;
       end;
     end;

     Writeln('end.');
     Writeln;
     Writeln('-- Input --');
     Writeln('Possible resolutions: ', WriteResolutions(DefResolutions));
     Writeln('UniqueCombinations: ', BoolToStr(UniqueCombinations, True));
     Writeln('ResolutionSteps: ', ResolutionSteps);
     Writeln('MaxValue: ', MaxValue);
     Writeln('-- Results --');
     Writeln('Count: ', Count);
     for Cycle := 0 to High(Errors) do
       Writeln(Format('Error count after the cycle #%d: %d (%.2f%%)', 
[Cycle+1, Errors[Cycle], Errors[Cycle] / Count * 100]));
     Writeln(Format('No errors after cycle #%d', [Length(Errors)+1]));
     Writeln(Format('Maximal error value (difference between start and 
end value): %d (%.2f%% from value %d to %d for combination %s) ', 
[MaxDiff, MaxDiffRatio*100, MaxDiffStartValue, MaxDiffEndValue, 
MaxDiffCombination]));
   except
     on E: Exception do
       Writeln(E.ClassName, ': ', E.Message);
   end;
   ReadLn;
end.



More information about the lazarus mailing list