Recent

Author Topic: The Lazarus TImage.Canvas is too slow  (Read 1517 times)

QuinnMartin

  • New Member
  • *
  • Posts: 18
The Lazarus TImage.Canvas is too slow
« on: February 16, 2023, 08:57:34 pm »
I have been trying to write a simple program (below) that draws a sine wave.  However it's slow... it takes 50 milliseconds to draw a single wave, even with optimization changed to -O3.  This is very simple mathematics that should be trivial on a modern CPU.  It's plotting a series of 1200 line segments (MoveTo) to draw the sine wave, but still it should really not take half a second considering there are all kinds of games that draw more complex renders (even outside of OpenGL and DirectX) much more quickly.

I can't accomplish what I want to do because I'm trying to draw 20 of these waves, and that adds up to very sluggish performance when you're trying to watch the results of moving the TTrackBar in realtime.

Any thoughts how I can speed this up?  Or is it just not possible in FreePascal?  I tried BitBlt to buffer clearing the image but it was only 5% faster.  I'm not sure if I'm just writing bad code or if there are other settings I need to look at.


Code: Pascal  [Select][+][-]
  1.  
  2. // NOTES: The code below has been modified to just draw a single wave and is triggered by a TTrackBar that changes the wavelength; it also has some code to time the operation and send it to a TMemo.
  3.  
  4. function TForm1.DrawWave : integer;
  5. var
  6.   xx, yy, f1, verticaloffset, yy1, yy2, centerratio, r1, r2, r3 : real;
  7.   s1 : string;
  8. begin
  9.   Image1.canvas.rectangle(0,0,Image1.Canvas.Width,Image1.Canvas.Height);
  10.  
  11.   r1 := time*24*60*60;
  12.  
  13.   verticaloffset := 300;
  14.  
  15.   f1 := Wave1Frequency.position;
  16.   f1 := f1/10;
  17.   if (f1 > 0) then
  18.     begin
  19.       yy := 300;   // was 0
  20.       while (yy < 301) do  // was 600
  21.         begin
  22.           yy := yy+10;
  23.           xx := 1;
  24.           while (xx < 1200) do
  25.             begin
  26.               xx := xx+1;
  27.               yy1 := (sin((xx/f1)*pi/180) * 100);
  28.               yy2 := (sin(((xx+1)/f1)*pi/180) * 100);
  29.               centerratio := 1 - (abs(yy-verticaloffset))/verticaloffset;
  30.               yy1 := yy1*centerratio;
  31.               yy2 := yy2*centerratio;
  32.               yy1 := yy1+yy;
  33.               yy2 := yy2+yy;
  34.               Image1.Canvas.MoveTo(round(xx),round(yy1));
  35.               Image1.Canvas.LineTo(round(xx+1),round(yy2));
  36.             end;
  37.         end;
  38.     end;
  39.   r2 := time*24*60*60;
  40.   r3 := (r2-r1)*1000;
  41.   str(r3:0:0,s1);   Memo1.Lines.Add('Time '+s1+' ms');
  42. end;        

howardpc

  • Hero Member
  • *****
  • Posts: 4139
Re: The Lazarus TImage.Canvas is too slow
« Reply #1 on: February 16, 2023, 09:45:10 pm »
Try something like this:
Code: Pascal  [Select][+][-]
  1. procedure TForm1.DrawWave;
  2. const
  3.   PiOver180 = Pi/180;
  4. var
  5.   frequency: Single = 1;
  6.   verticalOffset: Integer = 300;
  7.   xx: SizeInt = 1;
  8.   yy: SizeInt = 300;
  9.   yy1, yy2, centerRatio: Single;
  10. begin
  11.   Image1.Canvas.Rectangle(Image1.ClientRect);
  12.   if frequency <= 0 then Exit;
  13.  
  14.   while yy < 301 do
  15.     begin
  16.       Inc(yy, 10);
  17.       while xx < 1200 do
  18.         begin
  19.           Inc(xx);
  20.           yy1 := (Sin((xx/frequency)*PiOver180) * 100);
  21.           yy2 := (sin(((xx+1)/frequency)*PiOver180) * 100);
  22.           centerRatio := 1 - (abs(yy-verticaloffset))/verticaloffset;
  23.           yy1 := yy1*centerRatio + yy;
  24.           yy2 := yy2*centerRatio + yy;
  25.           Image1.Canvas.Line(xx, Trunc(yy1), Succ(xx), Trunc(yy2));
  26.         end;
  27.     end;
  28. end;
  29.  
  30. procedure TForm1.FormCreate(Sender: TObject);
  31. var
  32.   start, elapsed: LongInt;
  33. begin
  34.   start := DateTimeToTimeStamp(Time).Time;
  35.   DrawWave;
  36.   elapsed := DateTimeToTimeStamp(Time).Time - start;
  37.   Caption := Format('Time to draw sine wave = %d ms', [elapsed]);
  38. end;
