Scaling DesktopX to 1000s of Objects for Fun and Profit
The script described in the article below is available for use in free DesktopX objects - see the snowman's script for details.
This is really as much an article on what mistakes to avoid as it is an article on creating flakes - if you just want to see the end result, skip to the end (or try clicking this link to get to the object). As WinCustomize is having a problem with submitting skins for Journeymen right now, I've duplicated the object description and shots here, as well as links to download the object.
Snowflakes are nice. I think we all like to see them - just as long as we don't have to go out in them!
However, I had a few problems with the snowflakes currently available for DesktopX:
- They don't actually look like snow. The flakes are too big - they may be nice representations, but they don't look realistic.
- There aren't enough of them. Ten, or twenty, not two hundred or a thousand.
- They don't act like snow. They fall down in a straight line at a constant velocity, possibly rotating - which leads to the next problem . . .
- They can take up a heck of a lot of CPU all the time (especially if they try to look better by rotating). They don't have any big errors that make them do this, and it obviously isn't always the case, especially if you have just a few - but to make it look good, you need a lot. And this means that people have to choose between having a pretty desktop in the background and being able to do work. Not good.
Anyway, I decided to make my own!
Understanding the Problem
I'm a programmer, and I already had an inkling as to what one problem with current snowflakes was - taking a quick look at one, I could see that they relied on each snowflake having its own script and timer. This is a nice clean way to do it - it means you can clone the flakes (if you can catch them!) and the snowflake is generally self-contained, which makes it easy to understand. The only problem is that it is incredibly wasteful! Each snowflake needs its own timer, which (for technical reasons) means it needs its own window. The more windows you have, the more the whole system slows down.
To test this theory (always test your theories!) I cloned a bunch of snowflakes - about 30 to start with - and immediately ran into big problems. DesktopX had sucked up all the CPU! Talking with a few of my friends at Stardock, I discovered the reason; I wasn't running in IconX mode, and thus not only did each object have a timer window, it was also using a real (layered) window to paint itself onto the desktop. This was bad news for all sorts of reasons. Duly advised, I changed to IconX mode. You may wish to advise users of your scripts to do the same, if you create large numbers of objects.
Haivng done that, the amount of CPU time used by DesktopX suddenly became more reasonable, and so naturally I decided to be evil and clone a bunch more - 100 more, in fact - until I was once again at the CPU limit. I was actually quite impressed that it got this far, but I still wanted to find out what the limiting factor was, so I ran AMD's CodeAnalyst to see what was taking all the time. This program basically takes lots of snapshots of the computer's state, and so it can tell you what it was doing at the time.
Not exactly to my surprise, I found that DesktopX was spending a significant amount of time trying to draw all those objects to the screen. However, I also found it taking up almost 8% of the time just handling the timers - on my computer, that's over a hundred million cycles per second! There were also a few unexpected "hot points" in the code which I reported to the DesktopX development team.
With the above knowledge in hand, I was able to define the following objectives for my better snowflake objects:
- Make the flakes look and act like real snow. Consider adding flurries and the like, but at the very least the flakes should not all be moving in the same direction at constant speeds.
- Have changes in the "wind" now and again.
- Have only one object controlling all of this to minimise scripting overhead.
- Try to keep CPU usage within reasonable limits, or adjust to the amount being used already by slowing down flake update speed, reducing the number of flakes, or otherwise limiting the amount of CPU time they use.
- Learn something!
Getting Down to Flakes
The original snowflake objects were quite large and complex. I didn't want that - real snow doesn't look like christmas-tree decorations! I therefore started with a very simple snowflake object - just a 5x5 alpha-blended white circular pixel! Amazingly enough, this proved very realistic, although I added 4x4 and 3x3 for a bit of variety. The snowflake controller can use any number of objects as flakes - to add one, just clone the last of the existing "GR_Snow_OriginalFlake_number" objects to get the next number in sequence.
The key to realism was in getting the snowflakes to move correctly. Moving down at a steady speed was not sufficient. A variety of techniques were used, but the base movement was derived from the description - but not the code - of a Flash snowflake script by Kirupa Chinnathambi. This script provided the main algorithm of varying the x-position of the snowflake by a cosine wave, which proved quite successful. If you wish to see how I implemented this, look at the Class Flake's Sub Update() for details.
Trimming the Load
Given that this was meant to be a background object, I really wanted to reduce the CPU cost. Most programmers know that loops are where it really starts to hurt - if you want to improve performance, increase the speed of a loop by taking calculations outside of it. In my case, I noticed that I was checking System.WorkAreaBottom inside the main loop. This was a bad idea, because each time I accessed that property it would make an API call to check the workspace area.
Just by changing this call over to a global variable that I kept updated, I managed to trim 15-20% of the time spent in the VBScript libraries and half of the time spent in DesktopX's ActiveX host (the bit that provides the System object). That's an extra 10 or 20 snowflakes right there. I found I was using this method in other places too (mostly when repositioning the snowflakes after they fell off the screen), but the big gain was in removing this expensive call from the main repositioning loop. If you make objects, you might want to check this isn't happening in yours.
Other more modest gains were made in the same way. For example, to ensure that time was not wasted drawing snowflakes that fell off the screen, those that drifted a certain distance offscreen to the left or right were recycled. This meant that each time, the following statement was evaluated:
If y > VSCREEN_BOTTOM Or x > VSCREEN_RIGHT + 20 Or x < VSCREEN_LEFT - 20 Then . . .
Looks fine, right? However, I found that a small but significant proportion of time was taken in processing this line - in particular, the margin values. Removing this calculation from outside the loop replaced it with this:
If y > VSCREEN_BOTTOM Or x > VSCREEN_RIGHT_MARGIN Or x < VSCREEN_LEFT_MARGIN Then
which ended up making the script 3-4% faster overall. Not much, perhaps, but every little helps. I would later split this code up to add support for wind, but removing the redundant additions remained a benefit.
It is important to note that just because you think something is faster doesn't mean it will be. There were several cases where I thought something would be faster if done in one way, only to find out that the way it was before worked just as well; or worse, the new way was slower! Always test your theories with a standard test - this can be as simple as watching the CPU time in Task Manager while you run a loop 1000 times, or something a little more involved, like using CodeAnalyst - and if you do find a difference, double-check it!
Keeping it Smooth
Obviously, no matter how much I did to reduce overhead, there is some limit to how many flakes can be displayed onscreen, even if you have the very latest in harware; the snowflake script is essentially a fullscreen animation at 25+ FPS, which is hard enough when you have full control of the computer, let alone in GDI mode. What I wanted was a way to make sure that no matter what settings the user chose, the snowfall stayed relatively smooth and - more importantly - it didn't become too much of a burden on the user's computer. After all, a pretty desktop background is one thing, but if that gets in the way of a video chat to your parents, that's quite another thing!
The first thing I tried was using WMI. For those who haven't run into it before, WMI is the Windows Management Infrastructure - it contains lots of ways to measure and set various things, such as hard disk size, process priority levels and CPU usage. You may have been using it without knowing, through the System Performance Meters plugin.
My plan was to monitor DesktopX's CPU usage and do something if it got too high. I tried using WMI to do this both directly and through the plugin, and both times ran into problems. The plugin did not seem to give me the numbers I wanted, while calling WMI directly did . . . but at the same time, it caused a 5% CPU overhead (including a helper process that seemed to be servicing the requiests), which just made the problem worse!
After a bit of thinking, I decided to try a different approach. VBScript offers a Timer function that returns the number of seconds since midnight. However, it does so to a millisecond accuracy - or at least, 10ms - enough for me to use to measure how long it took to execute one of my flake updates. If this time was significantly longer than the time it was meant to take, I would know that the computer was lagged and that I should take some flakes away.
This approach eventually worked, but required a lot of tweaking to get working well. One problem was that the number of flakes tended to vary around the maximum level of flakes, when ideally we would want it to find that value quickly and stay there. To smooth this variation I introduced two threshold constants - one for how long past the timer interval the tick could go before allowing the addition of a new flake, and one for how long past before it was considered lagged (the first being lower than the second). These were multiples of the base tick time.
I also had two counters - one that measured how many ticks had gone by without being considered lagged, and one which measured the opposite. Once the algorithm was modified not to remove snowflakes before a certain number of lagged ticks had passed and (more importantly) not to add them back until a certain number of unlagged ticks had passed, things suddenly worked much better.
Drifting in the Breeze
I tried these flakes out with a few beta testers from #stardock IRC, but according to them they seemed to be missing a few things. The first was snow settling. This was relatively simple - just set a counter for each flake when it reached the bottom of the screen, and if the counter runs out, fade the flake away and reposition it.
A more interesting problem was wind. I planned on doing something rather complicated for this, but it turned out that a simple but effective solution to this was to just choose a direction and a magnitude, and have an addition to each flake's velocity that approached that linearly. To do this, I figured out the difference between the current wind and the future wind, chose a random time for the transition, and then sliced that up into chunks - so if we were starting from no wind at all, each update the wind's effect on the snowflakes would be a little stronger until it reached its full speed, at which point the wind would start changing to something else. This created a very realistic effect, as long as you weren't looking for a blizzard.
A couple of other things I had to think about were people resizing the taskbar or even changing resolution while using the object. The snowflakes had to adapt to this, which is where the Sub System_OnScreenChange and Sub System_OnWorkAreaChange (new in DX 2.4) came in. These subroutines are called for each object whenever the screen resolution or workspace area change, respectively, and can be useful for updating cached data.
Let it snow, let it snow, let it snow . . .
So, at the end, what do we have? Well, it's a pretty complicated widget to do what seems to be a pretty simple thing. The most complicated work was done to ensure that the widget did not take too much CPU time from everything else, although making the snowflakes move in just the right way certainly adds to that. The majority of the time is taken by DX in moving the flake objects around, and by Windows in painting them. Perfect snow that is not interrupted is not really possible without increasing the priority of DesktopX (and so potentially starving other, more deserving apps of CPU time), but I think the solution presented in this article works well. Enjoy!
** PLEASE READ IMPORTANT NOTICES BEFORE DOWNLOADING FOR BEST EXPERIENCE **
Realistic Snowflakes by Laurence "GreenReaper" Parry - images by PixelPirate.
Bring the winter snows to your desktop! Features hundreds of swaying, wind-swept flakes floating in the background. User options - click the snowman to change - include maximum number of flakes (actual count is balanced for idle CPU use), flake interval (lower = faster), gravity and settle time on the bottom of your screen. Try the flakes out today - no need to wait for Christmas!
* PLEASE READ: You must hide desktop icons or be in IconX mode for best performance. This snow uses lots of objects, and these objects must be at desktop level to perform well. This will only work if you are hiding the desktop icons (this option is on the Theme tab when configuring DesktopX), or are in IconX mode. If you do not do this, expect high CPU usage and a low number of flakes. Sorry, that's just the way Windows XP works! Expect improvments in Longhorn.
* READ ALSO: This does not occur on all machines, but if you find DesktopX's user interface freezes up above a certain number of flakes, please terminate DesktopX via Task Manager (Ctrl+Alt+Del/Processes tab/right-click DesktopX.exe/End Process) and restart with a lower value for Max Flakes - it should restart with the previous session's settings, not the ones that locked up. This should not affect other programs, only DesktopX. Make sure you have the very latest version of DesktopX available as future updates may fix this.
Having said the above, do experiment with the settings to find the best level for your computer - the initial settings are conservative, particularly the Max Flake count. The snowflake script will automatically compensate for CPU usage by other programs by removing snowflakes when CPU use is high, and adding them back when it is low - you should be able to run this object in the background without trouble, although you might want to set it to 0 flakes while gaming.
Flakes are customizable - just clone another OriginalFlake or replace existing images. Text works too, try some floating Wingdings! You may also tweak certain variables in the script - see script comments. Many thanks to PixelPirate for creating the snowman and settings dialog images for this - all rights are reserved for these. You may use this script in your own objects as long as they are free to download - edit the snowman's script for details.
The wallpaper in this skin shot is available here.
** DID YOU READ THE INSTRUCTIONS BEFORE DOWNLOADING? Yes? Good. Download! [alternate download] **