Some platforms are more equal than others

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?

  1. We could say "hey, this is not consistent, so let's just cut it entirely from all platforms"
  2. We could remove the API only from the platforms that do not support it
  3. We could leave the API in place, but have it throw an exception
  4. We could expose caps bits to query if the API is supported
  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Comments (11)

  1. Erzengel says:

    Can you tell us why "the powers that be didn’t want [to] [allow full LIVE support for Windows games]"? Or at least your understanding?

  2. JoelBennett says:

    Erzengel: I’m guessing probably a fair bit of politics behind something like that.  Just think about this: if the whole Live platform was free to  develop for and run on, what would prevent developers from making games and sucking the Live bandwidth dry, while Microsoft foots the bill for everything?  At least with Xbox Live, Gold members pay a fee to use the service, which I imagine is used to maintain all the servers that everything is running on.  That, and what would prevent people from reverse engineering the Live components, especially if they have access to the API, more information, and the distributable DLLs?  I’m guessing cheating would definitely become a bigger issue for Live enabled PC games.

    That’s just a guess, but that could be part of it.

  3. Matthew says:

    How does your verdict fit with an example like TextureAddressMode.Border?

  4. Iarus says:

    One thing to note is that "Game for Windows" and "Game for Windows LIVE" are not just a brand name that takes lots of place on the cover of your game’s box.

    They are APIs, a good run of QA testing and more importantly TCRs (A TCR (Technical Certification Requirement) is a test case that must pass before software can be certified as compliant to a given standard).

    In XNA they are only good practices, with the GRWL stamp you MUST implement some things in a specific way if you ever want your game being released.

  5. Clayton Hughes says:

    And here I was rooting for cap bits for being the least surprising.

    While they certainly require more work on the client programmer’s end–they ought to query anything they want to do, and come up with an alternative (or let the user know why they’re failing–e.g. they’re on 1995 hardware) if it’s not supported.  You could even throw an exception if they call something unsupported.

    At some point, you’ll have to rely on the programmer to do a little bit of thinking on their own.

    Cap bits don’t really become a surprise if it’s the standard procedure for most of the API.

    I can see where they would fail, though:  In the worst case, cap bits could be essentially useless–if they’re going to have to roll their own Audio system because they’re doing cross-3-platform audio, then it might be easier to just ditch XACT entirely and use the same custom code on all three devices.

    On the other hand, cap bits make the most sense in an environment like the GPU department where you can ask things like "do you support x in hardware?" and if so taking that path, otherwise doing it in code.

  6. Morten says:

    Are you seriously implying that anyone cares about avatars on Xbox? 🙂

  7. ShawnHargreaves says:

    > And here I was rooting for cap bits for being the least surprising.

    Caps bits suffer from what Raymond Chen likes to call the "what if everybody did that" problem.

    For any single feature, sure, attaching a boolean to it is simple and not too confusing.

    But what happens when you have 100 different features, all with their associated boolean?

    That gives more possible feature permutations than a 32 bit integer can evaluate without overflowing 🙂

    I think the resulting minefield is a poor developer experience. When you can’t do anything without checking caps first, that makes it hard to write clear, maintainable code! And it’s even harder to properly test code that contains so many conditional branches and optional execution paths.

    In practice, I’ve never seen anyone properly check all of the possible DX9 caps. Most developers choose one of these simplification strategies:

    – Some people just code for whatever happens to work on their development PC, and are surprised when the resulting game doesn’t run on other hardware.

    – Some people code for the lowest common denominator hardware, carefully avoiding any optional features. Trouble is, pretty much everything in DX9 is technically optional!

    – Some people pick just two or three "feature levels" that their game will adjust to: the fancy version with all the special effects, the more limited version where they disable some of the fancier stuff, plus maybe a super stripped down version where they just render basic textured objects with no effects at all. They check caps at startup to decide which of these codepaths best fits the current hardware, then just use the selected rendering logic. This way they have just a handful of codepaths to maintain and test, as opposed to billions of possible overlapping permutations.

  8. JoelBennett says:

    Shawn: That’s one thing that amazed me about the Halo port for PC – it’d run on some amazingly low hardware (like Intel integrated graphics).  I was impressed that they even went that low with things.  It ran like a dog, but it did run.

    Oh, the amazing things you find out when bored in a college class and the person before you had installed Halo on the machine…

  9. AR says:

    When you enumerated that list of possibilities, it gave me another possible idea:

    Instead of checking cap-bits, why not *request* certain capabilities.

    This removes the possibility that you use a function without checking for capability (and end up with an exception or a mess when you port it). It also means the runtime could report what you’re using ("XNA: this won’t run on Zune").

  10. Adam says:

    That lack of the border caps bit being set on old ATI cards is not because the  cards can’t do border addressing at all.

    I’m not sure if XNA will let you do this, but with D3D9 on ATI cards that don’t have the cap bit set, if you ignore that and set the render state anyway it will usually work correctly.

    That doesn’t work with the debug runtimes enabled, because they stop the call getting through to the driver.

    According to the discussion linked below the cap bit is disabled because there are some edge cases where border doesn’t work according to spec, so the driver would fail WHQL testing if the cap bit was set.

  11. ShawnHargreaves says:

    > Instead of checking cap-bits, why not *request* certain capabilities.

    That makes the opt-in more explicit, but it doesn’t really fix the underlying problem if there are so many optional features that it isn’t clear how to navigate your algorithms through the resulting maze. What do you do when you request 50 optional features, but only 20 of them come back as supported? How do you test all the crazy number of possible permutations that can result?

Skip to main content