« Last Edit: February 16, 2023, 11:41:38 pm by howardpc »

QuinnMartin

  • New Member
  • *
  • Posts: 18
Re: The Lazarus TImage.Canvas is too slow
« Reply #2 on: February 17, 2023, 12:14:21 am »
Thanks, I tried out the code but it doesn't address the speed issue... it runs about the same speed.

I do appreciate the suggestions for writing better code.

For whatever reason yy isn't initializing properly the way you have it set up... if I set a breakpoint on the very first line, yy is always some random large number.  It seems like the code is ignoring the value that was set.  I tried changing yy to yyy, same problem.  If I hardcode yyy := 0; on the line before the breakpoint, then the value is set properly.  I don't understand why it is ignoring the value being set in the var section.

howardpc

  • Hero Member
  • *****
  • Posts: 4139
Re: The Lazarus TImage.Canvas is too slow
« Reply #3 on: February 17, 2023, 12:34:52 am »
Perhaps you should show a compilable example of yours which is so slow.

Here the sine drawing code I showed times at 0ms, and yy is initialized correctly.



TRon

  • Hero Member
  • *****
  • Posts: 1030
Re: The Lazarus TImage.Canvas is too slow
« Reply #4 on: February 17, 2023, 12:45:07 am »
@QuinnMartin
If you require full horsepower then you (sh/c)ould make use of a pre-calculated sinus table. Although howardpc' results seem to be satisfactory enough.
« Last Edit: February 17, 2023, 12:46:50 am by TRon »

QuinnMartin

  • New Member
  • *
  • Posts: 18
Re: The Lazarus TImage.Canvas is too slow
« Reply #5 on: February 17, 2023, 01:00:11 am »
Thanks for the suggestions... I made some changes and I managed to speed it up significantly.  I did backtrack to my old code (below) and used a double buffer technique, drawing to a non-visible image (ImgBuf) and then transferred it via BitBlt to the visible Image1. Unfortunately I'm not sure why it started running faster as I got distracted trying to troubleshoot why Image1 wasn't drawing (I had to add Image1.refresh for some reason below or nothing would ever appear).

The code is enormously faster and I am guessing something was trying to run updates on the visible component Image1 whenever anything was drawn to it.  Now Image1 is just used for display and only receives the transfer from the double buffer image and that's all.

This is the faster code:

Code: Pascal  [Select][+][-]
  1. function TForm1.DrawWave : integer;
  2. var
  3.   xx, yy, f1, verticaloffset, yy1, yy2, centerratio, r1, r2, r3 : real;
  4.   s1 : string;
  5. begin
  6.   ImgBuf.Canvas.Rectangle(ImgBlank.ClientRect);
  7.   r1 := time*24*60*60;
  8.   verticaloffset := 200;
  9.  
  10.   f1 := Wave1Frequency.position;
  11.   f1 := f1/10;
  12.   if (f1 > 0) then
  13.     begin
  14.       yy := 0;   // was 0
  15.       while (yy < 400) do  // was 600
  16.         begin
  17.           yy := yy+50;
  18.           xx := 1;
  19.           while (xx < 1200) do
  20.             begin
  21.               xx := xx+1;
  22.               yy1 := (sin((xx/f1)*pi/180) * 100);
  23.               yy2 := (sin(((xx+1)/f1)*pi/180) * 100);
  24.               centerratio := 1 - (abs(yy-verticaloffset))/verticaloffset;
  25.               yy1 := yy1*centerratio;
  26.               yy2 := yy2*centerratio;
  27.               yy1 := yy1+yy;
  28.               yy2 := yy2+yy;
  29.               ImgBuf.Canvas.MoveTo(round(xx),round(yy1));
  30.               ImgBuf.Canvas.LineTo(round(xx+1),round(yy2));
  31.             end;
  32.         end;
  33.     end;
  34.   Image1.refresh;  // This is needed for some reason before anything will draw
  35.   BitBlt(Image1.Canvas.Handle, 0, 0, ImgBuf.Canvas.width, ImgBuf.Canvas.height, ImgBuf.Canvas.Handle, 0, 0, SRCCOPY);
  36.   r2 := time*24*60*60;
  37.   r3 := (r2-r1)*1000;
  38.   str(r3:0:0,s1);   Memo1.Lines.Add('Time '+s1+' ms');
  39.   result := 1;
  40. end;              
