A wavy distortion effect that bends the entire screen like a funhouse mirror. Press A to toggle, D-pad to control amplitude and animation. This is the same trick used for water reflections in Chrono Trigger.
| Button | Action |
|---|---|
| A | Toggle wave on/off |
| D-Pad Left/Right | Decrease/increase amplitude |
| D-Pad Up | Start animation |
| D-Pad Down | Stop animation (freeze) |
Then open hdma_wave.sfc in your emulator (Mesen2 recommended).
Normal DMA transfers a block of data from one place to another — useful for loading tiles during VBlank. HDMA (Horizontal DMA) is different: it feeds a small amount of data to a PPU register every single scanline while the screen is being drawn.
Think about it: the PPU draws 224 horizontal lines per frame. If you change the BG1 horizontal scroll register between each line, every line on screen shifts by a different amount. Feed it a sine wave pattern and the whole screen undulates.
That's exactly what this demo does.
An HDMA table is a sequence of entries in RAM or ROM. The hardware reads one entry per group of scanlines:
The 0x81 byte means: repeat mode (bit 7 = 1), for 1 scanline (bits 6-0 = 1). Repeat mode writes the data on every scanline in the group. Without it, the data is written once and held — fine for registers that latch, but scroll registers need fresh writes every line.
This demo has 335 entries per table (224 visible lines + 111 extra for animation phase wrapping) × 3 bytes each + 1 end marker = 1006 bytes per amplitude level.
The demo pre-computes tables for 7 wave amplitudes (0, 4, 8, 12, 16, 20, 24 pixels):
All 7 × 1006 = 7042 bytes live in ROM. Zero runtime math — the CPU just points the HDMA channel at the right offset and the hardware does the rest.
Why 335 entries instead of 224? Animation works by shifting the starting offset into the table. With only 224 entries, the phase offset would run off the end. The extra 111 entries (one full sine period = 112, minus 1) provide wrap-around space so animation can cycle smoothly without bounds checking.
The SNES has 8 DMA channels (0-7). This demo uses channel 6 for HDMA:
Mode 2 writes two bytes to the target register, which is exactly what BG1HOFS needs — it's a 16-bit register written as two consecutive bytes to the same address.
Why $210D specifically? BG1HOFS (BG1 Horizontal Offset) controls how far BG1 is scrolled horizontally. By writing a different value to it on every scanline, each line of pixels shifts by a different amount. That's the wave.
Each frame, the main loop advances the phase and recalculates the table address:
amp_offsets[] is a pre-computed lookup table to avoid multiplying by 1006 at runtime:
The animation "moves" by starting the HDMA read from a different point in the sine table. Phase 0 starts at the beginning, phase 56 starts halfway through the sine cycle. The wave appears to flow because each frame shifts the starting position by one entry.
To make the wave visible, the demo fills BG1 with alternating black and white tiles:
Without some visual pattern, the wave would be invisible — you'd just see a flat color shifting left and right. The checkerboard makes the distortion obvious.
animating is set (press Up). Without animation, the wave is static and only changes when you adjust amplitude.HW_HDMAEN in the main loop after WaitForVBlank(), which is safe because we're still in VBlank at that point.Notice there's no ASMSRC and no graphics tools. The entire HDMA table — all 7042 bytes of pre-computed sine waves — is a static const array in main.c. The compiler places const data directly in ROM (in a SUPERFREE section), so it costs zero RAM.
The checkerboard background tiles are also generated at runtime with a simple loop, not loaded from an asset file. This makes the example fully self-contained: one C file, no external dependencies.
When should data go in C vs. assembly? Rule of thumb: if the data is generated by a tool (gfx4snes, smconv) or is a binary blob (
.pic,.pal,.brr), put it in assembly with.INCBIN. If the data is hand-written or computed (lookup tables, HDMA tables, font tiles), aconstarray in C is simpler and keeps everything in one place.
| Module | Why it's here |
|---|---|
console | consoleInit(), WaitForVBlank(), NMI handler |
dma | Required by sprite module (OAM DMA). Not used directly — HDMA is configured via raw register writes, not library functions. |
sprite | OAM buffer for the NMI handler. Even with no visible sprites, the NMI handler DMAs the OAM buffer every frame. |
input | Joypad reading — the NMI handler fills pad_keys[] for the A/D-pad controls. |
**"Where's the HDMA module?"** The library has
hdma.c/hdma.asm, but this example doesn't use them. HDMA setup is just 5 register writes —DMAP6,BBAD6,A1T6L/H,A1B6,HDMAEN— simple enough to do inline. The library's HDMA API is more useful when you need to manage multiple HDMA channels dynamically.
| Register | Address | Role in this example |
|---|---|---|
| BG1HOFS | $210D | HDMA target — per-scanline horizontal scroll |
| DMAP6 | $4360 | Channel 6 mode (mode 2 = 2 bytes to same reg) |
| BBAD6 | $4361 | Channel 6 target register ($0D = BG1HOFS) |
| A1T6L/H | $4362-63 | Channel 6 source address (table pointer) |
| A1B6 | $4364 | Channel 6 source bank |
| HDMAEN | $420C | HDMA channel enable (bit 6 = channel 6) |
| File | What's in it |
|---|---|
main.c | Everything — sine tables, HDMA setup, input loop (~573 lines, mostly data) |
Makefile | LIB_MODULES := console dma sprite input hdma |