Skip to content
View in the app

A better way to browse. Learn more.

ResHax

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
Help us keep the site running.
Zero Tolerance for Disrespect

Dreamcast-Kita e Series .cb and .pvr

Featured Replies

Hi everyone, I'm trying to extract texture files from two Dreamcast games—Kita e White Illumination and Kita e Photo Memories—and I'm stuck on two issues. First, for White Illumination, the .CB file (which I opened with a hex editor and found to contain .dds and .set files) was extracted using a script generated by DeepSeek, but the resulting .dds files won't open in any image viewer I've tried (see SSYF.zip). Second, for Photo Memories, the PVR files have GBIX and PVRT headers, but none of the usual tools—pvr2image, Noesis, sprt2png, or PVRTexTool—can convert or even open them (yuko_PVR.zip). I suspect the data might be compressed, encrypted, or using non‑standard headers. I know I can use the Flycast emulator to dump textures as the game runs, but that's pretty tedious. Has anyone successfully extracted these formats, or does anyone know of a working tool or script that can handle them? Any help would be greatly appreciated.

yuko_PVR.zip SSYF.zip

Solved by piken

What offset are you seeing a DDS subfile in SSFY.CB? Scrolling through it, there are very few regular block patterns, and changing the wrap width rapidly, you can see wavy patterns indicative of variable length compression. Does the exported DDS have a standard DDS header at least (e.g. this)? The "PVR" files are also wrapped in some variable length compression (not PVR standard blocks).

Edited by piken

11 hours ago, Tefolium said:

Hi everyone, I'm trying to extract texture files from two Dreamcast games—Kita e White Illumination and Kita e Photo Memories—and I'm stuck on two issues. First, for White Illumination, the .CB file (which I opened with a hex editor and found to contain .dds and .set files) was extracted using a script generated by DeepSeek, but the resulting .dds files won't open in any image viewer I've tried (see SSYF.zip). Second, for Photo Memories, the PVR files have GBIX and PVRT headers, but none of the usual tools—pvr2image, Noesis, sprt2png, or PVRTexTool—can convert or even open them (yuko_PVR.zip). I suspect the data might be compressed, encrypted, or using non‑standard headers. I know I can use the Flycast emulator to dump textures as the game runs, but that's pretty tedious. Has anyone successfully extracted these formats, or does anyone know of a working tool or script that can handle them? Any help would be greatly appreciated.

yuko_PVR.zip SSYF.zip

The PVR files look to be LZSS0 compressed from offset 8, and one of the files I tested (IMG0088_A) is 512x512 BGRA5551 with DC swizzling. Something like this:

image.png

  • Author
13 hours ago, piken said:

What offset are you seeing a DDS subfile in SSFY.CB? Scrolling through it, there are very few regular block patterns, and changing the wrap width rapidly, you can see wavy patterns indicative of variable length compression. Does the exported DDS have a standard DDS header at least (e.g. this)? The "PVR" files are also wrapped in some variable length compression (not PVR standard blocks).

there are not standard DDS header. I don't know the offset, but the first 76 bytes of the extracted file header are:

CLSS<bh:10><bh:00><bh:00><bh:00>CTRFImageBuffer<bh:00>IBUF<bh:81><bh:97><bh:00><bh:00><bh:00><bh:00><bh:00><bh:00><bh:00><bh:01><bh:00><bh:01><bh:00><bh:02><bh:01><bh:00><bh:00><bh:00><bh:10><bh:00><bh:00><bh:80><bh:00><bh:00><bh:00>|<bh:00><bh:00><bh:e0><bh:03><bh:00><bh:00><bh:1f><bh:00><bh:00><bh:00><bh:01><bh:05><bh:05><bh:05><bh:0f><bh:0e><bh:09><bh:04>LZSS

The DDS files(bag2.dds) not inside the .cb file (presumably icons from the game) have this header:

