The last change for the cheesy OSD application - preventing volume recursion.

Yesterday, at the close of my article about adding notifications support to the cheesy OSD application, I mentioned that there was one remaining problem.

The problem is actually a big deal - as it's written right now, the application can't filter out changes made by the application.  You see, your notification routine should only listen to changes that were made by applications other than your own - you already know about your applications changes.

This problem is compounded by the use of floating point numbers for the volume values - it's possible to get into what is affectionately known as "the floating point rounding spiral of death" if the right values happen to be selected.  If you're not familiar with floating point rounding issues, then you REALLY should go and read the MOST excellent "What Every Computer Scientist Should Know About Floating-Point Arithmetic" by David Goldberg.

Here's the simple version of how the "floating point rounding spiral of death" works.  Imagine you've got an application with a volume slider.  When a volume change notification is received by the application, it changes the position of the slider to match the new volume.  Simple enough, right? 

When a volume change notification is received, the volume notification handler looks at the value in the notification, and if it doesn't match the current position of the slider, it sets the slider's position to match the value in the notification.  Setting the slider generates a notification that the slider's position changed (this is the way the slider common control works, as I understand it, it's impossible to distinguish the notification generated by a program setting the slider from the user changing the slider's position).  So the slider's "set the current position" logic looks at the position of the slider and compares it against the current volume.  If they're different, the slider's code updates the system volume, which triggers a notification, which checks to see if the value in the notification doesn't match the position of the slider, etc...

This works great IF there's no rounding in the system.  But we're dealing with floating point integers here.  One of the characteristics of floating point numbers is that it's essentially impossible to compare two floating point numbers for equality - because of rounding errors, the values can only be compared relative to some amount of precision.

Let's consider what happens with our application.  It receives a notification that someone has changed the volume to 0.49999999937.  It compares it against the current value of the volume (0.5) and decides that they're not equal.  So it changes the slider to appropriate relative position, which rounds down to 0.4.  The slider change logic compares the current position of the slider (0.4) against the current volume (0.49999999937) and sets the volume to .4.  That triggers a notification that the master volume is 0.39999, the slider change logic compares the current position of the slider (0.4) against the current volume (0.39999), resets the slider to .3 and you watch in horror as the volume quickly spirals to 0.  What's worse, once this happens, you can't fix it - you move the slider up and it spirals down to 0 again.  Watching this effect in action can be quite comical (if rather annoying).

Nitpickers corner: Yeah, careful coding could avoid this particular example, and yeah, I'm using made-up numbers, these values are unlikely to cause rounding errors, and certainly not as quickly as I'm showing.  It doesn't matter.  Even with careful coding, the fact that you can't distinguish an external notification from user generated input is a huge deal.

If you could differentiate between notifications generated by your application from notifications generated by someone else's application, this problem would go away - you simply update your UX on external notifications and you throw away the notifications generated by your UX changes.  That cuts the loop off after the first notification is received.

When people realize that this problem exists, their first suggestion is to say "That's stupid - why does the system tell you about changes made by your process?  The system should be smart enough to figure this out and simply not tell the process about its own changes".  Unfortunately what the users consider an "application" doesn't neatly fit into the windows process model.  But what do you do about applications like the shell or the sidebar?  They host 3rd party code - "Applications" as far as the user is concerned. If the system filtered notifications by process, what would happen you had two different volume control gadgets in the sidebar?  It would be "bad" if the system didn't tell one of them about changes made by the other.

For the volume logic, we chose to solve the problem slightly differently.  If you remember, the OSD application's call to VolumeStepUp specified a NULL parameter.  That parameter is known as the "Event Context" - every volume related "Set" operation takes one.  The system passes this EventContext value through the system all the way to the notification handler for volume changes.  An application can specify whatever value it wants in the EventContext and when it receives a notification, it can check the EventContext value to see if the application generated the notification.

 

I'm not going to show the code changes for this - basically you define a GUID and change the VolumeStepUp (and VolumeStepDown) calls to specify this GUID for the event context, and then in the OnNotify method of the CVolumeNotification, if the EventContext member value of the NotificationData parameter matches the GUID, return without updating the UX.