2016/10/25

Development Insight #1: SMB3 Status Bar

For the longest time now, I've been brainstorming what the perfect status bar would be like, ignoring the actual content of the status bar (as it is usually hack-specific) and focusing on the design and implementation. If I had to use a single word to describe the perfect status bar, it would be "unintrusive". Unintrusive not only to the player, but also to the hack designer. The perfect status bar can just be patched once and forgotten, as it is completely isolated from everything else. As a hack author, there is no need to worry about your level ideas being compromised due to the status bar getting in the way.



Super Mario World's status bar is a standard but effective one, remniscent of the original Super Mario Bros' HUD. Being on a separate layer from the backgrounds (layer 3), it can stay static while everything else scrolls under it. However, there is one major flaw with the status bar. The IRQ that fires below it allows for an image that also resides on layer 3 to show up. But if that image scrolls into the status bar, it will cut off. This limits what can be done with layer 3 backgrounds, and thus becomes an intrusive aspect of the status bar.

There was a patch made by Roy a long time ago that set a black background on the status bar. Every single status bar that I've made is derived in some form from this patch. I make that noticeable by keeping the names of the original NMI and IRQ hijacks, PreStatusBar and PostStatusBar, even if they no longer apply or make sense. Anyways, the point is that setting a (black) background on the status bar solves the layer 3 cutoff issue. If a layer 3 image scrolls into the status bar, it cutting off is no longer noticeable, as it'll just appear as if it's going "behind" the status bar. However, this method does not completely solve the intrusiveness of the status bar. Since the status bar is still rendered onto the layer 3 tilemap, if the layer 3 image scrolls high or low enough, a copy of the status bar (a "ghost" status bar) will show up onscreen. In terms of achieving the perfect status bar though, Roy was on the right track.


