BLZ & the plaintext-ARM9 trick

DS game only γ¨γ‚“γŒγ‚Šγƒœγ‚¦γ‚·γ¨γŠγ—γ‚ƒγ‚Œγͺまほう぀かい The field guide documents the DS game's internals.

Backward-LZ compression Nintendo uses for executable ARM9 code, plus the CompressedStaticEnd patch that lets us load ARM9 as plaintext for in-place string editing.

Nintendo's DS firmware boots an ARM9 binary that's compressed with a custom backward-LZ scheme commonly called BLZ ("Backwards LZ"). Unlike the LZ11 wrapper used for asset containers, BLZ is designed for in-place decompression of executable code: the compressed bytes occupy the high end of a buffer, and the decompressor walks backward through them, writing literal bytes and back-references into the same buffer toward lower addresses. The decompressed code ends up at the buffer base, and the BLZ footer at the high end is overwritten by the last few decompressed bytes.

Where BLZ is used in this ROM

SurfaceROM offsetCompressed sizeDecompressed size
Main ARM9 binary 0x4000 564,488 bytes 723,032 bytes (0xB0858)
Overlay overlay_NNNN.bin (most) per overlay table at 0x8DE00 varies (see overlay table) varies; runtime decompressed by engine

Overlays use the same code-LZ variant but with a per-file is_compressed flag in the overlay table. Flip that bit to 0 and store the overlay body uncompressed and the engine will load it as plaintext β€” that's the same idea as the ARM9 CompressedStaticEnd trick, applied per overlay.

BLZ footer

The last 8 bytes of a BLZ-compressed payload form the footer the backward decompressor reads first:

Offset (from end)SizeField
-0x084add_size β€” bytes to grow the buffer past the compressed region (= decompressed length βˆ’ compressed length).
-0x043header_size β€” bytes from the buffer end that are footer + literal-tail (decompressor stops after consuming these).
-0x011Reserved / pad.

Decompressed-payload layout (the "module params" struct)

Once the BLZ payload is decompressed, the resulting ARM9 plaintext starts with a small NitroSDK module params struct that the secure-boot loader uses to set up .bss initialization and decide whether to skip the compression step on subsequent loads. The fields most relevant to patching are at the very start of the plaintext:

Plaintext offsetFieldNotes
+0x14AutoLoadDoneCallbackSet after .bss init runs.
+0x1CCompressedStaticEndPointer to the end of the BLZ-compressed region in main RAM. If this equals StaticBSSStart, the loader treats the binary as already-decompressed and skips BLZ entirely.
+0x20StaticBSSStartPointer to the start of .bss (zero-initialized region).
+0x24StaticBSSEndPointer to the end of .bss.

The "plaintext ARM9" patcher trick

For string-pool edits like the 26-slot ARM9 menu pool (see ARM9 menu pool), re-running the BLZ encoder on every patch attempt is awkward β€” the encoder is fiddly, the compressed-size footprint shifts as strings change, and round-trip verification is slow. Easier: load the ARM9 as plaintext and skip BLZ entirely.

  1. Patch the module-params field CompressedStaticEnd := StaticBSSStart in the ARM9 plaintext (in this ROM, StaticBSSStart = 0x020A87A0).
  2. Store the entire decompressed ARM9 (723,032 bytes) verbatim at ROM offset 0x4000 in place of the original 564,488-byte BLZ blob.
  3. Update the NDS header's ARM9 size field to 0xB0858 (the decompressed length).
  4. Boot. The loader sees CompressedStaticEnd == StaticBSSStart, decides "nothing to decompress," and copies the plaintext bytes verbatim to main RAM.

From that point on, editing the embedded string pool is just bytes-at-an-offset: locate the slot, write the UTF-16-LE EN string with NUL pad, done. The Nintendo SDK validation marker NitroCodeBE isn't present in this ROM (Konami stripped it), so there's no extra fix-up needed for that.

Re-compression research

The plaintext-ARM9 trick is the simple path. For overlays we keep an alternative path open: re-LZ-compress the modified overlay binary using ndspy.codeCompression.compress() and emit the re-compressed blob with the original is_compressed bit still set β€” this matches the Konami on-disk shape more closely than a plaintext-overlay payload. The per-overlay fit/no-fit analysis lives at notes/_lz_compression_research.py in the translation repo; it round-trips an overlay through ndspy.codeCompression, simulates the worst-case body (slot filled with placeholder x's) and an alternate EN-shaped body, and reports whether the re-compressed size fits inside the overlay's original on-disk slot.

Source