Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,11 +420,14 @@ Emitted `; XREF:` comments above labels list the **enclosing caller label** and

Every caller listed in `; XREF:` can be jumped to immediately in `src/hunk*.asm` via `rg "^<label>:"`.

### 3. Cross-Hunk & Jump-Vector Target Hints
Instructions that jump/call across hunks or through a register jump-vector table automatically receive a trailing target comment:
### 3. Cross-Hunk, Jump-Vector & Hardware Register Bitfield Auto-Comments
Instructions that jump/call across hunks or through a register jump-vector table automatically receive a trailing target comment, and immediate `.w` / `.l` writes to key Amiga custom registers (`DMACON`, `INTENA`, `INTREQ`, `ADKCON`, `BPLCON0`, `BLTCON0`, `BLTCON1`, `BLTSIZE`) automatically decode their bitfields into persistent auto-comments that survive `emit`:
```asm
jmp LAB_5_2810E.l ; -> hunk5
jsr 414(a5) ; -> hunk3:LAB_3_26432
move.w #$83C0,_custom+DMACON ; SET|DMAEN|BPLEN|COPEN|BLTEN
move.l #$09F00002,BLTCON0(a5) ; BLTCON0: USEA|USED|LF=$F0 (D=A) | BLTCON1: DESC
move.w #$1004,BLTSIZE(a5) ; 64x64 px (4w x 64h)
```

---
Expand Down Expand Up @@ -463,15 +466,15 @@ Instructions that jump/call across hunks or through a register jump-vector table
| `Backspace` / `-` | Navigate back in history stack (up to 100 entries across segments) |
| `g` | Goto (`<seg>:<label>`, `<seg>:+<offset>`, `<label>`, `+offset`, or hex address) |
| `/` | Prefix-scoped search (`[seg:]addr` / `[seg:]label` navigation, or `m:`/`o:`/`s:`/`c:`/`b:`/`l:`/`cluster:` with optional `seg/` or `all/` scope, e.g. `m:jsr`, `s:hunk3/disk`, `cluster:blitter`) |
| `f` | Function list popup (shows `<seg> +<offset> <name>`, type to filter, `Enter` to jump) |
| `f` | Function list popup (shows `<seg> +<offset> <name>` and self/inclusive cycle timing, type to filter, `Enter` to jump) |
| `x` | XREFs popup to current address (shows `<seg> +<offset> <type> (in <caller_label>) <mnem> <ops>`, `Enter` to jump) |

### Type Marking & Selection Keys

