64kramsystem / 64kramsystem/ghidra-vice-connector

Sprite data viewer: render active sprite frames from live memory

Open
#31 0 comments 0 reactions 0 assignees View on GitHub
priority: medium
Dominant language
Python
Stars
1
Forks
0
PR merge metrics
No merged PRs in 30d

Description

## Summary

Read sprite pointers and data blocks from live VICE memory and render them as annotated bitmaps, saved to PNG files. This lets you identify which memory addresses hold sprite graphics during a game session — a core challenge when the 64KB space mixes code, sprite frames, and other data.

Source: RetroDebugger (Sprite View with scaling), Regenerator 2000 (inline sprite rendering).

---

## How C64 sprites work

The VIC-II supports 8 hardware sprites. Their data addresses are controlled by **sprite pointers**: 8 bytes at `screen_base + $03F8`–`screen_base + $03FF`. Each pointer byte × 64 = address of the 63-byte sprite data block.

**Screen base address** = `(VIC_VMCSB >> 4) * $0400`, where `VIC_VMCSB` is the byte at `$D018`. The VIC bank (which 16KB window) is determined by CIA2 Port A (`$DD00`) bits 0–1: `bank = 3 - (CIA2_PRA & 0x03)`, so `vic_base = bank * $4000`.

**Sprite data block:** 63 bytes = 3 bytes × 21 rows = 24×21 pixels (hires) or 12×21 pixels (multicolor).

**Multicolor flag:** bit N of `$D01C` = sprite N is multicolor. Multicolor sprites use 4 colors: `$D025` (color 1), `$D026` (color 2), `$D027+N` (sprite color), transparent.

---

## Implementation plan

### 1. `util.py` — no new BMP commands needed

All data is read via the existing `memory_get()`. Use `side_effects=False` and the I/O bank for chip registers (see issue #21 for bank discovery pattern).

### 2. `methods.py` — new method

```python
@REGISTRY.method(display='Save Sprite Sheet')
def save_sprite_sheet(process: C64):
"""
Read all 8 sprite data blocks and save them as a PNG sprite sheet.
Output: /tmp/vice_sprites.png — 8 sprites in a row, 2x scaled.
"""
vice = commands.STATE.require_vice()

# 1. Read VIC-II registers (I/O bank)
io_bank = _get_io_bank(vice) # shared helper — see issue #21
vic = vice.memory_get(0xD000, 0xD02E, bank_id=io_bank, side_effects=False)
vmcsb = vic[0xD018 - 0xD000]
spena = vic[0xD015 - 0xD000]
spmc = vic[0xD01C - 0xD000]
mc0 = vic[0xD025 - 0xD000] & 0x0F # multicolor 0
mc1 = vic[0xD026 - 0xD000] & 0x0F # multicolor 1
sp_cols = [vic[0xD027 + i - 0xD000] & 0x0F for i in range(8)]

# 2. Determine VIC bank and screen base
cia2 = vice.memory_get(0xDD00, 0xDD00, bank_id=io_bank, side_effects=False)
vic_bank = (3 - (cia2[0] & 0x03)) * 0x4000
screen_base = vic_bank + ((vmcsb >> 4) & 0x0F) * 0x0400

# 3. Read sprite pointers and data
ptrs = vice.memory_get(screen_base + 0x03F8, screen_base + 0x03FF)
sprites = []
for i, ptr in enumerate(ptrs):
addr = vic_bank + ptr * 64
data = vice.memory_get(addr, addr + 62)
sprites.append({'data': data, 'multicolor': bool(spmc & (1 << i)),
'color': sp_cols[i], 'enabled': bool(spena & (1 << i)),
'ptr': ptr, 'addr': addr})

# 4. Render and save PNG
_render_sprite_sheet(sprites, mc0, mc1, path='/tmp/vice_sprites.png')
for i, s in enumerate(sprites):
status = 'ON' if s['enabled'] else 'off'
mode = 'MC' if s['multicolor'] else 'HI'
log.info(f' Sprite {i}: ptr=0x{s["ptr"]:02X} addr=0x{s["addr"]:04X} {mode} {status}')
log.info('Sprite sheet saved to /tmp/vice_sprites.png')
```

### 3. Rendering helper

```python
C64_PALETTE = [ # standard C64 16-color palette (RGB)
(0,0,0),(255,255,255),(136,0,0),(170,255,238),(204,68,204),(0,204,85),
(0,0,170),(238,238,119),(221,136,85),(102,68,0),(255,119,119),(51,51,51),
(119,119,119),(170,255,102),(0,136,255),(187,187,187),
]

def _render_sprite_sheet(sprites, mc0, mc1, path, scale=3):
# Each sprite: 24×21 hires or 12×21 multicolor
# Rendered at scale×scale pixels per C64 pixel
# 8 sprites side by side with 4px gap
import zlib, struct as s, pathlib
W_SP = 24 * scale
H_SP = 21 * scale
GAP = 4
W = 8 * W_SP + 7 * GAP
H = H_SP
pixels = bytearray(W * H * 3)
for i, sp in enumerate(sprites):
ox = i * (W_SP + GAP)
for row in range(21):
b0, b1, b2 = sp['data'][row*3], sp['data'][row*3+1], sp['data'][row*3+2]
bits = (b0 << 16) | (b1 << 8) | b2
if sp['multicolor']:
for col in range(12):
pair = (bits >> (22 - col*2)) & 0x03
color = [None, C64_PALETTE[mc0], C64_PALETTE[sp['color']],
C64_PALETTE[mc1]][pair]
if color is None: continue
for dy in range(scale):
for dx in range(scale * 2): # MC pixels are 2× wide
px = ox + col * scale * 2 + dx
py = row * scale + dy
pixels[(py * W + px) * 3:(py * W + px) * 3 + 3] = color
else:
for col in range(24):
if not (bits >> (23 - col)) & 1: continue
color = C64_PALETTE[sp['color']]
for dy in range(scale):
for dx in range(scale):
px = ox + col * scale + dx
py = row * scale + dy
pixels[(py * W + px) * 3:(py * W + px) * 3 + 3] = color

_write_png(path, W, H, bytes(pixels)) # shared PNG writer (see issue #23)
```

---

## Files to change

- `src/main/py/src/vice/methods.py` — add `save_sprite_sheet`
- `src/main/py/src/vice/commands.py` — add `_get_io_bank()` shared helper (also used by issue #21), `_render_sprite_sheet()`, `_write_png()` (shared with issue #23)

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.