CLSS<bh:10><bh:00><bh:00><bh:00>CTRFImageBuffer<bh:00>IBUF<bh:19><bh:05><bh:00><bh:00><bh:00><bh:00><bh:00><bh:00>@<bh:00>@<bh:00><bh:80><bh:00><bh:01><bh:00><bh:00><bh:00><bh:10><bh:00><bh:00><bh:80><bh:00><bh:00><bh:00>|<bh:00><bh:00><bh:e0><bh:03><bh:00><bh:00><bh:1f><bh:00><bh:00><bh:00><bh:01><bh:05><bh:05><bh:05><bh:0f><bh:0e><bh:09><bh:04>LZSS

BAG2.DDS

  • Author
11 hours ago, DKDave said:

The PVR files look to be LZSS0 compressed from offset 8, and one of the files I tested (IMG0088_A) is 512x512 BGRA5551 with DC swizzling. Something like this:

image.png

LZSS0 compressed from offset 8, 512x512 BGRA5551, swizzle morton swap, then I got the picture... 7.py is the python script for test by DeepSeek:Decompresses the LZSS0 stream using the confirmed parameters.

  • Takes the first width * height * 2 bytes (default 512×512 = 524288 bytes).

  • Applies five different reverse‑swizzle algorithms:

    • linear – no reordering

    • morton_swap – the confirmed DC swizzle with swapped Morton coordinates

    • morton_noswap – standard Morton coordinates

    • block_4x4_morton – 4×4 tiles, Morton order inside each tile

    • block_8x8_morton – 8×8 tiles, Morton order inside each tile

  • Converts the resulting BGRA5551 data to RGBA8888 and writes a PNG for each algorithm.

IMG0088_A_swizzle_morton_swap.png

7.py

Edited by Tefolium

  • Supporter

But not sure how to decompress. The LZSS sign it's just coincidence I guess.

//------------------------------------------------
//--- 010 Editor v14.0 Binary Template
//
//      File: 
//   Authors: 
//   Version: 
//   Purpose: 
//  Category: 
// File Mask: 
//  ID Bytes: 
//   History: 
//------------------------------------------------
LittleEndian();OutputPaneClear();

local uint32 i,j,k,l,SignCheck;

char CABSign[4];
uint32 FCBSize;

FSeek(FCBSize + 8);
struct
{
    char STRSign[4];
    uint32 STRSize;
    struct
    {
        uint32 StrCount;
        struct
        {
            FSkip(3);
            string ResourcePathName;
        }STRDefinition[StrCount]<optimize=false>;
    }STRDefinition;
}ResourcePathDefinition;

FSeek(startof(FCBSize) + sizeof(FCBSize));
struct
{
    char FCBSign[4];
    uint32 INFOSize;
    struct
    {
        char INFOSign[4];
        uint32 TOCSize;
        struct
        {
            uint32 ResourceCount;
            struct
            {
                uint32 Unknown_0;
                uint32 Unknown_1;
                uint32 ResourceSize;
                uint32 ResourceOffset;
                uint32 ResourceNameId;
            }ResourceDefinition[ResourceCount]<optimize=false>;
        }TOCDefinition;
        
        char STRSign[4];
        uint32 STRSize;
        struct
        {
            uint32 StrCount;
            struct
            {
                string ResourceName;
            }STRDefinition[StrCount]<optimize=false>;
        }STRDefinition;
    }INFODefinition;
    