| Key | Action |
|-----|--------|
| `v` | Toggle range selection (`Esc` cancels active selection); when code instructions are selected, the status bar displays aggregate byte size, instruction count, and MC68000 execution cycles + `(R/W)` bus words |
| `V` | Select (or cancel selection of) the current subroutine and display its aggregate byte size, instruction count, and MC68000 execution cycles + `(R/W)` bus words in the status bar |
| `v` | Toggle range selection (`Esc` cancels active selection); when code instructions are selected, the status bar displays aggregate byte size, instruction count, self MC68000 execution cycles + `(R/W)` bus words, and `[incl: ... cyc]` when calls (`jsr`/`bsr`) are included |
| `V` | Select (or cancel selection of) the current subroutine and display its aggregate byte size, instruction count, self MC68000 execution cycles + `(R/W)` bus words, and inclusive `[incl: ... cyc]` timing across resolved callees in the status bar |
| `c` | Mark cursor (or selection) as `CODE` (`confidence = 100`) and run incremental analysis |
| `b` | Mark as `DATA_B` (1 byte) |
| `w` | Mark as `DATA_W` (2 bytes) |
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ Amiga M68k Reverse Engineering System — terminal-native, keyboard-first disass
- **Trace-seeded M68000 + Amiga OCS/ECS chipset analysis (`analyze --trace`)**: Built-in sandboxed M68000 interpreter (`m68kemu`) + deterministic Agnus/Denise/Paula/Blitter/Copper/8520-CIA state machine that resolves indirect dispatch, self-installed interrupt/trap handlers, and base-register hardware accesses by observation
- **Shared SQLite database (`work/<module>/project.db`)** and unified virtual address space across all segments of a module (powered by pure-Go `modernc.org/sqlite`)
- **Relocation-exact symbol resolution** (`HUNK_RELOC32` / `HUNK_DREL32`), automatic cross-hunk `XDEF` exports, and collision-free `LAB_<hunk>_<offset>` auto-labels (`LAB_5_24BE8`)
- **Source-linkable `; XREF:` comments** (`; XREF: hunk5:LAB_5_24E84 (jsr 498(a5))`) and automatic cross-hunk / jump-vector target hints (`; -> hunk3:LAB_3_26432`)
- **Static Call Graph CLI (`hunks -m <module> cg`)** with automatic `d16(a5)` jump-vector resolution, bottom-up `--ready` worklists, `--leaves`, `--anchors` (strings & `$DFFxxx` hardware), `--suspect` audits, and `--code` caller/callee inspection
- **Source-linkable `; XREF:` comments** (`; XREF: hunk5:LAB_5_24E84 (jsr 498(a5))`), automatic cross-hunk / jump-vector target hints (`; -> hunk3:LAB_3_26432`), and **Amiga custom register bitfield auto-comments** (`DMACON`, `INTENA`, `INTREQ`, `ADKCON`, `BPLCON0`, `BLTCON0`/`BLTCON1`, `BLTSIZE` decoded into persistent auto-comments that survive `emit`)
- **Static Call Graph CLI (`hunks -m <module> cg`)** with automatic `d16(a5)` jump-vector resolution, self & inclusive (`+callees`) MC68000 cycle bounds (`+loop`, `+ind`), bottom-up `--ready` worklists, `--leaves`, `--anchors` (strings & `$DFFxxx` hardware), `--suspect` audits, and `--code` caller/callee inspection
- **Standalone Amiga music module scanner (`hunks rip <file>`, experimental)** with 26 ExoticRipper recognizers (including sandboxed self-executing M68000 driver probes for David Whittaker and Final Music System)
- **Interactive TUI (`hunks -m <module> tui`)** with full-width Sub-Row Code View, inline 16-byte `dc.b` hex/ASCII grid, 3-tier EOL comment priority, exact MC68000 single-instruction, block-selection (`v`), and subroutine (`V`) cycle & R/W bus timing, interactive **Amiga Bitplane Image Viewer & Ripper (`B`)** (Kitty/iTerm2/Sixel/Half-Block pixel graphics, `BPLCON0` 1–7 planes, EHB/HAM6, Copper/`$0RGB` palette extraction, PNG & `FORM ILBM` export), experimental **TUI Music Ripper (`M`)** (scan, jump, rip, & carve), virtual segment `incbin` carves (`v` + `K` in editor, `K` in asset viewers with metadata block comments and round-trip on `B`), configurable truecolor themes (`~/.config/hunks/theme.json`), vertical scrollbar, floating modal overlays, byte-exact cursor splitting, cross-hunk `Enter` navigation, `x` XREFs modal, `f` Functions filter, and struct/alias editors
- **Interactive TUI (`hunks -m <module> tui`)** with full-width Sub-Row Code View, inline 16-byte `dc.b` hex/ASCII grid, 3-tier EOL comment priority, exact MC68000 single-instruction, block-selection (`v`), and subroutine (`V`) self + inclusive (`[incl: ... cyc]`) cycle & R/W bus timing, interactive **Amiga Bitplane Image Viewer & Ripper (`B`)** (Kitty/iTerm2/Sixel/Half-Block pixel graphics, `BPLCON0` 1–7 planes, EHB/HAM6, Copper/`$0RGB` palette extraction, PNG & `FORM ILBM` export), experimental **TUI Music Ripper (`M`)** (scan, jump, rip, & carve), virtual segment `incbin` carves (`v` + `K` in editor, `K` in asset viewers with metadata block comments and round-trip on `B`), configurable truecolor themes (`~/.config/hunks/theme.json`), vertical scrollbar, floating modal overlays, byte-exact cursor splitting, cross-hunk `Enter` navigation, `x` XREFs modal, `f` Functions filter, and struct/alias editors
- **100% bit-for-bit reassembly** verified with `vasmm68k_mot`

