T2 Design: Asset Extraction Readiness

Task: 20260511-T001
Phase: T2 Design
Lead: lead
Status: IN PROGRESS

Design Overview

This design document outlines the improvements to asset extraction documentation and tooling to prepare for D2 title-menu asset extraction when private assets become available.

1. Documentation Structure

New File: docs/ASSET_EXTRACTION_WORKFLOW.md

Purpose: Step-by-step guide for extracting D2 title-menu assets when private source material is available.

Proposed Structure:

# D2 Title-Menu Asset Extraction Workflow

## Overview

This guide explains how to extract D2 Disc 1 title-menu assets for D3-TUI validation when private D2 source material is available.

## Prerequisites

### Required Tools
- Python 3.8+
- D3-TUI repository cloned
- Private D2 assets staged at expected location

### Expected Asset Location

Private D2 material should be staged at:

/mnt/kitchen/from-house/assets/private/d2-title

Inside agent containers:

/from/assets/private/d2-title

### Directory Structure

Recommended structure for private assets:

private/d2-title/ ├── raw/ │ ├── D2_USA_Disc1.bin # Raw disc image │ └── D2_USA_Disc1.cdi # CDI format (emulator use only) └── extracted/ ├── Q_TITLEBGMT0.PVM # Title menu background ├── Q_TITLEBGMT1.PVM # Title menu background layer 1 ├── Q_TITLEBGMT2.PVM # Title menu background layer 2 ├── Q_DMTITLE.PVM # D2 logo ├── Q_TITLEMENU.PVM # Menu elements ├── Q_DJSNOW.PVM # Snow particles ├── SAKA_MNSNOW1A.PVM # Snow variant A ├── SAKA_MNSNOW1B.PVM # Snow variant B ├── SAKA_MNSNOW1C.PVM # Snow variant C ├── P_COMTIT.PVR # Copyright elements └── ...

## Phase 1: Discovery

### Step 1: Verify Asset Availability

