[RESOLVED] Drawing using BackGroundWorker, I don't see my where I'm wrong.
I'm using drawing routines that are all called by the .Paint Event of my Control.
The calculation of the objects is done in the Routine "SystemTracks_Simulation", also the positional regions where the objects have been and are now get invalidated. This routine is called once for each Frame.
If I use that routine in the GUI thread, everything works as it should. All the different objects are drawn.
However if I use a BackGroundWorker-thread to call the "SystemTracks_Simulation" the respective objects are most of the time not drawn. Only if I do a ReScale (complete Redraw, i.e. Invalidate without Region) the objects are drawn. If the simulation in running the objects are only shown momentarily (which is the problem), if it isn't running the redraw will show all the objects.
Here are the codelines that call the routine either by the BGW or directly.
Code:
If Not MainForm.BackgroundSystemTracksWorker.IsBusy Then
MainForm.BackgroundSystemTracksWorker.RunWorkerAsync()
Else
debug_schreiben("SystemTracks_Sim ausgefallen! ")
End If
or
Code:
SystemTracks_Simulation()
Re: Drawing using BackGroundWorker, I don't see my where I'm wrong.
I think I know what may be happening as I had this happen a lot during some games that I made. In my case, one thing that would happen is that the drawing would occur before the physics causing terrible ripping and glitches. The solution in my case was a managed game loop. What does the actual drawing look like and are you using GDI+ or something else?
Re: Drawing using BackGroundWorker, I don't see my where I'm wrong.
The (shortened) code that does draw one object (Track)
Code:
Private Sub TrackDrawing(ByRef e As Graphics, ByRef Pen As Pen, ByRef Brush As SolidBrush, ByVal Track As SystemTrack)
'Draw a Systemtrack
Dim j As Integer
Dim SymbolPos As Point
Dim Linksymbol As Bitmap
Dim LinkString As String
Try
SymbolPos = Track.PixelPosition
SymbolPos.X -= 16 'SymbolSize 32*32!!
SymbolPos.Y -= 16
Pen.Width = 2
Pen.LineJoin = Drawing2D.LineJoin.Round
' .......
Linksymbol = CType(Bitmap.FromFile(LinkDirectory & "/" & LinkString), Bitmap)
Dim TheImage As New Bitmap(Linksymbol, 32, 32)
TheImage.MakeTransparent()
Dim ptsArray As Single()() = {New Single() {1, 0, 0, 0, 0}, New Single() {0, 1, 0, 0, 0}, New Single() {0, 0, 1, 0, 0}, New Single() {0, 0, 0, 0.5F, 0}, New Single() {0, 0, 0, 0, 1}}
Dim clrMatrix As New System.Drawing.Imaging.ColorMatrix(ptsArray)
Dim imgAttributes As New System.Drawing.Imaging.ImageAttributes
imgAttributes.SetColorMatrix(clrMatrix, Imaging.ColorMatrixFlag.Default, Imaging.ColorAdjustType.Bitmap)
e.DrawImage(TheImage, New Rectangle(SymbolPos.X, SymbolPos.Y, TheImage.Width, TheImage.Height), 0, 0, TheImage.Width, TheImage.Height, GraphicsUnit.Pixel, imgAttributes)
Pen.Width = 1
Dim Text As String
If Track.TrackNumber = 0 Then
Text = "_" & Track.SystemID
Else
Text = Track.TrackNumber.ToString
End If
Brush.Color = Pen.Color
e.DrawString(Text, Me.Font, Brush, KursLinie(0).X + 11, KursLinie(0).Y + 11)
Catch ex As Exception
ErrLog_schreiben("TrackZeichnen", ex.Message)
End Try
End Sub
My GameLoop looks like that
Code:
Public Sub TimeControll()
Static LastSimTime As Integer = Environment.TickCount
Dim ActualZeit As Integer
Static CycleCounter As Double
Try
ActualZeit = Environment.TickCount
If LastSimTime + SpeedFactor / FPS <= ActualZeit Then
Zeitschritte = 1
If ActualZeit - LastSimTime > SpeedFactor / FPS Then
ZeitSchritte = (ActualZeit - LastSimTime) / (SpeedFactor / FPS) 'If the time is lagging, do a bigger step
End If
LastSimTime = ActualZeit
Playtime = ((DateTime.UtcNow.Hour) * 60 + DateTime.UtcNow.Minute) * 60 + DateTime.UtcNow.Second
Simulation(MainForm, System.EventArgs.Empty)
End If
'Application.DoEvents()
If StartStop = True Then
'as long as StartStop=True these Routine is calling itself again!
ExecuteAfterPause(CInt(SpeedFactor / FPS), New MethodInvoker(AddressOf TimeControl))
End If
Catch ex As Exception
ErrLog_schreiben("Zeitkontrolle", ex.Message, StartStop.ToString, ActualZeit.ToString, Playtime.ToString, Mode.ToString)
End Try
End Sub
The code from post #1 are called in "Simulation((MainForm, System.EventArgs.Empty)".
I guess you do recognise your technique, and yes its the Pauser from JMcIlhinney
Re: Drawing using BackGroundWorker, I don't see my where I'm wrong.
I'd say that's the issue. You don't really want the Paint routine to be waiting on a background thread, but that means that your paint event may complete before your calculations. The results could be totally bizarre, depending on what you were doing. In the worst case, you could be sharing variables between the paint routine and the foreground, thus setting up a race condition for the painting.
Re: Drawing using BackGroundWorker, I don't see my where I'm wrong.
Edit - I already knew the information I asked for :/
Altered post. It's certainly a race condition that's occurring like what Shaggy said. What you'll have to do is rework your code to try and prevent that as much as possible. One thing that I could see what would be causing it is your Try/Catch, that alone is such a resource hog. One other thing which is what I do on my game projects is set it up in this format:
Step 1. Update
Step 2. Draw
Step 3. Render
Each step has a separate thread and one will not perform until the other has finished. Finally my last suggestion is to change your game loop. While the one that I posted using JMcIlhinney's pauser works well, it doesn't work as efficiently as this one here:
Code:
Private game_thread As Threading.Thread
Private Sub Form1_FormClosing(sender As Object, e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
quit = True
End Sub
Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load
game_thread = New Threading.Thread(AddressOf GameLoop)
game_thread.Start()
End Sub
Private quit As Boolean = False
Private Sub GameLoop()
Dim s As New Stopwatch
s.Start()
'Update
'Draw
'Render
'Loop
While s.Elapsed <= TimeSpan.FromMilliseconds(16.6)
Application.DoEvents()
End While
Console.WriteLine(s.ElapsedMilliseconds)
s.Stop()
If quit = False Then
game_thread = New Threading.Thread(AddressOf GameLoop)
game_thread.Start()
End If
End Sub
The reason is because of the nature of the timer, it's just not a high precision instrument where as the stopwatch will be if the computer will allow it to be.
Re: Drawing using BackGroundWorker, I don't see my where I'm wrong.
Quote:
Originally Posted by
Shaggy Hiker
You don't really want the Paint routine to be waiting on a background thread, but that means that your paint event may complete before your calculations.
I was thinking that the Paint routine would use the actually "known" values, i.e. possibly old data, but why doesn't it show up at all (except for a complete .Invalidate of the Control?).
Re: Drawing using BackGroundWorker, I don't see my where I'm wrong.
This one is really bugging me!
I made a small example of what I'm trying to do, and guess what, it is working!
Now I have to look real deep into my application where the failure is. Will report on results.
The small example (Form1 has two Buttons (btnStartStop and btnAdd) and a PictureBox1 and a BGW
Code:
Public Class Form1
Private Sub PictureBox1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint
Try
DrawObjects(e.Graphics, MyObjects, Color.Black)
DrawObjects(e.Graphics, MyObjects2, Color.Red)
Catch
Debug.Print("error")
End Try
End Sub
Private Sub btnAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAdd.Click
Dim p As New Point(0, 0)
Dim o As New clEntity("LaLa", p)
MyObjects.Add(o)
p = New Point(PictureBox1.Width, 0)
o = New clEntity("Bla", p)
MyObjects2.Add(o)
End Sub
Private Sub btnStartStop_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnStartStop.Click
Dim btn As Button = DirectCast(sender, Button)
If btn.Text = "Start" Then
btn.Text = "Stop"
started = True
GameLoop()
Else
btn.Text = "Start"
started = False
End If
End Sub
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
MyObjects = New List(Of clEntity)
MyObjects2 = New List(Of clEntity)
AddHandler PicBoxInvalidate, AddressOf PictureBox_Invalidate
End Sub
Private Sub PictureBox1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles PictureBox1.Click
Dim pic As PictureBox = DirectCast(sender, PictureBox)
pic.Invalidate()
End Sub
Private Sub PictureBox_Invalidate(ByVal sender As Object, ByVal k As KarteEventArgs)
PictureBox1.Invalidate(k.Region)
End Sub
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
MoveObjects(MyObjects, InvalRegions, 1, 1)
End Sub
Private Sub DrawObjects(g As Graphics, Objects As List(Of clEntity), Color As Color)
If Not Objects Is Nothing Then
For Each o As clEntity In Objects
Using Pen As New Pen(Color)
g.DrawLine(Pen, o.Position.X - 3, o.Position.Y, o.Position.X + 3, o.Position.Y)
g.DrawLine(Pen, o.Position.X, o.Position.Y - 3, o.Position.X, o.Position.Y + 3)
End Using
Next
End If
End Sub
End Class
The module with all the logic. Note that MyObjects.Bewegung (Movement) is calculated using the BGW. MyObjects2.Bewegung is done on the GUI-thread. All the drawing is done in the Paint-Event
Code:
Module Module1
Public MyObjects As List(Of clEntity)
Public MyObjects2 As List(Of clEntity)
Public Started As Boolean = False
Public Event PicBoxInvalidate As EventHandler(Of KarteEventArgs)
Public Class KarteEventArgs
Inherits EventArgs
Private _Region As Rectangle
Public Property Region() As Rectangle
Get
Return Me._Region
End Get
Set(ByVal value As Rectangle)
_Region = value
End Set
End Property
Public Sub New(Optional ByVal Region As Rectangle = Nothing)
Me._Region = Region
End Sub
End Class
Public Sub GameLoop()
Static LastTime As Integer = Environment.TickCount
Dim ActualTime As Integer
Dim FrameIntervall As Integer = 10
ActualTime = Environment.TickCount
If LastTime + FrameIntervall <= ActualTime Then
If Not Form1.BackgroundWorker1.IsBusy Then
Form1.BackgroundWorker1.RunWorkerAsync()
End If
End If
MoveObjects(MyObjects2, InvalRegions, 1, -1)
LastTime = ActualTime
If Started = True Then
ExecuteAfterPause(FrameIntervall, New MethodInvoker(AddressOf GameLoop))
End If
End Sub
Public Sub MoveObjects(ByVal Objects As List(Of clEntity), ByVal Regions As Queue(Of Region), ByVal Vertical As Integer, ByVal Horizontal As Integer)
If Not Objects Is Nothing Then
Dim RemoveObjects As New List(Of clEntity)
For Each o As clEntity In Objects
Dim size As New Size(8, 8)
Dim p As New Point(o.Position.X - 4, o.Position.Y - 4)
Dim k As New KarteEventArgs
k.Region = New Rectangle(p, size)
RaiseEvent PicBoxInvalidate(Form1, k) 'to wipe off on the old position
o.Bewegung(Vertical, Horizontal)
p.X = o.Position.X - 3
p.Y = o.Position.Y - 3
k.Region = New Rectangle(p, size)
RaiseEvent PicBoxInvalidate(Form1, k) 'to draw the new position
'If outside of PictureBox, Remove it
If o.Position.X > Form1.PictureBox1.Width Or o.Position.X < 0 Or o.Position.Y > Form1.PictureBox1.Height Or o.Position.Y < 0 Then
RemoveObjects.Add(o)
End If
Next
If Not RemoveObjects.Count = 0 Then
For Each o As clEntity In RemoveObjects
Objects.Remove(o)
Next
End If
End If
End Sub
End Module
The clEntity
Code:
Imports System.Math
Public Class clEntity
#Region "New Prozeduren"
Public Sub New(ByVal _Name As String, ByVal _Position As Point)
With Me
.Name = _Name
.Position = _Position
End With
End Sub
#End Region
#Region "Eigenschaften"
Public Mutex As New System.Threading.Mutex
Private _Name As String
Public Property Name() As String
Get
Return _Name
End Get
Set(ByVal value As String)
_Name = value
End Set
End Property
Private _Position As Point
Public Property Position() As Point
Get
Return _Position
End Get
Set(ByVal value As Point)
_Position = value
End Set
End Property
#End Region
#Region "Methoden"
Public Sub Bewegung(ByVal Vertical As Integer, ByVal Horizontal As Integer)
Dim p As New Point(Position.X + Horizontal, Position.Y + Vertical)
Position = p
End Sub
#End Region
End Class
Re: Drawing using BackGroundWorker, I don't see my where I'm wrong.
Me.Stupid!
I was precomputing the actual PixelLocation of each object when calculating the movements. That is because I'm using a chart projection that needs much calculation (Mercator). I try to compute each PixelPosition only once, and out of some reasons that does go along with the movement calculus.
By moving that into another thread I missed that I need the correct (centre) position and size of the control. Guess what, I had those values in Public properties of the Form. No problem if you're on a single thread, however........................
All those objects were drawn, however somewhere on a screen in Timbuktu or whatever.
Thanks everybody for the helpfull hints.