Skip to content
Vitaly Murashov edited this page Oct 4, 2026 · 3 revisions

Usage

SLTextureDecoder 0.x.y
Decodes Second Life / Firestorm JPEG 2000 texture caches into PNG.

Usage:
  SLTextureDecoder --cache-dir <dir> [options]

Options:
  --cache-dir <dir>   texture cache directory to read (required)
  --out-dir <dir>     where the PNG files go (default: <cache-dir>/png)
  --id <uuid>         decode one texture instead of the whole cache
  --index <n>         decode only the record at this index
  --limit <n>         stop after n textures
  --complete-only     skip records whose cached codestream is truncated
  --overwrite         write over existing PNG files
  --dry-run           report what a run would write and change nothing
  --keep-j2k          also write the assembled codestream next to the PNG
  --no-alpha          write an RGB PNG instead of RGBA
  -v, --verbose       log every record
  -h, --help          show this help
  -V, --version       show version and linked libraries

Output:
  <uuid>.png          decoded from a complete codestream
  <uuid>.partial.png  best effort result from a truncated codestream
  <uuid>.j2c          assembled codestream, only with --keep-j2k

Interruption:
  Every file is written to <name>.part first and renamed when it is
  complete. Ctrl+C therefore stops after the record in progress, leaves
  no half written PNG behind and exits with code 10.

Run the tool without any argument to print the same text (and exit with code 1, because the invocation is incomplete). --help exits with 0.

What gets converted

Invocation Selection
--cache-dir <dir> every record that has a body file (mBodySize > 0)
--cache-dir <dir> --complete-only only the records whose codestream is complete on disk
--limit <n> the first n of that selection, in cache index order
--index <n> exactly the record n, whatever --limit/--complete-only say
--id <uuid> the record with that texture id (looked up in texture.entries)
  • --index wins over --id when both are given.
  • The next run skips a texture whose PNG is already in the output directory (that is what makes resuming an interrupted run cheap); --overwrite rewrites it. The name that is checked is the one the record would produce, i.e. <uuid>.partial.png for a truncated record, <uuid>.png otherwise.
  • A record without a body file (mBodySize <= 0, a free slot) is counted as failed and reported - it cannot be decoded by definition.
  • The output directory is created on demand, including <cacheDir>/png when --out-dir is omitted.
  • --dry-run decodes nothing: it reads texture.entries and checks the body files (name and size, no OpenJPEG) and prints the counts a real run would produce. Nothing is created, not even the output directory, and the counters in the final line are prefixed with dry run:. A predicted failure still makes the process exit with the matching code, so --dry-run can gate a long run in a script.