## Quick Start
Expand Down
286 changes: 286 additions & 0 deletions internal/amiga/bitfields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package amiga

import (
"fmt"
"strings"
)

type bitFlag struct {
mask uint16
name string
}

var dmaconFlags = []bitFlag{
{1 << 14, "BBUSY"},
{1 << 13, "BZERO"},
{1 << 10, "BLTPRI"},
{1 << 9, "DMAEN"},
{1 << 8, "BPLEN"},
{1 << 7, "COPEN"},
{1 << 6, "BLTEN"},
{1 << 5, "SPREN"},
{1 << 4, "DSKEN"},
{1 << 3, "AUD3EN"},
{1 << 2, "AUD2EN"},
{1 << 1, "AUD1EN"},
{1 << 0, "AUD0EN"},
}

var intFlags = []bitFlag{
{1 << 14, "INTEN"},
{1 << 13, "EXTER"},
{1 << 12, "DSKSYN"},
{1 << 11, "RBF"},
{1 << 10, "AUD3"},
{1 << 9, "AUD2"},
{1 << 8, "AUD1"},
{1 << 7, "AUD0"},
{1 << 6, "BLIT"},
{1 << 5, "VERTB"},
{1 << 4, "COPER"},
{1 << 3, "PORTS"},
{1 << 2, "SOFT"},
{1 << 1, "DSKBLK"},
{1 << 0, "TBE"},
}

var adkconFlags = []bitFlag{
{1 << 14, "PRECOMP1"},
{1 << 13, "PRECOMP0"},
{1 << 12, "MFMPREC"},
{1 << 11, "UARTBRK"},
{1 << 10, "WORDSYNC"},
{1 << 9, "MSBSYNC"},
{1 << 8, "FAST"},
{1 << 7, "USE3PN"},
{1 << 6, "USE2P3"},
{1 << 5, "USE1P2"},
{1 << 4, "USE0P1"},
{1 << 3, "USE3VN"},
{1 << 2, "USE2V3"},
{1 << 1, "USE1V2"},
{1 << 0, "USE0V1"},
}

func decodeSetClrFlags(val uint16, flags []bitFlag) string {
if (val & 0x7FFF) == 0 {
return ""
}
var parts []string
if (val & 0x8000) != 0 {
parts = append(parts, "SET")
} else {
parts = append(parts, "CLR")
}
for _, f := range flags {
if (val & f.mask) != 0 {
parts = append(parts, f.name)
}
}
if len(parts) <= 1 {
return ""
}
return strings.Join(parts, "|")
}

func blitterMintermDesc(lf uint8) string {
switch lf {
case 0x00:
return "D=0"
case 0xFF:
return "D=1"
case 0xF0:
return "D=A"
case 0x0F:
return "D=~A"
case 0xCC:
return "D=B"
case 0x33:
return "D=~B"
case 0xAA:
return "D=C"
case 0x55:
return "D=~C"
case 0xCA:
return "D=AB+~AC"
case 0xAC:
return "D=AC+~AB"
case 0xFC:
return "D=A|B"
case 0xFA:
return "D=A|C"
case 0xEE:
return "D=B|C"
case 0xFE:
return "D=A|B|C"
case 0xC0:
return "D=A&B"
case 0xA0:
return "D=A&C"
case 0x88:
return "D=B&C"
case 0x80:
return "D=A&B&C"
case 0x3C:
return "D=A^B"
case 0x5A:
return "D=A^C"
case 0x66:
return "D=B^C"
case 0x4A:
return "D=AB+~AC (line/xor)"
case 0xB8:
return "D=AB+~BC"
case 0xE2:
return "D=BC+~BC"
}
return ""
}

