Skip to the content.

Library usage guide

RVZSharp is a pure managed library (no native code) for .NET 8, .NET 9 and .NET 10. It reads and writes Dolphin RVZ and WIA disc images, writes the legacy GCZ format, reads the remaining legacy GameCube/Wii formats (CISO/WBI, WBFS, TGC, NFS), and exposes every format through one interface that serves the original disc bytes.

Install-Package RVZSharp          # Package Manager
dotnet add package RVZSharp       # .NET CLI

All public types live in the RVZSharp assembly; the main namespaces are:

Namespace Contents
RVZSharp.Blobs IBlobReader, Blob (factory), BlobType, per-format readers
RVZSharp RvzReader, RvzWriter, WiaWriter, GczWriter, CisoWriter, WbfsWriter, TgcWriter, DiscHasher, BlobStream, RvzWriteOptions, GczWriteOptions
RVZSharp.Models models: DiscInfo, DiscFileInfo, container structs (WiaFileHead, WiaDisc, WiaPartEntry, GroupEntry, HashExceptionEntry, DiscHashes), options records and enums (CompressionType, BlobType, DiscType, WiaRvzFormat)
RVZSharp.Chunks ChunkDecoder, ExceptionListParser
RVZSharp.Compression codec factories: CompressionCodecFactory, CompressionEncoderFactory (the vendored 7-Zip LZMA port is internal)
RVZSharp.IO Adler32, SpanReader, SectionStream, NonDisposingStream
RVZSharp.Packing RvzPackingDecoder, RvzPackingEncoder, LaggedFibonacciGenerator
RVZSharp.Wii PartitionRegionBuilder, WiiHashCalculator, WiiVolume, WiiPartitionExtractor, PartitionReader
RVZSharp.Files DiscFileSystem (FST parsing and file extraction; the DiscFileInfo tree nodes live in RVZSharp.Models)
RVZSharp.Verification DiscVerifier and the VerificationReport/VerificationIssue/PartitionVerification/VerificationSeverity models

Quickstart

using RVZSharp;
using RVZSharp.Blobs;
using RVZSharp.Models;

// 1. Open any disc image (format is auto-detected from the magic bytes).
using var blob = Blob.Open(@"C:\games\my-game.rvz");

// 2. Read the whole disc as ISO bytes.
byte[] iso = blob.ReadFully();          // fine for small images

// 3. Or convert it to RVZ with a different codec.
using var output = File.Create(@"C:\games\my-game-v2.rvz");
RvzWriter.Write(blob, output, new RvzWriteOptions
{
    Compression = CompressionType.Zstd,
    CompressionLevel = 5,
    ChunkSize = 0x200000,               // 2 MiB
});

That is the entire core API: open anything, read ISO bytes, write RVZ.


Opening disc images

Everything starts with Blob.Open, which sniffs the first four bytes and returns the right reader:

Magic bytes Format Reader type
52 56 5A 01 (RVZ\x01) RVZ RvzReader
57 49 41 01 (WIA\x01) WIA RvzReader
43 49 53 4F (CISO) CISO/WBI CisoBlob
01 C0 0B B1 GCZ GczBlob
57 42 46 53 (WBFS) WBFS WbfsBlob
45 47 47 53 (EGGS) NFS NfsBlob
AE 0F 38 A2 TGC TgcBlob
anything else plain ISO PlainBlob

Overloads

// From a path (the reader owns and closes the file stream).
using IBlobReader blob = Blob.Open(@"C:\games\game.gcz");

// From an existing stream.
using var stream = File.OpenRead(@"C:\games\game.iso");
using IBlobReader blob = Blob.Open(stream, filePath: @"C:\games\game.iso", leaveOpen: false);

// NFS with an explicit AES key (bypasses the code/htk.bin lookup).
byte[] key = ReadKeyFromSomewhere();          // 16 bytes
using IBlobReader blob = Blob.Open(@"C:\content\hif_000000.nfs", key);

Notes:

Is it a real disc?

Container sniffing only recognizes the file format; a plain file that is not a disc opens as PlainBlob too. To ask “is this actually a GameCube/Wii disc?”, inspect the decoded disc header (Wii magic at 0x18, GameCube magic at 0x1C — the same check Dolphin’s TryCreateDisc and RvzWriter.Write perform):

using IBlobReader blob = Blob.Open(path);

