Monday, May 04, 2009

Dealing with Image Formats

One of the most common tasks when working with video is dealing with colorspaces and image formats. In this post, I'll discuss the two major colorspaces commonly used in Microsoft code, converting between different formats of a given colorspace. In some future post, I might talk about converting one colorspace to a totally separate colorspace, but that topic is worthy of its own discussion.

In the Microsoft world, there are two colorspaces that we're concerned about: YUV and RGB.

RGB Color Space
RGB is generally the easiest colorspace to visualize, since most of us have dabbled with finger paints or crayons. By mixing various amounts of red, green, and blue, the result is a broad spectrum of colors. Here is a simple illustration to convey this colorspace:

The top image of the barn is what you see. Each of the three pictures below are the red, green and blue components, respectively. When you add them together, voila, you get barnyard goodness. (sidenote: because you "add" colors together in the RGB colorspace, we call this an "additive" color model)

In the digital world, we have a convenient representation for RGB. Typically 0, 0, 0 corresponds with black (i.e. red, green and blue values are set to 0), and 255, 255, 255 is white. Intermediate values result in a large palette of colors. A common RGB format is RGB24, which allocates three 8 bit channels for red, green, and blue values. Since each channel has 256 possible values, the total number of colors this format can represent is 256^3, or 16,777,216 colors. There are also other RGB formats that use less/more data per channel (and thus, less/more data per pixel), but the general idea is the same. To get an idea of how many RGB formats exist, one need not go any farther than fourcc.org.

Despite the multitude of RGB formats, in the MSFT world, you can basically count on dealing with RGB24 or RGB32. RGB32 is simply RGB24, but with 8 bits devoted to an "alpha" channel specifying how translucent a given value is.

YUV Color Space
YUV is a substantially different from RGB. Instead of mixing three different colors, YUV separates out the luminance and chroma into separate values, whereas RGB implicitly contains this information in the combination of its channels. Y represents the luminance component (think of this as a "black and white" channel, much like black and white television) and U and V are the chrominance (color) components. There are several advantages to this format over RGB that make it desirable in a number of situations:
  • The primary advantage of luminance/chrominance systems such as YUV is that they remain compatible with black and white analog television.
  • Another advantage is that the signal in YUV can be easily manipulated to deliberately discard some information in order to reduce bandwidth.
  • The human eye is more sensitive to luminance than chroma; in this sense, YUV is generally considered to be "more efficient" than RGB because more information is spent on data that the human eye is sensitive to.
  • It is more efficient to perform many common operations in the YUV colorspace than in RGB--for example, image/video compression. By nature, these operations occur more easily in a YUV colorspace. Often, the heavy lifting in many image processing algorithms is applied only to the luminance channel.
Thus far, the best way I've seen to visualize the YUV colorspace was on this site.

Original image on the left, and the single Y (luminance) channel on the right:



...And here are the U and V channels combined:



Notice that the Y channel is simply a black and white picture. All of the color information is contained in the U and V channels.

Like RGB, YUV has a number of sub-formats. Another quick trip to fourcc.org reveals a plethora of YUV types, and Microsoft also has this article on a handful of the different YUV types used in Windows. YUV types are even more varied than RGB when it comes to different format.

The bad news is there's a lot of redundant YUV image formats. For example, YUY2 and YUYV are the exact same format entirely, but merely have different fourcc names. YUY2 and UYVY are exactly the same thing (16 bpp, "packed" format) but merely have the per-pixel byte order reversed. IMC4 and IMC2 are exactly the same thing (both 12 bpp, "planar" formats) but merely have the U and V "planes" swapped. (more on planar/packed in a moment)

The good news is that it's pretty easy to go between the different formats without too much trouble, as we'll demonstrate later.

Packed/Planar Image Formats
The majority of image formats (in both the RGB and YUV colorspaces) are in either a packed or a planar format. These terms refer to how the image is formatted in computer memory:
  • Packed: the channels (either YUV or RGB) are stored in a single array, and all of the values are mixed together in one monolithic chunk of memory.
  • Planar: the channels are stored as three separate planes. Fo
For example, the following image shows a packed format:



This is YUY2. Notice that the different Y, U, and V values are simply alongside one another. Also note that the above represents six pixels. They are not segregated in memory in any way. RGB24/RGB32/YUV2 are all examples of packed formats.

This image shows a planar format:



This is YV12. Notice that the three planes have been separated in memory, rather than being in a single, monolithic array. Often times this format is desirable (especially in the YUV colorspace, where the luminance values can then easily be extracted). YV12 is an example of a planar format.


Converting Between Different Formats in the Same Color Space
Within a given colorspace are multiple formats. For example, YUV has multiple formats with differing amounts of information per pixel and layout in memory (planar vs. packed). Additionally, you may have different amounts of information for the individual Y, U, and V values, but most Microsoft formats typically allocate no more than 8 bits per channel.

As long as the Y, U, and V values for the source and destination images have equivalent allocation, converting between various YUV formats is reduced to copying memory around. For this section we'll deal with YUV formats, since RGB will follow the same general principles. As an example, let's convert from YUY2 to AYUV.

YUY2 is a packed, 16 bits/pixel format. In memory, it looks like so:

The above would represent the first six pixels of the image. Notice that each pixel ends up with a Y value, and every other pixel contains a U and a V value. There is no alpha channel. The image contains a 2:1 horizontal down sampling.

A common misconception is that the # of bits per pixel is directly related to the color depth (i.e. the # of colors that can be represented). In YUY2, our color depth is 24 bits (there are 2^24 possible color combinations), but it's only 16 bits/pixel because the U and V channels have been down sampled.

AYUV, on the other hand, is a 32 bits/pixel packed format. Each pixel contains a Y, U, V, and Alpha channel. In memory, it ends up looking like so:

The above would represent the first three pixels of the image. Notice that each pixels has three full 8 bit values for the Y, U and V channels. There is no down sampling. There is also a fourth channel for an alpha value.