// DecodeCustomRegWrite decodes a 16-bit immediate write to custom register offset (addr - 0xDFF000)
// into a concise bitfield summary for AutoComment, or returns "" if not applicable.
func DecodeCustomRegWrite(offset uint32, val uint16) string {
switch offset {
case 0x096: // DMACON
return decodeSetClrFlags(val, dmaconFlags)
case 0x09A, 0x09C: // INTENA, INTREQ
return decodeSetClrFlags(val, intFlags)
case 0x09E: // ADKCON
return decodeSetClrFlags(val, adkconFlags)
case 0x100: // BPLCON0
bpu := int((val >> 12) & 0x7)
if (val & (1 << 4)) != 0 {
bpu |= 8
}
var parts []string
if (val & 0x8000) != 0 {
parts = append(parts, "HIRES")
}
switch {
case bpu == 0:
parts = append(parts, "0BPU")
case bpu == 6 && (val&(1<<11)) == 0 && (val&(1<<10)) == 0:
parts = append(parts, "6BPU (EHB 64c)")
case bpu == 6 && (val&(1<<11)) != 0:
parts = append(parts, "6BPU (HAM6)")
case bpu == 8 && (val&(1<<11)) != 0:
parts = append(parts, "8BPU (HAM8)")
default:
parts = append(parts, fmt.Sprintf("%dBPU (%dc)", bpu, 1<<bpu))
}
if (val & (1 << 11)) != 0 {
parts = append(parts, "HAM")
}
if (val & (1 << 10)) != 0 {
parts = append(parts, "DPF")
}
if (val & (1 << 9)) != 0 {
parts = append(parts, "COLOR")
}
if (val & (1 << 8)) != 0 {
parts = append(parts, "GAUD")
}
if (val & (1 << 2)) != 0 {
parts = append(parts, "LACE")
}
return strings.Join(parts, "|")

case 0x040: // BLTCON0
ash := (val >> 12) & 0xF
var parts []string
if ash > 0 {
parts = append(parts, fmt.Sprintf("ASH=%d", ash))
}
if (val & (1 << 11)) != 0 {
parts = append(parts, "USEA")
}
if (val & (1 << 10)) != 0 {
parts = append(parts, "USEB")
}
if (val & (1 << 9)) != 0 {
parts = append(parts, "USEC")
}
if (val & (1 << 8)) != 0 {
parts = append(parts, "USED")
}
lf := uint8(val & 0xFF)
if desc := blitterMintermDesc(lf); desc != "" {
parts = append(parts, fmt.Sprintf("LF=$%02X (%s)", lf, desc))
} else {
parts = append(parts, fmt.Sprintf("LF=$%02X", lf))
}
return strings.Join(parts, "|")

case 0x042: // BLTCON1
if val == 0 {
return ""
}
bsh := (val >> 12) & 0xF
var parts []string
if bsh > 0 {
parts = append(parts, fmt.Sprintf("BSH=%d", bsh))
}
if (val & 1) != 0 {
parts = append(parts, "LINE")
if (val & (1 << 6)) != 0 {
parts = append(parts, "SIGN")
}
if (val & (1 << 1)) != 0 {
parts = append(parts, "SING")
}
} else {
if (val & (1 << 4)) != 0 {
parts = append(parts, "EFE")
}
if (val & (1 << 3)) != 0 {
parts = append(parts, "IFE")
}
if (val & (1 << 2)) != 0 {
parts = append(parts, "FCI")
}
if (val & (1 << 1)) != 0 {
parts = append(parts, "DESC")
}
}
return strings.Join(parts, "|")

case 0x058: // BLTSIZE
h := int((val >> 6) & 0x3FF)
if h == 0 {
h = 1024
}
w := int(val & 0x3F)
if w == 0 {
w = 64
}
return fmt.Sprintf("%dw x %dh (%dx%d px)", w, h, w*16, h)
}
return ""
}