if (!Blob.IsDisc(blob))                 // false for arbitrary data
    throw new InvalidDataException("not a GameCube/Wii disc");

DiscType type = Blob.GetDiscType(blob); // DiscType.Wii / DiscType.GameCube / DiscType.Unknown

Blob.GetDiscType / Blob.IsDisc read the decoded bytes, so they work for every input format (plain ISO or any container). RvzWriter.Write rejects inputs that fail this check with RvzFormatException before writing a byte.

Disc metadata

DiscInfo.TryRead(blob) reads the volume metadata from the decoded disc header (Dolphin: VolumeDisc); DiscInfo.Read throws RvzFormatException for non-discs:

var info = DiscInfo.TryRead(blob);
if (info is not null)
{
    Console.WriteLine($"{info.GameId} {info.MakerId} rev {info.Revision}");
    Console.WriteLine($"{info.InternalName} ({info.Region}, {info.Country})");
    if (info.TitleId is ulong titleId)
        Console.WriteLine($"title 0x{titleId:X16}");
}
Property Meaning
DiscType GameCube / Wii (from the header magic)
GameId / MakerId 6- and 2-character IDs; non-alphanumeric bytes become -
Revision revision byte (header offset 7)
InternalName name at 0x20 (CP1252, or Shift-JIS for NTSC-J)
Region NTSC-J, NTSC-U, PAL, NTSC-K or Unknown
Country Japan, USA, Europe, … (with Dolphin’s region fallback)
CountryCode raw country byte (header offset 3)
TitleId Wii title ID from the game partition’s ticket, or null

As a Stream

BlobStream exposes any blob as a read-only, seekable Stream of any size:

using var blob = Blob.Open(path);
using var stream = new BlobStream(blob);   // leaveOpen defaults to false
var reader = new BinaryReader(stream);

Scrubbing

ScrubbedBlob.Create(blob) wraps any disc image and zeroes the data of every non-game Wii partition (update/channel) — the safe subset of Dolphin’s DiscScrubber that needs no filesystem (FST) parser. It returns null for discs that cannot be scrubbed (non-Wii, or no game partition). The CLI’s convert -s/--scrub uses it, and the writers expose the same behavior through RvzWriteOptions.Scrub / GczWriteOptions.Scrub (ignored for GameCube discs and Wii images without a game partition).

Lifetime

All readers implement IDisposable. Dispose them to release the underlying stream; the readers themselves hold no other unmanaged resources.


The IBlobReader contract

public interface IBlobReader : IDisposable
{
    BlobType Type { get; }      // detected format
    long Length { get; }        // decoded ISO size in bytes
    int BlockSize { get; }      // natural block size, 0 when the format has none
    int ReadAt(long position, Span<byte> buffer);
    byte[] ReadFully();         // default implementation (streams via ReadAt, ≤ 2 GiB)
    byte[] ReadFully(IProgress<double>? progress, CancellationToken cancellationToken = default);
    long CopyTo(Stream destination, IProgress<double>? progress = null,
        CancellationToken cancellationToken = default);
    long CopyTo(Stream destination, IProgress<double>? progress, int maxThreads,
        CancellationToken cancellationToken = default);
    Task<byte[]> ReadFullyAsync(IProgress<double>? progress = null, CancellationToken cancellationToken = default);
    Task<long> CopyToAsync(Stream destination, IProgress<double>? progress = null,
        CancellationToken cancellationToken = default);
    Task<long> CopyToAsync(Stream destination, IProgress<double>? progress, int maxThreads,
        CancellationToken cancellationToken = default);
}

Every format — RVZ, GCZ, WBFS, a raw ISO — decodes to the same thing: the original disc image bytes, randomly accessible.

using var blob = Blob.Open(path);

// Random access: read one 0x8000-byte sector.
Span<byte> sector = stackalloc byte[0x8000];
blob.ReadAt(0x1234 * 0x8000, sector);

// Whole-image decode (images up to 2 GiB).
byte[] iso = blob.ReadFully();

Streaming pattern (large images)

GameCube/Wii images are 0.5–9.4 GiB. To avoid holding the whole image in memory, stream it with CopyTo:

using var blob = Blob.Open(path);
using var output = File.Create(@"C:\games\out.iso");
var progress = new Progress<double>(f => Console.Error.Write($"\r{f,6:P1}"));

blob.CopyTo(output, progress);