    struct
    {
        for (i=0; i < INFODefinition.TOCDefinition.ResourceCount; i++)
        {
            FSeek(INFODefinition.TOCDefinition.ResourceDefinition[i].ResourceOffset);
            struct
            {
                char Sign[4];
                if (Sign == "ENC2")
                {
                    uint32 Size;
                    byte ENC2Data[Size];
                    FSeek(startof(ResourcePathDefinition.STRDefinition.STRDefinition[INFODefinition.TOCDefinition.ResourceDefinition[i].ResourceNameId].ResourcePathName));
                    string ResourceName;
                }
                else if (Sign == "ENC0")
                {
                    uint32 CLSSSize;
                    struct
                    {
                        char CLSSSign[4];
                        uint32 StrLen;
                        char CTRFSign[StrLen];
                        struct
                        {
                            char IBUFSign[4];
                            uint32 IBUFSize;
                            uint32 Unknown_0;
                            uint16 TextureWidth;
                            uint16 TextureHeight;
                            uint32 Unknown_1;
                            uint32 Unknown_2;
                            uint32 Unknown_3;
                            uint32 Unknown_4;
                            uint32 Unknown_5;
                            uint32 Unknown_6;
                            uint32 Unknown_7;
                            uint32 Unknown_8;
                            
                            struct
                            {
                                do
                                {
                                    struct
                                    {
                                        char LZSSSign[4];
                                        uint32 Size;
                                        byte LZSSData[Size];
                                        SignCheck=ReadUInt(FTell());
                                    }LZSSDefinition;
                                 }while(SignCheck == 1397971532);
                            }LZSSDefinition;
                        }IBUFDefinition;
                    }CLSSDefinition;
                    FSeek(startof(ResourcePathDefinition.STRDefinition.STRDefinition[INFODefinition.TOCDefinition.ResourceDefinition[i].ResourceNameId].ResourcePathName));
                    string ResourceName;
                }
            }Resource;
        }
    }Resources;
}FCBDefinition;

Edited by h3x3r

  • Author
4 hours ago, DKDave said:

用LZSS0 / BGRA5551 / Morton做了一些手动测试——看起来没错

image.png image.png

I can not get the expected result through the script genreated by DeepSeek. I looked through ImageHeat but didn't find the support for LZSS0 compression. Could you please advise what tool or code you are using for this? Alternatively, could it be that the Morton swap implementation is wrong in the script?

  • Solution

I can not get the expected result

I don't know DKDave's strategy for finding the specific decompression algorithm and correct offset, but he certainly got it.

For IMG0088_A.PVR using the Okumura-style LZSS0 variant starting at byte offset 8, and then skipping 32 bytes after that, I get:

image.png

Which then becomes, after applying the PS Vita/Dreamcast address unswizzling (y2x2y2x2...):

image.png

From your previous post, you appeared really close. Try just shifting your decompressed data offset by 32 bytes.

Edited by piken

15 hours ago, Tefolium said:

I can not get the expected result through the script genreated by DeepSeek. I looked through ImageHeat but didn't find the support for LZSS0 compression. Could you please advise what tool or code you are using for this? Alternatively, could it be that the Morton swap implementation is wrong in the script?

I decompressed the data with QuickBMS, then you can manipulate it in ImageHeat. The decompressed data has a header of 32 bytes, which includes the image dimensions. I'm not sure if some images use a slightly different format. The swizzle seems to be standard Morton, at least for the ones I tried.

Your script probably works, but be aware of rule 17 for using AI to generate scripts.

  • Author

Thank you all. Although we still haven't found a good way to handle WhiteIllumination's .cb files, at least the .pvr files from PhotoMemories can now be extracted. A .tab file extracted from WI's GDI image files seems to be from Sakura Taisen, so perhaps WI and Sakura Taisen share similar extraction methods. For PM, the background and event images are basically composed of A and B spliced together to form a complete 640×480 image, or sometimes with C added—not just 640×480, but for some backgrounds, the B part seems to be strange, though I'm not sure if that's original.

BGF294_A (2).png

BGF294_A.png

BGF294_B (2).png

BGF294_B.png

BGF294_B.zip

13 hours ago, Tefolium said:

Thank you all. Although we still haven't found a good way to handle WhiteIllumination's .cb files, at least the .pvr files from PhotoMemories can now be extracted. A .tab file extracted from WI's GDI image files seems to be from Sakura Taisen, so perhaps WI and Sakura Taisen share similar extraction methods. For PM, the background and event images are basically composed of A and B spliced together to form a complete 640×480 image, or sometimes with C added—not just 640×480, but for some backgrounds, the B part seems to be strange, though I'm not sure if that's original.