« Last Edit: February 17, 2023, 01:02:48 am by QuinnMartin »

speter

  • Sr. Member
  • ****
  • Posts: 305
Re: The Lazarus TImage.Canvas is too slow
« Reply #6 on: February 17, 2023, 01:57:13 am »
QuinnMartin, do you need to draw onto an image?

I am attaching a project which uses a simplified version of your code, and draws onto a (non image) canvas _or_ onto a image canvas...

The code seems quite fast (though your milage may vary).

Note that I made the trackbars have a min value of 1, so you don't need to check f1 > 0.

cheers
S.
I climbed mighty mountains, and saw that they were actually tiny foothills. :)

keeslazarus

  • Newbie
  • Posts: 1
Re: The Lazarus TImage.Canvas is too slow
« Reply #7 on: May 06, 2023, 02:55:45 pm »
I also tried to simplify and speed up the code. The fastest way is to do the drawing in a bitmap and draw the bitmap on the image when finished. Have a look at the code and comments and try it yourself.


Handoko

  • Hero Member
  • *****
  • Posts: 4835
  • My goal: build my own game engine using Lazarus
Re: The Lazarus TImage.Canvas is too slow
« Reply #8 on: May 06, 2023, 03:40:25 pm »
@QuinnMartin

There should be something wrong with your Lazarus installation, settings or OS/driver/system/hardware.

I tested your code with a bit changes but nothing essential, on my Lazarus 2.3.0 GTK2 Linux 64-bit with its default compilation settings on Intel G2020 2 cores processor. It wasn't too slow as you can see on the screenshot.

Code: Pascal  [Select][+][-]
  1. procedure TForm1.Button1Click(Sender: TObject);
  2. var
  3.   xx, yy, f1, verticaloffset, yy1, yy2, centerratio, r1, r2, r3 : real;
  4.   s1 : string;
  5. begin
  6.   Image1.canvas.rectangle(0,0,Image1.Canvas.Width,Image1.Canvas.Height);
  7.  
  8.   r1 := time*24*60*60;
  9.  
  10.   verticaloffset := 300;
  11.  
  12.   Memo1.Clear;
  13.   Image1.Canvas.Pen.Color := clYellow;
  14.  
  15.   f1 := 3; //Wave1Frequency.position;
  16.   f1 := f1/10;
  17.   if (f1 > 0) then
  18.     begin
  19.       yy := 300;   // was 0
  20.       while (yy < 301) do  // was 600
  21.         begin
  22.           yy := yy+10;
  23.           xx := 1;
  24.           while (xx < 1200) do
  25.             begin
  26.               xx := xx+1;
  27.               yy1 := (sin((xx/f1)*pi/180) * 100);
  28.               yy2 := (sin(((xx+1)/f1)*pi/180) * 100);
  29.               centerratio := 1 - (abs(yy-verticaloffset))/verticaloffset;
  30.               yy1 := yy1*centerratio;
  31.               yy2 := yy2*centerratio;
  32.               yy1 := yy1+yy;
  33.               yy2 := yy2+yy;
  34.               Image1.Canvas.MoveTo(round(xx),round(yy1));
  35.               Image1.Canvas.LineTo(round(xx+1),round(yy2));
  36.             end;
  37.         end;
  38.     end;
  39.   r2 := time*24*60*60;
  40.   r3 := (r2-r1)*1000;
  41.   str(r3:0:0,s1);   Memo1.Lines.Add('Time '+s1+' ms');
  42. end;