For explicit random-access control, the same loop can be written by hand with ReadAt (1 MiB at a time, as CopyTo does internally).

Parallel and async decoding

CopyTo(..., maxThreads) decodes RVZ/WIA chunks on a worker pool (raw chunks and 64-sector partition regions are independent units; only the short file reads are serialized, so LZMA/LZMA2 decompression runs in parallel). Results are written in disc order, so the output is byte-identical for any thread count. Other formats fall back to the sequential default implementation. 0 uses the processor count, 1 forces sequential.

blob.CopyTo(output, progress, maxThreads: 0);   // parallel where supported

The ...Async forms run the synchronous, CPU-bound decoders on the thread pool and return a task; progress and cancellation behave exactly like the synchronous methods.

byte[] iso = await blob.ReadFullyAsync(progress, cancellationToken);
await blob.CopyToAsync(output, progress, maxThreads: 0, cancellationToken);

Reading RVZ / WIA in detail

RvzReader exposes the container metadata plus the decoded bytes:

using var file = File.OpenRead("game.rvz");
using var reader = RvzReader.Open(file, leaveOpen: true);   // RVZ
using var reader = RvzReader.OpenWia(file, leaveOpen: true); // WIA

reader.IsWia;              // true for WIA containers
reader.Length;             // decoded ISO size
reader.BlockSize;          // the chunk size
reader.FileHead;           // WiaFileHead: magic, versions, SHA-1s, sizes
reader.Disc;               // WiaDisc: disc type, codec, chunk size, table locations
reader.Partitions;         // WiaPartEntry[]: key + two data ranges per partition
reader.RawDataEntries;     // WiaRawDataEntry[]: raw data ranges
reader.GroupEntries;       // GroupEntry[]: group table

Opening validates the whole container: magic, versions, the file-head and disc-struct SHA-1s, the partition/raw/group table hashes, and structural rules (chunk size, group coverage). A damaged file throws RvzHashMismatchException or RvzFormatException at Open, never later.

WIA files (read-compatible version 0x00080000) support the PURGE codec and lack Zstd and packing; RvzReader handles both formats transparently.


Reading the file system (FST)

DiscFileSystem parses the GameCube/Wii file system table (Dolphin: FileSystemGCWii) and gives you the tree plus file data reads. GameCube discs open directly; Wii partitions open through a decrypted PartitionReader view, so paths and offsets match Dolphin.

using RVZSharp.Files;
using RVZSharp.Wii;

// GameCube (or any disc whose data starts at offset 0):
using var fs = DiscFileSystem.Open(blob);

// Wii: pick a partition (WiiVolume.GetPartitions, type 0 = DATA/game):
using var fs = DiscFileSystem.Open(blob, WiiVolume.GetPartitions(blob)[0]);

var file = fs.Find("files/maps/foo.dat");       // case-insensitive, '/' separators
foreach (var child in fs.Root.Children) { }     // Name, Path, IsDirectory, Size, Offset

using var output = File.Create("foo.dat");
fs.CopyFileTo(file, output);                    // streams the decrypted file bytes

Writing RVZ

RvzWriter.Write(IBlobReader input, Stream output, RvzWriteOptions? options = null,
    IProgress<double>? progress = null, CancellationToken cancellationToken = default);

The writer mirrors Dolphin’s converter:

RvzWriteOptions

Member Default Notes
Compression Zstd None, Bzip2, Lzma, Lzma2, Zstd. Purge throws RvzUnsupportedException (use WiaWriter for PURGE).
CompressionLevel 5 Bzip2/Lzma/Lzma2: 1–9. Zstd: −131072..22 (negative levels = fast modes, 0 = default). Dolphin’s converter default.
ChunkSize 0x200000 Power of two between 0x8000 (32 KiB) and 0x200000 (2 MiB), or a multiple of 0x200000 above that (Dolphin’s rule).
Packing true Set false to store junk literally (larger file, no packing overhead).
Scrub false Zero the data of non-game Wii partitions before encoding (Dolphin’s DiscScrubber). No-op for GameCube discs.
MaxThreads 0 Compression/packing worker count; 0 uses the processor count. Output bytes do not depend on this value.
var options = new RvzWriteOptions
{
    Compression = CompressionType.Lzma2,   // best ratio on compressible data
    CompressionLevel = 9,
    ChunkSize = 0x10000,                   // 64 KiB chunks
    Packing = true,
};

The writer does not dispose the output stream; the caller owns it.

