diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4e2de088b..cc75b8231 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -135,6 +135,7 @@ Here are some of the markdown rules:
* **No line between heading and first paragraph** - the next line after a heading should always be the first paragraph of the section
* **Never use numbered lists** - Just use `*` for all unordered lists.
* **Short inline code** - If the code is short, wrap it with backticks (e.g. `eax, 0x00`).
+* **Don't mix bold and inline code** - Avoid `**\`literal\`**` styling. Use backticks for literals (commands, file names, extensions) and bold for emphasis, but not both at the same time.
* **HR before H2/H3** - Have an HR before HR/H3 but only if its not the first sub heading under a heading
diff --git a/pages/consoles/gameboy/GameBoyFileFormats.md b/pages/consoles/gameboy/GameBoyFileFormats.md
index 1094e44a8..3f58645c1 100644
--- a/pages/consoles/gameboy/GameBoyFileFormats.md
+++ b/pages/consoles/gameboy/GameBoyFileFormats.md
@@ -19,7 +19,7 @@ recommend:
- gameboy
- fileformats
editlink: /consoles/gameboy/GameBoyFileFormats.md
-updatedAt: '2026-03-28'
+updatedAt: '2026-04-04'
excerpt: Find out about the most common Game Boy File formats in this post
---
@@ -90,6 +90,59 @@ The broad pattern is that Nintendo's Game Boy projects often kept graphics, colo
The most important refinement now is that the extension alone does not tell you whether a graphics bank is monochrome or color.
A `.CGX` file in a DMG-targeted workspace can still be ordinary 2bpp Game Boy tile data, while the matching `.SCR` and `.COL` files tell you how that bank was actually being laid out or colored inside the editor pipeline.
+---
+## IS-CGB-CAD data formats
+The IS-SUPPORT leak material includes an `IS-CGB-CAD` tool distribution with a small set of internal file formats documented in Japanese as `DCG`, `DSC`, `DOB`, and `DCL`.
+These are useful because they spell out the exact bit packing for Game Boy Color tile data, per-tile attributes, and object layout.
+
+The formats line up closely with what the hardware expects on CGB.
+They also clarify why the same project can carry both SNES-like CAD extensions (`CGX`, `COL`, `SCR`) and more directly named Game Boy specific blobs in the same workflow.
+
+### DCG format - Tile Data
+`DCG` is a tile + attribute + color bundle used by the CAD tool.
+
+Range | Size | Meaning
+---|---:|---
+0x0000-0x17FF | 0x1800 | Character (tile) data
+0x1800-0x197F | 0x0180 | Attributes (palette info in IS-CGB-CAD)
+0x1980-0x19FF | 0x0080 | Color data
+
+The character data is classic Game Boy 2bpp tiles: 16 bytes per 8x8 tile, with 2 bytes per row (low bitplane byte then high bitplane byte).
+
+This plays the same role as SNES `CGX` (a raw tile bank), but the packing is different: Game Boy uses 2bpp row pairs, while SNES `CGX` is planar bitplanes (commonly 4bpp).
+
+### DSC format - Screen Data
+`DSC` is a background screen map in the same split form that the CGB hardware uses: one byte of tile index per cell and one byte of attributes per cell.
+
+Range | Size | Meaning
+---|---:|---
+0x0000-0x03FF | 0x0400 | Character code (tile index)
+0x0400-0x07FF | 0x0400 | Attribute byte
+
+The attribute byte is documented with this bit layout:
+
+Bits | Meaning
+---|---
+0-2 | CGB palette index
+3 | character bank select
+4 | unused (but used inside IS-CGB-CAD)
+5 | horizontal flip
+6 | vertical flip
+7 | priority (0: follow OBJ-side priority, 1: BG highest priority)
+
+This plays the same role as SNES `SCR` (a background tilemap), but the storage model differs: SNES packs tile index and attributes into 16-bit words, while CGB splits the tile index and attributes into separate byte arrays.
+
+### DCL format - Palette data
+`DCL` is a small palette block. It stores 2 bytes per color with 5 bits each for R, G, and B.
+
+
+### DOB format - Object Data
+`DOB` is a chunked object and animation container with tagged blocks like `"CGB "`, `"ANIM"`, `"GRP "`, `"SIZE"`, `"LINK"`, `"VER "`, and `"END "`.
+Unlike many SNES CAD blobs that are fixed-size record regions, `DOB` is explicitly variable-length and carries its own version and filename link table.
+
+If you are comparing this to the SNES CAD families, `DSC` is closest to `SCR` (BG tilemap), while `DOB` is closer to `OBJ`/`OBX` (object placement plus animation-related data).
+
+
---
## ICE and Debugger Support Files
Some of the strangest extensions in the leak make more sense once you look at the debugger workflow rather than the game code alone.
@@ -121,4 +174,3 @@ Tool | Role
`isas32` | Later assembler used heavily in Color-era branches
`islk32` | Later linker used with the newer assembler flow
`isd` | Debugger front end used with ICE startup scripts and debugger images
-
diff --git a/pages/consoles/gameboy/GameBoySDK.md b/pages/consoles/gameboy/GameBoySDK.md
index 59ac9fe94..8dc20c1e0 100644
--- a/pages/consoles/gameboy/GameBoySDK.md
+++ b/pages/consoles/gameboy/GameBoySDK.md
@@ -21,7 +21,7 @@ recommend:
- gameboy
- sdk
editlink: /consoles/gameboy/GameBoySDK.md
-updatedAt: '2020-01-11'
+updatedAt: '2026-04-04'
---
# Official Game Boy Software Development Kit (by Intelligent Systems)
@@ -41,28 +41,84 @@ The official manual for the Software Development Kit was uploaded to archive.org
[Game Boy Development Manual V1.1 : Nintendo : Free Download, Borrow, and Streaming : Internet Archive](https://archive.org/details/GameBoyProgManVer1.1)
---
-## Tools included
+# Tools in the SDK
+Just before the Intelligent Systems development FTP server was shut down, someone managed to backup the contents of the Game Boy color sdk and the files that were saved are as follows:
+* IS-CGB-SDK.7z - Software development kit (libraries etc)
+* IS-CGB-EMULATOR.7z - Color Game Boy Emulator
+* IS-CGB-DEBUGGER.7z - Game Boy debugger
+* IS-CGB-CAD.7z - Character/Graphics development tool
+* IS-CGB-CHARACTER.7z - Character/Graphics development tool
+* IS-CGB-CHARACTER Documentation.7z - Documentation for the CAD graphics tool
-### Intelligent Systems Assembler (ISAS)
+## Intelligent Systems Assembler (ISAS)
To assemble your GameBoy source code into Z80 machine code you could use the official Nintendo (Intelligent Systems) assembler and linker, **ISAS** and **ISLK** respectively.
*Last Known Version*: ISAS 1.26 / ISLK 1.26 (1999/10/26)
-### Intelligent Systems eXecutable - ISX and CVTISX
+## Intelligent Systems eXecutable - ISX and CVTISX
ISX is the format that the Assembler (ISAS) compiles the programs into, it is a compressed version of the rom, to convert it into a standard Game Boy rom you need to run it through CVTISX (ConvertISX).
-### Intelligent Systems Character Designer (IS-CGB-CAD or DMG-CAD)
-The archive only seems to contains the **IS-CGB-CAD** tool, which I presume is some sort of graphics/map editor. Graphics, Sprites and Tiles are known as "characters" for the GameBoy.
+## Intelligent Systems CAD Tool (IS-CGB-CAD or DMG-CAD)
+
+The archive only seems to contains the **IS-CGB-CAD** tool, which is a CAD (COmputer Aided Design) tool for graphics, sprites and Tiles, which are known as are known as "characters" for the GameBoy.
Its known as a Character development system.
-So I presume it stands for Intelligent Systems Character A??? Designer or Development
-### Others
-Just before the Intelligent Systems development FTP server was shut down, someone managed to backup the contents of the Game Boy color sdk and the files that were saved are as follows:
-* IS-CGB-SDK.7z - Software development kit (libraries etc)
-* IS-CGB-EMULATOR.7z - Color Game Boy Emulator
-* IS-CGB-DEBUGGER.7z - Game Boy debugger
-* IS-CGB-CAD.7z - Character/Graphics development tool
-* IS-CGB-CHARACTER.7z - Character/Graphics development tool
-* IS-CGB-CHARACTER Documentation.7z - Documentation for the CAD graphics tool
+
+Based on the `IS-CGB-CHARACTER-000703jp.exe` installer found in the IS-SUPPORT leak material, the CAD tool payload includes the following notable files:
+
+Path in installer | What it is
+---|---
+`Program Executable Files\\ISCGBCAD.exe` | Main Win32 CAD editor executable
+`Program Executable Files\\Iscgbcad.com` | 12 KB companion program (purpose unclear without deeper RE)
+`Shared DLLs\\ISCGB.DLL` | Main shared DLL shipped with the tool
+`Help Files (Japanese)\\FORMAT.TXT` | Japanese format notes (documents `DCG`, `DSC`, `DOB`, `DCL`)
+`Help Files (Japanese)\\help\\*.html` | Japanese HTML help set for the editor UI
+`Example Files\\O2T.C` | Small C example shipped with the installer
+
+The `FORMAT.TXT` file is especially valuable because it gives concrete byte layouts and bitfields:
+* `DCG` - a combined tile + attributes + color bundle (2bpp tile data plus palette metadata)
+* `DSC` - a screen map with one byte of tile index plus one byte of attributes per cell (the split map model used by CGB)
+* `DOB` - a chunked object and animation container with tagged blocks like `"CGB "`, `"ANIM"`, `"GRP "`, `"SIZE"`, `"LINK"`, `"VER "`, and `"END "`
+* `DCL` - a small packed RGB555 palette block (2 bytes per color)
+
+The executable payload also reveals how the editor talks to Nintendo's emulator / hardware layer.
+
+Editor component | What it does
+---|---
+`ISCGBCAD.exe` | The Windows GUI editor. It loads `iscgb.dll` at runtime and resolves the `CGB*` API via `GetProcAddress`.
+`ISCGB.DLL` | A shared DLL that exports the `CGB*` API used by the editor.
+`Iscgbcad.com` | Despite the `.com` extension, this is a small CGB ROM image with the title `IS-CGB-CAD PIC`. The Windows editor looks for it and shows an error if it is missing.
+
+The `ISCGB.DLL` export table includes these named entry points:
+
+Export name | Likely role
+---|---
+`CGBOpen` | Open a connection / session
+`CGBClose` | Close a connection / session
+`CGBRead` | Read from the target
+`CGBWrite` | Write to the target
+`CGBSetMBC` | Configure MBC state (banking) for transfers
+`CGBGo` | Start / run (or resume) execution
+`CGBFindFirst` / `CGBFindNext` / `CGBFindClose` | Enumeration helpers used by the editor UI
+
+The editor also looks up two unnamed exports by ordinal (`8324` and `16345`) in addition to the named ones.
+
+At the strings-and-API level, `ISCGB.DLL` looks like it can use low-level SCSI-style device paths (for example `\\\\.\\SCSI%d:`), and it references `WNASPI32.DLL`, which suggests an ASPI-based transport path for talking to the emulator hardware.
+
+In other words, `ISCGB.DLL` is not just a random dependency: it is the CAD tool's device and transfer layer.
+The Windows GUI editor resolves these `CGB*` entry points at runtime and uses them to enumerate available targets, set bank state, and move data to and from the emulator/hardware environment.
+
+When reverse engineering these binaries you will also see a lot of MFC plumbing.
+For example, `AfxGetModuleState` is an internal MFC helper that returns a module state pointer (`AFX_MODULE_STATE`) used for resource loading and per-module bookkeeping in MFC-based EXEs, DLLs, and ActiveX controls.
+
+Other shipped components in the installer are also worth calling out, because they hint at the UI layer and the Windows 9x-era driver stack the tool expected:
+
+Component | Where it appears | Notes
+---|---|---
+VxD driver | `Shared DLLs\\CGBVIEW.VXD` | Windows 9x-style VxD, likely used by the viewer / emulator transport path
+SYS companion | `Shared Sys\\Cgbview.sys` | Small `.sys` file shipped alongside the VxD
+OCX UI controls | `Program DLLs\\PathBox.ocx`, `Program DLLs\\SolidPalette.ocx` | ActiveX controls likely used for path picking and palette preview UI
+Version notes | `Help Files (Japanese)\\version.txt` | Shipped version/change notes for the tool
+Bundled runtime DLLs | `Shared DLLs\\Mfc42.dll`, `Msvcrt.dll`, `Oleaut32.dll`, `Olepro32.dll`, `msvcirt.dll` | Redistributed Microsoft runtime components typical of the era
---
## Original Game Boy DMG SDK (Contained in Gigaleak archive called Other.7z)
@@ -137,7 +193,7 @@ This is a very old Game Boy assembler kit created around 1991 by Gremlin Graphic
The SDK can be downloaded from [Romhacking.net - Utilities - AZ40 GameBoy Assembler Kit](https://www.romhacking.net/utilities/731/).
-## Executables included
+### Executables included
There are a few executables that can be used in DOS for assembling, debugging and a few sound tools.
Name | Description
@@ -163,4 +219,3 @@ R.H.C was possibly Richard Hutchison as that was a college of Barry and worked o
# References
[^1]: [Barry Leitch - Video Game Music Preservation Foundation Wiki](http://www.vgmpf.com/Wiki/index.php/Barry%20Leitch#Game_Boy)
[^2]: [Romhacking.net - Utilities - AZ40 GameBoy Assembler Kit](https://www.romhacking.net/utilities/731/)
-
diff --git a/pages/consoles/snes/SNESFileFormats.md b/pages/consoles/snes/SNESFileFormats.md
index 184972da2..209beb8c5 100644
--- a/pages/consoles/snes/SNESFileFormats.md
+++ b/pages/consoles/snes/SNESFileFormats.md
@@ -59,7 +59,7 @@ These file types come from a Sony NEWS workstation art pipeline tool commonly re
In practice it behaves like an editor that saves multiple linked layers as separate files, rather than exporting one baked screen per save.
## S-CG-CAD
-The Computer Aided Design tool known as `S-CG-CAD` or `S-CAD` was developed by long term Nintendo partner company SRD (Systems Research and Development). It was intended to un on **MIPS-based Sony NEWS** workstations, the executables themselves are **MIPS big-endian ECOFF** (often reported as "MIPSEB ECOFF executable (paged)").
+The Computer Aided Design tool known as `S-CG-CAD` or `S-CAD` was developed by long term Nintendo partner company **SRD** (Systems Research and Development). It was intended to un on **MIPS-based Sony NEWS** workstations, the executables themselves are **MIPS big-endian ECOFF** (often reported as "MIPSEB ECOFF executable (paged)").
If you have the ability to run these executables they can be found in the gigaleak in the NEWS.7z archive, specifically the NEWS_11 tape backup under the user `hino`'s home directory.
@@ -88,7 +88,7 @@ Main CAD application binaries:
Location | File | Type | Purpose
---|---|---|---
`srd/cad/bin` | `cad` | ECOFF binary | The main S-CG-CAD editor.
-`srd/cad/bin` | `cad_test` | ECOFF binary | Test build of the CAD editor.
+`srd/cad/bin` | `cad_test` | ECOFF binary | Unstripped S-CG-CAD test build.
`srd/cad/bin` | `pr_chr_B` | ECOFF binary | Printer output helper: character sheets (large).
`srd/cad/bin` | `pr_chr_M` | ECOFF binary | Printer output helper: character sheets (medium).
`srd/cad/bin` | `pr_chr_S` | ECOFF binary | Printer output helper: character sheets (small).
@@ -108,13 +108,13 @@ Location | File | Type | Purpose
`srd/cad/environment` | `mkcad_srd` | text/script | Creates a `.CAD_SRD` workspace directory and moves CAD state files into it.
### Reverse Engineering the executables
-The main `cad` executable is unfortunetly stripped (no debug symbols to get function names), however there is a `cad_test` executable that is completely unstripped!
+The main `cad` executable is unfortunately stripped (no debug symbols to get function names), but there is also an unstripped S-CG-CAD test build shipped as `cad_test`.
It is a **MIPS big-endian ECOFF** executable, and its ECOFF a.out optional header has `vstamp = 0x020B`, i.e. **2.11**.
-That `vstamp` is the “toolchain version stamp” written by the compiler/linker in MIPS ECOFF output, so this binary was built with a **MIPS ECOFF toolchain stamping version 2.11** (the classic vendor `cc`/`ld` style toolchain used on NEWS-OS era MIPS systems), rather than something like modern GCC/ELF.
+That `vstamp` is the "toolchain version stamp" written by the compiler/linker in MIPS ECOFF output, so this binary was built with a **MIPS ECOFF toolchain stamping version 2.11** (the classic vendor `cc`/`ld` style toolchain used on NEWS-OS era MIPS systems), rather than something like modern GCC/ELF.
-Unfortunetly it is in ECOFF format which Ghidra has a hard time understanding so the best thing to do is to convert to an elf like so (note that despite the strip debug it will still have the names):
+Unfortunately it is in ECOFF format which Ghidra has a hard time understanding, so the best thing to do is to convert it to an ELF like so (note that despite `--strip-debug` it will still have symbol names):
```
mipsel-linux-gnu-objcopy --strip-debug -O elf32-tradbigmips cad_test cad_test.elf
```
@@ -150,9 +150,9 @@ sfx_main | full screen preview transfer | CGX, OBJ, COL, SCR
tl_main1 | tile and palette focused preview | CGX, OBZ, COL
tl_main2 | full screen preview but with OBZ format and CAD.DAT | CGX, OBZ, COL, SCR, CAD.DAT
-`sfx_main` and `tl_main2` both transfer enough data to show a full composed screen (.SCR), but they’re aimed at different CAD workflows and they move different payload sets:
-* sfx_main (the “full CAD screen” transfer) uploads CGX + OBJ + COL + SCR. It’s the only one that explicitly expects the standard .OBJ object-layout files.
-* tl_main2 (the “tile/layout” transfer) uploads CGX + OBZ + COL + SCR and also a CAD.DAT blob. It uses CAD.OBZ instead of per-slot .OBJ, and it has extra per-transfer state/config via CAD.DAT.
+`sfx_main` and `tl_main2` both transfer enough data to show a full composed screen (.SCR), but they are aimed at different CAD workflows and they move different payload sets:
+* sfx_main (the "full CAD screen" transfer) uploads CGX + OBJ + COL + SCR. It is the only one that explicitly expects the standard .OBJ object-layout files.
+* tl_main2 (the "tile/layout" transfer) uploads CGX + OBZ + COL + SCR and also a CAD.DAT blob. It uses CAD.OBZ instead of per-slot .OBJ, and it has extra per-transfer state/config via CAD.DAT.
This is a useful lens for interpreting the formats on this page: the file types were designed to be transferred as a bundle, then interpreted by a small runtime on the SNES devkit.
@@ -165,9 +165,10 @@ It has 2 bytes per color and uses SNES `BGR555` packed color in little-endian or
Field | Value
---|---
Encoding | little-endian SNES BGR555
-Common size | 1,024 bytes
-Colors | 512 color words
-Layout | 32 palette rows of 16 colors
+Record region | `0x200` bytes (256 colors)
+Common size | `0x400` bytes in S-CG-CAD saves (record region + tool metadata)
+Colors | 256 color words
+Layout | 16 palette rows of 16 colors
Row slot 0 | commonly treated as transparent or backdrop-like
### Byte Layout
@@ -197,6 +198,35 @@ Decode rules:
* `r5 = (v >> 0) & 0x1F`, `g5 = (v >> 5) & 0x1F`, `b5 = (v >> 10) & 0x1F`
* expand 5-bit to 8-bit with `(c5 << 3) | (c5 >> 2)`
+### Tool metadata (S-CG-CAD)
+S-CG-CAD saves often append tool metadata after the `0x200`-byte record region, making a common on-disk size of `0x400` bytes:
+
+Offset | Size | Meaning | Notes
+---|---:|---|---
+`0x0200` | `0x100` | tool header block | Observed to begin with ASCII `NAK1989 S-CG-CADVer...` in multiple assets.
+`0x0300` | `0x100` | per-row tool metadata | S-CG-CAD writes `0x80` bytes of row data (32 rows x 4 bytes) and leaves the remaining bytes as reserved/zero.
+
+In the S-CG-CAD toolchain, only a small part of this metadata is clearly used in normal open/save flows. If you are writing a rewriter, preserve `0x0200..0x03FF` exactly where possible.
+
+The per-row metadata is written as a flat table at `0x0300..0x037F`:
+
+* 32 entries (one per palette row), 4 bytes per entry.
+* Each entry is copied from the low byte of four 32-bit words in a 16-byte-per-row structure (bytes at offsets `+3`, `+7`, `+0x0B`, `+0x0F` from a `0x10`-byte stride per row).
+* These bytes appear to be S-CG-CAD editor-side palette table state (preview colors / attribute selectors) rather than SNES runtime palette data. The tool updates them via the palette table UI and uses `cgbank` to decide which bits are meaningful when rendering the palette table.
+
+Important note: S-CG-CAD does not appear to use the `0x0200..0x03FF` metadata when opening a normal `.COL` through its usual file open path. The metadata is still saved and is useful for round-tripping, but the separate "restore from backup" path uses a different stream shape: `0x200` bytes of color words, followed by `0x200` bytes of palette metadata, followed by a single byte of `cgbank`.
+
+### What cgbank does (S-CG-CAD)
+The `cgbank` setting is the CAD-side tile bank selector used when interpreting and exporting CGX data.
+Although the name is misleading, `cgbank` in this tool acts like a per-palette "color mode" selector that affects how the editor maps palette indices into its workstation-side 8-bit color table.
+
+The tool's palette update routine shows three main behaviors based on `cgbank`:
+
+* `cgbank = 0` - updates a 32-entry repeating window (`index & 0x1F`)
+* `cgbank = 1` - updates a 128-entry repeating window (`index & 0x7F`)
+* `cgbank = 2` - updates all 256 entries (with special-cased reserved indices like `fore`, `back`, and `mixc`)
+
+This is editor/UI behavior, not SNES runtime palette data, but the chosen mode is still persisted in S-CG-CAD's save/backup flows.
### Interactive COL Viewer
This viewer loads a COL file and shows every row as swatches.
@@ -224,6 +254,39 @@ Size | 4bpp tiles | Notes
34,048 bytes | 1,064 | common menu and title banks
65,792 bytes | 2,056 | large shared banks
+S-CG-CAD also uses fixed "standard bank" CGX containers that bundle 1024 tiles plus tool metadata. These are easiest to recognize by file size:
+
+File size | Record region | Bit depth | Tiles | Tail bytes | Notes
+---|---:|---:|---:|---:|---
+`0x4500` | `0x4000` | 2bpp | 1024 | `0x100` header + `0x400` per-tile table | S-CG-CAD reads 2bpp planes and ORs `tile_table[i] << 2` into each pixel index.
+`0x8500` | `0x8000` | 4bpp | 1024 | `0x100` header + `0x400` per-tile table | S-CG-CAD reads SNES 4bpp planes and ORs `tile_table[i] << 4` into each pixel index.
+`0x10100` | `0x10000` | 8bpp | 1024 | `0x100` header | S-CG-CAD reads SNES 8bpp planes; no per-tile table is stored in this variant.
+
+In these S-CG-CAD variants the first `record region` bytes are still standard planar tile data and can be decoded normally. The extra `0x100` header commonly begins with ASCII `NAK1989 S-CG-CADVer...`, and the per-tile table is tool-specific metadata that affects how the editor constructs 8-bit pixel indices.
+
+### Per-tile table (S-CG-CAD)
+In the `0x4500` and `0x8500` variants, S-CG-CAD stores a 1024-byte table after the `0x100` header. Each entry is one byte per tile. The tool uses it as a per-tile "pixel index prefix":
+
+* For 2bpp CGX (`0x4500`), S-CG-CAD combines it as `pixel_index = (tile_table[tile] << 2) | pixel_2bpp`.
+* For 4bpp CGX (`0x8500`), S-CG-CAD combines it as `pixel_index = (tile_table[tile] << 4) | pixel_4bpp`.
+
+That means the table acts like a per-tile palette selector in editor space (it chooses which group of 4 or 16 colors the tile's pixels index into), even though the underlying record region is still standard planar 2bpp/4bpp tile data.
+
+In the 2bpp variant there is an extra twist: the tool packs a bank-wide setting into the top bits of each per-tile table byte. After the tool's decode step, the effective 8-bit editor pixel index behaves like:
+
+* `pixel_index = (header_0x23 << 5) | ((chars[tile] >> 2 & 7) << 2) | pixel_2bpp`
+
+In other words, part of the "prefix" is per-tile and part is effectively a per-bank constant in the CAD preview pipeline.
+
+Some bytes inside the `0x100` CGX header are set by the tool and appear to mirror editor state:
+
+Offset | Size | Meaning | Notes
+---|---:|---|---
+`0x20` | `1` | `cgbank` mode | Same mode described in the COL section; affects how pixel indices are interpreted in the editor.
+`0x21` | `1` | colormap bank index | A small selector (0-3) that affects the CAD tool's workstation-side preview colormap.
+`0x22` | `1` | BG/OBJ palette toggle | A 1-bit toggle that affects how the CAD tool configures its preview colormap.
+`0x23` | `1` | cell index (2-bit) | A small selector (0-3) that affects CAD preview paths. In the 2bpp standard-bank variant it also feeds the bank-wide prefix contribution described above.
+
The Nintendo CGE UI (Color Graphics Editor) explicitly shows 4bit and 8x8 editing for this data family.
@@ -290,7 +353,7 @@ Each tilemap block is a 32x32 grid of 16-bit words:
* 32 x 32 = 1,024 entries
* 1,024 x 2 bytes = 2,048 bytes per block
-Each entry is a 16-bit little-endian word with this bitfield layout:
+Each entry is a 16-bit word with this bitfield layout:
Bits | Meaning
---|---
@@ -302,13 +365,48 @@ Bits | Meaning
If you are writing a parser:
-* read 16-bit `w` little-endian
+* read 16-bit `w` from the file
* `tile = w & 0x03FF`
* `pal = (w >> 10) & 0x07`
* `pri = (w >> 13) & 0x01`
* `hflip = (w >> 14) & 0x01`
* `vflip = (w >> 15) & 0x01`
+Endianness note:
+
+* In a ROM or 65c816 runtime context, SNES tilemap words are typically stored little-endian.
+* In the S-CG-CAD `save_scr` path, the tool explicitly byte-swaps each 16-bit word before writing it, and `load_scr` swaps it back after reading. That means S-CG-CAD-authored `.SCR` files are big-endian on disk for the 16-bit tilemap words.
+
+S-CG-CAD also writes a `0x100` metadata header (starting with ASCII `NAK1989 S-CG-CADVer...`) at `0x8000`, followed by a `0x200` trailer region. A few header bytes appear to mirror editor state:
+
+Offset (from 0x8000) | Size | Meaning | Notes
+---|---:|---|---
+`0x40` | `1` | `scbank` bit depth mode | Selects how the screen tool interprets tile graphics and pixel indices in its internal preview buffer. In the traced paths: `0` behaves like 2bpp (pixel indices `0-3` plus a per-tile 3-bit prefix), `1` behaves like 4bpp (pixel indices `0-15` plus a per-tile 3-bit prefix), and `2` behaves like 8bpp (raw `0-255` pixel indices).
+`0x41` | `1` | unknown SCR mode byte (2-bit) | A small per-screen mode field used by the CAD tool. The exact meaning has not been traced yet, but the value is preserved on load/save.
+`0x42` | `1` | screen tile size mode | Controls how the CAD tool composes tiles for preview and stamping. In the traced behavior, `0` acts like 8x8 and `2` acts like 16x16 (composed from 4 tiles: `tile`, `tile+1`, `tile+0x10`, `tile+0x11`).
+`0x43` | `1` | referenced CGX bank (2-bit) | Which of the CAD tool's CGX banks this screen uses when previewing tiles (0-3).
+`0x44` | `1` | screen colormap bank index | A small selector used by the CAD tool's preview colormap (0-3).
+`0x45` | `1` | screen color selector byte | Additional CAD preview color selector byte (interpretation depends on `scbank`).
+`0x46` | `1` | screen color selector byte | Additional CAD preview color selector byte (interpretation depends on `scbank`).
+`0x47..0x48` | `2` | stamp base tile index (16-bit) | Base tile index used by the CAD tool when stamping tiles into the screen, and displayed in the UI as a 3-nibble hex value like `ABC`.
+
+Trailer region note:
+
+* In `save_scr`, the trailing `0x200` bytes are written as `0xFF` fill.
+* In `load_scr`, the tool reads the `0x200` bytes (four `0x80` chunks) but does not appear to interpret them in the traced code paths.
+
+### Toolchain conversion
+Some pipelines include small conversion utilities that export a CAD-authored `.SCR` into a simpler table for PC-side tooling.
+
+One example is `stdscr.c` (header: "NEWS-CAD (SCR_File) -> STD_SCR (*.STD)", MS-DOS ver 1.11, 1992-04-13). It reads `base.SCR` as 4 blocks of `0x400` entries, but for each entry it takes one byte and discards the next byte, which effectively strips the upper byte of the 16-bit tilemap word (the byte that carries most attribute bits) and keeps the low byte.
+
+It then repacks the four 32x32 blocks into a single `0x1000` byte output called `base.STD`:
+
+* blocks 0 and 1 are placed side-by-side to form a 64x32 region (each 32-byte row becomes 64 bytes)
+* blocks 2 and 3 are placed side-by-side to form another 64x32 region below it
+
+In other words, this "MS-DOS screen" is not a DOS graphics format like a VGA framebuffer. It is a raw 4KB 64x64 grid of 8-bit tile indices, intended for quick viewing, printing, or importing into PC-side tools that do not need per-tile SNES attributes.
+
The CAD tool UI shows the same workflow split: screen, object, map, and SFX metadata as separate operations.
@@ -320,6 +418,161 @@ This viewer composes SCR using CGX and COL.
app="/public/js/sandpack/examples/SnesScrViewer.tsx">
+---
+## PNL Format
+PNL is the CAD-side "panel" format: a large tile selection surface used as the source for MAP stamping and MAP-to-SCR conversion.
+
+In the S-CG-CAD toolchain, PNL is stored as a fixed-size container.
+In the NEWS archives, `*.PNL` files are consistently `0x10100` bytes (65,792 bytes).
+
+Range | Size | Meaning
+---|---:|---
+`0x0000` to `0x00FF` | `0x100` | header (CAD provenance + editor state)
+`0x0100` to `0x80FF` | `0x8000` | tile table (16-bit words, big-endian)
+`0x8100` to `0x100FF` | `0x8000` | per-tile flag table (16-bit words, big-endian)
+
+This viewer can derive a screen tilemap directly from a `*.MAP` + `*.PNL` pair so you can validate flip, priority, and palette propagation without exporting a `*.SCR` from the original tool.
+
+
+
+
+### Header fields (S-CG-CAD)
+The first `0x20` bytes are typically an ASCII CAD provenance string like `NAK1989 S-CG-CADVer...`.
+
+The S-CG-CAD load/save path consumes a small set of header bytes as per-bank editor settings.
+Not every field is fully understood yet, but the offsets and bit masking are stable:
+
+Offset | Size | Meaning | Notes
+---|---:|---|---
+`0x0060` | `1` | colormap mode | Stored as `plbank` per panel bank. This controls which colormap upload path is used (for example 32-color uploads vs 128-color uploads vs full 256-color uploads).
+`0x0061` | `1` | Mode 7 enable flag | A 0/1 flag used to toggle the Mode 7 label in the UI for the currently selected bank.
+`0x0062` | `1` | panel graphics mode | A small mode selector used to choose panel and map conversion routines. In traced code paths, only bit 1 is consumed (`header[0x62] & 0x02`), so the stored value behaves like `0` or `2`.
+`0x0063` | `1` | referenced CGX bank | `header[0x63] & 0x03`. This is the bank used when sampling tile graphics in some panel operations.
+`0x0064` | `1` | panel colormap bank index | `header[0x64] & 0x03`. This is the "color bank" selector used by the panel preview colormap UI.
+`0x0065` | `1` | colormap selector A | A per-bank selector used when choosing which slice of the internal preview colormap tables to upload.
+`0x0066` | `1` | colormap selector B | A secondary selector used only in some colormap modes (see below).
+`0x0067..0x0068` | `2` | base tile index | A 16-bit value set from the character window selection. In traced UI code it is displayed as a 3-nibble hex value as a reminder of the last "character tile" you picked, and it is preserved in the PNL header across saves.
+`0x0069` | `1` | panel tile width exponent | Used as `1 << (header[0x69] & 0x1F)` in panel and map conversion logic.
+`0x006A` | `1` | panel tile height exponent | Used as `1 << (header[0x6A] & 0x1F)` in panel and map conversion logic.
+
+Other header bytes in the `0x60..0x6A` range are preserved and appear in the S-CG-CAD save path, but their exact UI meaning has not been traced yet.
+
+#### How the header drives panel preview colors
+The tool uses the header bytes above to decide how to upload and index colors for the panel preview:
+
+* `colormap mode` selects the upload mode (a small fixed palette vs a larger subset vs a full 256-color ramp).
+* the `panel colormap bank index` and the two `colormap selector` bytes choose which slice of the internal colormap tables is sent to X11 for preview.
+
+In other words, PNL does not embed actual SNES palette data. It stores the panel editor's "how to interpret pixel indices and which colormap slice to show" state.
+
+The two selector bytes are not always used in the same way:
+
+* In the 32-color upload mode, the tool uses both selector A and selector B to pick a 32-color slice.
+* In the 128-color upload mode, only selector A is used (selector B is effectively ignored).
+* In the 256-color upload mode, neither selector is used (the upload is controlled by the bank index byte instead).
+
+#### How the width and height exponents are used
+The `panel tile width exponent` and `panel tile height exponent` do not change the on-disk table size.
+They change how the tool treats a single panel tile selection when converting between panel, map, and screen:
+
+* `metaWidth = 1 << (header[0x69] & 0x1F)`
+* `metaHeight = 1 << (header[0x6A] & 0x1F)`
+
+When these values are greater than 1, one "logical tile" selection is treated as a block of multiple 8x8 tiles.
+This is how the editor supports workflows like 16x16 stamping and map conversion based on larger metatiles without changing the underlying 8x8 tile storage.
+
+### Tile table word layout
+The tile table is `0x4000` 16-bit words.
+Each word encodes the tile id plus a few per-tile attributes used by S-CG-CAD's panel editor.
+
+Bits | Mask | Meaning
+---|---|---
+15 | `0x8000` | vertical flip
+14 | `0x4000` | horizontal flip
+13 | `0x2000` | priority
+bits 12 to 10 | `0x1C00` | palette row (0-7)
+bits 9 to 0 | `0x03FF` | tile id (0-1023)
+
+The S-CG-CAD decomp stores these into an internal 6-byte per-tile entry used throughout the panel and map code:
+
+* `vflip = (word >> 15) & 0x01`
+* `hflip = (word >> 14) & 0x01`
+* `priority = (word >> 13) & 0x01`
+* `palRow = (word >> 10) & 0x07`
+* `tileId = word & 0x03FF`
+
+These bit meanings line up with how the tool's panel flip actions work:
+
+* a horizontal flip toggles bit 14 (and swaps tiles left-to-right), and
+* a vertical flip toggles bit 15 (and swaps tiles top-to-bottom).
+
+That behavior confirms the two high bits are flip flags, not a CGX bank selector.
+
+If you are round-tripping words, the S-CG-CAD load/save path treats the 16-bit word as big-endian on disk and does not byte-swap it in the PNL I/O functions.
+
+#### What the priority bit means in practice
+The `priority` bit is the same concept as the SNES BG tilemap "priority" flag (bit 13 in an on-console tilemap word).
+It controls whether the tile is drawn in the high priority group for that background layer.
+
+In S-CG-CAD workflows, this matters because:
+
+* the panel editor lets you set priority per tile (for example with the panel priority tool), and
+* when the tool converts panel or map data into SCR cell data, it copies the panel priority bit through so the resulting SCR tiles preserve the intended priority.
+
+In traced MAP-to-SCR conversion, priority is taken from the panel tile entry whenever a MAP cell resolves to a panel tile.
+It is not gated by the MAP cell's "attribute source" flag, which only affects how the final tile id is chosen.
+
+### Per-tile flag table
+The second table is also `0x4000` 16-bit words.
+In the traced S-CG-CAD load path, only bit 15 is consumed and stored as a 1-byte per-tile flag:
+
+* `flag = (word2 >> 15) & 0x01`
+
+This flag is used as the "tile present" / "tile active" check in multiple paths (for example, panel and MAP conversion logic treats tiles as empty when this flag is `0`).
+
+In real NEWS archive `*.PNL` files, the lower 15 bits of this second-table word are often non-zero.
+S-CG-CAD does not appear to consume those bits in the traced code paths, so they are best treated as unknown/reserved metadata that other tool builds may use.
+
+### Common editor operations (what changes which bits)
+The decomp shows that the panel editor treats the two big-endian tables as the authoritative state, and the UI tools mutate specific fields in-place:
+
+* **Erase / clear** - clears the "present" flag for the selected region (this is what makes a tile slot empty).
+* **Palette row change** - writes a new `palRow` value (bits 12 to 10) for all present tiles in the selected region.
+* **Priority change** - writes a new `priority` bit (bit 13) for all present tiles in the selected region.
+* **Flip** - toggles `hflip` (bit 14) and/or `vflip` (bit 15) for tiles in the selected region and swaps entries so the image stays visually consistent.
+
+This is useful when reverse engineering real assets, because it tells you which fields are intended to be mass-edited as attributes rather than being treated as part of the tile id itself.
+
+### Attribute propagation into SCR
+In the S-CG-CAD toolchain, PNL is the source of truth for per-tile attributes that eventually end up inside the SCR tilemap word.
+When a MAP cell resolves to a panel tile, the conversion copies these fields through:
+
+PNL field | SCR meaning
+---|---
+`vflip` | SCR vertical flip bit (bit 15)
+`hflip` | SCR horizontal flip bit (bit 14)
+`priority` | SCR priority bit (bit 13)
+`palRow` | SCR palette row (bits 12 to 10)
+
+The tile id bits (SCR bits 0 to 9) are filled from either the panel tile id or from a default value depending on the MAP cell's attribute source flag.
+
+### Panel dimensions and indexing
+Although the file stores `0x4000` entries, many UI paths treat the panel as having a fixed row stride of 32 tiles.
+
+This matches the MAP format, where `panelX` is 5 bits (0-31) and `panelY` is 9 bits (0-511), and the tool flattens the coordinates as:
+
+* `panelTileIndex = panelY * 32 + panelX`
+
+So a practical mental model is "a 32x512 tile panel", with scrolling and grouping handled in the editor.
+
+### Notes on the remaining unknown header bytes
+The PNL header is `0x100` bytes, but the S-CG-CAD load/save path only consumes a small subset of offsets (`0x60..0x6A` and the provenance string).
+
+If you are writing a parser, it is safest to treat the entire header as opaque and preserve it byte-for-byte on save.
+Even if a byte is not used by the code paths we traced, it may still matter for other tools, other CAD builds, or project-specific workflows.
+
---
## OBJ and OBX Format
OBJ and OBX are framed object-layout containers used to place sprites and object-form text.
@@ -333,51 +586,70 @@ In the source archives there are at least two closely related "OBJ" layouts:
Format | Record region | Frame count | Slots per frame | Bytes per slot
---|---:|---:|---:|---:
OBJ | 12,288 bytes | 32 | 64 | 6
-CAD workspace OBJ | 49,152 bytes | 64 | 128 | 6
-OBX | 49,152 bytes | 64 | 128 | 6
+Extended OBJ | 24,576 bytes | 64 | 64 | 6
+OBX (observed) | 49,152 bytes | 64 | 128 | 6
Files commonly append a CAD metadata tail after the record region.
+### Toolchain conversion
+Some projects also include small conversion utilities that turn CAD-authored `.OBX` data into assembler-friendly tables for the game runtime.
+
+One example is `eobjcnvX.c`, which reads a full 49,152-byte `.OBX` record region (`64 * 128 * 6`), filters to slots where the "display enable" flag (byte 0 bit `0x80`) is set, applies a global X/Y offset, and writes the results out as `eobj.dat` using `BYTE` and `HEX` directives.
+
+This is not a different on-disk `.OBX` format: it is a build-time export that (a) changes representation (text + directives rather than a binary container), and (b) intentionally alters some fields (it emits the packed word as two bytes and clears bits `0x30` in the high byte, which corresponds to dropping the priority bits in the packed attribute + tile word described below).
+
### Entry Layout
#### CAD OBJ / OBX (6-byte slots)
-Each slot is 6 bytes:
+Each slot is 6 bytes. In the S-CG-CAD toolchain, bytes `4..5` are treated as a single big-endian 16-bit value, not two independent bytes.
Byte | Meaning
---|---
-1 | flags: bit 7 display enable, other bits tool-dependent (see notes below)
+1 | flags: bit 7 display enable, bit 0 size select
2 | group info (tool classification)
3 | Y displacement (signed 8-bit)
4 | X displacement (signed 8-bit)
-5 | attributes: YXPPCCCT
-6 | tile number
+bytes 5 to 6 | packed attribute + tile word (big-endian, see below)
### Byte Layout
OBJ and OBX record regions are a flat array of 6-byte slots, grouped into frames.
-Parsers should treat each slot as six independent bytes.
+Parsers should not assume byte `4` is the attribute byte and byte `5` is the tile byte. In the main S-CG-CAD parsing path, bytes `4..5` are decoded as a single 16-bit value.
Slot byte | Meaning | Decode
---|---|---
-`0` | flags | bit 7 display enable
+`0` | flags | bit 7 display enable, bit 0 size select
`1` | group info | tool classification byte
`2` | Y | signed 8-bit (two's complement)
`3` | X | signed 8-bit (two's complement)
-`4` | attributes | `YXPPCCCT`
-`5` | tile number | tile index byte
+`4..5` | packed attr + tile | big-endian 16-bit word
+
+Flag byte notes (S-CG-CAD):
+
+* bit `0x80` (display enable) and bit `0x01` (size select) are the only flag bits referenced in the S-CG-CAD paths traced so far.
+* other bits may exist for other tools or workflows, but are not clearly interpreted in the S-CG-CAD OBJ editor logic traced so far.
+Group info notes (S-CG-CAD):
+
+* byte 1 is preserved on load/save, but is not clearly consumed by the S-CG-CAD editing, printing, or selection paths traced so far.
+* treat it as a tool classification byte and preserve it exactly when rewriting files.
+* in some observed S-CG-CAD OBJ assets, this byte is `0x00` for all slots (so it may be unused in at least some projects).
+* in other observed OBJ assets, it takes small integer values (commonly `0x00..0x05`, and sometimes higher like `0x09`), suggesting it is used as a lightweight grouping or classification tag by some pipelines even if S-CG-CAD does not interpret it directly.
Signed coordinate decode:
* `x = X` if `X < 0x80`, else `X - 0x100`
* `y = Y` if `Y < 0x80`, else `Y - 0x100`
-Attribute byte bits:
+Packed attribute + tile word bits (from the S-CG-CAD decode):
Bits | Meaning
---|---
-7 | vertical flip (Y)
-6 | horizontal flip (X)
-5 to 4 | priority (PP)
-3 to 1 | palette row (CCC)
-0 | tile page / name select (T)
+15 to 14 | flip selector (2-bit enum: `0` = no flip, `1` = horizontal, `2` = vertical, `3` = both)
+13 to 12 | priority (PP)
+11 to 9 | palette row (CCC)
+8 to 0 | tile index (9-bit, `0x000..0x1FF`)
+
+Flip selector notes (S-CG-CAD):
+
+* The CAD tool treats the 2-bit flip selector as a 0..3 enum and uses it consistently across load/save and preview paths.
Rendering notes:
@@ -385,6 +657,25 @@ Rendering notes:
* the size flag selects the SNES OAM small/large size pair, which is configurable
* VRAM and CGRAM offsets shift the tile and palette bases when the project expects an offset load
* project tooling may use a smaller or customized container, so parsers should not assume one fixed size without checking (a common heuristic is to locate the `NAK1989` tool header and treat everything before it as the record region)
+* the S-CG-CAD read/write loops walk slot indices `63` down to `0`, so the first 6 bytes in a frame correspond to slot index `63`
+* in S-CG-CAD preview/image paths, the tile index high bit (bit 8) is also used as a coarse bank select: if `tileId < 0x100` it uses the per-bank "F address" base, otherwise it uses the per-bank "B address" base
+
+Priority and palette usage notes (S-CG-CAD):
+
+* The palette row (CCC) is used by S-CG-CAD's object image generation and preview paths (it is applied as a high-nibble / palette selection when composing pixels), and the UI can edit it for selected objects.
+* The priority (PP) is editable and is printed in report output, but the S-CG-CAD on-screen selection and arrangement paths traced so far do not use it for overlap ordering (draw order is still slot-order based).
+
+Tile banking and "F/B address" notes (S-CG-CAD preview):
+
+* S-CG-CAD treats the tile index high bit (bit 8) as a coarse bank selector. In preview/image paths it checks `tileId >> 8`:
+ * `tileId < 0x100` - uses the per-bank "F address" preset from header byte `0x55`
+ * `tileId >= 0x100` - uses the per-bank "B address" preset from header byte `0x56`
+* Those presets are indices into a 16-bit base table (a list of words like `0x0000, 0x0100, 0x0200, 0x0300, ...`). The selected base word is then OR'd into the character fetch address used to look up tiles in the active OBJ CGX bank.
+* In other words, even though the on-disk tile id is only 9-bit, S-CG-CAD can shift the effective tile base through the header presets during preview.
+* Practical interpretation for preview renderers: treat each preset as selecting a 0x100-tile page, then add the low 8 bits of the on-disk tile id:
+ * `baseTiles = (preset & 0x0F) * 0x100`
+ * `effectiveTile = baseTiles + (tileId & 0xFF)`
+* This is S-CG-CAD tool behavior for preview and report generation, not a documented SNES OAM hardware field.
#### What "size select" means on SNES
The SNES PPU chooses sprite sizes in two layers: a global mode and a per-sprite toggle.
@@ -396,10 +687,11 @@ Flag notes:
* CAD-side tooling clearly uses bit `0x80` in the first byte as a display/enable toggle.
* The exact bit used for "size select" differs between toolchains and file layouts:
* In the `pr_obj__` printer/report tool, the entry flag byte uses bit `0x01` as the size-select toggle.
+ * In the S-CG-CAD CAD toolchain path (`load_obj` / `save_obj`), byte 0 bit `0x01` is also used as the size-select toggle.
* In `.OBZ`, size select is stored in bit `0x40` of OBZ byte 0 (see OBZ section below).
- * For CAD `.OBJ` / `.OBX` files, many observed assets only toggle bit `0x01` (and never set `0x40`), so treat bit `0x40` as unconfirmed for the on-disk CAD format until traced end-to-end in `obj_tool`.
-#### Printer OBJ (`pr_obj__`) (10-byte entries)
+#### Printer OBJ (pr_obj__) (10-byte entries)
+This is the CAD print pipeline's OBJ-like format used by the `pr_obj__` binary.
The `pr_obj__` printer/report utility reads an "OBJ" region as 10-byte entries, 64 entries per frame, 64 frames:
Field | Value
@@ -415,7 +707,7 @@ Byte layout for each 10-byte entry (what `pr_obj__` actually uses):
Offset | Meaning | Notes
---|---|---
`0` | flags | bit 7 display enable, bit 0 size select
-`1` | flip selector | used as a 0..3 selector; behaves like H/V flip bits
+`1` | flip selector | 2-bit enum: `0` = none, `1` = horizontal, `2` = vertical, `3` = both
`2` | unknown (nibble printed) | printed in report output, not used for rendering
`3` | palette/attribute nibble | used as `value << 4` and also printed as a nibble
`4` | unknown | not referenced by `pr_obj__` rendering path
@@ -427,34 +719,25 @@ Offset | Meaning | Notes
### Sequence Data
Some object containers also include a small sequence table used to define simple animations as a timed list of frame indices.
-The sequence table is stored as fixed-size blocks of raw bytes:
-
-Field | Value
----|---
-Sequence count | 16 sequences
-Sequence size | tool-dependent (see below)
-Steps per sequence | tool-dependent
-End marker | first pair where both bytes are zero
-
-Byte layout for each sequence:
+In the S-CG-CAD toolchain, the sequence table is a fixed block stored after the `0x100` header, and it is decoded as raw (timer, frame) pairs.
-Offset | Size | Meaning
----|---:|---
-`0x00` | `2` | duration 0, frame 0
-`0x02` | `2` | duration 1, frame 1
-... | ... | ...
-`0x1E` | `2` | duration 15, frame 15
+Container | Total sequence table size | Sequences | Steps per sequence | Bytes per step
+---|---:|---:|---:|---:
+OBJ (`0x3000` record region) | `0x400` bytes | 16 | 32 | 2
+Extended OBJ (`0x6000` record region) | `0x800` bytes | 16 | 64 | 2
Each step is two bytes:
-Byte | Meaning
----|---
-`duration` | number of ticks to hold this frame (commonly treated as 16ms units by viewers)
-`frame` | zero-based frame index to display
+Byte | Meaning | Notes
+---|---|---
+`0` | timer / duration | Tool UI edits this as an 8-bit value; `0x00` is treated as "unused" in printer output paths.
+`1` | frame index | Treated as a 0..63 value in the UI (masked to `0x3F`); used to select which OBJ frame to preview.
+
+The table is stored as a flat array: for each sequence (0..15), write `steps_per_sequence` steps, each step two bytes.
Common location rules:
-* in many OBJ/OBX-style files with a trailing CAD tail, the table starts at `record_region_bytes + 0x100` (for example `0x3100` in a `0x3000`-byte OBJ record region)
+* in OBJ/OBX-style files with a trailing S-CG-CAD tail, the sequence table starts at `record_region_bytes + 0x100` (for example `0x3100` in a `0x3000`-byte OBJ record region)
* in OBZ files, sequence data (when present) lives in the tail region starting at `0x6000`
`pr_obj__` sequence table variant:
@@ -466,7 +749,34 @@ Sequence size | `0x80` bytes each
Steps per sequence | 64 (duration, frame) pairs
Total size | `0x800` bytes
-This viewer renders OBJ or OBX frames and optionally applies CGX and COL.
+### Header and per-bank settings (S-CG-CAD)
+S-CG-CAD OBJ containers include an additional fixed `0x100` bytes after the record region (and before the sequence table). The tool copies a small set of bytes out of this block into per-bank state, and those values affect how it previews and edits OBJ data.
+
+Only a few offsets are clearly referenced in the S-CG-CAD paths traced so far, but they are worth documenting because they are persisted on save/load:
+
+The start of the block is a tool signature and build string in observed assets. For example, multiple OBJ files in the NEWS archives have ASCII text beginning with `NAK1989 S-CG-CADVer...` followed by a date-like string (for example `901226`).
+
+Offset | Size | Meaning | Notes
+---|---:|---|---
+`0x00` | `0x20` | tool signature and version string | Observed as `NAK1989 S-CG-CADVer1.23 901226 ` (ASCII), but treat as tool metadata, not part of the sprite record format.
+`0x50` | `1` | OAM size mode selector | Used as an index bit in size and hit-test tables (combined with per-sprite size bit 0).
+`0x51` | `1` | CGX bank for OBJ | Masked to `& 3` on load (values `0..3`).
+`0x52` | `1` | COL bank for OBJ | Masked to `& 3` on load (values `0..3`).
+`0x53` | `1` | COL base/address preset | Menu-driven setting used by the OBJ color preview path.
+`0x54` | `1` | V-mode selector | Used as a mode switch in selection/hit-test and preview paths. Observed values are `0` and `1` in S-CG-CAD UI menus.
+`0x55` | `1` | "F address" preset | Menu-driven setting used by character-sheet preview routines.
+`0x56` | `1` | "B address" preset | Menu-driven setting used by character-sheet preview routines.
+
+Bytes outside the offsets above are not clearly consumed by the S-CG-CAD OBJ editing path yet. If you are writing an extractor or rewriter, preserve the full `0x100` block exactly where possible.
+
+Practical notes if you are writing tools:
+
+* **Observed tool metadata** - The leading ASCII signature/version string is stable and is best treated as CAD provenance, not sprite data.
+* **Known-used settings** - The offsets `0x50..0x56` are actively used by S-CG-CAD and should be preserved if you want the tool to reopen a file consistently.
+* **Unknown/reserved bytes** - Other bytes in the `0x100` block are not clearly consumed by the S-CG-CAD OBJ editing path yet. Preserve the full `0x100` block exactly where possible.
+
+### Interactive OBJ Viewer
+This viewer renders OBJ or OBX frames and optionally applies CGX and COL right inside the browser.
> 5) & 0x1FF`
+
+When `bit14` is `0`, the tool treats the cell as using a "default" SCR attribute word rather than copying per-tile attributes from the panel table.
+When `bit14` is `1`, the tool copies the panel's attribute bytes through into the generated SCR cell data.
+
+#### How MAP maps into panel tiles
+The `panelX` and `panelY` fields are used as a 2D coordinate into the selected PNL bank.
+The tool flattens them into a panel tile index using 32 tiles per row:
+
+* `panelTileIndex = panelY * 32 + panelX`
+
+This is a strong hint that the underlying panel table is stored as a linear list of 8x8 tiles with a fixed row stride of 32, even when the UI presents the panel as a larger scrollable surface.
+
+#### What the attribute source flag does in practice
+The attribute source flag controls where the SCR cell's attribute word comes from during MAP-to-SCR conversion:
+
+* If the flag is clear, the tool writes a per-bank default attribute value into each generated SCR cell (so all generated cells share the same base attributes).
+* If the flag is set, the tool copies the per-tile attribute bytes from the panel tile entry into the SCR cell (so each generated cell can preserve attributes like palette row, priority, and flip).
+
+This makes MAP useful for two different workflows:
+
+* a "shape only" workflow where MAP picks panel tiles but keeps a consistent attribute style, and
+* a "faithful copy" workflow where MAP also preserves the panel's per-tile attributes.
+
+#### Header preservation note
+When saving a MAP file, the tool always writes the ASCII provenance string into the first `0x20` bytes and writes the `panelBank` byte at `0x0070`.
+Other header bytes are read and written as part of the fixed `0x100` header, but are not clearly consumed by the conversion logic.
+If you are round-tripping MAP files between tools, prefer preserving the header bytes as-is rather than assuming they are unused.
+
---
## Revision Markers
Marker | Meaning
@@ -562,6 +1006,52 @@ Marker | Meaning
BAK | saved backup revision, often meaningful
old | older saved revision, rarer but valuable
+---
+## CAD state and config blobs
+Many NEWS-era projects include a `.CAD_SRD/` directory beside the working art assets.
+These files are not SNES runtime formats.
+They are workstation-side state and configuration used by the S-CG-CAD editor and its option modules.
+
+If you are archiving projects, these blobs are worth preserving because they capture default banks, menu options, and other editor settings that explain why a given `SCR` or `PNL` is interpreted a certain way.
+
+### Common CAD_SRD files (observed)
+The table below summarizes common filenames and what they appear to contain based on real archives and traced code paths.
+
+File | Typical size | Type | Notes
+---|---:|---|---
+`CAD_pglist.dat` | `0x008F` | text | A small menu/list file that names option modules (for example `sfx_main`, `tl_main1`, `tl_main2`) inside an ASCII art frame. This likely populates the tool's option list UI.
+`CAD_prn.dat` | ~`0x0950` | text | Printer configuration file inside an ASCII art frame, with Japanese labels. The tool reads it as plain text and uses it as a short list of printer command names when building `lpq` commands.
+`cadoprf.dat` | `0x0800` | binary | A fixed-size per-user/per-project preference blob. It does not begin with an ASCII signature in observed sets. The internal layout is not decoded yet.
+`CAD.sfx_main.DAT` / `sfx_main.DAT` | `0x0078` | binary | SFX option module parameter table. These are short fixed-size blobs used by the SFX option module alongside the `*.sfx_main.LST` exports.
+`CAD.sfx_main.LST` | ~`0x1480` | text | SFX option module list export. This is the plain-text view of the SFX path-entry records described in the SFX section.
+`CAD.tl_main*.LST` | ~`0x1450` | text | Similar list exports for other option modules. The exact semantics depend on the module.
+`cadbak` / `cadbak_` / `ccc.bak` | ~`0xEED00` | binary | Large backup blobs that contain many embedded path strings and appear to be multi-file snapshots. The full structure has not been decoded yet.
+`img_cad_backup` | ~`0x90304` | binary | Large image-oriented backup blob. The structure has not been decoded yet.
+`*.SPR` | variable | binary | Small per-tool or per-project state files (for example `obj_tool.SPR`, `CLR.SPR`). These are not decoded yet.
+
+### Helper executables invoked by S-CG-CAD
+S-CG-CAD does not operate in isolation. It shells out to small helper executables for printing, for transferring files to the target hardware, and for queue management. The table below summarizes the helpers and the argument patterns visible in the binary.
+
+Helper | Typical command line | Purpose | Notes
+---|---|---|---
+`trans` | `trans -h ` | Transfer a SNES-side helper program | This path is used when S-CG-CAD is uploading an Intel HEX style payload, not raw binary data.
+`trans` | `trans -b ` | Transfer an asset file | The three trailing arguments come from the current mode's transfer settings table (the UI-editable "send" destinations). S-CG-CAD builds variants for `CGX`, `COL`, `SCR`, `PNL`, `MAP`, `OBJ`, `MD7`, and similar.
+`lpq` | `lpq -P` | Query printer queue | Parsed with `popen()` and tokenized into an internal list for the UI.
+`lprm` | `lprm -P` | Delete a print job | Used for single-job and "delete all" queue actions.
+`xwdconv` + `xwud` | `xwdconv -in -xmax 1280 -ymax 985 | xwud` | Display a captured window dump | Used after a print/export action in "display" mode. The temporary `.xwd` is deleted after the pipeline runs.
+`pr_chr_B`, `pr_chr_M`, `pr_chr_S` | `pr_chr_* []` | Print character (CHR/CGX) data | The exact helper (`B/M/S`) depends on the requested print scale/layout.
+`pr_scr_B`, `pr_scr_S` | `pr_scr_* []` | Print screen (SCR) data | The helper consumes a temporary packed record produced by S-CG-CAD, not a raw `SCR` file.
+`pr_col_B`, `pr_col_S` | `pr_col_* []` | Print palette (COL) data | As with the other `pr_*` helpers, S-CG-CAD writes a temporary data blob then invokes the helper on it.
+`pr_pnl__` | `pr_pnl__ []` | Print panel (PNL) data | The helper expects the same temporary record structure as other print modes.
+`pr_map__` | `pr_map__ []` | Print MAP-derived tilemaps | The temporary record can include both MAP and its referenced panel state.
+`pr_obj__`, `pr_obj_Q` | `pr_obj_* []` | Print sprite/object (OBJ) data | The tool writes a large grid of per-sprite data into the temporary record before calling the helper.
+`pr_para` | `pr_para []` | Print parameter pages | Seen in the helper list, but the exact UI route that builds this record is not documented yet.
+Option module (per project) | `.opt` | Run an optional module | S-CG-CAD derives the command name from the current project path and runs it via `system()`. This appears to be how menu-driven "option" tools are launched.
+
+Practical note:
+
+* These files are best treated as tool state. If you are building converters, prefer leaving them untouched and use them as a reference when checking how the original tool would have been configured.
+
---
## What Still Needs More Work
Some parts of the workstation ecosystem are still better described as formats than fully decoded specs:
diff --git a/public/css/theme.overrides.css b/public/css/theme.overrides.css
index cbcae0385..75e0d7a4e 100644
--- a/public/css/theme.overrides.css
+++ b/public/css/theme.overrides.css
@@ -39,6 +39,7 @@ body {
justify-content: space-between;
display: flex;
min-width:95vw;
+ flex-wrap: nowrap;
}
.nav {
@@ -54,6 +55,8 @@ body {
#header .logo {
color: white;
font-weight: 600;
+ min-width:120px;
+ margin-right: 5px;
}
/* #endregion */
diff --git a/public/generated/placeholders/snes-file-formats.jpg b/public/generated/placeholders/snes-file-formats.jpg
new file mode 100644
index 000000000..ba9db633f
Binary files /dev/null and b/public/generated/placeholders/snes-file-formats.jpg differ
diff --git a/public/images/GameBoy/IS-CGB-Character-Importing-CGX-from-DMG-Zelda.png b/public/images/GameBoy/IS-CGB-Character-Importing-CGX-from-DMG-Zelda.png
new file mode 100644
index 000000000..28d28c247
Binary files /dev/null and b/public/images/GameBoy/IS-CGB-Character-Importing-CGX-from-DMG-Zelda.png differ
diff --git a/public/js/sandpack/examples/SnesCgxViewer.tsx b/public/js/sandpack/examples/SnesCgxViewer.tsx
index a6e408800..f0bd5c840 100644
--- a/public/js/sandpack/examples/SnesCgxViewer.tsx
+++ b/public/js/sandpack/examples/SnesCgxViewer.tsx
@@ -20,6 +20,8 @@ interface ParsedCgx {
tileCount: number;
bytesPerTile: number;
tiles: Uint8Array[];
+ tileTable?: Uint8Array;
+ tileTableShift?: number;
warnings: string[];
}
@@ -86,8 +88,33 @@ function decodePaletteColor(value: number): PaletteColor {
function parseCgx(buffer: Uint8Array, bitDepth: BitDepth): ParsedCgx {
const warnings: string[] = [];
const tileSize = bytesPerTile(bitDepth);
- const tileCount = Math.floor(buffer.length / tileSize);
- const remainder = buffer.length % tileSize;
+ const standardBankBytes = 1024 * tileSize;
+ let source = buffer;
+ let tileTable: Uint8Array | undefined;
+ let tileTableShift: number | undefined;
+
+ // S-CG-CAD CGX files commonly have:
+ // - record region: 1024 tiles (0x4000/0x8000/0x10000)
+ // - 0x100 tool header (often "NAK1989 S-CG-CAD...")
+ // - optional 0x400 per-tile table (2bpp/4bpp variants)
+ if (buffer.length >= standardBankBytes + 0x100) {
+ const header = new TextDecoder().decode(buffer.slice(standardBankBytes, standardBankBytes + 0x20));
+ if (header.includes('NAK1989') && header.includes('S-CG-CAD')) {
+ if (buffer.length >= standardBankBytes + 0x500) {
+ tileTable = buffer.slice(standardBankBytes + 0x100, standardBankBytes + 0x500);
+ tileTableShift = bitDepth === 2 ? 2 : bitDepth === 4 ? 4 : undefined;
+ warnings.push(
+ 'Detected S-CG-CAD CGX metadata (+0x100 header, +0x400 per-tile table). Rendered only the front tile record region.',
+ );
+ } else {
+ warnings.push('Detected S-CG-CAD CGX metadata (+0x100 header). Rendered only the front tile record region.');
+ }
+ source = buffer.slice(0, standardBankBytes);
+ }
+ }
+
+ const tileCount = Math.floor(source.length / tileSize);
+ const remainder = source.length % tileSize;
if (remainder !== 0) {
warnings.push(
@@ -98,14 +125,16 @@ function parseCgx(buffer: Uint8Array, bitDepth: BitDepth): ParsedCgx {
const tiles: Uint8Array[] = [];
for (let i = 0; i < tileCount; i += 1) {
const start = i * tileSize;
- tiles.push(buffer.slice(start, start + tileSize));
+ tiles.push(source.slice(start, start + tileSize));
}
return {
- byteLength: buffer.length,
+ byteLength: source.length,
tileCount,
bytesPerTile: tileSize,
tiles,
+ tileTable,
+ tileTableShift,
warnings,
};
}
@@ -228,11 +257,24 @@ function drawTiles(
const tile = parsed.tiles[tileIndex];
const tileX = (tileIndex % tilesPerRow) * 8;
const tileY = Math.floor(tileIndex / tilesPerRow) * 8;
+ const tilePrefix =
+ parsed.tileTable && parsed.tileTableShift != null ? parsed.tileTable[tileIndex] << parsed.tileTableShift : 0;
for (let y = 0; y < 8; y += 1) {
for (let x = 0; x < 8; x += 1) {
- const colorIndex = decodePixel(tile, bitDepth, x, y);
- const [red, green, blue] = getPaletteColor(colorIndex, bitDepth, palette, selectedRow);
+ const colorIndex = tilePrefix | decodePixel(tile, bitDepth, x, y);
+ let resolvedRow = selectedRow;
+ let resolvedIndex = colorIndex;
+ if (palette && parsed.tileTable && parsed.tileTableShift != null && bitDepth !== 8) {
+ if (bitDepth === 4) {
+ resolvedRow = (colorIndex >> 4) & 0x0f;
+ resolvedIndex = colorIndex & 0x0f;
+ } else if (bitDepth === 2) {
+ resolvedRow = (colorIndex >> 2) & 0x3f;
+ resolvedIndex = colorIndex & 0x03;
+ }
+ }
+ const [red, green, blue] = getPaletteColor(resolvedIndex, bitDepth, palette, resolvedRow);
const pixelIndex = ((tileY + y) * width + (tileX + x)) * 4;
data[pixelIndex] = red;
data[pixelIndex + 1] = green;
diff --git a/public/js/sandpack/examples/SnesColPaletteViewer.tsx b/public/js/sandpack/examples/SnesColPaletteViewer.tsx
index 74e383046..28a0b6d57 100644
--- a/public/js/sandpack/examples/SnesColPaletteViewer.tsx
+++ b/public/js/sandpack/examples/SnesColPaletteViewer.tsx
@@ -38,17 +38,31 @@ function decodeColor(value: number): PaletteColor {
function parseColFile(buffer: Uint8Array): ParsedPalette | null {
if (buffer.length < 2) return null;
+ let source = buffer;
const warnings: string[] = [];
if (buffer.length % 2 !== 0) {
warnings.push('Odd file size detected. The final byte was ignored.');
}
- const wordCount = Math.floor(buffer.length / 2);
+ // S-CG-CAD COL saves commonly store:
+ // - 0x200-byte palette record region (256 colors)
+ // - 0x100-byte tool header at 0x200 (often beginning with "NAK1989 S-CG-CAD...")
+ // - 0x100-byte tail metadata
+ // In that case, render only the front 0x200 bytes as palette data.
+ if (buffer.length >= 0x400) {
+ const header = new TextDecoder().decode(buffer.slice(0x200, 0x200 + 0x20));
+ if (header.includes('NAK1989') && header.includes('S-CG-CAD')) {
+ source = buffer.slice(0, 0x200);
+ warnings.push('Detected S-CG-CAD COL metadata at 0x200. Rendered only the front 0x200-byte palette region.');
+ }
+ }
+
+ const wordCount = Math.floor(source.length / 2);
const colors: PaletteColor[] = [];
for (let i = 0; i < wordCount; i += 1) {
const offset = i * 2;
- const value = buffer[offset] | (buffer[offset + 1] << 8);
+ const value = source[offset] | (source[offset + 1] << 8);
colors.push(decodeColor(value));
}
@@ -57,14 +71,14 @@ function parseColFile(buffer: Uint8Array): ParsedPalette | null {
rows.push(colors.slice(i, i + 16));
}
- if (wordCount !== 512) {
- warnings.push(
- `This file has ${wordCount} colors instead of the common 512-color SNES workstation palette bank.`,
- );
+ if (wordCount === 256) {
+ warnings.push('This COL contains 256 colors (16 rows x 16), which matches the S-CG-CAD palette record region.');
+ } else if (wordCount !== 512) {
+ warnings.push(`This file has ${wordCount} colors. Common layouts are 256 (S-CG-CAD) or 512 (some other toolchains).`);
}
return {
- byteLength: buffer.length,
+ byteLength: source.length,
colorCount: wordCount,
rowCount: rows.length,
rows,
diff --git a/public/js/sandpack/examples/SnesMapPnlViewer.tsx b/public/js/sandpack/examples/SnesMapPnlViewer.tsx
new file mode 100644
index 000000000..a7a137042
--- /dev/null
+++ b/public/js/sandpack/examples/SnesMapPnlViewer.tsx
@@ -0,0 +1,21 @@
+import SnesScrViewer from './SnesScrViewer';
+
+function SnesMapPnlViewer() {
+ return (
+
+
Load a CAD .MAP + .PNL pair to preview the derived screen:
+
+ This view derives a 64x64 tilemap from a S-CG-CAD MAP (panel coordinates) and PNL (tile
+ attributes and optional tile id). Add the matching CGX and COL files to render the full
+ preview. You can load up to four PNL files (bank 0-3); the viewer will select the correct
+ one using the MAP header panelBank value (or a manual override). You can also optionally
+ load a SCR to auto-detect the default tile id used when the MAP disables tile-id copying.
+ This is useful for validating flip, priority, and palette propagation without first exporting
+ a SCR from the original tool.
+
+
+
+ );
+}
+
+export default SnesMapPnlViewer;
diff --git a/public/js/sandpack/examples/SnesObjViewer.tsx b/public/js/sandpack/examples/SnesObjViewer.tsx
index fa0b94fad..e308437b7 100644
--- a/public/js/sandpack/examples/SnesObjViewer.tsx
+++ b/public/js/sandpack/examples/SnesObjViewer.tsx
@@ -9,6 +9,8 @@ type SizeOverride = 'auto' | 'small' | 'large';
type SizePreviewMode = 'normal' | 'compare';
type ObjFormatOverride = 'auto' | 'obj' | 'obj10' | 'obz';
type FlipOverride = 'auto' | 'off' | 'on';
+type CadPreviewMode = 'standard' | 'real';
+type CadVMode = 0 | 1;
interface PaletteColor {
value: number;
@@ -25,6 +27,8 @@ interface ParsedCgx {
bytesPerTile: number;
tiles: Uint8Array[];
pixels: Uint8Array;
+ tileTable?: Uint8Array;
+ tileTableShift?: number;
warnings: string[];
}
@@ -41,6 +45,10 @@ interface ObjRecord {
tileIndex: number;
tileId: number;
attr: number;
+ flipSel?: number;
+ priority?: number;
+ paletteRow?: number;
+ packedWord?: number;
}
interface ParsedObj {
@@ -126,8 +134,13 @@ function parseCol(buffer: Uint8Array): ParsedCol | null {
if (buffer.length < 2) return null;
let source = buffer;
- if (buffer.length > 0x200 && buffer.length % 0x1000 === 0x200) {
- source = buffer.slice(0, buffer.length - 0x200);
+ // S-CG-CAD COL files often store a 0x200-byte record region, followed by a 0x100 header and a 0x100 tail.
+ // Prefer decoding only the front record region when we can confidently detect that layout.
+ if (buffer.length >= 0x400) {
+ const header = new TextDecoder().decode(buffer.slice(0x200, 0x200 + 0x20));
+ if (header.includes('NAK1989') && header.includes('S-CG-CAD')) {
+ source = buffer.slice(0, 0x200);
+ }
}
const colors: PaletteColor[] = [];
@@ -148,7 +161,29 @@ function parseCgx(buffer: Uint8Array, bitDepth: BitDepth): ParsedCgx {
let source = buffer;
const tileSize = bytesPerTile(bitDepth);
const warnings: string[] = [];
- if (buffer.length > 0x500 && buffer.length % 0x1000 === 0x500) {
+ const standardBankBytes = 1024 * tileSize;
+ let tileTable: Uint8Array | undefined;
+ let tileTableShift: number | undefined;
+
+ // S-CG-CAD CGX files commonly store 1024 tiles followed by a 0x100 tool header and (for 2bpp/4bpp) a 0x400 per-tile table.
+ if (buffer.length >= standardBankBytes + 0x100) {
+ const header = new TextDecoder().decode(buffer.slice(standardBankBytes, standardBankBytes + 0x20));
+ if (header.includes('NAK1989') && header.includes('S-CG-CAD')) {
+ if (buffer.length >= standardBankBytes + 0x500) {
+ tileTable = buffer.slice(standardBankBytes + 0x100, standardBankBytes + 0x500);
+ tileTableShift = bitDepth === 2 ? 2 : bitDepth === 4 ? 4 : undefined;
+ warnings.push(
+ 'Detected S-CG-CAD CGX metadata (+0x100 header, +0x400 per-tile table). Rendered only the front tile record region.',
+ );
+ } else {
+ warnings.push('Detected S-CG-CAD CGX metadata (+0x100 header). Rendered only the front tile record region.');
+ }
+ source = buffer.slice(0, standardBankBytes);
+ }
+ }
+
+ // Legacy heuristic: some CAD toolchains append a 0x500-byte tail to CGX (not the S-CG-CAD CGX standard bank layout above).
+ if (source === buffer && buffer.length > 0x500 && buffer.length % 0x1000 === 0x500) {
source = buffer.slice(0, buffer.length - 0x500);
warnings.push('Trimmed the standard 0x500-byte CAD tail from the CGX file before decoding.');
}
@@ -168,9 +203,11 @@ function parseCgx(buffer: Uint8Array, bitDepth: BitDepth): ParsedCgx {
const start = i * tileSize;
const tile = source.slice(start, start + tileSize);
tiles.push(tile);
+ const prefix =
+ tileTable && tileTableShift != null && i < tileTable.length ? tileTable[i] << tileTableShift : 0;
for (let y = 0; y < 8; y += 1) {
for (let x = 0; x < 8; x += 1) {
- pixels[i * 64 + y * 8 + x] = decodePixel(tile, bitDepth, x, y);
+ pixels[i * 64 + y * 8 + x] = prefix | decodePixel(tile, bitDepth, x, y);
}
}
}
@@ -180,6 +217,8 @@ function parseCgx(buffer: Uint8Array, bitDepth: BitDepth): ParsedCgx {
bytesPerTile: tileSize,
tiles,
pixels,
+ tileTable,
+ tileTableShift,
warnings,
};
}
@@ -188,6 +227,7 @@ function parseObj(
buffer: Uint8Array,
fileName?: string | null,
override: ObjFormatOverride = 'auto',
+ acceptLegacySizeFlag = false,
): ParsedObj | null {
if (buffer.length < 6) return null;
@@ -354,7 +394,7 @@ function parseObj(
// 8..9 tile/char id (16-bit)
for (let offset = 0; offset + 9 < recordRegionBytes; offset += 10) {
const flags = buffer[offset];
- const flipSel = buffer[offset + 1];
+ const flipSel = buffer[offset + 1] & 0x03;
const groupInfo = buffer[offset + 2];
const attr = buffer[offset + 3];
const yByte = buffer[offset + 5];
@@ -376,6 +416,7 @@ function parseObj(
// Store the palette/attribute nibble in `attr` so existing UI can surface it.
// Note: pr_obj__ treats this as a nibble and shifts it left by 4 for its printer routines.
attr,
+ flipSel,
});
}
} else {
@@ -383,19 +424,26 @@ function parseObj(
const byte1 = buffer[offset];
const groupInfo = buffer[offset + 1];
const rawXY = buffer[offset + 2] | (buffer[offset + 3] << 8);
- const packed = buffer[offset + 4] | (buffer[offset + 5] << 8);
+ // S-CG-CAD stores the packed attribute+tile word in big-endian order.
+ const packed = (buffer[offset + 4] << 8) | buffer[offset + 5];
const xByte = (rawXY >> 8) & 0xff;
const yByte = rawXY & 0xff;
- const tileIndex = (packed >> 8) & 0xff;
- const attr = packed & 0xff;
- // For CAD OBJ/OBX files, byte 6 is the tile number and byte 5 is a SNES-style attribute byte.
- // Bit 0 is commonly used as a tile-page/name-select bit, effectively making the tile id 9-bit.
- const tileId = ((attr & 0x01) << 8) | tileIndex;
+ const flipSel = (packed >> 14) & 0x03;
+ const priority = (packed >> 12) & 0x03;
+ const paletteRow = (packed >> 9) & 0x07;
+ const tileId = packed & 0x01ff;
+ const tileIndex = tileId & 0xff;
+ // Keep the high byte available for diagnostics/UI, but do not treat it as a SNES OAM attribute byte.
+ const attr = (packed >> 8) & 0xff;
const sawLegacySize = (byte1 & 0x40) !== 0;
- const largeTile = (byte1 & 0x01) !== 0 || sawLegacySize;
- if (sawLegacySize && (byte1 & 0x01) === 0) {
+ const largeTile = (byte1 & 0x01) !== 0 || (acceptLegacySizeFlag && sawLegacySize);
+ if (!acceptLegacySizeFlag && sawLegacySize) {
warnings.push(
- 'Found OBJ/OBX entries with flag bit 0x40 set. This viewer treats bit 0 as size select by default and also accepts bit 0x40 as a legacy size flag.',
+ 'Found OBJ/OBX entries with flag bit 0x40 set. S-CG-CAD uses bit 0 as size select; enable "Accept legacy size flag (0x40)" only if your source toolchain expects it.',
+ );
+ } else if (acceptLegacySizeFlag && sawLegacySize && (byte1 & 0x01) === 0) {
+ warnings.push(
+ 'Found OBJ/OBX entries with flag bit 0x40 set. This viewer is treating it as a legacy size flag (in addition to bit 0).',
);
}
records.push({
@@ -411,6 +459,10 @@ function parseObj(
tileIndex,
tileId,
attr,
+ flipSel,
+ priority,
+ paletteRow,
+ packedWord: packed,
});
}
}
@@ -484,9 +536,17 @@ function parseObj(
}
function parseObjSequences(buffer: Uint8Array, format: ObjFormat, recordRegionBytes: number): ObjSequence[] {
- // Known sequence format used by CAD-side object containers:
- // each sequence is 0x20 bytes, storing up to 16 pairs of (duration, frame).
- // sequence ends at the first 0x00 0x00 pair.
+ // Sequence formats seen in the S-CG-CAD toolchain:
+ // - OBJ:
+ // - 16 sequences
+ // - each step is 2 bytes: (timer, frameIndex)
+ // - on-disk table size depends on record region:
+ // - 0x3000 record region: 32 steps/seq (0x40 bytes) -> 0x400 total
+ // - 0x6000 record region: 64 steps/seq (0x80 bytes) -> 0x800 total
+ // - frameIndex is treated as 0..63 (masked to 0x3F by the UI)
+ // - duration 0x00 is treated as "unused" in printer output paths (not a hard terminator)
+ // - pr_obj__ / OBJ10:
+ // - 16 sequences, 64 steps/seq, 0x80 bytes/seq
//
// Common locations:
// - OBJ/OBX with a trailing CAD tail: recordRegionBytes + 0x100 (often 0x3100 for OBJ, 0xC100 for OBX-like)
@@ -496,7 +556,8 @@ function parseObjSequences(buffer: Uint8Array, format: ObjFormat, recordRegionBy
const fallbackBase = format === 'obz' ? 0x6000 : recordRegionBytes + 0x100;
const sequenceCount = 0x10;
- const sequenceBytes = format === 'obj10' ? 0x80 : 0x20;
+ const objSteps = recordRegionBytes >= 0x6000 ? 0x40 : 0x20;
+ const sequenceBytes = format === 'obj10' ? 0x80 : format === 'obj' ? objSteps * 2 : 0x20;
function parseAt(base: number): ObjSequence[] {
if (base < 0 || base + sequenceCount * sequenceBytes > buffer.length) return [];
@@ -505,12 +566,13 @@ function parseObjSequences(buffer: Uint8Array, format: ObjFormat, recordRegionBy
const start = base + seq * sequenceBytes;
const raw = buffer.slice(start, start + sequenceBytes);
const steps: ObjSequenceStep[] = [];
- const maxSteps = format === 'obj10' ? 0x40 : 0x10;
+ const maxSteps = format === 'obj10' ? 0x40 : format === 'obj' ? objSteps : 0x10;
for (let i = 0; i < maxSteps; i += 1) {
const duration = raw[i * 2];
- const frame = raw[i * 2 + 1];
- if (duration === 0 && frame === 0) break;
- steps.push({ duration, frame });
+ const frameRaw = raw[i * 2 + 1];
+ const frame = format === 'obj' ? (frameRaw & 0x3f) : frameRaw;
+ if (format !== 'obj' && duration === 0 && frameRaw === 0) break;
+ if (duration !== 0) steps.push({ duration, frame });
}
if (steps.length > 0) out.push({ index: seq, steps, rawBytes: raw });
}
@@ -647,6 +709,11 @@ function drawObjects(
objFormat: ObjFormat,
hFlipOverride: FlipOverride,
vFlipOverride: FlipOverride,
+ fAddressPreset: number,
+ bAddressPreset: number,
+ useCadHeaderDefaults: boolean,
+ cadPreviewMode: CadPreviewMode,
+ cadVMode: CadVMode,
) {
const margin = 8;
const visibleRecords = records.filter((record) => record.display);
@@ -666,8 +733,12 @@ function drawObjects(
return;
}
+ const isSnesCadObj = objFormat === 'obj';
+ const useCadVMode = isSnesCadObj && useCadHeaderDefaults && cadVMode === 1;
+ const yDiv = useCadVMode ? 2 : 1;
+
const xs = visibleRecords.map((record) => record.xSigned);
- const ys = visibleRecords.map((record) => record.ySigned);
+ const ys = visibleRecords.map((record) => Math.trunc(record.ySigned / yDiv));
const minX = Math.min(...xs);
const minY = Math.min(...ys);
const maxX = Math.max(
@@ -679,7 +750,9 @@ function drawObjects(
const maxY = Math.max(
...visibleRecords.map((record) => {
const largeTile = sizeOverride === 'auto' ? record.largeTile : sizeOverride === 'large';
- return record.ySigned + getObjectDimensions(objectSizeMode, largeTile)[1] - 1;
+ const height = getObjectDimensions(objectSizeMode, largeTile)[1];
+ const scaledHeight = Math.ceil(height / yDiv);
+ return Math.trunc(record.ySigned / yDiv) + scaledHeight - 1;
}),
);
const width = maxX - minX + 1 + margin * 2;
@@ -699,21 +772,43 @@ function drawObjects(
for (const record of [...visibleRecords].reverse()) {
const startX = record.xSigned - minX + margin;
- const startY = record.ySigned - minY + margin;
- const autoHFlip = objFormat === 'obj10' ? (record.byte1 & 0x01) !== 0 : (record.attr & 0x40) !== 0;
- const autoVFlip = objFormat === 'obj10' ? (record.byte1 & 0x02) !== 0 : (record.attr & 0x80) !== 0;
+ const startY = Math.trunc(record.ySigned / yDiv) - minY + margin;
+ const autoHFlip =
+ objFormat === 'obj'
+ ? ((record.flipSel ?? 0) & 0x01) !== 0
+ : objFormat === 'obj10'
+ ? ((record.flipSel ?? 0) & 0x01) !== 0
+ : (record.attr & 0x40) !== 0;
+ const autoVFlip =
+ objFormat === 'obj'
+ ? ((record.flipSel ?? 0) & 0x02) !== 0
+ : objFormat === 'obj10'
+ ? ((record.flipSel ?? 0) & 0x02) !== 0
+ : (record.attr & 0x80) !== 0;
const hFlip = hFlipOverride === 'auto' ? autoHFlip : hFlipOverride === 'on';
const vFlip = vFlipOverride === 'auto' ? autoVFlip : vFlipOverride === 'on';
- const paletteRow = paletteMode === 'attr' ? getAttrPaletteRow(record.attr) : manualPaletteRow;
+ const paletteRow =
+ paletteMode === 'attr'
+ ? objFormat === 'obj'
+ ? record.paletteRow ?? 0
+ : getAttrPaletteRow(record.attr)
+ : manualPaletteRow;
const largeTile = sizeOverride === 'auto' ? record.largeTile : sizeOverride === 'large';
const [objectWidth, objectHeight] = getObjectDimensions(objectSizeMode, largeTile);
+ const outObjectHeight = Math.ceil(objectHeight / yDiv);
// VRAM offset is entered in SNES VRAM words (16-bit). Convert that to a tile offset, then to pixels.
// parsedCgx.pixels is indexed in 8x8 tile pixels (tileIndex * 64).
const vramBytes = vramOffset * 2;
const vramTileOffset = parsedCgx ? Math.floor(vramBytes / parsedCgx.bytesPerTile) : 0;
- const tileBase = (vramTileOffset + record.tileId) * 64;
-
- for (let py = 0; py < objectHeight; py += 1) {
+ const useCadNibbleAddressing = isSnesCadObj && useCadHeaderDefaults;
+ const addressPreset =
+ isSnesCadObj && record.tileId >= 0x100
+ ? Math.max(0, bAddressPreset | 0)
+ : Math.max(0, fAddressPreset | 0);
+ const addressBaseTiles = isSnesCadObj ? (addressPreset & 0x0f) * 0x100 : 0;
+ const tileBase = (vramTileOffset + addressBaseTiles + (isSnesCadObj ? (record.tileId & 0xff) : record.tileId)) * 64;
+
+ for (let py = 0; py < outObjectHeight; py += 1) {
for (let px = 0; px < objectWidth; px += 1) {
const outX = startX + px;
const outY = startY + py;
@@ -726,23 +821,34 @@ function drawObjects(
if (parsedCgx) {
const sourceX = hFlip ? objectWidth - 1 - px : px;
- const sourceY = vFlip ? objectHeight - 1 - py : py;
+ const rawSourceY = py * yDiv;
+ const sourceY = vFlip ? objectHeight - 1 - rawSourceY : rawSourceY;
+ if (sourceY < 0 || sourceY >= objectHeight) continue;
// parsedCgx.pixels is a linear array of 8x8 tiles (tileIndex * 64).
// For SNES OBJ rendering, the tile number is an index into OBJ VRAM arranged in 16 tiles per row.
// So the next tile row is +16 in tile index, not +tilesAcross.
const tilesPerRow = Math.max(1, objTilesPerRow | 0);
const tileInSpriteX = sourceX >> 3;
const tileInSpriteY = sourceY >> 3;
- const tileIndex = vramTileOffset + record.tileId + tileInSpriteX + tileInSpriteY * tilesPerRow;
+ const effectiveTileId = isSnesCadObj ? addressBaseTiles + (record.tileId & 0xff) : record.tileId;
+ const tileIndex = useCadNibbleAddressing
+ ? (() => {
+ const baseLow = record.tileId & 0xff;
+ const baseX = baseLow & 0x0f;
+ const baseY = baseLow & 0xf0;
+ const tileX = (baseX + tileInSpriteX) & 0x0f;
+ const tileY = (baseY + ((tileInSpriteY & 0x0f) << 4)) & 0xf0;
+ const low = tileY | tileX;
+ return vramTileOffset + addressBaseTiles + low;
+ })()
+ : vramTileOffset + effectiveTileId + tileInSpriteX + tileInSpriteY * tilesPerRow;
const pixelOffset = tileIndex * 64 + (sourceY & 0x7) * 8 + (sourceX & 0x7);
const colorIndex = parsedCgx.pixels[pixelOffset] ?? 0;
- if (colorIndex === 0) {
+ const paletteIndex =
+ bitDepth === 8 ? cgramOffset + colorIndex : cgramOffset + paletteRow * 16 + colorIndex;
+ if (colorIndex === 0 && isSnesCadObj && useCadHeaderDefaults && cadPreviewMode === 'real') {
alpha = 0;
} else {
- const paletteIndex =
- bitDepth === 8
- ? cgramOffset + colorIndex
- : cgramOffset + paletteRow * 16 + colorIndex;
[red, green, blue] = getPaletteRgb(parsedCol, paletteIndex, bitDepth);
}
} else {
@@ -851,6 +957,7 @@ function SnesObjViewer() {
const [objName, setObjName] = useState('object-layout');
const [objFileName, setObjFileName] = useState(null);
const [objFormatOverride, setObjFormatOverride] = useState('auto');
+ const [acceptLegacySizeFlag, setAcceptLegacySizeFlag] = useState(false);
const [sequenceIndex, setSequenceIndex] = useState(null);
const [sequenceStepIndex, setSequenceStepIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
@@ -861,7 +968,11 @@ function SnesObjViewer() {
const [groupMode, setGroupMode] = useState('frame64');
const [groupIndex, setGroupIndex] = useState(0);
const [clusterIndex, setClusterIndex] = useState(0);
+ const [groupInfoFilter, setGroupInfoFilter] = useState(null);
const [objectSizeMode, setObjectSizeMode] = useState(0);
+ const [useCadHeaderDefaults, setUseCadHeaderDefaults] = useState(true);
+ const [cadPreviewMode, setCadPreviewMode] = useState('standard');
+ const [cadVMode, setCadVMode] = useState(0);
const [sizePreviewMode, setSizePreviewMode] = useState('normal');
const [sizeOverride, setSizeOverride] = useState('auto');
const [objTilesPerRow, setObjTilesPerRow] = useState(16);
@@ -869,13 +980,15 @@ function SnesObjViewer() {
const [vFlipOverride, setVFlipOverride] = useState('auto');
const [vramOffset, setVramOffset] = useState(0);
const [cgramOffset, setCgramOffset] = useState(0);
+ const [fAddressPreset, setFAddressPreset] = useState(0);
+ const [bAddressPreset, setBAddressPreset] = useState(0);
const canvasRef = useRef(null);
const compareSmallRef = useRef(null);
const compareLargeRef = useRef(null);
const parsedObj = useMemo(
- () => (objBuffer ? parseObj(objBuffer, objFileName, objFormatOverride) : null),
- [objBuffer, objFileName, objFormatOverride],
+ () => (objBuffer ? parseObj(objBuffer, objFileName, objFormatOverride, acceptLegacySizeFlag) : null),
+ [objBuffer, objFileName, objFormatOverride, acceptLegacySizeFlag],
);
const parsedCgx = useMemo(() => (cgxBuffer ? parseCgx(cgxBuffer, bitDepth) : null), [cgxBuffer, bitDepth]);
const parsedCol = useMemo(() => (colBuffer ? parseCol(colBuffer) : null), [colBuffer]);
@@ -885,6 +998,26 @@ function SnesObjViewer() {
setGroupMode(parsedObj.suggestedGroupMode);
}, [parsedObj]);
+ useEffect(() => {
+ if (!parsedObj) return;
+ if (!objBuffer) return;
+ if (parsedObj.format !== 'obj') return;
+ if (!useCadHeaderDefaults) return;
+
+ // S-CG-CAD stores preview VRAM base presets ("F address" and "B address") in the 0x100-byte OBJ header tail.
+ // Auto-load them when the CAD metadata tail is present so the preview matches S-CG-CAD defaults.
+ const headerStart = parsedObj.recordRegionBytes;
+ if (headerStart + 0x57 > objBuffer.length) return;
+ const headerMarker = new TextDecoder().decode(objBuffer.slice(headerStart, headerStart + 0x20));
+ if (!headerMarker.includes('NAK1989') || !headerMarker.includes('S-CG-CAD')) return;
+
+ setObjectSizeMode(objBuffer[headerStart + 0x50] & 0x07);
+ setCadVMode((objBuffer[headerStart + 0x54] & 1) as CadVMode);
+ setFAddressPreset(objBuffer[headerStart + 0x55] & 0x0f);
+ setBAddressPreset(objBuffer[headerStart + 0x56] & 0x0f);
+ setObjTilesPerRow(16);
+ }, [parsedObj, objBuffer, useCadHeaderDefaults]);
+
useEffect(() => {
// Avoid confusing "sticky" state when switching between unrelated OBJ files.
setObjectSizeMode(0);
@@ -897,6 +1030,13 @@ function SnesObjViewer() {
setObjTilesPerRow(16);
setHFlipOverride('auto');
setVFlipOverride('auto');
+ setFAddressPreset(0);
+ setBAddressPreset(0);
+ setGroupInfoFilter(null);
+ setUseCadHeaderDefaults(true);
+ setCadPreviewMode('standard');
+ setCadVMode(0);
+ setAcceptLegacySizeFlag(false);
}, [objBuffer]);
useEffect(() => {
@@ -905,7 +1045,13 @@ function SnesObjViewer() {
}
}, [objBuffer]);
- const groups = useMemo(() => buildGroups(parsedObj?.records || [], groupMode), [parsedObj, groupMode]);
+ const filteredRecords = useMemo(() => {
+ if (!parsedObj) return [];
+ if (groupInfoFilter == null) return parsedObj.records;
+ return parsedObj.records.filter((record) => record.groupInfo === groupInfoFilter);
+ }, [parsedObj, groupInfoFilter]);
+
+ const groups = useMemo(() => buildGroups(filteredRecords, groupMode), [filteredRecords, groupMode]);
const frameRecords = useMemo(() => {
if (groups.length === 0) return [];
const safeIndex = Math.max(0, Math.min(groupIndex, groups.length - 1));
@@ -964,7 +1110,7 @@ function SnesObjViewer() {
if (!parsedObj) return;
if (sizePreviewMode === 'compare') {
- if (compareSmallRef.current) {
+ if (compareSmallRef.current) {
drawObjects(
compareSmallRef.current,
visibleRecords,
@@ -982,6 +1128,11 @@ function SnesObjViewer() {
parsedObj.format,
hFlipOverride,
vFlipOverride,
+ fAddressPreset,
+ bAddressPreset,
+ useCadHeaderDefaults,
+ cadPreviewMode,
+ cadVMode,
);
}
if (compareLargeRef.current) {
@@ -1002,6 +1153,11 @@ function SnesObjViewer() {
parsedObj.format,
hFlipOverride,
vFlipOverride,
+ fAddressPreset,
+ bAddressPreset,
+ useCadHeaderDefaults,
+ cadPreviewMode,
+ cadVMode,
);
}
return;
@@ -1025,6 +1181,11 @@ function SnesObjViewer() {
parsedObj.format,
hFlipOverride,
vFlipOverride,
+ fAddressPreset,
+ bAddressPreset,
+ useCadHeaderDefaults,
+ cadPreviewMode,
+ cadVMode,
);
}, [
visibleRecords,
@@ -1043,6 +1204,11 @@ function SnesObjViewer() {
vFlipOverride,
vramOffset,
cgramOffset,
+ fAddressPreset,
+ bAddressPreset,
+ useCadHeaderDefaults,
+ cadPreviewMode,
+ cadVMode,
]);
useEffect(() => {
@@ -1212,9 +1378,76 @@ function SnesObjViewer() {
max={64}
value={objTilesPerRow}
onChange={(event) => setObjTilesPerRow(Math.max(1, Math.min(64, Number(event.target.value) || 16)))}
+ disabled={parsedObj.format === 'obj' && useCadHeaderDefaults}
style={{ marginLeft: '0.5rem', width: '5rem' }}
/>
+
+ {parsedObj.format === 'obj' ? (
+ <>
+
+
+
+ >
+ ) : null}
+
+
{sizePreviewMode === 'compare' ? (
@@ -1357,7 +1590,12 @@ function SnesObjViewer() {
+ {parsedObj?.format === 'obj' ? (
+
+ ) : null}
+
+ {parsedObj?.format === 'obj' && useCadHeaderDefaults ? (
+
+ ) : null}
+
+ {parsedObj?.format === 'obj' && useCadHeaderDefaults ? (
+
+ ) : null}
+
+ If provided and it contains an S-CG-CAD metadata tail, the viewer uses its stamp base
+ tile value as the default tile id for MAP cells with attribute-source=0.
+