Check that private assets are available:
```bash
ls -la /from/assets/private/d2-title/extracted/

Step 2: Run Discovery Mode

Use the --discover flag to scan available assets and report their structure:

cd /work/repo
python3 tools/extract_d2_menu_assets.py \
    --discover \
    --d2-dir /from/assets/private/d2-title/extracted

Expected Output: - Lists all PVM/PVR files found - Shows available entries in each PVM file - Reports pixel formats and dimensions - Identifies any format parsing errors

Step 3: Compare with Expected Assets

Compare discovery output with expected title-menu assets:

Expected File Purpose Found?
Q_TITLEBGMT0.PVM Snowy mountain background ❌/✅
Q_TITLEBGMT1.PVM Background layer 1 ❌/✅
Q_TITLEBGMT2.PVM Background layer 2 ❌/✅
Q_DMTITLE.PVM Translucent red D2 logo ❌/✅
Q_TITLEMENU.PVM Menu stack elements ❌/✅
Q_DJSNOW.PVM Snow particle textures ❌/✅
SAKA_MNSNOW1A.PVM Snow variant A ❌/✅
SAKA_MNSNOW1B.PVM Snow variant B ❌/✅
SAKA_MNSNOW1C.PVM Snow variant C ❌/✅
P_COMTIT.PVR WARP copyright ❌/✅

Phase 2: Manifest Validation

Step 4: Review Current Manifest

Examine the current manifest:

cat tools/title_menu_manifest.json

Step 5: Update Manifest with Actual Entry Names

Based on discovery output, update tools/title_menu_manifest.json:

{
  "version": "1.0.0",
  "target": "D2 Disc 1 Title Menu",
  "assets": [
    {
      "symbol": "d2_asset_title_bg",
      "source_file": "Q_TITLEBGMT0.PVM",
      "source_entry": "TITLEBGMT0",  // Update with actual entry name
      "decoder": "auto",
      "role": "background",
      "description": "Snowy mountain background layer 0"
    }
    // ... other assets
  ]
}

Important: Entry names in the manifest must exactly match the names reported by --discover.

Step 6: Validate Manifest Schema

Use the --dry-run flag to validate manifest without extraction:

python3 tools/extract_d2_menu_assets.py \
    --manifest tools/title_menu_manifest.json \
    --dry-run

Expected Output: - Schema validation passes or fails - List of assets that would be extracted - Warnings about missing files or entries

Phase 3: Asset Extraction

Step 7: Run Extraction

Execute the extraction:

make d2-assets

Or manually:

python3 tools/extract_d2_menu_assets.py \
    --manifest tools/title_menu_manifest.json \
    --d2-dir /from/assets/private/d2-title/extracted

Expected Output: - Generated src/d3tui/assets/d2_menu_assets.c (C source) - Generated src/d3tui/assets/d2_menu_assets.h (header) - List of extracted assets with dimensions

Step 8: Verify Generated Assets

Check that assets were generated:

ls -la src/d3tui/assets/d2_menu_assets.*
head -20 src/d3tui/assets/d2_menu_assets.h

Verify asset symbols match manifest:

grep "d2_asset_title_" src/d3tui/assets/d2_menu_assets.h

Phase 4: Integration

Step 9: Update Main Rendering Loop

Modify src/d3tui_main.c to use title-menu assets instead of decoder-evidence screen:

// Replace draw_frame() with title-menu composition:
static void draw_title_menu_frame() {
    // Draw background layers
    blit_scaled(&d2_asset_title_bg, 0, 0, SCREEN_W, SCREEN_H);
    blit_scaled(&d2_asset_title_bg1, 0, 0, SCREEN_W, SCREEN_H);
    blit_scaled(&d2_asset_title_bg2, 0, 0, SCREEN_W, SCREEN_H);

    // Draw D2 logo (with translucency)
    blit_scaled(&d2_asset_title_logo, 100, 50, 440, 200);

    // Draw menu stack
    blit_scaled(&d2_asset_title_menu, 250, 250, 300, 150);

    // Draw snow particles (animated)
    draw_snow_particles();

    // Draw copyright
    blit_scaled(&d2_asset_comtit, 200, 450, 240, 30);
}

Step 10: Build and Validate

make flycast-image

Verify build succeeds and assets are included.

Troubleshooting

Common Issues and Solutions

Issue: FileNotFoundError: [Errno 2] No such file or directory: 'Q_TITLEBGMT0.PVM'

Solution: Verify private assets are staged at correct location. Check: - Asset directory exists: ls /from/assets/private/d2-title/extracted/ - Files are readable: ls -la /from/assets/private/d2-title/extracted/ - Path is correct in manifest

Issue: ValueError: entry 'TITLEBGMT0' not in Q_TITLEBGMT0.PVM

Solution: Run discovery mode to find correct entry names:

python3 tools/extract_d2_menu_assets.py --discover --d2-dir /from/assets/private/d2-title/extracted

Update manifest with exact entry names from discovery output.

Issue: ValueError: Q_TITLEBGMT0.PVM is not RGB565 square twiddled

Solution: The asset uses a different pixel format. Check discovery output for actual format. The extraction tool supports: - RGB565 square twiddled - ARGB4444 square twiddled - ARGB1555 square twiddled - RGB565 VQ/SMALLVQ

If format is unsupported, file an issue with the discovery output.

Issue: Extraction succeeds but build fails with undefined references

Solution: Verify asset symbols in header match those used in d3tui_main.c:

grep "extern const d2_menu_asset_t" src/d3tui/assets/d2_menu_assets.h

Issue: Python not found or extraction tool fails

Solution: Ensure Python 3.8+ is installed:

python3 --version

If missing, install Python or use container with Python pre-installed.

Advanced Topics

Adding New Asset Formats

The extraction tool supports these PVRT formats: - RGB565 (0x01) - 16-bit RGB - ARGB4444 (0x02) - 16-bit ARGB - ARGB1555 (0x03) - 16-bit ARGB with 1-bit alpha - VQ/SMALLVQ (0x03/0x10) - Vector quantization

To add support for additional formats: 1. Add format constant to tools/extract_d2_menu_assets.py 2. Implement decoder function (see _decode_square_twiddled_rgb565 for example) 3. Update decode_pvrt_auto() to dispatch to new decoder 4. Add test cases in tests/python/test_extraction.py

CTS Animation Files

.CTS files contain animation sequences. These are not yet decoded by the extraction tool. If you need animation support: 1. File an issue with sample CTS file 2. Include discovery output showing CTS structure 3. Note which assets require animation

Custom Manifest Locations

You can specify custom manifest and output locations:

python3 tools/extract_d2_menu_assets.py \
    --manifest custom_manifest.json \
    --out-h custom_header.h \
    --out-c custom_source.c \
    --d2-dir /path/to/assets

References

Appendix: Asset Format Reference

PVM Container Format

Offset | Size | Description
0x00   | 4    | Magic: "PVMH"
0x04   | 4    | TOC size
0x08   | 2    | Unknown (usually 0x0100)
0x0A   | 2    | Entry count
0x0C   | var  | TOC entries (34 bytes each)

PVRT Texture Format

Offset | Size | Description
0x00   | 4    | Magic: "PVRT"
0x04   | 4    | Chunk size
0x08   | 1    | Pixel format
0x09   | 1    | Data format
0x0A   | 2    | Unknown (usually 0x0000)
0x0C   | 2    | Width
0x0E   | 2    | Height
0x10   | var  | Pixel data

Supported Pixel Formats

Code Format Description
0x01 RGB565 16-bit RGB (5-6-5)
0x02 ARGB4444 16-bit ARGB (4-4-4-4)
0x03 ARGB1555 16-bit ARGB (1-5-5-5)
0x04 YUV422 YUV 4:2:2 (not yet supported)
0x05 BUMP Bump map (not yet supported)
0x06 PAL4 4-bit palettized (not yet supported)
0x07 PAL8 8-bit palettized (not yet supported)

Supported Data Formats

Code Format Description
0x01 SQ_TWID Square twiddled
0x03 VQ Vector quantization
0x10 SMALLVQ Small vector quantization

Changelog

## 2. Tooling Improvements

### Enhancement: Better Error Handling

**Current Issues**:
- Generic "File not found" errors don't suggest solutions
- Manifest validation errors aren't clear about what's wrong
- Discovery mode output could be more structured

**Proposed Improvements**:

1. **File Not Found Error**:
```python
# Before:
print(f"SKIP {symbol}: {source_path} not found ({notes})", file=sys.stderr)

