This tutorial covers the panel module — bordered boxes on a background layer, the building block of dialog windows and HUDs. Add panel to LIB_MODULES (it pulls in dma and console).
Every SNES RPG draws the same thing: a bordered box over the map with text in it. Every status bar is the same shape. Both are a 9-slice panel — four corners, four edges and a fill, stamped from a small 3×3 tile sheet so a box of any size reuses nine tiles:
The module owns the stamping and, importantly, the VRAM upload — which is the part that is easy to get wrong (see the forced-blank gotcha below).
Not to be confused with window — that's the PPU's masking registers. panel is unrelated furniture.
You declare the 32×32 tilemap buffer, not the module. That is deliberate: a panel tilemap is 2 KB and C RAM is only an 8 KB band, so a module that allocated one silently would spend a quarter of a game's RAM without saying so. Declaring it yourself keeps the cost visible — and lets two BG layers each have their own panel.
stride is the sheet's width in tiles. It is 3 for a bare 9-slice, but a sheet often carries other art in later columns — the RPG's is 4 wide, with HUD icons in the fourth column reached via panelPut().
Panels are stamped into the buffer, not uploaded individually. So a HUD at the top and a dialog box at the bottom are two panelDraw calls into the same map and a single panelFlush:
Closing just the dialog leaves the HUD untouched:
This is exactly how examples/games/rpg draws its HUD (hearts + a purse) and dialog box on one BG2 tilemap.
A 32×32 tilemap is 2 KB, which does not reliably fit the ~4 KB VBlank DMA budget alongside a game's own transfers. So panelFlush wraps its DMA in setScreenOff() / setScreenOn() — real forced blank (INIDISP bit 7).
If you were tempted to hand-roll the upload with setBrightness(0) around a dmaCopyVram, that is the trap the module exists to remove: setBrightness(0) blacks the screen but leaves the PPU fetching, so the VRAM write is still dropped — the tail of the transfer never lands. Only setScreenOff() opens the write window. Let panelFlush do it.
The panel is a BG layer; the text sits on another BG in front of it (text_config.priority + the mode's BG-priority bit). Layers stack scene < panel < text. Drawing text into the panel's own tilemap is not how it works — draw the box on BG2, the text on BG3.
The nine slices are read in raster order from base_tile across stride columns: base+0/1/2 (top row), base+stride/+1/+2 (middle), and so on. If the sheet is 4 wide (icons in column 4), stride is 4, not 3, or the middle row reads the wrong tiles.