Progress and cancellation

Long conversions (multi-GiB images) can be monitored and canceled on both the write and the decode path:

using var cts = new CancellationTokenSource();
var progress = new Progress<double>(fraction =>
    Console.Error.Write($"\r{fraction,6:P1}"));

try
{
    // Encode: progress is the fraction of input bytes processed.
    RvzWriter.Write(blob, output, options,
        progress: progress, cancellationToken: cts.Token);

    // Decode/stream: progress is the fraction of image bytes decoded.
    blob.CopyTo(isoStream, progress, cts.Token);
}
catch (OperationCanceledException)
{
    Console.Error.WriteLine("\ncanceled.");
}

Verifying your output

DiscHasher computes the CRC-32, MD5 and SHA-1 of the decoded image in one streaming pass (the same digests Dolphin’s volume verifier reports):

using var check = Blob.Open(outputPath);
DiscHashes hashes = DiscHasher.Compute(check, progress);

// Compare with the source image's hashes:
using var source = Blob.Open(inputPath);
var expected = DiscHasher.Compute(source);
if (!hashes.Sha1.AsSpan().SequenceEqual(expected.Sha1))
    throw new InvalidDataException("round-trip mismatch");

For RVZ/WIA the container’s own integrity (all SHA-1s, structure rules) is already validated by RvzReader.Open / RvzReader.OpenWia, so DiscHasher only needs to hash the decoded bytes.

Verifying Wii partitions

DiscVerifier.Verify mirrors Dolphin’s verify tab: it enumerates the disc’s Wii partitions, checks each partition header, TMD structure and H3 table, and walks every data sector’s h0/h1/h2/h3 hash tree (decrypting sectors with the plaintext partition key derived by WiiVolume.GetPartitions). Problems are reported per partition with a severity; only VerificationSeverity.High problems (corrupt data) make a report invalid.

using RVZSharp.Verification;

VerificationReport report = DiscVerifier.Verify(blob, progress);
foreach (var partition in report.Partitions)
{
    Console.WriteLine($"{partition.Name}: {partition.VerifiedBlocks} blocks, "
        + $"{partition.FailedBlocks} failed, tmd={partition.TmdValid}, h3={partition.H3TableValid}");
}

if (!report.IsValid)
{
    foreach (var issue in report.Issues.Concat(report.Partitions.SelectMany(p => p.Issues)))
        Console.Error.WriteLine($"[{issue.Severity}] {issue.Message}");
}

Writing WIA

WiaWriter shares the writer core with RvzWriter (Dolphin: ConvertToWIAOrRVZ):

using var input = Blob.Open(@"C:\games\game.iso");
using var output = File.Create(@"C:\games\game.wia");

WiaWriter.Write(input, output, new RvzWriteOptions
{
    Compression = CompressionType.Lzma2,   // None, Purge, Bzip2, Lzma, Lzma2
    CompressionLevel = 5,
    ChunkSize = 0x200000,                  // multiple of 2 MiB
});

Writing GCZ

GczWriter mirrors Dolphin’s ConvertToGCZ: the image is split into blocks, each block is deflated (level 9) and stored compressed unless that saves fewer than 10 bytes, and the block table carries a per-block Adler-32 of the stored bytes. GCZ has no compression choice — it is always zlib deflate.

using var input = Blob.Open(@"C:\games\game.iso");
using var output = File.Create(@"C:\games\game.gcz");

GczWriter.Write(input, output, new GczWriteOptions
{
    BlockSize = 0x4000,   // power of two; 16 KiB classic, Dolphin's GUI defaults to 128 KiB
    MaxThreads = 0,       // 0 = processor count; output is byte-identical for any value
});

Writing CISO / WBFS / TGC

CisoWriter, WbfsWriter and TgcWriter complete the container matrix; all three accept any input format (IBlobReader) or path and take an optional IProgress<double> and cancellation token.

using var input = Blob.Open(@"C:\games\game.iso");

// CISO/WBI: a presence map + only the blocks that contain data.
CisoWriter.Write(input, ciso, new CisoWriteOptions
{
    BlockSize = 0x200000, // power of two; the decoded image is BlockSize × 0x7FF8 bytes
    Scrub = true,         // zero non-game Wii partitions; all-zero blocks are not stored
});