# After:
print(
    f"ERROR: Asset file not found: {source_path}",
    f"  Expected location: {source_path}",
    f"  Check that private D2 assets are staged at:",
    f"    /from/assets/private/d2-title/extracted/",
    f"  Or custom location specified with --d2-dir",
    file=sys.stderr
)
  1. Entry Not Found Error:
# Before:
print(f"SKIP {symbol}: entry '{entry}' not in {source}. Available: {available}", file=sys.stderr)

# After:
print(
    f"ERROR: PVM entry not found: {source}:{entry}",
    f"  Available entries in {source}:",
    file=sys.stderr
)
for available_entry in sorted(entries.keys()):
    print(f"    - {available_entry}", file=sys.stderr)
print(
    f"  SUGGESTION: Run discovery mode to see all available entries:",
    f"    python3 tools/extract_d2_menu_assets.py --discover --d2-dir {args.d2_dir}",
    file=sys.stderr
)
  1. Format Not Supported Error:
# Before:
raise ValueError(f"{source_name} is not RGB565 square twiddled")

# After:
raise ValueError(
    f"Unsupported PVRT format in {source_name}: "
    f"pixel_format=0x{pixel_format:02x}, data_format=0x{data_format:02x}"
    f"\nSupported formats:"
    f"  RGB565 square twiddled (PF=0x01, DF=0x01)"
    f"  ARGB4444 square twiddled (PF=0x02, DF=0x01)"
    f"  ARGB1555 square twiddled (PF=0x03, DF=0x01)"
    f"  RGB565 VQ (PF=0x01, DF=0x03)"
    f"  RGB565 SMALLVQ (PF=0x01, DF=0x10)"
    f"\nIf you need support for this format, file an issue with:"
    f"  - Source file name"
    f"  - Discovery mode output"
    f"  - Visual target description"
)

