Asset packing

Offline data conversion tool of Jazz² Resurrection.

AssetPacker is a standalone command-line tool in "Sources/Utilities/AssetPacker" that converts original Jazz Jackrabbit 2 data into the layout a given platform loads. The game performs the same conversion on its first run (see GameEventHandler::RefreshCache, which forwards to the shared Jazz2::Compatibility::AssetConverter driver), and that stays the second, in-game way of doing it. This tool exists so the data can be prepared ahead of time — for the platforms that cannot convert anything themselves (the consoles and the web build), and for build pipelines.

Beside the full conversion it offers several single-file commands: packing and unpacking bitmap fonts (see Jazz2::UI::FontFormat), making the indexed font images editable, and re-encoding .j2v cinematics into the game's own Jazz2::VideoFormat container.

Building the tool

The tool is host-only — it is built by default as part of the desktop CMake build (NCINE_BUILD_ASSET_PACKER, default ON) and skipped for every cross-compiled target. It links only the base layer and the original-data converters, deliberately not the engine, so it has no renderer, no window backend and no networking. After a regular desktop build the binary is at "build/Utilities/AssetPacker/AssetPacker".

Command-line reference

AssetPacker [<command>] [<source>] <target> [options]

The command may be omitted, in which case convert is assumed:

  • convert [<source directory>] <target directory> — Converts the original game data
    • <source directory> — Directory containing the original game files (Anims.j2a, *.j2l, *.j2t, ...), or a game installation that keeps them in a Source subdirectory — in which case its Content is copied to the target as well. May be given as --source= instead, which leaves <target directory> the only positional argument
    • <target directory> — Directory the converted data is written to (created if needed)
    • --source=<dir> — The same directory as <source directory>, named explicitly. Giving both is an error rather than an override, since they name the same thing
    • --content=<dir> — Directory holding the game's own content — the fonts, Animations, Metadata and translations that are not derived from the original data, which is the repository's Content. Overrides one found beside the originals, and is what makes a console or web tree self-contained when the two halves are not kept together (which outside a game installation they usually are not)
    • --target=<profile> — desktop (default) | console | dreamcast | wii | gamecube | psp | ps2 | n64 | emscripten (or web)
    • --n64-tools=<dir> — Where libdragon's host tools are (the toolchain prefix or its bin), for --target=n64; defaults to $N64_INST. See Preparing content for the Nintendo 64
    • --video-downscale=N — Downscale cinematics by N (1–4), 1 keeps them at their original size. Cinematics are re-encoded for dreamcast and ps2 (or any N > 1) and otherwise copied unchanged, desktop gets none, as the game reads the originals. Defaults to what the profile asks for — 2 for ps2, which is what that console displays — and to 1 everywhere else
    • --originals-only — Convert only the episodes and levels the original game shipped
    • --shareware-only — Convert only what the Shareware Demo shipped (implies --originals-only)
    • --all-videos — Deploy every cinematic found, not just the two the game plays
    • --skip-non-episode-levels — Convert only levels that belong to an episode
  • pack-font <source .png> <target .font> — Packs a grid image and the character list next to it into a single file. The list is read from <source .png>.json, or from the binary <source .png>.font if there is no JSON next to the image
  • unpack-font <source .font> <target .png> — Unpacks a font back into a grid image and a character list, ready to be edited and packed again. The list is written both as <target .png>.json, which is the one to edit, and as the binary <target .png>.font
  • apply-palette <source .png> <target .png> — Replaces the palette indices of an image with the colors they stand for, so it can be edited
  • to-indices <source .png> <target .png> — Resolves the colors of an edited image back to the nearest palette indices
  • recompress-video <source .j2v> <target .j2v> [--video-downscale=N] — Re-encodes one cinematic on its own, N defaults to 1, which keeps the original resolution
  • convert-music <source .j2b> <target .xm> — Translates one of the game's Galaxy Music System modules into a FastTracker II module, the first half of what --target=n64 does to the music
  • swap-content <source image> [<target image>] --content=<dir> — Replaces the Content directory of an already built console image — a Dreamcast .cdi, a PlayStation 2 .iso or a Nintendo 64 .z64 — keeping its bootstrap and its executable exactly as they are. The image is rewritten in place when no target is given

