There are many reasons for differences between platforms, and several ways an API designer can try to rationalize such things.
- Some features fundamentally don’t exist, and are not practical to emulate. Earlier Radeon graphics cards do not support TextureAddressMode.Border, and there’s nothing we can do to change that.
- Other features could exist, but are too expensive to implement. Sure, we could have ported XACT to Zune, but we would have had to cut other features to find time. Would you be willing to trade XACT on Zune for Avatars on Xbox? No? I suspected as much 🙂
- Sometimes things are blocked for business policy rather than technical reasons. Personally, I would love to someday allow full LIVE support for Windows games, but I also understand why the powers that be didn’t want that. Pragmatism requires us to figure out the most consistent story possible within the constraints we are given.
So, there will always be some differences across platforms. What should we do about them?
- We could say "hey, this is not consistent, so let’s just cut it entirely from all platforms"
- We could remove the API only from the platforms that do not support it
- We could leave the API in place, but have it throw an exception
- We could expose caps bits to query if the API is supported
- We could make the API a graceful no-op, ignoring calls and returning default values
XNA Game Studio currently uses a mishmash of all five techniques, chosen for individual features on an ad-hoc basis. This is an area I think we have much room to improve.
The ultimate goal can be summed up in two words: "avoid surprises".
A surprise occurs when a friend tries to play your game, but it renders incorrectly because his GPU does not support TextureAddressMode.Border, and you didn’t know you were supposed to check the caps for that. Or when you try to port your game from Windows to Zune, only to realize you must rewrite your sound code because Zune does not support XACT.
The XNA Framework provides a set of features that are available and work the same way on all platforms, surrounded by a smaller set of features that are not so consistent. Trouble is, there is no good way to tell which features are which! It is too easy to accidentally stumble outside the consistent zone.
Let’s look again at my list of five techniques, this time with the goal of avoiding surprise:
- Cutting a feature certainly does avoid surprise, but also leaves us with a lowest common denominator platform. Sometimes this can be the right thing to do, but it’s not a good choice for important features that many people want to use.
- Removing APIs from specific platforms avoids runtime surprise, but creates a different kind of surprise if overused ("huh? This built fine for Windows, but now I get a ton of compile errors on Xbox"). I think this is best done at a very coarse level, where entire namespaces or assemblies are only available on some platforms. If individual classes or methods change per platform, it gets too hard to document and remember all the subtle differences.
- APIs that throw runtime exceptions are pretty bad. The exception can provide a nice error message, but you only get to see that message if you happen to test on a machine where the feature is not supported.
- Caps bits are even worse than exceptions. You still have to test on many machines, plus you have to figure out the cause of any compatibility problems, which isn’t always obvious even after you notice them.
- No-op can be a good choice, depending on how the API is used. When the Zune KeyboardState or Windows PictureCollection come back empty, that is a legal value even on platforms where these things are supported, so the caller should not be surprised and will not need special code to handle this case. But if OcclusionQuery always returned 0, that would surprise me, since I’m most likely planning on using the return value for something and am counting on it being correct!
Verdict: #1 and #5 are appropriate for some APIs. #2 is good at a coarse level, but should not be too fine grained. #3 and #4 should be avoided.