Enhancement: --dry-run Mode

Purpose: Validate manifest without requiring actual asset files.

Implementation:

def validate_manifest_only(d2_dir: Path, manifest: dict) -> None:
    """Validate manifest schema and logic without extracting assets."""
    errors = []
    warnings = []

    # Schema validation
    if "assets" not in manifest:
        errors.append("Manifest missing 'assets' key")
        return errors, warnings

    for idx, item in enumerate(manifest["assets"]):
        if "symbol" not in item:
            errors.append(f"Asset {idx}: missing 'symbol' key")
        if "source_file" not in item and "source" not in item:
            errors.append(f"Asset {idx} ({item.get('symbol', 'unnamed')}): missing 'source_file' or 'source' key")

        # Check for PVM files without entry names
        source = item.get("source_file", item.get("source", ""))
        if source.upper().endswith(".PVM"):
            if "source_entry" not in item and "entry" not in item:
                warnings.append(f"Asset {idx} ({item.get('symbol', 'unnamed')}): PVM file without entry name")

    return errors, warnings

# In main():
if args.dry_run:
    errors, warnings = validate_manifest_only(args.d2_dir, manifest)
    if errors:
        print("MANIFEST VALIDATION ERRORS:", file=sys.stderr)
        for error in errors:
            print(f"  - {error}", file=sys.stderr)
        sys.exit(1)
    if warnings:
        print("MANIFEST VALIDATION WARNINGS:", file=sys.stderr)
        for warning in warnings:
            print(f"  - {warning}", file=sys.stderr)
    print("Manifest validation passed. Assets that would be extracted:")
    for item in manifest.get("assets", []):
        print(f"  - {item.get('symbol', 'unnamed')}: {item.get('source_file', item.get('source', 'unknown'))}")
    sys.exit(0)

Enhancement: Improved Discovery Output

Current: Basic listing of entries

Proposed: Structured output with clear guidance

def discover_improved(d2_dir: Path) -> None:
    """Improved discovery mode with structured output and guidance."""

    print("=" * 70)
    print("D2 ASSET DISCOVERY REPORT")
    print("=" * 70)
    print(f"Scan directory: {d2_dir}")
    print(f"Generated: {datetime.datetime.now().isoformat()}")
    print()

    all_paths = sorted(
        p for p in d2_dir.rglob("*")
        if p.is_file() and p.suffix.upper() in (".PVM", ".PVR", ".CTS")
    )

    if not all_paths:
        print("⚠️  NO ASSETS FOUND")
        print()
        print("Expected assets should be staged at:")
        print("  /from/assets/private/d2-title/extracted/")
        print()
        print("Or specify custom location with --d2-dir")
        print()
        print("Expected files:")
        for expected in EXPECTED_ASSETS:
            print(f"  - {expected}")
        return

    print(f"Found {len(all_paths)} asset files:")
    print()

    for path in all_paths:
        rel = path.relative_to(d2_dir)
        print(f"─" * 70)
        print(f"File: {rel}")

        if path.suffix.upper() == ".PVM":
            try:
                blob = path.read_bytes()
                entries = pvm_entries(blob)
                print(f"Type: PVM container")
                print(f"Entries: {len(entries)}")
                print()
                print("  Entry Name          Offset    Format          Size")
                print("  " + "-" * 54)

                for name, offset in sorted(entries.items()):
                    try:
                        _, pf, df, w, h = parse_pvrt_header(blob, offset)
                        pf_label = PF_LABELS.get(pf, f"0x{pf:02x}")
                        df_label = DF_LABELS.get(df, f"0x{df:02x}")
                        print(f"  {name:20s} 0x{offset:06x} {pf_label}/{df_label:8s} {w:4d}x{h:3d}")
                    except ValueError as e:
                        print(f"  {name:20s} 0x{offset:06x} [ERROR: {str(e)[:30]}]")

                cts_path = path.with_suffix(".CTS")
                if cts_path.exists():
                    print(f"  [CTS companion: {cts_path.name}]")

            except Exception as e:
                print(f"  [ERROR reading file: {e}]")

        elif path.suffix.upper() == ".PVR":
            try:
                blob = path.read_bytes()
                offset = 16 if blob.startswith(b"GBIX") else 0
                _, pf, df, w, h = parse_pvrt_header(blob, offset)
                pf_label = PF_LABELS.get(pf, f"0x{pf:02x}")
                df_label = DF_LABELS.get(df, f"0x{df:02x}")
                print(f"Type: PVR texture")
                print(f"Format: {pf_label}/{df_label}")
                print(f"Size: {w}x{h}")
            except Exception as e:
                print(f"  [ERROR reading file: {e}]")

        elif path.suffix.upper() == ".CTS":
            print(f"Type: CTS animation sequence")
            print(f"Status: Not yet decoded (file issue if needed)")

        print()

    print("=" * 70)
    print("NEXT STEPS")
    print("=" * 70)
    print()
    print("1. Compare discovered assets with expected title-menu assets")
    print("2. Update tools/title_menu_manifest.json with correct entry names")
    print("3. Run extraction: make d2-assets")
    print("4. Update src/d3tui_main.c to use title-menu assets")
    print()
    print("For more information:")
    print("  docs/ASSET_EXTRACTION_WORKFLOW.md")
    print("  docs/D2_TITLE_MENU_TARGET.md")