In going from YUY2 to AYUV, notice that the YUY2 image contains 16 bits/pixel whereas the AYUV contains 32 bits/pixel. If we wanted to convert from YUY2 to AYUV, we have a couple of options, but the easiest way is to simply reuse the U and V values contained in the first two pixels of the YUY2 image. Thus, we have to do no interpolation at all to go from YUY2 to AYUV--it's simply a matter of re-arranging memory. Since all the values are 8 bit, there isn't any additional massaging to do; they can simply be reused as is.

Here is a sample function to converty YUY2 to AYUV:
  
// Converts an image from YUY2 to AYUV. Input and output images must
// be of identical size. Function does not deal with any potential stride
// issues.
HRESULT ConvertYUY2ToAYUV( char * pYUY2Buffer, char * pAYUVBuffer, int IMAGEHEIGHT, int IMAGEWIDTH )
{
if( pYUY2Buffer == NULL || pAYUVBuffer == NULL || IMAGEHEIGHT < 2
|| IMAGEWIDTH < 2 )
{
return E_INVALIDARG;
}

char * pSource = pYUY2Buffer; // Note: this buffer will be w * h * 2 bytes (16 bpp)
char * pDest = pAYUVBuffer; // note: this buffer will be w * h * 4 bytes (32 bpp)
char Y0, U0, Y1, V0; // these are going to be our YUY2 values

for( int rows = 0; rows < IMAGEHEIGHT; rows++ )
{
for( int columns = 0; columns < (IMAGEWIDTH / 2); columns++ )
{
// we'll copy two pixels at a time, since it's easier to deal with that way.
Y0 = *pSource;
pSource++;
U0 = *pSource;
pSource++;
Y1 = *pSource;
pSource++;
V0 = *pSource;
pSource++;

// So, we have the first two pixels--because the U and V values are subsampled, we *reuse* them when converting
// to 32 bpp.
// First pixel
*pDest = V0;
pDest++;
*pDest = U0;
pDest++;
*pDest = Y0;
pDest += 2; // NOTE: not sure if you have to put in a value for the alpha channel--we'll just skip over it.

// Second pixel
*pDest = V0;
pDest++;
*pDest = U0;
pDest++;
*pDest = Y1;
pDest += 2; // NOTE: not sure if you have to put in a value for the alpha channel--we'll just skip over it.
}
}

return S_OK;
}

Note that the inner "for" loop processes two pixels at a time.

For a second example, let's convert from YV12 to YUY2. YV12 is a 12 bit/pixel, planar format. In memory, it looks like so:

...notice that every four pixel Y block has one corresponding U and V value, or to put it a different way, each 2*2 Y block has a U and V value associated with it. And, yet another way to visualize it: the U and V planes are one quarter the size of the Y plane.

Since all of the YUV channels are 8 bits/pixel, again--it comes down to selectively moving memory around. No interpolation is required:
  
// Converts an image from YV12 to YUY2. Input and output images must
// be of identical size. Function does not deal with any potential stride
// issues.
HRESULT ConvertYV12ToYUY2( char * pYV12Buffer, char * pYUY2Buffer, int IMAGEHEIGHT, int IMAGEWIDTH )
{
if( pYUY2Buffer == NULL || pYV12Buffer == NULL || IMAGEHEIGHT < 2
|| IMAGEWIDTH < 2 )
{
return E_INVALIDARG;
}

// Let's start out by getting pointers to the individual planes in our
// YV12 image. Note that the Y plane in a YV12 image's size is
// simply the image height * image width. This is because all values
// are 8 bits. Also notice that the U and V planes are one quarter
// the size of the Y plane (hence the division by 4).
BYTE * pYV12YPlane = pYV12Buffer;
BYTE * pYV12VPlane = pYV12YPlane + ( IMAGEHEIGHT * IMAGEWIDTH );
BYTE * pYV12UPlane = pYV12VPlane + ( ( IMAGEHEIGHT * IMAGEWIDTH ) / 4 );

BYTE * pYUV2BufferCursor = pYUV2Buffer;

// Keep in mind that YV12 has only half of the U and V information that
// a YUY2 image contains. Because of that, we need to reuse the U and
// V plane values, so we only increment that buffer every other row
// of pixels.
bool bMustIncrementUVPlanes = false;

for( int ImageHeight = 0; ImageHeight < IMAGEHEIGHT; ImageHeight++ )
{
// Two temporary cursors for our U and V planes, which are the weird ones to deal with.
BYTE * pUCursor = pYV12UPlane;
BYTE * pVCursor = pYV12VPlane;

// We process two pixels per pass through this equation,
// hence the (IMAGEWIDTH/2).
for( int ImageWidth = 0; ImageWidth < ( IMAGEWIDTH / 2 ) ; ImageWidth++ )
{
// first things first: copy our Y0 value.
*pYUY2BufferCursor = *pYV12YPlane;
pYUY2BufferCursor++;
pYV12YPlane++;

// Copy U0 value
*pYUY2BufferCursor = *pUCursor;
pYUY2BufferCursor++;
pUCursor++;

// Copy Y1 value
*pYUY2BufferCursor = *pYV12YPlane;
pYUY2BufferCursor++;
pYV12YPlane++;

// Copy V0 value
*pYUY2BufferCursor = *pVCursor;
pYUY2BufferCursor++;
pVCursor++;
}

// Since YV12 has half the UV data that YUY2 has, we reuse these
// values--so we only increment these planes every other pass
// through.
if( bMustIncrementUVPlanes )
{
pYV12VPlane += IMAGEWIDTH / 2;
pYV12UPlane += IMAGEWIDTH / 2;
bMustIncrementUVPlanes = false;
}
else
{
bMustIncrementUVPlanes = true;
}
}

return S_OK;
}


This code is a little more complicated than the previous sample. Because YV12 is a planar format and contains half of the U and V information contained in a YUY2 image, we end up reusing U and V values. Still, the code itself isn't particularly daunting.

