views:

869

answers:

9

I'm writing a simple control in C# that works like a picture box, except the image is constantly scrolling upwards (and re-appearing from the bottom). The animation effect is driven by a timer (System.Threading.Timer) which copies from the cached image (in two parts) to a hidden buffer, which is then drawn to the control's surface in its Paint event.

The problem is that this scrolling animation effect is slightly jerky when run at a high frame rate of 20+ frames per second (at lower frame rates the effect is too small to be perceived). I suspect that this jerkiness is because the animation is not synchronized in any way with my monitor's refresh rate, which means that each frame stays on the screen for a variable length of time instead of for exactly 25 milliseconds.

Is there any way I can get this animation to scroll smoothly?

You can download a sample application here (run it and click "start"), and the source code is here. It doesn't look horribly jerky, but if you look at it closely you can see the hiccups.

WARNING: this animation produces a pretty weird optical illusion effect which might make you a little sick. If you watch it for awhile and then turn it off, it will look as if your screen is stretching itself vertically.

UPDATE: as an experiment, I tried creating an AVI file with my scrolling bitmaps. The result was less jerky than my WinForms animation, but still unacceptable (and it still made me sick to my stomach to watch it for too long). I think I'm running into a fundamental problem of not being synced with the refresh rate, so I may have to stick to making people sick with my looks and personality.

+2  A: 

Use double buffering. Here are two articles: 1 2.

Another factor to ponder is that using a timer doesn't guarantee you to be called at exactly the right time. The correct way to do this is to look at the time passed since the last draw and calculate the correct distance to move smoothly.

David Schmitt
It is double buffered already. The problem is not flickering.
MusiGenesis
then consider the second part of my answer: your timer event is not called at the exact time you'd expect it to be.
David Schmitt
Yeah, thanks to windows multitasking my times are quantized to 15ms, and I'm trying to animate frames that last 20ms. I think my problem is unsolvable.
MusiGenesis
That just means that you have to skip a frame once in a while or move the content for non-integral distances. Look into basic gaming texts to see how that is calculated.
David Schmitt
No can do on frame skipping or non-integral distance moves. The smoothness of the scrolling is exactly what I was looking for. From what I've read I would have to synchronize the animation with the monitor's refresh rate, which I fundamentally can't do because I also have to synchronize the animation with music.
MusiGenesis
A: 

Well, if you wanted to run the timer at a lower speed, you can always change the ammount the image is scrolled in the view. This gives better preformance, but makes the effect look kinda jerky.

Just change the _T += 1; line to add the new step...

Actually, you could even add a property to the control to adjust the step ammount.

MiffTheFox
The effect is even worse when I slow down the timer but move more than 1 pixel at a time.
MusiGenesis
+1  A: 

I had a similar problem a couple of months ago, and solved them by switching to WPF. The animated control ran a lot smoother than with a standard timer-based solution and I didn't have to take care of synchronization any more.

You might want to give it a try.

m_oLogin
Pleaaaase, anonymous downvoters, express yourselves!
m_oLogin
It wasn't me. I voted you up, even though I can't use WPF. Someone might have thought your comment wasn't really an answer to my question, since WinForms isn't technically WPF. I should have just said "windows app" instead of WinForms.
MusiGenesis
you're right. i didn't notice the importance of using WinForms. i'll take another look at your code then.
m_oLogin
+1  A: 

Some ideas (not all good!):

  • When using a threading timer, check that your rendering time is considerably less than one frame interval (from the sound of your program, you should be fine). If rendering takes longer than 1 frame, you will get re-entrant calls and will start rendering a new frame before you've finished the last. One solution to this is to register for only a single callback at startup. Then in your callback, set up a new callback (rather than just asking to be called repeatedly every n milliseconds). That way you can guarantee that you only schedule a new frame when you've finished rendering the current one.

  • Instead of using a thread timer, which will call you back after an indeterminate amount of time (the only guarantee is that it is greater than or equal to the interval you specified), run your animation on a separate thread and simply wait (busy wait loop or spinwait) until it is time for the next frame. You can use Thread.Sleep to sleep for shorter periods to avoid using 100% CPU or Thread.Sleep(0) simply to yield and get another timeslice as soon as possible. This will help you to get much more consistent frame intervals.

  • As mentioned above, use the time between frames to calculate the distance to scroll, so that the scroll speed is independent of the frame rate. But note that you will get temporal sampling/aliasing effects if you try to scroll by a non-pixel rate (e.g. if you need to scroll by 1.4 pixels in a frame, the best you can do is 1 pixel, which will give a 40% speed error). A workaround for this would be to use a larger offscreen bitmap for scrolling and then scale it down when blitting to screen, so you can effectively scroll by sub-pixel amounts.

  • Use a higher thread priority. (really nasty, but may help!)

  • Use something a bit more controllable (DirectX) rather than GDI for rendering. This can be set up to swap the offscreen buffer on a vsync. (I'm not sure if Forms' double buffering bothers with syncing)