3. Manifest Improvements

Current Manifest Analysis

Issues: - Entry names are speculative placeholders - No clear indication of which are confirmed vs. speculative - Missing metadata about asset roles and usage

Proposed Improvements:

  1. Add Status Field:
{
  "symbol": "d2_asset_title_bg",
  "source_file": "Q_TITLEBGMT0.PVM",
  "source_entry": "TITLEBGMT0",
  "status": "speculative",  // "confirmed" or "speculative"
  "decoder": "auto",
  "role": "background",
  "description": "Snowy mountain background layer 0",
  "notes": "Entry name not yet validated against actual file"
}
  1. Add Validation Section:
{
  "version": "1.0.0",
  "target": "D2 Disc 1 Title Menu",
  "validation_status": "unvalidated",
  "validation_date": null,
  "validated_by": null,
  "description": "Asset manifest for D2 title menu. Entry names are speculative and must be validated by running --discover against actual private assets.",
  "assets": [...]
}
  1. Add Expected Assets Section:
{
  "expected_assets": [
    "Q_TITLEBGMT0.PVM",
    "Q_TITLEBGMT1.PVM", 
    "Q_TITLEBGMT2.PVM",
    "Q_DMTITLE.PVM",
    "Q_TITLEMENU.PVM",
    "Q_DJSNOW.PVM",
    "SAKA_MNSNOW1A.PVM",
    "SAKA_MNSNOW1B.PVM",
    "SAKA_MNSNOW1C.PVM",
    "P_COMTIT.PVR"
  ],
  "assets": [...]
}

4. Implementation Plan

Phase 1: Documentation (T3)

Phase 2: Tooling (T3)

Phase 3: Manifest (T3)

Phase 4: Testing (T4)

5. Risk Assessment

Low Risk: - Documentation changes only - No source file modifications - No licensed content handling - Existing build continues to work

Medium Risk: - Tooling changes require testing - Need to ensure backward compatibility

High Risk (Mitigated): - No actual asset extraction (avoided) - No visual target changes (avoided) - No build system modifications (avoided)

6. Success Criteria

✅ Documentation provides clear, actionable guidance ✅ Tooling fails gracefully with helpful error messages ✅ Manifest clearly indicates speculative vs. confirmed entries ✅ Existing decoder-evidence build continues to work ✅ No changes require actual D2 licensed content

Next Steps

Proceed to T3 Implementation phase upon team agreement.