One thing to realize: neither of the above functions are optimized in any way, and there are multiple ways of doing the conversion. For example, here's an in-depth article about converting YV12 to YUY2 and some performance implications on P4 processors. Some people have also recommended doing interpolation on pixel values, but in my (limited and likely anecdotal) experience, it doesn't make a substantial difference.

Monday, February 02, 2009

How to fix Vista lag spikes

Vista has a nasty bug that can cause lag spikes when attached to a wireless network; every sixty seconds, the PC will experience a rather substantial lag spike:

10.0.0.1 is my router; notice everything is going along just fine, and then out of the blue, I get a ping time of 836 milliseconds, which for any latency-sensitive application is a very consequential amount of time. And it isn't like my computer is attempting to ping some distant target over the nebulous framework of the Internet: this is my router. The most adjacent device to my computer on the network. If you let ping run for ten or fifteen minutes, you'll see the spacing between lag spikes is exactly 60 seconds.

It goes without saying that this is completely unacceptable, especially if you're into online gaming (WoW, CS, etc.) or VOIP. Even more unacceptable is that this issue has been going on for years and MSFT has yet to fix it. A quick google search for 60-second Vista Lag Spike reveals just how pervasive the issue is. Numerous people have "fixes" which involve installing weird applications from the Internet, which I was opposed to even trying since I don't like running "weird applications from the Internet."

The issue has to do with the WLAN Autoconfig service:

Unfortunately, this service is also in control of a whole host of junk, so stopping the service results in your wireless connection dying. For a while, it seemed like there may be some love in registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Wlansvc\Interfaces\INTERFACE_NAME\ScanInterval, which is set to 60 EA 00 00 (EA60 is 60000 in hex, which is the number of milliseconds that elapse between each lag spike), but the Scan Interval field is simply overwritten every time the WLAN Autoconfig service is started with good ol' EA 60. (side rant: why bother putting a value in the registry if the service is simply going to overwrite it every time it starts??? Is this quality of code really worth paying several hundred dollars for?)

Finally, I found a fix that is explained in this article, which simply disables the autoconfig portion on a specific network adapter. Unfortunately, that fix has some problems--if you reboot your computer and forget to turn autoconfig on, the next time it runs your internet will be busted, which means restarting the service and then shutting it off once you've connected. It also means I have to explain to non-computer savvy significant others how to do this, since said significant other enjoys a lag-free WoW experience just as much as I do.

In any event, the quick and dirty fix is to open up a command prompt, and type in netsh wlan set autoconfig enabled=no interface="Wireless Network Interface" where "Wireless Network Interface is the name of your NIC card. The problem with this is if you forget to re-enable the service when you shutdown or reboot your computer, your network connection won't come up correctly. To fix this, enable the service (change "enabled=no" to "enabled=yes") and once the network is connected, disable the service.

All I can say is: totally unacceptable for a product that's been in the market for a few years now. Hopefully Windows 7 doesn't share this same "feature." And hopefully MSFT realizes that crap like this makes me want to either keep on using Windows XP SP3 or "upgrading" to Ubuntu; c'mon, if you're going to charge us for software, at least perform basic QA and resolve issues in a timely manner.

Saturday, December 06, 2008

It's big, it's heavy, it's wood!


Everybody loves log:


...this posting concerns a different type of log: the type we programmers use to keep a history of what our application did.

I love Jeff Atwood. I really do. I read his blog on a regular basis. I enjoy his style of writing, and both of us are unabashed nerds. We both post on Silent PC Review. His blog, Coding Horror, has possibly been my favorite blog over the last few years. Most of the time, he's spot on, but occasionally he stumbles, as is the case in The Problem With Logging. Atwood makes it clear how little respect he has for logging:
So is logging a giant waste of time? I'm sure some people will read about this far and draw that conclusion, no matter what else I write. I am not anti-logging. I am anti-abusive-logging. Like any other tool in your toolkit, when used properly and appropriately, it can help you create better programs. The problem with logging isn't the logging, per se -- it's the seductive OCD "just one more bit of data in the log" trap that programmers fall into when implementing logging. Logging gets a bad name because it's so often abused. It's a shame to end up with all this extra code generating volumes and volumes of logs that aren't helping anyone.

We've since removed all logging from Stack Overflow, relying exclusively on exception logging. Honestly, I don't miss it at all. I can't even think of a single time since then that I'd wished I'd had a giant verbose logfile to help me diagnose a problem.

When it comes to logging, the right answer is not "yes, always, and as much as possible." Resist the tendency to log everything. Start small and simple, logging only the most obvious and critical of errors. Add (or ideally, inject) more logging only as demonstrated by specific, verifiable needs.
This sounds perfectly reasonable, until you consider a few facts:
  1. Jeff's application, Stack Overflow, is a web application, so logging may or may not be a valuable tool. For many applications, logging isn't that useful.
  2. Nobody wants a giant, verbose logfile. At least nobody sane, that is. The goal of logging has never been to mindlessly "log" every conceivable message, and anyone who thinks that is a good thing has A) never spent time parsing a log file or B) is prone to wasting their own time.
I, for the record, have spent an inordinate amount of time sifting through our customer's log files. Our application is real-time so debugging can be very difficult and logging provides a crucial method by which we solve problems. We go to great pains to only log things that are useful, and even more pain to remove messages that are not useful.

Atwood claims he's not anti-logging, but that's hard to believe given that Jeff's application no longer contains any logging beyond exceptions. The impetus for this rash decision?

Oh, sure, logging seems harmless enough, but let me tell you, it can deal some serious hurt. We ran into a particularly nasty recursive logging bug:

  • On thread #1, our code was doing Log (lock) / DB stuff (lock)
  • On thread #2, our code was doing DB stuff (lock) / log stuff (lock)

If these things happened close together enough under heavy load, this resulted in -- you guessed it -- a classic out-of-order deadlock scenario. I'm not sure you'd ever see it on a lightly loaded app, but on our website it happened about once a day on average.