Jason Williams
I'm actually driving this animation off of callbacks from an audio output engine, so theoretically my timing is accurate to 1/44 of a millisecond. I've experimented with non-whole-pixel movements, but with these images in particular the interpolation artifacts are severe, and it still seems jerky anyway. I've tried the higher thread priority but it had no effect I could detect.I think I may have to go the DirectX route, although I was hoping to avoid this (I don't think it works in Windows Mobile, is one problem).
MusiGenesis
+3  A: 

You would need to wait for a VSYNC before you draw the buffered image.

There is a CodeProject article that suggests using a multimedia timer and DirectX' method IDirectDraw::GetScanLine().

I'm quite sure you can use that method via Managed DirectX from C#.

EDIT:

After some more research and googling I come to the conclusion that drawing via GDI doesn't happen in realtime and even if you're drawing in the exact right moment it might actually happen too late and you will have tearing.

So, with GDI this seems not to be possible.

VVS
What led you to conclude GDI doesn't happen in realtime but DirectX does?
MusiGenesis
I stumbled across this article: http://www.virtualdub.org/blog/pivot/entry.php?id=74 - since smooth scrolling without tearing is actually possible with DirectX or DirectDraw I concluded it's "more realtime" ;)
VVS
.. and since drawing in GDI happens via messages it's always possible that the message gets queued up.
VVS
Thanks for the link. I was already convinced that this was an essentially impossible problem (and I hate to ever admit that about anything), and that article confirms it. You're the top candidate for the bounty here so far.
MusiGenesis
+1  A: 

I had the same problem before and found it to be a video card issue. Are you sure your video card can handle it?

Nazadus
Probably not. I use a Toshiba Satellite notebook that can only do 60 Hz.
MusiGenesis
Which model notebook are you using?
Nazadus
It's a Satellite A135, relatively underpowered (1.5G ram, 1.60GHz processor). If you have a better video card, does the animation in the sample app look smooth to you?
MusiGenesis
Acer Veriton L460 and my desktop -- it runs very smooth.
Nazadus
The Acer uses a LCD monitor and the desktop uses a flat panel (both Dell's).
Nazadus
+1  A: 

I modified your example to use a multimedia timer that has a precision down to 1 ms, and most of the jerkiness went away. However, there is still some little tearing left, depending on where exactly you drag the window vertically. If you want a complete and perfect solution, GDI/GDI+ is probably not your way, because (AFAIK) it gives you no control over vertical sync.

Alan
Are you running a refresh rate higher than 60 Hz? I tried using a multimedia timer after posting this question, but I didn't see any noticeable improvement.
MusiGenesis
No, I'm running at 60Hz (on a TFT monitor). It's hard to describe jerkiness in approachable terms, but the improvement was that the "frequency" of "jerks" went down significantly, ie the occasional "jumps" upwards occurred every 10-12 seconds rather than every 1-2. The tearing was still distracting, though, so I'm not saying the MM timer is a 100% solution, but it sure delivers on timing accuracy.
Alan
+1  A: 

You need to stop relying on the timer event firing exactly when you ask it to, and work out the time difference instead, and then work out the distance to move. This is what games, and WPF do, which is why they can achieve smooth scrolling.

Let's say you know you need to move 100 pixels in 1 second (to sync with the music), then you calculate the time since your last timer event was fired (let's say it was 20ms) and work out the distance to move as a fraction of the total (20 ms / 1000 ms * 100 pixels = 2 pixels).

Rough sample code (not tested):

Image image = Image.LoadFromFile(...);
DateTime lastEvent = DateTime.Now;
float x = 0, y = 0;
float dy = -100f; // distance to move per second

void Update(TimeSpan elapsed) {
    y += (elapsed.TotalMilliseconds * dy / 1000f);
    if (y <= -image.Height) y += image.Height;
}

void OnTimer(object sender, EventArgs e) {
    TimeSpan elapsed = DateTime.Now.Subtract(lastEvent);
    lastEvent = DateTime.Now;

    Update(elapsed);
    this.Refresh();
}

void OnPaint(object sender, PaintEventArgs e) {
    e.Graphics.DrawImage(image, x, y);
    e.Graphics.DrawImage(image, x, y + image.Height);
}
Jon Grant
+3  A: 

(http://www.vcskicks.com/animated-windows-form.html)

This link has an animation and they explain the way they accomplish it. There is also a sample project that you can download to see it in action.

Dusty