Robustness

  • Atomic output. Each file is written to <name>.part (<uuid>.png.part, <uuid>.j2c.part, ...) and renamed over the target once it is complete, so a killed or interrupted process leaves a stale .part file - which no later run mistakes for a finished PNG - instead of a truncated PNG.
  • Ctrl+C. The CLI installs a console handler that only sets a flag; the run finishes the record it is working on and stops, logging interrupted: stopping before record N and ending the summary line with [interrupted]. The exit code is 10. The same applies to SLTextureDecoderGUI.exe --batch.
  • One bad record never ends the run. Errors, and unexpected std::exceptions such as std::bad_alloc, are logged and counted; only problems that stop the whole run (missing cache directory, unusable output directory) abort it.
  • Sanity limits. A record that announces a body larger than kMaxBodySize (512 MiB) is refused before the file is read, the body file size is compared with mBodySize before it is loaded, and a codestream that announces an image larger than 8192x8192, a reversed image area or an implausible component count is rejected before OpenJPEG allocates the samples (see src/utils/Constants.h). The cache itself is only ever read.
  • Sharing violations of the viewer that has the cache open are not retried yet - a record fails and is reported (issue #6 tracks the retry and the "the cache changed while it was read" check).

Output naming

<uuid>.png           the whole codestream was on disk and decoded losslessly
<uuid>.partial.png   only the cached prefix existed; OpenJPEG recovered a blurry
                     but usable image (best effort, not the original texture)

Two files for the same texture can never appear at once, so the suffix is a reliable "this output is not the real thing" marker for scripts.

Exit codes

The numeric exit code is the error category, so a script can react without parsing the log (src/utils/Errors.h):

Code Name Typical cause
0 Ok every selected texture was written (best effort results count as written)
1 Usage bad command line, missing --cache-dir, unknown option
2 CacheNotFound --cache-dir is not a directory, or texture.entries / texture.cache is missing
3 EntryNotFound --id is not in the cache, --index out of range, body file missing
4 CacheTooSmall header block or texture.cache shorter than the record announces
5 SizeMismatch body file size differs from mBodySize
6 DecodeError at least one selected texture could not be decoded even leniently, or a codestream is inconsistent
7 WriteError output directory/file could not be created or written
8 IoError read failed, short read, file vanished
9 InvalidFormat texture.entries is not a plausible cache (too small, truncated, absurd count, body size beyond the format)
10 Interrupted the run was stopped with Ctrl+C before the end of the selection

Exit code 0 with a non-zero written count is the normal case for a cache full of truncated records: the tool reports them as best effort instead of failing. 10 is only used when a record boundary was reached after the interruption, so a run that ignores Ctrl+C until the very end reports the ordinary code.

Logging

Plain lines on stdout/stderr, no colour codes, so redirecting works:

[INFO] cache D:\FS Cache\texturecache: 196700 records, encoder KDU v8.4.1
[INFO] 120 texture(s) to decode into D:\textures
[INFO] 250/54825 written
[WARN] record 123386: texture has 5 components, dropping the surplus plane
[INFO] done: 120 PNG written (83 best effort), 0 skipped, 0 failed
[WARN] interrupted: stopping before record 87
[INFO] done: 86 PNG written (30 best effort), 0 skipped, 0 failed [interrupted]
[INFO] dry run: 120 texture(s) would be written (83 best effort), 0 skipped, 0 failed
Level When
INFO cache opened, selection, every 250 written PNGs, the final summary
WARN a texture decoded with a caveat (5 components, lenient recovery)
ERROR a record failed; the run continues
verbose (-v) one line per record: index, uuid, file name, size, components, OpenJPEG diagnostics

Examples

# the whole cache, default output <cache-dir>\png, skip what exists
SLTextureDecoder --cache-dir "$env:APPDATA\Firestorm_x64\texturecache"

# a quick smoke test: five records, each one logged
SLTextureDecoder --cache-dir "D:\FS Cache\texturecache" --out-dir D:\textures --limit 5 -v

# only losslessly recoverable textures, into a separate tree
SLTextureDecoder --cache-dir "D:\FS Cache\texturecache" --out-dir D:\textures\complete --complete-only

# one texture you know the id of
SLTextureDecoder --cache-dir "D:\FS Cache\texturecache" --id 8f7d1b1e-0a1b-4c5d-8e9f-0123456789ab

# record 123386 by index
SLTextureDecoder --cache-dir "D:\FS Cache\texturecache" --out-dir D:\one --index 123386 -v

# scripted, quiet: only failures matter
SLTextureDecoder --cache-dir "D:\FS Cache\texturecache" 2> errors.log
if ($LASTEXITCODE -ne 0) { Write-Host "see errors.log" }

# preflight: what would a run do, without writing anything?
SLTextureDecoder --cache-dir "D:\FS Cache\texturecache" --out-dir D:\textures --dry-run -v

Options of the GUI binary

SLTextureDecoderGUI.exe accepts the same options plus:

--jobs <n>   worker threads (0 = one per logical CPU)
--batch      convert without opening a window
# the whole cache on four threads, unattended, log to a file
SLTextureDecoderGUI.exe --batch --jobs 4 --cache-dir "D:\FS Cache\texturecache" `
    --out-dir D:\textures > run.log

The headless mode shares the record loop of the CLI, so it accepts the same options (including --dry-run) and reacts to Ctrl+C the same way: the workers stop between records and the process exits with code 10.

See GUI for the window and Performance for what --jobs buys you.

Clone this wiki locally