I don't blame log4net for this, I blame our crappy code. We spent days troubleshooting these deadlocks by .. wait for it .. adding more logging! Which naturally made the problem worse and even harder to figure out. We eventually were forced to take memory dumps and use dump analysis tools. With the generous assistance of Greg Varveris, we were finally able to identify the culprit: our logging strategy. How ironic. And I mean real irony, not the fake Alanis Morrissette kind.

Correction: with the generous assistance of Greg Varveris, you were finally able to identify the culprit: your locking strategy.

Furthermore, why would you troubleshoot a deadlock with log messages? Think about this for a moment: logging only happens if the threads are active. Deadlocked threads: not active. The only sane conclusion one can arrive at in the event of a deadlock is that logging isn't going to help you much. Taking a crash dump and doing post-mortem debugging is what you do in the event of a deadlock (Varveris++). Reviewing your locking strategy is what you do in the event of a deadlock. But logging can't help you with a thread that is stuck waiting for a lock to free up because it is incapable of logging. It may be possible to add logging to figure out how the threads got in the state they're in, but the mere act of doing this implies your application has a threading/locking model that you don't understand.

This post was highly ineffective because the basic problems are A) failing to acknowledge a flawed locking strategy and B) misusing a tool (in this case, logging). And then when said tool is (unfairly) implicated as being the source of the problem, removing it from the application entirely. It's more like mindless thrashing than calculated debugging.

I do agree with Jeff that overzealous logging is a bad thing, and I would strongly advise against it. But I can't even tell you how many times logging has helped me solve a problem in our application. Here are some general rules of thumb I follow for logging:
  1. Keep a low signal-to-noise ratio; logging unnecessary messages only makes it more difficult for you to find interesting ones. Be brutal; remove anything you can deduce without the message.
  2. Keep a low signal-to-noise ratio; you do not have an infinite amount of disk space, so logs must be purged at some point in time. This means excessive logging impacts how much prior history you have. Sometimes it means log messages that were useful get purged and replaced by stuff that is not.
  3. Do not print out periodic messages to the log. If you must, print them as infrequently as possible.
  4. Keep messages as short as possible without making them cryptic.
  5. Always include variable values in messages (e.g. if you have the option between "blah failed!" and "blah failed with error code: E_INVALIDARG", the second is vastly preferable)
  6. If you are a real-time programmer (e.g. we do live video streaming), logging is always preferable to debugging. The act of debugging influences the system enough that logging is a far better option.
  7. There must be a way to access your logs. For a web application, this is obvious. You get them from the web server. For an application you distribute to customers, it can be more difficult. Implement a mechanism to fetch your logs, or give customers an easy way to generate and send them.
  8. We use two log levels: info and error. Some people prefer to add a dozen different levels, but in practice nobody knows the difference between INFO and IMPORTANT and ERROR and FATAL and SYSTEMEXPLODED and OMFG and JUSTKILLMENOW, etc.
  9. Always log date/time with each log message.
  10. Make errors visually distinguishable from info messages.
  11. On application start, log crucial information like application version, OS type, system info, etc. This can be really useful, especially if you have a versioned desktop application. (for a web app, not so much).
  12. If you're using C#, System.Reflection.MethodBase.GetCurrentMethod().Name is your friend. Avoid hard-coding class/method names in your log messages; when you go to refactor, it is a royal pain.
  13. Do implement a log rotation scheme. My favorite is to close logs at xx MB (we use 5 MB), and then zip them. We allocate yy MB of space for archived, zipped logs. Delete logs that exceed the allocated space (so maximum space occupied by logs is xx + yy; for us). There's also a ping-pong method, but it is less efficient overall.
  14. Provide a method to disable all logging.
Logging is valuable. Unfortunately, there is no general rule of thumb for determining if a given message is useful or just noise. It takes practice and maintenance to make a logfile a useful addition to an application. But knowing how to log messages in a way that is useful is a crucial skill for any programmer.

Wednesday, November 05, 2008

Ye Olde Rippin' Budget Build

I have to be honest: one of my favorite things to do is go on newegg and see the best budget system I can piece together.

Anyone can toss a buttload of money at a computer and end up with something really nice. It doesn't take any creativity, really--money solves a lot of problems. The real talent comes in making a shoe-string-budget system that is decently fast, efficient, has room to grow, uses quality components (i.e. your case that looks like it was from The Fast and the Furious? Just say no), and provides a delightful user experience.

I place a big emphasis on spending money to get a good case, power supply and peripherals. The reason for this is these are the only components in the computing industry that don't obsolete themselves in mere months. Video cards have an eight-month release cycle. Desktop processors have about a year between releases. Purchasing cutting edge often nets you 15-50% more performance, but often at 2-4 times the cost. Completely not worth it, IMO, unless you're a die-hard enthusiast and demand nothing short of the best.

A few other quirks about my PC preferences:
  • I believe my computers should be seen and not heard. I will never recommend a product that does not take your ears into consideration.
  • Energy efficiency is important. Not only does an efficient computer save you money on your electric bill, but I believe they are typically more stable (because they run cooler), quieter (because they require less cooling) and less prone to hardware failure (because you are not pushing crucial components like the power supply).
  • Riced out computer cases are for boys. Real men want sophisticated, proven industrial design. If SilentPCReview hasn't endorsed it, I generally don't consider it a viable option. If it has a spoiler, flaming decals, ground effects packages, seriously...it's just fugly. Please, no.
  • I hate cable clutter. It makes your case look like spaghetti, and reduces airflow/cooling performance. I opt for PSUs with modular cables, thinner SATA cabling, etc.
  • Death to the floppy drive!
  • SLI: sort of dumb unless you have a gigantic monitor (1920*1200, minimum) and want to play cutting edge games with uncompromising graphics at absurd frame rates. Very few people fit into this category, and the cost is substantial (two bleeding edge video cards + power supply capable of driving them = a whole lot of top ramen).
  • Nothing delights like a quality case, keyboard, mouse, speakers and monitor. These are the components that you truly interact with, and they are the biggest difference between a "cheap" system and something that feels refined.