BGF294_A (2).png

BGF294_A.png

BGF294_B (2).png

BGF294_B.png

BGF294_B.zip

For some of the image files, they're a slightly different format. The BGF294_B is still BGRA5551 but the image just isn't swizzled in this case. Some images are also BGR565, so it depends what's in the PVRT header.

As for the .CB file, none of the usual compression methods seem to work on it, so there's something different about it. There's an implication of LZSS in the data, but I haven't managed to get it to work. The image files seem to start with "ENC0", so that could be code for encryption of some kind.

  • Localization

i took a look at the .CB file and managed to crack it.

1. ENC0 / ENC2 are not encryption

ENC is short for *encoding*, and the digit is the method number.

*ENC0 — stored, no compression.** The CLSS blob follows the 8-byte header verbatim. That is exactly why CLSS / CTRFImageBuffer / IBUF were readable in a hex editor in the first place. The LZSS chunks live *inside* that blob. Every image resource in SSYF.CB is ENC0.

*ENC2 — the whole record is LZSS-compressed.** Same bitstream as below, but with no 4-byte size prefix; the decompressed size is the resource size already in the TOC. Only the 20 .SET files use it.

2. The compression

Chunk layout

"LZSS" | u32 chunk_size | u32 decompressed_size | bitstream

- Bits are read LSB-first within each byte — bit 0, then bit 1, … bit 7.

- Multi-bit fields are assembled MSB-first — the first bit read is the field's most significant bit.

Grammar.

- flag bit 1literal: the next 8 bits are emitted directly.

- flag bit 0match: 12-bit ring position, then 4-bit length, length = field + 2 (so 2..17 bytes).

Window.

- A 4096-byte ring buffer, initialised to 0x00, write pointer starting at 0.

- pos is a 1-based ring index, so the copy source is ring[(pos - 1 + i) & 0xFFF]. pos == 0 never appears in a real token.

- Every copied byte is written back into the ring at the write pointer, as usual.

- The stream ends as soon as the declared byte count is produced. There is no end marker, and up to 2 bytes of zero padding follow.

int  bitpos = 0;
int  getbit (const u8 *s)        { int b = (s[bitpos>>3] >> (bitpos&7)) & 1; bitpos++; return b; }
u32  getbits(const u8 *s, int n) { u32 v = 0; while (n--) v = (v<<1) | getbit(s); return v; }

u8  ring[4096] = { 0 };
u32 r = 0, o = 0;

while (o < decompressed_size) {
    if (getbit(src)) {                          /* 1 = literal */
        u8 c = getbits(src, 8);
        out[o++] = c;  ring[r] = c;  r = (r + 1) & 0xFFF;
    } else {                                    /* 0 = match */
        u32 pos = getbits(src, 12);             /* 1-based ring index */
        u32 len = getbits(src, 4) + 2;          /* 2..17 */
        u32 s   = (pos - 1) & 0xFFF;
        for (u32 i = 0; i < len; i++) {
            u8 c = ring[(s + i) & 0xFFF];
            out[o++] = c;  ring[r] = c;  r = (r + 1) & 0xFFF;
        }
    }
}

One chunk decompresses to at most 0x20000 bytes. Bigger images are split across consecutive LZSS chunks that concatenate to exactly width * height * 2.

3. The .dds files — CTRFImageBuffer

"CLSS" | u32 len | class name, NUL-terminated ("CTRFImageBuffer")
"IBUF" | u32 size | u32 (0)
       | u16 width | u16 height | u16 pitch (= width*2) | u16 planes (1)
       | u32 flags (0x00100000)
       | u32 mask A | u32 mask R | u32 mask G | u32 mask B
       | u8  bits A, R, G, B            (e.g. 0, 5, 6, 5)
       | u8  top bit index A, R, G, B   (e.g. 0, 15, 10, 4)
