Page 1 of 1

Usage of 2D drawing library in a thread.

Posted: Tue Oct 30, 2007 8:55 am
by Hugo
Hi,

I have a problem in my current project.

The task is to collect and display data in realtime. The collection and display of the data must be not interruptable when the user interacts with the program. So I can not do this in the main loop but in a separate thread.

To display the data I must use a very big image (and an ImageGadget in a ScrollAreaGadget).

So far so good. The strange behaviour occurs when I move one of the sliders rapidly to simulate the user interaction. In some cases (sometimes after 5 sec., sometimes after 50 sec.) it crashes and the debugger identifies the StartDrawing and shows the error message: The specified output in NULL (0 value).

Here is the code to reproduce it:

Code: Select all

EnableExplicit

#WindowMain   = 0 
#StatusBar    = 0 
#Image1       = 3 

#ScrollGadget = 1 
#ImageGadget  = 2 

#ImgXSize     = 5000 
#ImgYSize     = 5000 


Procedure  MainThreadPr( dummy.l ) 
  
  Repeat 
    
    If IsImage( #Image1 ) 
      If StartDrawing( ImageOutput( #Image1 ) )  
        LineXY( 100, 100, 800, 100, Random( $FFFFFF ) ) 
        StopDrawing() 
        SetGadgetState( #ImageGadget, ImageID( #Image1 ) ) 
      EndIf
    EndIf 
    
    Delay( 20 ) 
    
  ForEver 

EndProcedure 

Procedure  MainPR() 
  
  If OpenWindow( #WindowMain, 50, 50, 1000, 700, "Test" ) 
  
    If CreateStatusBar( #StatusBar, WindowID( #WindowMain ) ) 
  
      If CreateImage( #Image1, #ImgXSize, #ImgYSize, 16 )
      
        If StartDrawing( ImageOutput( #Image1 ) )  
        
          Box( 0, 0, #ImgXSize, #ImgYSize, $C0C0C0 )  
          StopDrawing()    
          
          If CreateGadgetList( WindowID( #WindowMain ) ) 
          
            ScrollAreaGadget( #ScrollGadget, 0, 27, WindowWidth( #WindowMain ), WindowHeight( #WindowMain )-50 , #ImgXSize, #ImgYSize, 10, #PB_ScrollArea_Single ) 
            ImageGadget( #ImageGadget, 0, 0, #ImgXSize, #ImgYSize, ImageID( #Image1 ) ) 
            
            CreateThread( @MainThreadPr(), 0 ) 
              
            Repeat 
            Until WaitWindowEvent() = #PB_Event_CloseWindow 
            
          EndIf
        EndIf 
      EndIf
    EndIf
  EndIf
  
EndProcedure 


MainPR() 
And yes, I set the 'Threadsave' switch to on.

The question for me now is:
Is the 2D drawing library really threadsave and if not, what can I do to perform this task?

Thanks

Posted: Tue Oct 30, 2007 11:38 am
by srod
Any thread that interefers with the main GUI (in your case through SetGadgetState() ) is asking for trouble! :wink:

Removing the delay seems to work on my machine or indeed purging the message queue :

Code: Select all

EnableExplicit 

#WindowMain   = 0 
#StatusBar    = 0 
#Image1       = 3 

#ScrollGadget = 1 
#ImageGadget  = 2 

#ImgXSize     = 5000 
#ImgYSize     = 5000 


Procedure  MainThreadPr( dummy.l ) 
  
  Repeat 
    While WindowEvent()
    Wend 
    
    If IsImage( #Image1 ) 
      If StartDrawing( ImageOutput( #Image1 ) )  
        LineXY( 100, 100, 800, 100, Random( $FFFFFF ) ) 
        StopDrawing() 
        SetGadgetState( #ImageGadget, ImageID( #Image1 ) ) 
      EndIf 
    EndIf 
    
    
  ForEver 

EndProcedure 

Procedure  MainPR() 
  
  If OpenWindow( #WindowMain, 50, 50, 1000, 700, "Test" ) 
  
    If CreateStatusBar( #StatusBar, WindowID( #WindowMain ) ) 
  
      If CreateImage( #Image1, #ImgXSize, #ImgYSize, 16 ) 
      
        If StartDrawing( ImageOutput( #Image1 ) )  
        
          Box( 0, 0, #ImgXSize, #ImgYSize, $C0C0C0 )  
          StopDrawing()    
          
          If CreateGadgetList( WindowID( #WindowMain ) ) 
          
            ScrollAreaGadget( #ScrollGadget, 0, 27, WindowWidth( #WindowMain ), WindowHeight( #WindowMain )-50 , #ImgXSize, #ImgYSize, 10, #PB_ScrollArea_Single ) 
            ImageGadget( #ImageGadget, 0, 0, #ImgXSize, #ImgYSize, ImageID( #Image1 ) ) 
            
            Define thread = CreateThread( @MainThreadPr(), 0 ) 
              
           Repeat 
           Until WaitWindowEvent() = #PB_Event_CloseWindow 
           KillThread(thread)  
          EndIf 
        EndIf 
      EndIf 
    EndIf 
  EndIf 
  
EndProcedure 


MainPR() 
Actually, purging the message queue is perhaps not such a good idea! :)

Posted: Tue Oct 30, 2007 12:19 pm
by Hugo
Yes, I agree that threading is always a problem. But I thought that PureBasic with the 'Threadsave' option should avoid such problems.

On my system the removing off the delay does not solve the problem. When I test it, sometimes it runs for minutes and it looks ok. But after a while it crashes again.

Do you have any idea/method what I can do to solve the problem?

Posted: Tue Oct 30, 2007 12:51 pm
by srod
Well the threadsafe switch does not, in itself, guarantee a threadsafe application. There is a lot which a programmer can do to destroy a multi-threaded application with or without the switch etc.

I must admit this is a curious problem because I would have thought that the SetGadgetState() would be the cause of the problems, but it is not. As to why the StartDrawing() command should mess up...?

Try the following in which I substitute StartDrawing() etc. for its API counterpart :

Code: Select all

;EnableExplicit 

#WindowMain   = 0 
#StatusBar    = 0 
#Image1       = 3 

#ScrollGadget = 1 
#ImageGadget  = 2 

#ImgXSize     = 5000 
#ImgYSize     = 5000 


Procedure  MainThreadPr( dummy.l ) 
  hdcWin = GetDC_(WindowID(#WindowMain))
  If hdcWin
    hdc = CreateCompatibleDC_(hdcWin)
    If hdc = 0
      ReleaseDC_(WindowID(#WindowMain), hdcWin)
      ProcedureReturn
    EndIf
  Else
    ProcedureReturn
  EndIf

  Repeat 
    Delay(20)
        
    If IsImage( #Image1 ) 
      oldBmap = SelectObject_(hdc, ImageID(#Image1))
      pen = CreatePen_(#PS_SOLID, 1, Random($FFFFFF))
      oldpen = SelectObject_(hdc, pen)
        MoveToEx_(hdc, 100,100,0)
        LineTo_(hdc, 800,100)       
      SelectObject_(hdc, oldpen)
      DeleteObject_(pen)
      SelectObject_(hdc, oldBmap)
      SetGadgetState( #ImageGadget, ImageID( #Image1 ) ) 
    EndIf 
   
  ForEver 

EndProcedure 

Procedure  MainPR() 
  
  If OpenWindow( #WindowMain, 50, 50, 1000, 700, "Test" ) 
  
    If CreateStatusBar( #StatusBar, WindowID( #WindowMain ) ) 
  
      If CreateImage( #Image1, #ImgXSize, #ImgYSize, 16 ) 
      
        If StartDrawing( ImageOutput( #Image1 ) )  
        
          Box( 0, 0, #ImgXSize, #ImgYSize, $C0C0C0 )  
          StopDrawing()    
          
          If CreateGadgetList( WindowID( #WindowMain ) ) 
          
            ScrollAreaGadget( #ScrollGadget, 0, 27, WindowWidth( #WindowMain ), WindowHeight( #WindowMain )-50 , #ImgXSize, #ImgYSize, 10, #PB_ScrollArea_Single ) 
            ImageGadget( #ImageGadget, 0, 0, #ImgXSize, #ImgYSize, ImageID( #Image1 ) ) 
            
            Define thread = CreateThread( @MainThreadPr(), 0 ) 
              
           Repeat 
           Until WaitWindowEvent() = #PB_Event_CloseWindow 
           KillThread(thread)  
          EndIf 
        EndIf 
      EndIf 
    EndIf 
  EndIf 
  
EndProcedure 


MainPR() 

Posted: Tue Oct 30, 2007 1:07 pm
by Dare
Just a thought.

Instead of messing with gadgets from within a thread can you set a flag and/or information variables and then have the main process deal with gadgets using this information.

If this is difficult because the main loop is not triggered then perhaps have a timer callback to check the variables regularly?

Posted: Tue Oct 30, 2007 1:14 pm
by srod
Yes, personally I avoid any kind of GUI stuff within threads where possible.

However, I would have thought that using the 2d drawing library would be okay within a thread as long as no GUI messages are generated etc. Indeed I have programs which draw away quite happily within a thread - admitedly uisng GDI directly rather than the PB drawing library.

In this case then, I am wondering if Hugo has unearthed a bug?

Thinking about it I recall serious problems with egrid when creating a threadsafe version of the library. The problems were due to using PB's drawing library. On switching for native api all the problems disappeared! It was too long ago now for me to be more specific however.

Posted: Tue Oct 30, 2007 1:38 pm
by tinman
Why don't you check the result of the ImageOutput() call in the thread? If for some reason the output for the image can't be accessed (either because the threadsafe option prevents this when there's multiple accesses, or a bug, or because it doesn;t work from a thread, or whatever) then at least your thread will continue until it tries again.

Code: Select all

imgout.l = ImageOutput(#Image1)
If imgout
    If StartDrawing(imgout)
      LineXY( 100, 100, 800, 100, Random( $FFFFFF ) )
      StopDrawing()
      ; Do somethign to update the UI here
    EndIf
EndIf

Posted: Tue Oct 30, 2007 1:52 pm
by srod
Now why the friggin hell didn't I think of that?

DOH!

:)

Still, as you say, this could be masking a bug with threads and drawing etc.

Posted: Tue Oct 30, 2007 1:55 pm
by freak
The 2DDrawing library should be fully threadsafe. Each thread has its own drawing output and state.
The cause for this error is OS related, but it is not really a big problem. You just have to ignore the debugger error ;)

Here is the deal...
To draw on an image or to draw an image onto something else, it has to be
selected into a device context (DC) with the SelectObject() API.
The problem here is that Windows allows an image to be selected into only
one DC at a time. A second SelectObject() on the same image will fail.
In short: You cannot use the same image in 2 drawing operations at once (in 2 threads)

The problem now is that the window redrawing as a result of the scrolling is
also a drawing operation which selects the image into a DC (for a short time)
If at exactly this time, the thread calls ImageOutput(), it will fail because
the image cannot be selected.

As i said, this is no big deal though, because:
- ImageOutput() can handle this case and returns 0
- StartDrawing() can handle a 0 case as well and also returns 0
- You correctly check the StartDrawing() result and do not draw if it returns 0 (without this If check, there would be trouble of course)

So all code actually handles this case just fine (one drawing operation is simply skipped).
Its just the debugger that warns because a drawing output of 0 usually only happens as the result of a programming error under "normal" circumstances.
What you need to do is simply disable the debugger for this line (enclose in DisableDebugger/EnableDebugger)
and you should be fine.

Posted: Tue Oct 30, 2007 2:06 pm
by srod
Ah, yes I didn't think of the scrolling causing redrawing of the image in the main thread. Makes perfect sense.

Also of course with the api code I posted above, Windows will, in the case that the main image is already selected into another dc, just continue to draw on the default image (of dimensions 1 by 1 pixel) in the compatible dc and will not complain.

Excellent explanation of a quite subtle point. I'll have to remember that. :)

Posted: Wed Oct 31, 2007 7:26 am
by Hugo
Thank you for the great explanation. Now I know how to generate a usable workaround.