Anyway, without further ado, my latest budget system.

Case: Antec Mini P180

This is an attractive, smallish case with a segregated partition for the power supply and hard drives, and an upper partition with room for a mATX motherboard. Read the SPCR review here.

Eye candy:

...normally this case is quite pricey, at ~$150, but occasionally newegg discounts the hell out of it. Right now it can be had for $79.00, which is a ridiculous deal for a very high quality case. See the SPCR review for more details, but it would be difficult to get a better case for the money.

Power Supply: OCZ ModXStream Pro 600W

Ample power (actually, far more than this build would ever need. Far more than almost any build would need, for that matter), modular cables, reputable vendor...not much to dislike here. Even more important is combined with the memory, it's $50!

Motherboard: MSI G31M3-F

You might be wondering what this motherboard has going for it, but consider this: it costs less than $50. It uses an Intel chip set, so stability is a given. It will accept modern, 45 nm quad-core processors, so plenty of room for upgrades in the future...what's not to like? And as was previously noted, motherboards obsolete themselves so quickly that I see no point in splurging. Next year's motherboard is guaranteed to be shinier, more enticing, more attractive, etc. But you spent all your dough on some motherboard that costs three (or four) times as much, and the actual measurable performance differences are often 2-5%. It's simply not worth the money.

So my strategy with motherboards is to spend as little as possible. There are much more meaningful ways money can be spent to improve user experience and/or performance.

Processor: Intel P5200

A nice, overclockable, dual-core, somewhere-between-E2200-and-E7200 processor that is miserly when it comes to pulling watts. Should you want something more meaty, I'd suggest the quad-core Q9550, but that will set you back an additional ~$225. And it's all going to be obsolete in a year (actually, more like two months), so don't even bother buying bleeding edge.

Memory: OCZ 2 GB DDR2 800

2 GB seems to be a goodly amount for a system these days. Go for four if you really think you need it (doubtful), but it's hard to beat simple math: $36 - $25 = $11 for 2 GB of memory. Plus, with the combo deal on the PSU, newegg basically pays you to take this memory off their hands. Suhweeeeet.

Video Card: HIS Radeon 4670

Budget gaming has never looked so good. Across the board, the 4670 is by far the best budget gaming card available, benchmarking somewhere between last year's ~$200+ offerings, the AMD 3850 and AMD 3870. It should be capable of driving even high-resolution monitors on most recent games, decoding 1080P H.264/VC-1 with great image quality enhancements, and is amazingly efficient at idle and load. The HIS card in particular comes with a more effective cooling solution that other video card vendors, so it runs quieter and cooler. And at $70 after rebate, it's impossible to argue with the value. For a beefier solution, consider a 48xx version, but expect to pay at least another $50-100.

Hard Drive: Samsung Spinpoint F1

Cheap, roomy and speedy. I dream of a day when these aren't $600, but that day will have to wait.

Keyboard: Logitech Wired Wave Keyboard

Ergonomic: check. Nice feeling: check. Full featured: check. Reasonable price for quality: check.

Mouse: Logitech G9 Gaming Mouse

Even if you aren't a gamer, having a precise mouse makes working with your computer a pleasure. You don't realize how much effort you take trying to click on things and position a mouse until you go from using a shitty one to a good one. In my opinion, the absolute worst place you could possibly skimp on is the mouse, since it is the once device we use where precision and ergonomics really matter. Think about the difference between using a laptop touchpad and a normal mouse. IMO, the difference between a normal mouse and a quality pointing mouse is the same.

Speakers: Logitech Z-2300

I prefer two-channel audio for my computer setups because running wires for rear channels is always a mess. Sub is a must. Nice sound is a must. The Z-2300 pleases without fail.

Another option is to buy a nice pair of headphones. The sound quality can't be beat, and if you're trying to tune out distractions they can be a life-saver.

DVD Burner: SAMSUNG SH-S203N


SATA, lowest published access times of any drives, numerous customer satisfaction awards, and $26 bucks. Sold!

Cost for core components:
Peripherals:
Considering speakers, mouse and keyboard could be had for as little as $20-30, $246 is admittedly pricey, so use your better judgment. But for ~$655 after rebates, this system is totally beyond decent. It has a very nice case and an excellent power supply. The CPU and video card combined are easily enough to do some decent gaming.

Total investment in what I call "replaceable" parts (CPU, video card, motherboard and RAM) is about $200, so in a year or two, this entire system could be upgraded to the latest, greatest crap and still be completely competitive. It's the advantage of building upon quality peripherals that don't obsolete themselves in mere months!

Thursday, October 16, 2008

Why Windows Media Player is not VLC

ExtremeTech had an article by John Dvorak outlining his Windows 7 wish list.

First, let me say that I am not a Dvorak fan. I think most of his columns are prone to hyperbole, and his technical expertise is questionable at best. I think of him as a Bill O'Reilly/Michael Moore of the computing world: basically a talking head who represents the triumph of outrage over substance.

That said, in the article, he actually poses a good question:

4. Fix the media player so it actually is a universal player—The media player should function as a universal player and actually play everything from MOV files from my camera to old AVI files. Windows Media Player wants everything to be a WMV file, and half of those don't always work. It plays some MP4 files and not others; it's spotty as can be. I just hate when I get some message or other saying that I do not have the right codec or whatever. What's the point of having a media player embedded in the system that doesn't work as well as VLC media player, a free player actively given away by VideoLAN.org? How is it that this VLC product, coded by a few guys in their spare time, can do a job 20,000 Microsoft coders cannot make Media Player do? I'd seriously like to know. Does this make any sense to anyone?
Unfortunately, yes, it does make sense to some of us. Let's go through each question, starting at the top.