Progress and warnings are printed to stdout, errors to stderr, and the exit code is non-zero on failure.

Converting the game data

The source can be either a directory of original game files or a whole game installation, which keeps them in a Source subdirectory next to the Content the game ships and the Cache it converts into. Pointing the tool at the installation is the convenient thing to do, so it looks for both layouts — and when the source turns out to be an installation and the target is not the desktop profile, the installation's own Content (fonts, animations, metadata, translations — nothing of which is derived from the original data) is carried over as well, which is what makes the output a complete self-contained tree. The tool requires Anims.j2a (or the shareware AnimsSw.j2a) to be present, sprites and sounds are converted from it into a package — Source.pak for the desktop profile, Prebaked.pak for the others, which is what tells the game the tree needs no conversion — and the levels, tilesets, episodes and music follow via Jazz2::Compatibility::AssetConverter.

A conversion therefore has two inputs, and an installation is simply the case where they happen to sit together. Working from a checkout they do not: the repository has the game's own Content and no original data, and a copy of the original game is the other way round. Naming them separately is what that case wants, and it needs nothing staged:

AssetPacker ./build/ConsoleContent --source=<original Jazz Jackrabbit 2> --content=./Content --target=console

--source= names exactly what the positional argument does, so both go through the same resolution and an installation is still recognized through either; giving both is an error rather than an override. --content= wins over a Content found beside the originals, which is the one way the two can disagree. With --source= given, the target is the only positional argument left — which is why a lone one is read as the target and not as the source.

Target profiles

What the output directory is going to be loaded by decides its layout:

  • desktop — The converted data goes into a Cache subdirectory of the target, and an index file (Source.idx) is written alongside — the descriptor the game checks on startup before deciding to reconvert. It must stay identical to what GameEventHandler::WriteCacheDescriptor writes, field for field (signature, file type, cache version, flags, the modification time of Anims.j2a, the event count and the build version) — the game compares every one of them and reconverts everything if any disagrees. Music is not copied, because the desktop game reads it from its own Content/Music, and cinematics are left alone, because the game reads the originals where they are.
  • console (also selected by dreamcast, wii, gamecube, psp and ps2) — A staged Content tree written directly into the target, holding everything the console loads. The consoles all consume the same staged tree, so they share one profile — only the cinematics are decided per platform, which is why dreamcast and ps2 are tracked separately (see below). The sprite and sound package is named Prebaked.pak here, and that name is the marker: a console that can convert (the PSP, the Wii and the PlayStation 3, whose content is on writable storage) finds it and skips the conversion instead of looking for original files that were never deployed — see Jazz2::ContentResolver::IsContentPrebaked(). No index is written, deliberately: a prepared tree is never rewritten, and an index there would only invite the game to try.
  • emscripten — Likewise a tree prepared entirely ahead of time for the web build, with Prebaked.pak and without an index.

Both non-desktop profiles also put the game's own Animations and Metadata inside Prebaked.pak instead of next to it — a few hundred small files become part of one, which is what a console pays for on a memory card, a memory stick or a disc. They are the two directories the game reads through the package layer, everything else the tree carries (the translations, the levels, the tilesets, the music and the cinematics) is read as a loose file and stays one. The conversion writes its own files first, so an asset the original data provides is what a path present in both resolves to, exactly as when the two are kept apart. The desktop profile is untouched by this: its Source.pak holds only what was converted, and the game reads its Content from where it is installed.

A hand-made sprite sheet from Content/Animations that is a grid of more than one frame and wider or taller than 512 pixels is re-laid out on its way into the package (Jazz2::AssetPacker::SpriteRepacker): every frame is trimmed to the pixels it covers and packed the way the converted sheets are, keeping its place within its cell. A grid of that size puts frames across the 512-pixel page line of the PSP, which draws them cut off; the water shield (528×69) and Lori's end-of-level animation (657×464) are the ones this applies to today. Smaller grids are copied as they are, because some of them are sampled as a whole texture rather than frame by frame.