// DecodeCustomRegLongWrite decodes a 32-bit immediate write to two consecutive 16-bit custom registers
// at offset and offset+2 (e.g. move.l #$09F00000,_custom+BLTCON0).
func DecodeCustomRegLongWrite(offset uint32, val uint32) string {
hi := uint16(val >> 16)
lo := uint16(val & 0xFFFF)
hiDesc := DecodeCustomRegWrite(offset, hi)
loDesc := DecodeCustomRegWrite(offset+2, lo)
if hiDesc == "" && loDesc == "" {
return ""
}
hiName := customRegs[offset]
if hiName == "" {
hiName = fmt.Sprintf("CUSTOM_%03X", offset)
}
loName := customRegs[offset+2]
if loName == "" {
loName = fmt.Sprintf("CUSTOM_%03X", offset+2)
}
if hiDesc != "" && loDesc != "" {
return fmt.Sprintf("%s: %s | %s: %s", hiName, hiDesc, loName, loDesc)
}
if hiDesc != "" {
return fmt.Sprintf("%s: %s", hiName, hiDesc)
}
return fmt.Sprintf("%s: %s", loName, loDesc)
}
42 changes: 42 additions & 0 deletions internal/amiga/bitfields_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package amiga

import "testing"

func TestDecodeCustomRegWrite(t *testing.T) {
cases := []struct {
offset uint32
val uint16
want string
}{
{0x096, 0x0000, ""}, // No-op DMACON mask
{0x096, 0x8000, ""}, // No-op DMACON SET with 0 mask
{0x096, 0x83C0, "SET|DMAEN|BPLEN|COPEN|BLTEN"},
{0x096, 0x01FF, "CLR|BPLEN|COPEN|BLTEN|SPREN|DSKEN|AUD3EN|AUD2EN|AUD1EN|AUD0EN"},
{0x09A, 0x4000, "CLR|INTEN"},
{0x09A, 0xC030, "SET|INTEN|VERTB|COPER"},
{0x09C, 0x0060, "CLR|BLIT|VERTB"},
{0x09E, 0x9500, "SET|MFMPREC|WORDSYNC|FAST"},
{0x100, 0x4200, "4BPU (16c)|COLOR"},
{0x100, 0x9204, "HIRES|1BPU (2c)|COLOR|LACE"},
{0x100, 0x6200, "6BPU (EHB 64c)|COLOR"},
{0x100, 0x6A00, "6BPU (HAM6)|HAM|COLOR"},
{0x040, 0x09F0, "USEA|USED|LF=$F0 (D=A)"},
{0x040, 0x4FCA, "ASH=4|USEA|USEB|USEC|USED|LF=$CA (D=AB+~AC)"},
{0x042, 0x0000, ""},
{0x042, 0x0002, "DESC"},
{0x042, 0x0043, "LINE|SIGN|SING"},
{0x058, (64 << 6) | 20, "20w x 64h (320x64 px)"},
{0x058, 0x0000, "64w x 1024h (1024x1024 px)"},
}
for _, tc := range cases {
if got := DecodeCustomRegWrite(tc.offset, tc.val); got != tc.want {
t.Errorf("DecodeCustomRegWrite(0x%03X, 0x%04X) = %q, want %q", tc.offset, tc.val, got, tc.want)
}
}

gotLong := DecodeCustomRegLongWrite(0x040, 0x09F00002)
wantLong := "BLTCON0: USEA|USED|LF=$F0 (D=A) | BLTCON1: DESC"
if gotLong != wantLong {
t.Errorf("DecodeCustomRegLongWrite(0x040, 0x09F00002) = %q, want %q", gotLong, wantLong)
}
}
Loading
Loading