What is the point of having a media player that doesn't work as well as a free player actively given away by VideoLAN.org? This is actually a great question. What Dvorak doesn't know is that audio, video and container formats are a veritable intellectual property minefield. Microsoft cannot just toss an H.264 decoder into Windows 7 without having to pay royalties to an organization called MPEG-LA. These royalties are not cheap. The sole difference between Apple and Microsoft in this domain is Apple bit the bullet and paid royalty fees to license codecs like H.264, MPEG2 and MPEG4.

Now consider VLC: they are an open-source project released under the GPL. They are providing implementations of audio and video codecs (including H.264, MPEG2, MPEG4, WMV, MP3, AAC, etc., all of which require license fees), but who exactly is paying the licensing costs? The answer is: absolutely nobody.

The VLC FAQ has a breezy explanation of this in section 3.4:
Some of the codecs distributed with VLC are patented and require you to pay royalties to their licensors. These are mostly the MPEG style codecs.

With many products the producer pays the license body (in this case MPEG LA) so the user (commercial or personal) does not have to take care of this. VLC...cannot do this because they are Free and Open Source implementations of these codecs. The software is not sold and therefore the end-user becomes responsible for complying to the licensing and royalty requirements. You will need to contact the licensor on how to comply to these licenses.

This goes for playing a DVD with VLC for your personal joy ($2.50 one time payment to MPEG LA) as well as for using VLC for streaming a live event in MPEG-4 over the Internet.
So, let me restate this: if you use VLC, technically it is your responsibility to contact MPEG-LA and any other licensing organization and figure out how you should best comply with their licensing. VLC claims they cannot do this because they are "Free and Open Source implementations of these codecs."

This is one instance where Free and Open Source Software sounds more like a selective disregard for intellectual property rights than a legitimate enterprise. What's really going on is MPEG-LA has no financial incentive to sue the pants off the creators of VLC because they don't have any money. But this doesn't change the fact that the VLC project is arguably "stealing" intellectual property that belongs to other people and then hiding behind some weak FOSS excuse (and, I suppose, the non-existence of revenue). The notion that any of their users are actually going to navigate MPEG-LA's licensing conditions and pay up is patently absurd.

(side note: these are the same people who would shit sideways if Microsoft was caught violating the GPL, so apparently their ethical standards of ownership are selectively applied wherever it makes sense, but I digress.)

Were Microsoft to dump Windows Media Player and start distributing VLC, MPEG-LA would be all over Microsoft like a fly on shit. Measly open source project with no revenue stream? Meh, I'll pass. Fortune-50 company?

Show me the money!
Show me the money, baby. Show me the money. (and no, I did not pay royalties to Tom Cruise. I guess we all fudge it on occasion)

How is it that this VLC product, coded by a few guys in their spare time, can do a job 20,000 Microsoft coders cannot make Media Player do? Again, the problem isn't a lack of programming talent; it's that the same standards of intellectual property that Microsoft is held to do not apply to the owners of the VLC project. Plain and simple. One option Microsoft has is to pay the licensing fees for a few dozen codecs, but this isn't cheap. It's one reason XP never had an MPEG-2 decoder (and thus millions of us threw up our arms in despair whenever we tried to play a DVD on Windows XP) and also the reason it doesn't have an H.264 decoder. Apple absorbs those costs to help prevent their customers from going insane. In any event, the fundamental issue has nothing to do with a lack of programmers, and everything to do with licensing costs and legal minefields.

A few other pot-shots before I close out this sorry tirade:
  • Windows doesn't play any MP4 files out of the box, because it lacks an MP4 parser entirely. (MP4 is a container format, and not an actual payload--I'm not sure if this is because of a licensing issue, or if MSFT's 20k programmers never got around to it) Me 1, Dvorak 0.
  • Windows Media Player is a universal player, because the underlying framework (DirectShow) can be extended to support almost any container or format. There is no technical reason WMP cannot playback any media file. Me 2, Dvorak 0
  • Windows Media Player does not want everything to be a WMV file. In fact, it doesn't even care what the file is. All it cares about is whether or not it can locate a parser to open the file, and a decoder for content inside of the file. The Windows Media codecs themselves are implemented as DirectShow (DMO) filters that WMP loads up when you hand it a file. Me 3, Dvorak 0
  • What's the point in ranting about WMP when you can just download VLC and give MPEG-LA the middle finger? Me 4, Dvorak -12
And this concludes my rant. Apologies in advance to FOSS (which I am still a proponent of) and the makers of VLC (which is by any measure, a superb but ethically perilous product).

Friday, August 01, 2008

RAID-0 and Amdahl's Law

When I see comments like this:

Ok, yeah, a 10k drive is fast. Guess what? Two 7200 drives in Raid 0 are faster. Four drives in RAID 0+1 are bigger, cheaper, faster, and... yeah. Better. You have a measly 300GB drive - I have a 1TB (2TB counting mirroring) array that's faster and with automatic mirroring for less than what you spent on one 10k drive. And your cost doesn't include the second slower drive for archiving.

...the little nerd-alarm in my head goes off, and I feel compelled to present the case against RAID-0. For those who aren't aware, RAID stands for Redundant Array of Independent or Inexpensive Drives. The basic idea is simple: by spreading data out over several drives in varying configurations, one can theoretically improve performance and/or protect against data loss. For example, by duplicating data across two separate drives, one drive can fail and there will be no loss of data. This is commonly called RAID-1.

RAID-0, on the other hand, is when data is evenly split between two drives. By doing this, you can read data from both drives simultaneously. However, this also doubles the propensity for data loss--a single drive failure means you lose big.

First of all, I should note that the ground I am about to cover has been discussed before. Anandtech evaluated RAID-o performance using two Western Digital Raptor drives by running them through numerous benchmarks, and concluded that:
If you haven't gotten the hint by now, we'll spell it out for you: there is no place, and no need for a RAID-0 array on a desktop computer. The real world performance increases are negligible at best and the reduction in reliability, thanks to a halving of the mean time between failure, makes RAID-0 far from worth it on the desktop.
StorageReview.com, long the most trusted source with respect to drive performance, has also made it quite clear how unthrilled they are with RAID-0. The administrator of the site, Eugene, states:
Given the recent return of various "should I raid?!?!??!" posts in the community, I should also take time to point out that the FarCry data presented above is relatively STR-heavy compared to the Office and even the High-End pattens that are broken down in the TB4 article.