The SMB3 Status Bar patch is one of my most recent projects, one of the many that I've started and one of the few that I've finished. It's basically a patch that replaces the default SMW status bar with one that looks like SMB3's. Complete visual accuracy was not sought for reasons similar to those revolving the creation of the SMA2 Status Screen patch (which I'll cover another time). Although I initially started the project as another port attempt (like the SMA2 status screen), I realized that the SMB3 status bar has properties that can allow it to approach the perfect status bar that I've sought. And so I got to work, using the bottom version of the Minimalist Status Bars as a base. Here I will go in-depth on the various aspects of the patch and the ideas behind them.


Despite the original SMB3 status bar being on a 4bpp layer 1, I put this one on a 2bpp layer 3. There are several reasons why I took this route:
  1. The SMW status bar is on layer 3, so why not keep it there?
  2. Being 2bpp, the graphics takes up less space in VRAM, and less palette space
  3. Ultimately, it's easier to mess with layer 3 than with layers 1 and 2
Due to the original SMB3 status bar only using 1 palette and not too many colors on the frame, the 4bpp->2bpp downgrade is not noticeable except on things like the coin icon and P-meter.

(Note: whenever I refer to "the original SMB3" or "the original SMB3 status bar", I am referring to the SMAS version, and not the NES, which sounds weird since it came out after SMW. Please understand.)

Keeping in mind my idea of a perfect status bar, I decided to isolate the tilemap and the palette of the status bar, ignoring whether SMB3 did the same. The palette is dynamically uploaded to palette 0, and the tilemap is located in the layer 3 GFX region rather than the layer 3 tilemap region (I eventually isolated the tilemap even more, see later)


Implementing the various counters was not very difficult, since most Mario games share the same ones (lives, coins, score, etc). The "World" counter requires user-input, as the original SMW does not specify what level is part of what world (the same had to be done for the SMA2 status screen). Porting the P-Meter, a key feature of the SMB3 status bar, was easy thanks to an existing SMW P-Meter patch made by Ersanio, though I did optimize/alter the code a bit to fit it into the status bar routine rather than use free space.


Another key feature of the original SMB3 status bar are the level-end item cards displayed on the bottom right. As SMW features a totally different level end, keeping these would be useless. So I (cleverly) decided to reuse the boxes for 3 counters that the original SMB3 doesn't have: yoshi coins, bonus stars, and the item box.


Speaking of the item box... one of the BIGGEST roadblocks I faced, and one of the reasons the patch became the complex voodoo it is now, was implementing that darn thing. Allow me to explain.

As I mentioned earlier, the original SMB3 status bar featured level-end item cards, which displayed certain sprites. However, these sprites are simply part of the status bar; they are layer 1. The SMB3 status bar disables sprites on the scanline it begins so that none can possibly show up over it, and has GFX for the items alongside the other status bar GFX, and also throws in palettes for the items. A simple technique, but also rather limiting. Which means I did not want to go that route for the item box. First of all, since the status bar is layer 3, the item GFX would have to be 2bpp. Downgrading the rest of the status bar is ok, but the colorful items? Also that would be even more VRAM space wasted. So I decided to go with the "normal" route; keep the item box item as a sprite.

A layer 3 status bar is usually one with the priority bit set in $2105. This means that the status bar has maximum priority over everything, including sprites. So how does one display a single sprite, and ONLY that sprite, onto something that has priority over everything, without any strange side effects? "Lower the priority of layer 3". That would mean most sprites would appear over it, also layers 1 and 2. "Lower the priority only on the tiles that hold the item box item, and keep the high priority on every other tile". This is the natural logical flow, but it is flawed, unless you use it as a base for another technique (which I did for version 1.0/1.1 of the patch). The reason it's flawed is because if the item box item is not a perfect square shape (chances are they arent), then sprites will appear behind the item at the edges. Also, unless you changed FirstSprite, certain sprites (not standard) can show up above the item box item. So how to do it?

For the first and second versions of the patch, I went with a masking window +  sprite priority shenanigans. Basically:
  1. Create a window that masks sprites everywhere except for a certain 16x16 area where we want the item box item. This will prevent sprites from showing up on the status bar no matter its priority setting.
  2. Make sure the layer 3 tiles in that 16x16 area are low priority.
  3. Place the item box item within that area.
  4. Place a square sprite within that area. This square sprite needs to have lower priority than both the item box item (clarification: must be the very next OAM index) and layer 3. This means that the sprite itself can look like anything as long as its completely square. Thankfully, a SMW default exists in the form of the message box.
  5. Change FirstSprite to the item box item. The item box item is now the sprite with the highest priority, and the square masking sprite is the 2nd highest, and all other sprites will appear behind said masking sprite (and thus behind layer 3, due to how sprite-layer priority works).
The first iteration of the patch used several IRQs to achieve the above method, since windows only have a left and right position, and thus their vertical edges must be adjusted in H-blank. The second iteration of the patch used 1 IRQ, and an HDMA to change the window positions. To make it easier for the end-user (less intrusive), said HDMA uses the DMA channel, so no channels are technically wasted. However, since NMI overwrites the DMA registers all the time, we need to rewrite them in our IRQ every frame. And since we're starting HDMA mid-screen, we need to adjust several other HDMA registers. As genius as this method is, it requires 1 extra OAM slot for the masking sprite, alongside the regular OAM slot used for the item box item. Also, there's no easy way to have another desired sprite show up on the status bar. I was content nevertheless, and this version became the "final" version.

For round 1 of the ASM contest, I decided to recreate the Kefka fight from Final Fantasy VI. The bottom menus in that game (the ones showing party stats and attacks and stuff) are on layers 1/2. I did the same (except I used Chrono Trigger's menu GFX for unknown reasons, also Chrono Trigger's menu shading technique but that's off-topic). There's a little hand arrow that you use to select attacks/etc; this thing is a sprite. How does the game display a sprite in an area where other sprites don't show up? Well, it probably just doesn't have other sprites move into that region (the most likely situation). Or it uses the technique I used above. Nevertheless, for some strange reason, I didn't think about reusing the technique I used for the SMB3 status bar, but instead I went with a more... elegant method. A method that I ended up using in the SMB3 status bar, replacing the other one. Basically, "let's rewrite OAM midscreen" aka replacing OAM with a new one that shows different sprites in the menu area.

OAM is 544 bytes (512 bytes low table + 32 byte high table). This is a lot to upload midscreen; I quickly realized that. But you don't have to rewrite all of OAM. The goal is to move all sprites offscreen, and then overwrite whatever slots with your own. The standard way of moving sprites offscreen is to move them BELOW the screen. Rather than write to the Y position (and thus the low table), it would be much easier (and faster) to only write to the high X. High X resides in the 32byte high table. 544 bytes is a lot to upload midscreen, but 32 bytes is actually manageable. So I did just that. With a small window to block sprites on the left 8px, since if you set the high X they can still show up in that area when their position is > x1F0 (note that i also force a size of 8x8 for each sprite in addition to forcing a high X position).

It was only natural that I decide to revisit the SMB3 status bar patch with the intent of implementing this strange new technique. Not only will the item box item now use up 0 OAM slots rather than 2, but other sprites can show up in the status bar without wasting OAM slots.


At this point, I remember that, although I moved the status bar tilemap out of the layer 3 tilemap, it's still "wasting space" in the layer 3 GFX region. As Mario's GFX are dynamically uploaded every frame, I decided to move the tilemap there. Here's how it works:
  1. Mario's GFX (and Yoshi's GFX) are uploaded to $6000 during NMI every frame.
  2. The screen is displayed. Mario will have his proper GFX.
  3. IRQ hits. I upload the status bar tilemap to $6000. This overwrites Mario's GFX.
  4. The rest of the screen is show, this time displaying only the status bar. Since Mario does not show up in this region, his glitched GFX will not be apparent (except in ZSNES)

This tilemap change was implemented alongside a few customization options into what would become the now-final version of the patch, v1.4 (download here!)


So in the end, I managed to squeeze a tilemap, palette, and OAM upload into 3 lines of f-blank, including DMA setup (using 1 channel), setup of other registers for the status bar, and time for more palettes/OAM tiles. It literally just barely fits; a few more cycles and it'll glitch the next scanline. Optimization with a side effect of obfuscation. And that's how I turned the SMB3 status bar into a near-perfect status bar. The one aspect of the status bar that I was not able to isolate was the graphics. Dedicating a region of the regular layer 3 GFX to the status bar is the best I can do for now.

No comments:

Post a Comment