They also leave the .po files of a Translations directory behind. Those are the sources the .mo the game reads are compiled from and nothing loads one at run time — both the language list and the About section's translator credits require the .mo extension — while being about twice the size of the .mo they produce. That is a megabyte and a half of a tree that will be burnt onto a disc or packed into a cartridge, so it is not carried. The desktop profile copies no Content at all and is again unaffected.

Level filters

--originals-only keeps the episodes the original game shipped plus every level reachable from one of them, --shareware-only narrows that to the Shareware Demo content, and --skip-non-episode-levels drops the levels that belong to no episode. Whatever is skipped is listed by name rather than only counted — the list of levels the original game shipped is maintained by hand, and this is how a name missing from it shows up.

Music follows the same rule with one addition: a track is carried if a level that survived the filter asks for it, plus the five the game plays outside any level — menu, bonus2 and bonus3 for the main menu, and intro and ending for the cinematics. Those appear in no .j2l, so a tree built only from what the levels name left the menu and both cinematics silent on every console (the desktop build never noticed, because it reads the originals in place).

Cinematics

Only two cinematics are ever played by the game (Intro and Ending), Logo is present in the original data but nothing asks for it, so it is deployed only with --all-videos. The files land in a Cinematics subdirectory under lowercase names, which is how the player looks them up. What happens to them depends on the target:

  • Nothing for the desktop profile — the game reads the original files where they are
  • Copied unchanged for every target but the Dreamcast and the PlayStation 2 — they decode the original format perfectly well and are better off with the smaller file
  • Re-encoded for dreamcast and ps2, or whenever --video-downscale is greater than 1 (on any profile), since downscaling means re-encoding either way

Two consoles need the re-encoding, for the same reason at different sizes: inflating the original container costs the Dreamcast 55–115 ms per frame against a 42 ms budget, where the re-encoded one costs under one millisecond, and the PlayStation 2 is in the same position. The PS2 additionally defaults to --video-downscale=2, because it can only draw a 640×480 cinematic at 320×240 — the player caps the frame texture at the drawable and then picks every second index back out, per frame, on the CPU. Doing that here instead costs the same picture and none of the work.

The .j2v recompression

