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
- Visual Target:
docs/D2_TITLE_MENU_TARGET.md - Build Policy:
docs/BUILD_TARGET_POLICY.md - Asset Map:
docs/ASSET_EXTRACTION_MAP.md - Extraction Tool:
tools/extract_d2_menu_assets.py - Current Manifest:
tools/title_menu_manifest.json
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
- 2026-05-11: Initial version for asset extraction readiness
- 2026-05-10: Realignment to D2 title-menu target
- 2026-05-09: Pre-realignment decoder-evidence workflow
## 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
)
- 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
)
- 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:
- 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"
}
- 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": [...]
}
- 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)
- Create
docs/ASSET_EXTRACTION_WORKFLOW.mdwith comprehensive guide - Update
README.mdwith asset requirements section - Add troubleshooting to
BUILD.md - Create
tools/README.mdfor extraction tool usage
Phase 2: Tooling (T3)
- Implement
--dry-runmode for manifest validation - Enhance error messages with actionable guidance
- Improve discovery output formatting
- Add help text with examples
Phase 3: Manifest (T3)
- Add status fields to manifest
- Add validation metadata
- Clarify speculative vs. confirmed entries
- Add expected assets list
Phase 4: Testing (T4)
- Test
--dry-runwith current manifest - Test discovery mode on empty directory
- Test error messages are clear
- Verify existing build still works
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.