then 1..n LZSS chunks

The pixel data is a plain linear raster

4. The .SET files, and exact frame reassembly

Once decompressed, each .SET is a serialised CTRFDataSet that names the textures for a scene and gives the blit rectangles — so reassembly is read from the data.

"CLSS" | u32 len | "CTRFDataSet\0" | u32 nrec
per record:  FF FF FF FF | u32 id | u16 namelen | name | payload
  CTRFTexture   u32 | u32 count | count x NUL-terminated .dds names
  CTRFPictures  u32 | u32 npics
                npics x ( u16 (0x10) | u16 npieces | u16 W | u16 H | u16 ox | u16 oy
                          npieces x ( u32 texture index
                                      u16 sx0 sy0 sx1 sy1    <- inclusive source rect
                                      u16 dx0 dy0 dx1 dy1 )) <- inclusive dest rect
                npics x NUL-terminated picture name   <- names come after ALL pictures
                u32

Note that the picture names come after all the pictures,

The typical scene is 640x480 built from three pieces: 512x480 from the _00 texture, then the _01 texture folded in half to supply the right-hand 128x480 strip.

tex0 src (0,0)-(511,479)   -> dst (0,0)-(511,479)
tex1 src (0,0)-(127,255)   -> dst (512,0)-(639,255)
tex1 src (128,0)-(255,223) -> dst (512,256)-(639,479)

A few sets sSYf_11) store frames at half vertical resolution and give a destination rect twice as tall — i.e. the game line-doubles them.

Other record types are present CTRFContainer, CTRFClipperBuilder, CTRFPictureDraw, and TEXT / PICT / ANIM / LIPS / CLIP sections), but none are needed for images.

Script attached: kitae_cb.py

python kitae_cb.py .CB FILE -o extracted --assemble

python kitae_cb.py .DDS FILE -o extracted # loose CLSS/IBUF file

kitae_cb.py

Edited by Hazza12555

  • Author
On 8/22/2026 at 9:43 AM, Hazza12555 said:

i took a look at the .CB file and managed to crack it.

1. ENC0 / ENC2 are not encryption

ENC is short for *encoding*, and the digit is the method number.

*ENC0 — stored, no compression.** The CLSS blob follows the 8-byte header verbatim. That is exactly why CLSS / CTRFImageBuffer / IBUF were readable in a hex editor in the first place. The LZSS chunks live *inside* that blob. Every image resource in SSYF.CB is ENC0.

*ENC2 — the whole record is LZSS-compressed.** Same bitstream as below, but with no 4-byte size prefix; the decompressed size is the resource size already in the TOC. Only the 20 .SET files use it.

2. The compression

Chunk layout

"LZSS" | u32 chunk_size | u32 decompressed_size | bitstream

- Bits are read LSB-first within each byte — bit 0, then bit 1, … bit 7.

- Multi-bit fields are assembled MSB-first — the first bit read is the field's most significant bit.

Grammar.

- flag bit 1literal: the next 8 bits are emitted directly.

- flag bit 0match: 12-bit ring position, then 4-bit length, length = field + 2 (so 2..17 bytes).

Window.

- A 4096-byte ring buffer, initialised to 0x00, write pointer starting at 0.

- pos is a 1-based ring index, so the copy source is ring[(pos - 1 + i) & 0xFFF]. pos == 0 never appears in a real token.

- Every copied byte is written back into the ring at the write pointer, as usual.

- The stream ends as soon as the declared byte count is produced. There is no end marker, and up to 2 bytes of zero padding follow.

int  bitpos = 0;
int  getbit (const u8 *s)        { int b = (s[bitpos>>3] >> (bitpos&7)) & 1; bitpos++; return b; }
u32  getbits(const u8 *s, int n) { u32 v = 0; while (n--) v = (v<<1) | getbit(s); return v; }