The original .j2v videos (CineFeed signature) are 640×480 in four interleaved zlib streams — opcodes, the offset and row parts of copy-from-previous-frame runs, and the literal pixels plus palettes — and the decoder has to inflate every frame at playback time. Jazz2::Compatibility::J2vRecompressor decodes the video exactly the way the player does (including decoding into one reused frame buffer, so pixels no run covers keep the previous frame's value), optionally downscales it once by picking every n-th pixel of every n-th row — matching what the player used to do at runtime — and re-encodes it into the game's own Jazz2::VideoFormat container. The player detects the format by its signature and accepts both.

The output container keeps the same idea — 8-bit indexed frames encoded as changes against the previous one — but replaces the entropy coding with a byte-oriented delta-RLE codec whose decoder is nothing but memcpy and memset over a reused frame buffer. A file starts with the same signature and type/version fields every other file the game writes uses, followed by width, height, frame delay, frame count, pixel format, codec and an extension area older players skip, then one size-prefixed payload per frame. Each frame is a flag byte (bit 0 — a 256-entry BGRA palette follows) and then commands until the end-of-frame marker: literal spans stored as they are, runs of one repeated byte, and skips that leave the previous frame's pixels in place — which is what makes an unchanged region nearly free. Each command has a short single-byte form and a long form with a 16-bit count, the exact opcode ranges are documented on Jazz2::VideoFormat.

Note the recompressed file is substantially larger than the zlib-compressed original at the same resolution — that is the deliberate trade: entropy coding is exchanged for a decoder that fits the weakest platform's frame budget. Platforms that can afford the inflation get the original file copied instead. recompress-video re-encodes a single file outside a full conversion, which is the way to experiment with resolutions per target.

Font packing

A font is authored as a grid image — one equally sized cell per character — next to a sidecar description listing the cell size, the character set and how far the pen moves for each character. The game instead loads a single self-contained .font file (version 2, described by Jazz2::UI::FontFormat) in which every glyph has been measured down to the pixels it actually inks, packed against its neighbours and stored as palette indices. pack-font and unpack-font are the two directions of that conversion, so a shipped font can be opened up, changed and packed again without the grid form having to be kept around in the repository.

The description comes in two interchangeable forms. <image>.png.json is the one meant to be edited, <image>.png.font is the binary form the sidecar has always been kept in, holding the same fields packed into a few bytes with the characters outside the ASCII range spelled out in UTF-8. unpack-font writes both, and pack-font reads the JSON wherever it finds one next to the image and falls back to the binary form only when there is none — so editing the JSON is enough, and a font unpacked by an older build still packs. Which one was used is printed.

What pack-font does:

  • Trimming — Every glyph is reduced to its inked bounding box, what is left of the cell is recorded as the glyph's bearing, so drawing it at the trimmed size in the trimmed place puts every pixel exactly where the full cell used to put it. A glyph with no inked pixels at all (a space) has zero size and is only advanced over.
  • Index normalization — A font is entirely index based: every pixel either names a palette color or is index 0 and draws nothing, there is no per-pixel coverage. Pixels that say otherwise (transparent in one channel but not the other, or partially covered) are resolved to one of the two, with a warning listing how many were touched.
  • Packing — The same layout the converted sprite sheets get (Jazz2::Compatibility::JJ2Anims::PackRectangles()), with a margin of FontFormat::GlyphMargin (one pixel) on each side of every glyph, so neighbours are two pixels apart — enough that a bilinear sample taken at the very edge of one glyph cannot reach the next. Every atlas width is tried and the one chosen minimizes the padded area first (which is what the console backends that need power-of-two textures pay), then keeps the atlas within one 512×512 page if it can, then minimizes the exact area (which is what everyone else pays), and the atlas is stored at its exact size. No glyph is placed across a multiple of 512 on either axis, because the PSP splits a larger texture into pages of that size and draws every primitive from one of them. The atlas may not exceed 1024×1024, a glyph may not exceed 255 pixels in either direction and a bearing may not exceed 127 — exceeding any of these is an error.
  • Encoding — Everything except the few identifying bytes (signature, file type, version, flags, compressed size) is one deflate block: the atlas size, line height, base spacing and character counts, then a 9-byte glyph record per character (position, size, bearings, advance — with a 32-bit codepoint prefix for the non-ASCII ones), then the atlas itself as one palette index per pixel in the same QOI-based encoding every other image asset uses. The ASCII range is described by two bytes (where it starts and how long it is) and everything else by a 16-bit count, so a font may have at most 255 characters in the range and 65535 outside it — exceeding either is an error.

unpack-font rebuilds the grid image and both sidecars from a packed font — the glyphs are placed back into cells at their bearings, so packing the result again measures the pixels afresh and arrives back at the same font. Because the unpacked atlas holds palette indices, it looks like near-black noise in an image editor, apply-palette and to-indices are the two halves of making it editable — the first replaces the indices with the colors they stand for, the second resolves an edited image back to the nearest palette entries (warning when a color had no exact match). The tool carries its own minimal PNG codec (Jazz2::AssetPacker::PngCodec) precisely because it does not link the renderer.

The JSON description

{
    "CellWidth": 15,
    "CellHeight": 20,
    "Columns": 19,
    "LineHeight": 20,
    "BaseSpacing": -2,
    "AsciiFirst": 32,
    "AsciiCount": 95,
    "Characters": [
        { "Char": " ", "Advance": 5 },
        { "Char": "!", "Advance": 4 },
        { "Char": "\"", "Advance": 6 },
        { "Char": "é", "Advance": 8 },
        { "Codepoint": 120071, "Advance": 11 },
        { "Fallback": true, "Advance": 8 }
    ]
}
  • CellWidth, CellHeight, Columns — The grid the image is read as: the size of one cell and how many of them sit in a row. Required, as is Characters, everything else has a default.
  • LineHeight — How far one line of text sits below the previous one. Belongs to the font rather than to the grid it is authored in, so it can be set on its own, defaults to the cell height, which is what it almost always is.
  • BaseSpacing — Pixels added between characters on top of each advance, negative to tighten. Defaults to none.
  • AsciiFirst, AsciiCount — Where the contiguous run the packed font indexes directly starts and how far it reaches. Both are worked out from the list itself when omitted — the leading run of consecutive characters, which is how one is written out — and the first AsciiCount characters have to be exactly that run, since that is the order the packed file stores them in. The run is described by two bytes, so it has to end by U+00FF, and the game keeps only the part of it below U+0080.
  • Characters — One entry per cell, in the order the cells are laid out in the image, and at least one. Char names the character as itself, Codepoint as a bare number for the ones without a spelling that survives a text file, and Fallback marks the single entry drawn in place of anything the font doesn't have (the same thing as "Codepoint": 0). Advance is how far the pen moves after drawing the character and is required, the glyph's size and bearings are not listed at all, being measured from the image every time it is packed.

Every field is checked against what the packed file can hold — the cell up to 65535 pixels in either direction, Columns and every Advance up to 255, BaseSpacing a signed 16-bit value — and one that is out of range, missing where it is required, or not the type it should be is an error naming the field and, inside the list, the position. So is a list that doesn't begin with the ASCII range it claims, which is also how a character repeated inside that range is caught:

Error: "font_small.png.json" has to list the ASCII range first, one character after another
from U+0020, but the character at position 34 is U+0041 where U+0042 was expected

What still describes a loadable font is a warning rather than an error — a codepoint listed twice outside the ASCII range (only one of the two is ever drawn), and a character below U+0080 left out of the range, where the game never looks for it. Both cost a glyph quietly, so they are worth reading:

Warning: "font_small.png.json" lists U+0104 more than once

The file is otherwise read leniently: comments, trailing commas and a byte order mark are all tolerated, so it can be annotated while it is worked on and saved by any editor. It is written as UTF-8 without one.

Preparing content for the Nintendo 64

The Nintendo 64 has a profile that goes further than the others: --target=n64 converts the sound, the music and the cinematics into libdragon's own formats, which is what lets the console play music at all and fit everything on a 64 MB cartridge. It runs libdragon's host tools, so it needs them — audioconv64 always, and videoconv64 with ffmpeg and ffprobe on the PATH for the cinematics:

AssetPacker convert ./build/ConsoleContentN64 --source=<original Jazz Jackrabbit 2> --content=./Content \
    --target=n64 --n64-tools=<libdragon toolchain prefix>

What differs from a console tree:

  • Sound effects are taken out of the package and written beside it, one .wav64 for each (Animations/<path>.wav64), which the console streams from the cartridge while they play instead of holding them decoded in its 8 MB. They are VADPCM, which the mixer decodes on the RSP, without the Huffman stage the CPU would have to undo. Not raw PCM, which looks cheaper but is not: libdragon's mixer fetches at most about 120 samples of a raw stream at a time, so while a single raw effect played every mixing round of the frame was cut that short, and every playing channel - the sixteen of a module included - pays for every round. Music and effects together cost 6.4 ms a frame that way, against 3.1 ms now. The originals are not kept: the console decodes no audio at all, so a sound without its .wav64 stays silent.
  • Music: the game's modules are Galaxy Music System .j2b files no console decoder reads. They are translated into FastTracker II modules (the same translation convert-music does, with every effect, sample tuning and channel panning carried over) and those into libdragon's .xm64, which the console plays with its RSP mixer and its instruments streamed from the cartridge. A track in any other format is pre-rendered through libopenmpt into a compressed .wav64 recording that loops where the module does — which needs a packer built with libopenmpt.
  • Cinematics become MPEG-1 video (<name>.m1v, 320×240 at 24 fps) with a .wav64 soundtrack beside it, which libdragon's player decodes with the RSP's help. The soundtrack mixes the cinematic's music and its sound effects into one recording. Without videoconv64 or ffmpeg the cinematics are re-encoded the console way instead, at half size.

The sprite sheets and tilesets are written in LZ4, like those of the other consoles that need a prepared tree (see LZ4 sprite sheets and tilesets). The result is about 38 MB for the full game, both cinematics and the whole soundtrack included.

LZ4 sprite sheets and tilesets

The consoles that cannot convert the game data on the device at all — the Nintendo 64, the Dreamcast, the PlayStation 2 and the GameCube — only ever get a tree this tool made, so their trees can carry the images in a form made for them: --target=n64, dreamcast, ps2 and gamecube write the image content of every sprite sheet and tileset as LZ4 blocks of the raw pixels (compressed at the highest ratio, which costs the converter time and not the console), where every other target keeps the game's own format. Their builds decode it (NCINE_WITH_LZ4, on by default for exactly these four).

  • A tileset gets one block per band of 32 rows, one row of tiles, which is the unit the game builds its atlas in - it never holds the whole sheet decoded. Measured on the Nintendo 64, castle1's tileset decodes in 179 ms from LZ4 against 290 ms from the game's format, and its compressed pixels are read in 29 ms instead of 70; the tilesets of the whole game take 8.2 MB instead of 14.5.
  • A sprite sheet is one block, and its entry in the package is stored rather than compressed again (a second pass would only put an inflate in front of the decoder). The package grows a little for it — 2.9 MB against 2.2 for the Nintendo 64 — while every sheet reads without an inflate.
  • The hand-made sheets of the game's own Content are converted on the way in, so a tree is decoded one way throughout. The fonts and the episode images keep the game's format; they are few and small.

Both headers mark it with a flag (JJ2Anims::ImageContentLz4Flag), so a build that has no LZ4 reports such a file instead of drawing garbage from it — rebuild the tree for a target, or the target with NCINE_WITH_LZ4, when the two do not match.

Replacing the content of a disc image

The two consoles that boot from a read-only disc — the Dreamcast and the PlayStation 2 — bake their content into the image at build time (see Preparing the content tree and Preparing the content tree), which is the only way either of them can be given anything: neither has writable storage the game could be handed a tree on. The price is that changing what is on the disc used to mean building the whole image again, with the console toolchain and an image authoring tool at hand. swap-content is the other half of that: it opens a finished image, keeps everything on it except Content, and writes it back out around a different one.

AssetPacker convert ./build/DreamcastContent --source=<original Jazz Jackrabbit 2> --content=./Content --target=dreamcast
AssetPacker swap-content ./jazz2.cdi --content=./build/DreamcastContent

AssetPacker convert ./build/Ps2Content --source=<original Jazz Jackrabbit 2> --content=./Content --target=ps2
AssetPacker swap-content ./jazz2.iso --content=./build/Ps2Content

AssetPacker convert ./build/ConsoleContentN64 --source=<original Jazz Jackrabbit 2> --content=./Content --target=n64
AssetPacker swap-content ./jazz2.z64 --content=./build/ConsoleContentN64

The directory named by --content= becomes Content on the disc whatever it is called here, so it is exactly what a --target=dreamcast conversion produces. With no target image given the source is rewritten in place (through a .tmp beside it, which only takes its place once the whole image is written, so an interrupted run cannot leave half a disc behind); naming one leaves the original alone.

Nothing else on the disc is touched. On a Dreamcast disc that is IP.BIN — the bootstrap, the licence screen, the serial number and the title — carried over as the 32 KB it occupies, and 1ST_READ.BIN, copied across still scrambled exactly as it was written; on a PlayStation 2 one it is SYSTEM.CNF, the executable it names and the .IRX modules beside them. The tool therefore needs neither the toolchain the image was built with nor the executable it was built from, and runs on any platform the packer itself does.

A Nintendo 64 cartridge is not a disc, but the same applies: the game content is the DragonFS image at the end of the ROM, found at boot through the table of contents libdragon's n64tool writes after the bootcode. It is built again from the directory the way mkdfs builds it — byte for byte, so a ROM given back the tree it was built from comes out identical except for the table's cookie — and staged the way the build stages it (cmake/n64_stage_content.cmake: the original music formats and a cinematic that has a video are left out, and a Source.pak becomes Prebaked.pak). The header, with the save type and accessories marked in it, the bootcode, the executable and its symbol table are kept as they are, and the ROM is refused if it would outgrow the 64 MB the cartridge address space maps.

What it actually does

A disc cannot be patched in place: the files of a directory are laid out one after another at fixed addresses, so anything that changes a single size moves everything that follows. The file system is therefore generated again from scratch (Jazz2::AssetPacker::Iso9660Builder), and only the container around it is carried over byte for byte from the image that was opened. For a DiscJuggler .cdi that container is the audio track of the first session, the pregap, the track layout and the descriptor the file ends with; a plain .iso has no container at all — the file is the data track, one 2048-byte sector after another — so there the generated volume is the whole of the output. Which of the two an image is, is read off the last eight bytes of the file, where a DiscJuggler image names its layout.

The generated volume is the same shape as the one mkdcdisc writes, because that is what the console reads:

  • Two hierarchies. The plain ISO 9660 one is what the bootstrap of the Dreamcast looks 1ST_READ.BIN up in, and it can spell names only in upper case out of a very small alphabet — it turns christmas-x-core_jj2.it into CHRISTMAS_X_CORE_JJ2.IT, which is not a name the game asks for. A Joliet hierarchy beside it carries the names as they really are, and that is the one KallistiOS uses whenever a disc has one (see fs_iso9660.c, which looks for the supplementary descriptor in sectors 17 to 19 and ignores the plain hierarchy the moment it finds one).
  • Files at the outer edge. A CD passes the most data per rotation under the head at its outer edge, so the file data is placed at the end of the volume and the space in front of it is left empty. That is what makes the image of a 110 MB game 740 MB, and it is the default mkdcdisc ships (-N turns it off), so a disc that goes through this tool keeps it.
  • Mode 2 Form 1 sectors, for a DiscJuggler image. It stores 2336 bytes of every sector, so the error detection code and both Reed-Solomon parities are generated for each one — a disc whose parity is wrong is a disc a drive reports read errors on. A plain .iso stores the 2048 bytes of user data and nothing else, and needs none of it.

A .cdi is left exactly as long as it was whenever the new content fits, which is almost always: the image is mostly the empty space above, so the whole disc comes back the same size with only the data track rewritten. Content that does not fit grows the track, and the three length fields of the descriptor and the length of the disc are adjusted to match; a disc that outgrows an 80 minute CD is still written, with a warning, because what it is going to be burned onto is not something the tool can know. A plain .iso has no geometry to keep — it is a file as long as what is on it — so it is always sized to what it now holds.

Platform notes

  • Desktop builds do not need the tool at all — the game converts on first run. Running it is still useful to prepare the cache in a build pipeline, or to pre-downscale cinematics with --video-downscale.
  • Dreamcast, PlayStation Portable, Wii and GameCube all consume a tree prepared with --target=dreamcast / psp / wii / gamecube. All four share the same layout, the target name only decides the cinematics — the Dreamcast gets them re-encoded into Jazz2::VideoFormat, the others get the smaller originals copied. Where each console expects that tree, and how it gets there, is described in Building for consoles. The Dreamcast and the GameCube have no conversion compiled in at all and require the prepared tree — which is why dreamcast and gamecube also write their images in LZ4 (see LZ4 sprite sheets and tilesets) — the PSP and the Wii would convert an installation left in their Source directory, which is slow on both and is why they are given a prepared tree as well. The Dreamcast additionally authors the tree into a read-only disc image at build time, which is a procedure of its own — see Preparing the content tree; the content of a finished image can be replaced afterwards without building it again, see Replacing the content of a disc image.
  • PlayStation 2 has a profile of its own, --target=ps2, which differs from the generic console one only in the cinematics — re-encoded into Jazz2::VideoFormat and halved to the 320×240 the console actually displays, and its images in LZ4 (see LZ4 sprite sheets and tilesets). A console tree still plays, it is just the slow way round. The PS2 boots from a read-only disc and so requires the prepared tree, exactly as the Dreamcast does (and authors it into an ISO at build time, see Preparing the content tree, whose content can likewise be replaced afterwards with Replacing the content of a disc image); it can equally be run from an SD card in an MX4SIO adapter, which consumes the same tree (see Running from a USB stick or an MX4SIO SD adapter).
  • Nintendo 64 has a profile of its own, --target=n64, which needs libdragon's host tools and converts the sound, the music and the cinematics into the console's formats — see Preparing content for the Nintendo 64. A console tree still boots, without music. The tree is authored into the ROM at build time (see Preparing the content tree), and the content of a finished ROM can be replaced afterwards with Replacing the content of a disc image.
  • PlayStation 3 consumes the generic --target=console tree — it needs nothing the profile does not already do. It has a writable hard disk and could in principle convert, but is given a prepared tree for the same reason the PSP is: doing it on the console is slow.
  • PS Vita and Nintendo Switch do convert the data themselves on first run, so the tool is only a way to skip that wait there.
  • Emscripten likewise ships a fully prepared tree (--target=emscripten), typically with --shareware-only for the public demo.
  • The packed .font files are what all platforms load, but the consoles benefit the most: one palette index per pixel instead of expanded RGBA cuts both the file size and the texture memory, and the tightly packed atlas minimizes the power-of-two padding the PVR, the GX and the PSP's GE all have to pay.

Pitfalls

  • The desktop cache descriptor must match the game exactly. Source.idx is compared field for field against what the running game would write — including the event count and the build version — and the game silently reconverts everything on startup when any field disagrees. Prepare the cache with a tool built from the same sources as the game.
  • Never write an index for the console or web profiles. The tool deliberately omits it there, a prepared tree is never rewritten, and an index would only invite the game to try.
  • Do not rename Prebaked.pak in a prepared tree. Its name is what marks the tree as already converted, so a renamed package leaves the game looking for original files it does not have — and finding none, showing a main menu with nothing to play.
  • The source must contain a supported version of the game data. The tool looks for Anims.j2a (or AnimsSw.j2a) at the top of the source directory or in its Source subdirectory and refuses to run without it.
  • A non-desktop tree is self-contained only if it got the game's own content from somewhere. That is either a Content beside the originals or an explicit --content=. Without one the tool still converts and exits successfully, and the tree then holds the sprites and sounds of the original data and none of the game's own: no .font files, no Metadata at all, none of the UI graphics and no translations — which presents on the target as a game that cannot draw its own menu. Nothing downstream can catch it, so the conversion warns about it instead, naming the option to fix it; the Copying "…" line is what a run that did get the content prints.
  • Skipped levels are listed on purpose. The list of original levels is maintained by hand, so read the skipped-levels output after a --originals-only conversion — a level that should have been kept shows up there by name.
  • Recompressed cinematics are larger than the originals. Only re-encode where the platform needs it (the tool's per-target defaults already do the right thing), everything else wants the original file.
  • swap-content reads the kinds of image the game's own builds produce. A plain .iso, which is what the PlayStation 2 disc is, a DiscJuggler .cdi whose data track stores 2048 or 2336 bytes per sector, which is what mkdcdisc writes, and a .z64 ROM that ends with its DragonFS image, which is how the Nintendo 64 build lays it out. A .cdi that stores whole 2352-byte sectors is refused rather than written back in a form that was never checked, a byte-swapped .v64 or .n64 ROM has to be converted to .z64 first, and no other container (.mds, .nrg, .gdi) is read at all.
  • --target=n64 runs one audioconv64 per module on purpose. In one run over several XM files the tool writes every module after the first with the first one's internal offsets, which the console then hangs or crashes on when it opens them. The packer works around it; converting modules by hand with libdragon's tool, do the same.
  • Identical samples keep only the first instrument's seek points in an .xm64. audioconv64 stores a sample once however many instruments carry it, with the points a compressed sample can be started from (one per 9xx offset it is played with) collected for the first of them only. An instrument that shares the data and is played from an offset the first never uses makes the console assert ("invalid VADPCM seeking point") on that note. The translated modules carry copies of instruments at other pannings, so the packer makes the copies that need it differ inaudibly (ModuleConverter.cpp, about 1 MB of cartridge for the whole soundtrack).
  • The game plays pre-downscaled cinematics as they are — the player picks its runtime downscale from the actual frame width, so a file reduced by the tool is not halved a second time.