Despite this, however, note that while (sequential transfer rates represent about 70% of all accesses in FarCry, the array spends only 15% of its time on sequential transfers. Doubling STR through a two-drive array halves this 15%.

Hence, the 10-20% improvement we see in SR's tests when going from a single drive to 2xRAID0 comes from the doubling in capacity (which, in the past, has more or less established itself as a 7-10% performance boost in our tests) + this small improvement.

In other words, sequential transfers already complete so quickly that they have in effect written themselves out of the performance equation. Doubling the performance of a factor that exerts such a small effect nets a small improvement... as one should expect.
What Eugene is getting at is drive performance can be broken down into two distinct factors: physically moving the actuator arm to the location of the data (i.e. seek time), and then actually reading the data. The former is quite slow, while the latter happens rapidly. RAID-0 has no affect on seek times, and effectively doubles read speeds--which means it doubles something that already happens quickly.

There is a formal name for this rule: Amdahl's Law. Consider a program consisting of two distinct routines, A and B:

The length of the line corresponds with how much time each routine takes. Notice that doubling A results in a far greater improvement than making B five times faster. Programmers frequently refer to Amdahl's Law when optimizing software--they first identify the longest running task, and then seek to optimize that task. Bad Programmers™ (yes, we exist) often spend time optimizing B, which is clearly less effective.

Now consider this graph (complements of Storage Review):

The first bar consists of the amount of time it takes a single Western Digital Raptor drive to perform a random read. The second bar consists of the amount of time it takes two Western Digital drives to perform a random read. The yellow section is the actual process of reading data, and the red section is the time it takes to seek to the location on disk. And this brings us to an important conclusion: RAID-0 really only has a big affect with respect to sequential reads, but sequential reads take so little time on modern hard drives that they are completely dwarfed by seek times. Doubling sequential read speeds means jack squat. Again, Eugene states that:
In a typical access pattern that features significant localization such as the Office DriveMark, an adept drive such as the WD740GD can achieve about 600 I/Os per second. Inversely stated, each I/O (which, again, consists of positioning + transfer) takes about 1.7 milliseconds. In a single-drive, highly-localized scenario, the Raptor average 1.7 milliseconds per I/O. Of this 1.7 ms, 0.3 ms, or 18%, is the transfer of data to or from the platter. The other 82% of the operation consists of moving the actuator to or waiting for the platter to spin to the desired location. The situation further polarizes itself as transfer rates rise. At 126 MB/sec, transfers consist of just 11% of the total service time. In effect, sequential transfer rates ranging from 50 MB/sec to 130 MB/sec and higher "write themselves out of the equation" by trivializing the time it takes to read and write data when contrasted with the time it takes to position the read/write heads to the desired location.

...

RAID helps multi-user applications far more than it does single-user scenarios. The enthusiasm of the power user community combined with the marketing apparatus of firms catering to such crowds has led to an extraordinarily erroneous belief that striping data across two or more drives yields significant performance benefits for the majority of non-server uses. This could not be farther from the truth! Non-server use, even in heavy multitasking situations, generates lower-depth, highly-localized access patterns where read-ahead and write-back strategies dominate. Theory has told those willing to listen that striping does not yield significant performance benefits. Some time ago, a controlled, empirical test backed what theory suggested. Doubts still lingered- irrationally, many believed that results would somehow be different if the array was based off of an SATA or SCSI interface. As shown above, the results are the same. Save your time, money and data- leave RAID for the servers!

...or for those of us that don't like verbosity, Eugene's article can be distilled: Amdahl's Law, Baby. There are situations where sequential read is important (e.g. video editing), but the propensity for data loss needs to be carefully considered with the potential trade offs. For the average desktop user, RAID-0 does not provide a substantial performance benefit and comes with an increased chance of data loss, and should be avoided at all costs.

Monday, June 16, 2008

The Joy of Smart Pointers

One of the biggest mistakes people new to DirectShow (and COM, for that matter) is using raw COM pointers. This article aims to present a compelling alternative.

All COM objects implement the IUnknown interface. IUnknown defines three standard methods, of which two are a concern of this article: AddRef() and Release(). When a COM object is created, its "reference count" (the number of objects holding claim to the newly created object) is set to 1. A program never directly deletes these objects; instead, a program calls Release(), which de-increments the reference count. If the reference count reaches 0, the object self-destructs.

The benefit of this is an object will remain in memory only as long as it needs to; the lifetime of an object is managed by its reference count. A programmer cannot accidentally delete an object from memory that another module or portion of the application may be using. Only when the object's reference count goes to 0 (as a result of all involved parties calling Release() on their interface pointers) does the object automatically destroy itself.

This is, sadly, a double-edged sword; by mismanaging reference counts, you can easily get yourself in trouble, and leak memory in a very real way.

Take the following function as an example:


HRESULT StartGraph( IGraphBuilder * pIGraphBuilder )
{
IMediaControl * pIMediaControl;
hr = pIGraphBuilder->QueryInterface( IID_IMediaControl, ( void ** ) &pIMediaControl );
if( FAILED( hr ) )
return hr;

return pIMediaControl->Run(); // THIS WILL LEAK A REFERENCE COUNT
}

This is how we would normally start a filter graph in DirectShow. This code technically works, but it has a serious flaw. Because it queries the filter graph manager for the IMediaControl interface, this increments the reference count on pIGraphBuilder. However, when we return, because IMediaControl was declared locally, it goes out of scope without ever being released, thus leaving behind an extra reference count on pIGraphBuilder. The above code is leaking a reference count, which means abandoned COM objects galore!

One way to (sort of) fix the above code would be:


HRESULT StartGraph( IGraphBuilder * pIGraphBuilder )
{
IMediaControl * pIMediaControl;
HRESULT hr = pIGraphBuilder->QueryInterface( IID_IMediaControl, ( void ** ) &pIMediaControl );
if( FAILED( hr ) )
return hr;

hr = pIMediaControl->Run();
if( FAILED( hr ) )
return hr; // THIS WILL LEAK A REFERENCE COUNT!

// Release our interface....
SAFE_RELEASE( pIMediaControl );
return hr;
}

There are still major flaws with this code. We successfully release the interface before exiting the function at the end; this will automatically de-increment the reference count on the filter graph manager. However, what if the call to IMediaControl->Run() fails? Again, we leak a reference count because we fail to release pIMediaControl! We successfully queried for the interface, which adds a reference to our Filter Graph Manager, but by exiting before we've called SAFE_RELEASE() we've again leaked a reference count. We could easily add another SAFE_RELEASE() call to the middle return, but consider this: what if this method was 100 lines long? 200 lines long? What if it had elaborate loops or other conditional logic? It becomes easy to miss a release, and the code is visually convoluted by clean-up logic.

The solution to this problem is to use Smart Pointers to manage reference counting. When a COM pointer wrapped with a Smart Pointer goes out of scope, it automatically calls Release(), thus alleviating the pain of manually releasing:


#include ; // For Smart Pointers!

...

HRESULT StartGraph( IGraphBuilder * pIGraphBuilder )
{
CComPtr pIMediaControl;
HRESULT hr = pIGraphBuilder->QueryInterface( IID_IMediaControl, ( void ** ) &pIMediaControl );
if( FAILED( hr ) )
return hr;

return pIMediaControl->Run();
}

Notice that we don't have to release the pointer at the end of the method; upon going out of scope, the Smart Pointer will automatically call Release().

Alternately, you can also use CComQIPtr (QI = Query Interface) for slightly cleaner code; the above could be written like so:


HRESULT StartGraph( IGraphBuilder * pIGraphBuilder )
{
CComQIPtr pIMediaControl( pIGraphBuilder );
if( !pIMediaControl )
return E_FAIL;

return pIMediaControl->Run();
}

Either method will work; the second is slightly cleaner but I prefer the former for more accurate HRESULTs.

There are still instances where you'll need to manually release a smart pointer--take the following GetPin() function for example:


//---------------------------------------------------------------//
// GetPin - returns a pin of the specified direction on the
// specified filter. Note that this will return connected or
// unconnected pins.
HRESULT GetPin( IBaseFilter * pFilter, PIN_DIRECTION PinDir, IPin ** ppPin )
{
if( ppPin == NULL || pFilter == NULL )
{
return E_POINTER;
}

CComPtr pEnum;
HRESULT hr = pFilter->EnumPins( &pEnum );
if( FAILED( hr ) )
{
return hr;
}

CComPtr pPin;
hr = pEnum->Next( 1, &pPin, 0 );
while( hr == S_OK )
{
PIN_DIRECTION ThisPinDirection;
hr = pPin->QueryDirection( &ThisPinDirection );
if( FAILED( hr ) )
{
return hr;
}

if( PinDir == ThisPinDirection )
{
// Found a match. Return the IPin pointer to the caller.
return pPin.CopyTo( ppPin );
}
// Release the pin for the next time through the loop.
pPin.Release();
hr = pEnum->Next( 1, &pPin, 0 );
}

// No more pins. We did not find a match.
if( hr == VFW_E_ENUM_OUT_OF_SYNC )
{
return VFW_E_ENUM_OUT_OF_SYNC;
}
else
{
return E_FAIL;
}
}

The above function is the same function defined in the DirectShow documentation, with several errors corrected. And, of course, no more sloppy raw COM pointers.

Note the IPin object pPin--because we're enumerating the filter for a pin, we have to manually call pPin.Release() before reusing the object. Also of note is the pPin.CopyTo() call, which is another convenient feature of smart pointers. CopyTo() should be used in the event of copying a smart pointer to a dumb pointer. However, in the above function, since the passed in pPin is either going to be a dereferenced smart pointer (which is, again, a dumb pointer) or a regular dumb pointer, the CopyTo() is appropriate.

One other feature of Smart Pointers is the overloaded assignment operator. Observe:


CComPtr pSomeFilter;


//Somewhere else
HRESULT CreateFauxFilter()
{
CComPtr pSomeOtherFilter;
HRESULT hr = pSomeOtherFilter.CoCreateInstance( CLSID_OfSomeOtherFilter );
if( FAILED( hr ) )
{
return hr;
}
else
{
// Here, we can use the "=" operator to copy one smart pointer
// to another smart pointer.
pSomeFilter = pSomeOtherFilter;
return hr;
}
}

So, pSomeFilter is assigned to pSomeOtherFilter, and when pSomeOtherFilter goes out of scope, it'll clean up any remaining reference counting issues. It's a handy way to only keep around a pointer if we know everything has occurred successfully. A common use of this is to instantiate all of your filter graphs with locally declared smart pointers, and only assign the smart pointers to your member variables at the end of the function.

You may think these are equivalent to the CopyTo() method of a CComPtr, but the assignment operator should only be used when explicitly copying one smart pointer to another. The CopyTo() should be used when copying a smart pointer to a dumb pointer.

Also, be mindful of accidentally mixing smart pointers, dumb pointers, and the assignment operator:


IBaseFilter * pDumbPointer;
CComPtr pSmartPointer;

pSmartPointer = pDumbPointer; // THIS IS OKAY (sort of)

pDumbPointer = pSmartPointer; // THIS IS VERY, VERY BAD!!

The really nasty thing about the latter assignment is that pDumbPointer will not equal NULL (thus evading many common checks for validity) and subsequent attempt to call any methods on the dumb pointer will result in Very Bad Things™. If you're mixing smart pointers with dumb pointers, consider removing the dumb pointers entirely, as the use of dumb pointers is generally a sign of a greater problem and using both is generally confusing and/or odd.