u8  ring[4096] = { 0 };
u32 r = 0, o = 0;

while (o < decompressed_size) {
    if (getbit(src)) {                          /* 1 = literal */
        u8 c = getbits(src, 8);
        out[o++] = c;  ring[r] = c;  r = (r + 1) & 0xFFF;
    } else {                                    /* 0 = match */
        u32 pos = getbits(src, 12);             /* 1-based ring index */
        u32 len = getbits(src, 4) + 2;          /* 2..17 */
        u32 s   = (pos - 1) & 0xFFF;
        for (u32 i = 0; i < len; i++) {
            u8 c = ring[(s + i) & 0xFFF];
            out[o++] = c;  ring[r] = c;  r = (r + 1) & 0xFFF;
        }
    }
}

One chunk decompresses to at most 0x20000 bytes. Bigger images are split across consecutive LZSS chunks that concatenate to exactly width * height * 2.

3. The .dds files — CTRFImageBuffer

"CLSS" | u32 len | class name, NUL-terminated ("CTRFImageBuffer")
"IBUF" | u32 size | u32 (0)
       | u16 width | u16 height | u16 pitch (= width*2) | u16 planes (1)
       | u32 flags (0x00100000)
       | u32 mask A | u32 mask R | u32 mask G | u32 mask B
       | u8  bits A, R, G, B            (e.g. 0, 5, 6, 5)
       | u8  top bit index A, R, G, B   (e.g. 0, 15, 10, 4)
then 1..n LZSS chunks

The pixel data is a plain linear raster

4. The .SET files, and exact frame reassembly

Once decompressed, each .SET is a serialised CTRFDataSet that names the textures for a scene and gives the blit rectangles — so reassembly is read from the data.

"CLSS" | u32 len | "CTRFDataSet\0" | u32 nrec
per record:  FF FF FF FF | u32 id | u16 namelen | name | payload
  CTRFTexture   u32 | u32 count | count x NUL-terminated .dds names
  CTRFPictures  u32 | u32 npics
                npics x ( u16 (0x10) | u16 npieces | u16 W | u16 H | u16 ox | u16 oy
                          npieces x ( u32 texture index
                                      u16 sx0 sy0 sx1 sy1    <- inclusive source rect
                                      u16 dx0 dy0 dx1 dy1 )) <- inclusive dest rect
                npics x NUL-terminated picture name   <- names come after ALL pictures
                u32

Note that the picture names come after all the pictures,

The typical scene is 640x480 built from three pieces: 512x480 from the _00 texture, then the _01 texture folded in half to supply the right-hand 128x480 strip.

tex0 src (0,0)-(511,479)   -> dst (0,0)-(511,479)
tex1 src (0,0)-(127,255)   -> dst (512,0)-(639,255)
tex1 src (128,0)-(255,223) -> dst (512,256)-(639,479)

A few sets sSYf_11) store frames at half vertical resolution and give a destination rect twice as tall — i.e. the game line-doubles them.

Other record types are present CTRFContainer, CTRFClipperBuilder, CTRFPictureDraw, and TEXT / PICT / ANIM / LIPS / CLIP sections), but none are needed for images.

Script attached: kitae_cb.py

python kitae_cb.py .CB FILE -o extracted --assemble

python kitae_cb.py .DDS FILE -o extracted # loose CLSS/IBUF file

kitae_cb.py

Thank You Very Much. Though a few frames in some .cb file about mini game are not assembled well, but it works well on most .cb file CG、BG、Chara、SONG and mid.

for examble, sAMf_11_0.png and sAMf_11_1.png from SAMF.cb are not assembled well.

sAMf_11_00.png

sAMf_11_01.png

sAMf_11_00.dds sAMf_11_01.dds sAMf_11.set.zip SAMF.cb.zip samf_11.zip

Edited by Tefolium

Create an account or sign in to comment

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.