// WBFS: Wii only. All-zero clusters share one zero-filled volume cluster.
WbfsWriter.Write(input, wbfs, new WbfsWriteOptions
{
    BlockSize = 0x200000, // power of two ≥ 32 KiB; the u16 map caps the disc at 65535 clusters
});

// TGC: GameCube only. The ISO is stored verbatim after the 56-byte header.
TgcWriter.Write(input, tgc);

Compression codecs

Read side — decode any method found in a file:

ICompressionDecoder decoder = CompressionCodecFactory.Create(CompressionType.Lzma2);
using Stream stream = decoder.CreateDecompressor(file, props, inputSize, outputSize);

Write side — compress like the writer does:

var (encoder, props) = CompressionEncoderFactory.Create(CompressionType.Zstd, level: 5);
byte[] compressed = encoder.Compress(payload);

RVZ packing API

Packing is what makes RVZ files small: Wii junk is a Lagged-Fibonacci PRNG stream, so a 68-byte seed regenerates it. The writer detects junk and emits segment streams; the reader reconstructs it.

// Encode a chunk (returns the segment stream + its packed size).
var mainData = new List<byte>();
uint packedSize = 0;
RvzPackingEncoder.Pack(payload, dataOffset: 0, bytesPerChunk: payload.Length, chunks: 1,
    allowJunkReuse: true, compression: true, mainData, ref packedSize);

// Decode a packed stream.
using var decoder = new RvzPackingDecoder(stream, dataOffset: 0);
decoder.Read(buffer, 0, buffer.Length);

LaggedFibonacciGenerator is public for advanced use — including seed recovery:

var (seed, bytesReconstructed) = LaggedFibonacciGenerator.GetSeed(
    data, data.Length, dataOffset % LaggedFibonacciGenerator.BlockSize);
var lfg = new LaggedFibonacciGenerator();
lfg.SetSeed(seed);
lfg.ForwardBytes(dataOffset % LaggedFibonacciGenerator.BlockSize);
lfg.GetBytes(count, output);

In normal use you never touch these — they are needed only when implementing a custom container that embeds packed chunks.


Wii partition machinery

These types back the partition optimization:

foreach (var partition in WiiVolume.GetPartitions(blob))
{
    Console.WriteLine($"partition @ 0x{partition.Offset:X}, type {partition.Type}, "
        + $"{partition.DataSize} data bytes, key {Convert.ToHexString(partition.Key)}");
}

Error handling

Exception Raised when
RvzFormatException structural problems: truncated file, bad chunk size, unsupported method, missing disc coverage, decode stalls
RvzHashMismatchException a SHA-1 (file head, disc struct, partition/raw/group tables) does not match
RvzUnsupportedException a needed feature is not supported (e.g. PURGE inside RVZ, unknown codec)
ArgumentException programmer errors: non-seekable stream, invalid chunk size
OperationCanceledException RvzWriter.Write canceled via CancellationToken

All Rvz*Exception types derive from RvzException (which derives from Exception), so one catch covers the format-specific failures:

try
{
    using var blob = Blob.Open(path);
    byte[] iso = blob.ReadFully();
}
catch (RvzException e)   // format + hash problems
{
    Console.Error.WriteLine($"The image is damaged or unsupported: {e.Message}");
}

Hashing is verified eagerly: RVZ/WIA containers validate all checksums when opened, so a corrupt file fails fast at Blob.Open, not halfway through your reads.


Recipes

Convert any file to RVZ with progress, then decode back:

using var blob = Blob.Open(inputPath);
using var output = File.Create(outputPath);
RvzWriter.Write(blob, output, new RvzWriteOptions { Compression = CompressionType.Zstd },
    progress: new Progress<double>(p => Console.WriteLine($"{p:P1}")));

Read the game title and region without decoding the disc:

using var blob = Blob.Open(path);
Span<byte> header = stackalloc byte[0x80];
blob.ReadAt(0, header);
string gameId = System.Text.Encoding.ASCII.GetString(header[..6]);

List Wii partitions of any container:

using var blob = Blob.Open(path);
foreach (var partition in WiiVolume.GetPartitions(blob))
{
    Console.WriteLine($"0x{partition.Offset:X8}  type={partition.Type}  "
        + $"{partition.DataSize} bytes");
}

Stream a 9.4 GiB WBFS to ISO without buffering: use the streaming pattern above — WbfsBlob reports its fixed logical size and serves zero clusters without allocating them.


Thread safety

Compatibility notes