« Last Edit: May 06, 2023, 03:44:03 pm by Handoko »

Paolo

  • Sr. Member
  • ****
  • Posts: 386
Re: The Lazarus TImage.Canvas is too slow
« Reply #9 on: May 06, 2023, 10:29:59 pm »
For sure it is better having a doublebuffer bitmap.

In any case your code seems not well optimised

why you have the centerratio undpated inside the second while ? it is constant once you compute yy ?
why are you computing two times the value of yy1 ? save the last one for next couple of yy1-yy2.
have you noticed that updating xx:=xx+1 and then using xx and xx+1 you are skipping the copule of indexes 1-2 and start at 2-3 ?

here the simplified version  (Starting with copule 1-2)

Code: Pascal  [Select][+][-]
  1. function TForm1.DrawWave : integer;
  2. var
  3.  xx, yy, f1, verticaloffset, yy1, yy2, centerratio, r1, r2, r3 : real;
  4.  s1 : string;
  5. begin
  6.   Image1.canvas.rectangle(0,0,Image1.Canvas.Width,Image1.Canvas.Height);
  7.   Image1.canvas.Pen.Color:=clred;  //necessario per vedere il grafico se no vedo tutto nero
  8.   r1 := time*24*60*60;
  9.  
  10.   verticaloffset := 300;
  11.  
  12.   f1 := 3; //Wave1Frequency.position;
  13.   f1 := f1/10;
  14.   if (f1 > 0) then
  15.     begin
  16.       yy := 300;   // was 0
  17.       while (yy < 301) do  // was 600
  18.         begin
  19.           yy := yy+10;
  20.           centerratio := 100*(1 - (abs(yy-verticaloffset))/verticaloffset);  //<-- here
  21.           xx := 1;
  22.           yy1 := sin((xx/f1)*pi/180)*CenterRatio+yy;  //<-- first value pre computed outside the main loop
  23.           Image1.Canvas.MoveTo(round(xx),round(yy1));
  24.           while (xx < 1200) do
  25.             begin
  26.               xx := xx+1;
  27.               yy2 := sin((xx/f1)*pi/180)*centerRatio + yy;
  28.               Image1.Canvas.LineTo(round(xx),round(yy2));
  29.               yy1:=yy2;                                                        //<--  save latest value for next iteration
  30.               Image1.Canvas.MoveTo(round(xx),round(yy1));
  31.             end;
  32.         end;
  33.     end;
  34.   r2 := time*24*60*60;
  35.   r3 := (r2-r1)*1000;
  36.   str(r3:0:0,s1);
  37.   Memo1.Lines.Add('Time '+s1+' ms');
  38.  
  39. end;
  40.  

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 10716
  • FPC developer.
Re: The Lazarus TImage.Canvas is too slow
« Reply #10 on: May 06, 2023, 11:25:20 pm »
search for lockwindowupdate()

440bx

  • Hero Member
  • *****
  • Posts: 3412
Re: The Lazarus TImage.Canvas is too slow
« Reply #11 on: May 07, 2023, 01:54:35 am »
From the Windows API, I'd use either Polyline or PolyPolyline, that would allow multiple curves to be drawn in a single call which should speed things up considerably compared to LineTo and MoveTo combinations.

HTH.
FPC v3.0.4 and Lazarus 1.8.2 on Windows 7 SP1 64bit.

circular

  • Hero Member
  • *****
  • Posts: 4001
    • Personal webpage
Re: The Lazarus TImage.Canvas is too slow
« Reply #12 on: May 08, 2023, 12:37:58 pm »
Indeed. In the loop you compute twice each coordinates. Instead you can put them into an array (you know the size from the start) and then feed it into a Polyline call.
Conscience is the debugger of the mind

TRon

  • Hero Member
  • *****
  • Posts: 1030
Re: The Lazarus TImage.Canvas is too slow
« Reply #13 on: May 08, 2023, 02:40:59 pm »
In addition to what is already said: if you know the resolution beforehand then you  can even use pixels, and skip using lines. That combined with double buffering and a pre-calculated sine tab is afaik the fastest way to do it.

 

TinyPortal © 2005-2018