neser 1.2.0

NESER - Nintendo Emulation Systems Engine (Rust). Desktop and WebAssembly frontends.
Documentation
from pathlib import Path
import re

from py65.disassembler import Disassembler
from py65.devices.mpu6502 import MPU


ROM_PATH = Path(
	"/Users/henrikku/repos/neser/roms/nes/automated_tests/"
	"fadeout_and_triangle_tests/fadeout_and_triangle_test.nes"
)


def triangle_freq(timer: int) -> float:
	return 1_789_773.0 / (32.0 * (timer + 1))


def main() -> None:
	asm_path = ROM_PATH.with_suffix(".asm")

	data = ROM_PATH.read_bytes()
	if len(data) < 16 or data[0:4] != b"NES\x1A":
		raise SystemExit("Invalid iNES file")

	prg_banks = data[4]
	chr_banks = data[5]
	flags6 = data[6]
	flags7 = data[7]
	trainer = (flags6 & 0x04) != 0
	trainer_size = 512 if trainer else 0
	prg_size = prg_banks * 16 * 1024
	prg_start = 16 + trainer_size
	prg_end = prg_start + prg_size
	prg = data[prg_start:prg_end]

	mpu = MPU()
	mem = mpu.memory

	if prg_size == 0x4000:
		base = 0xC000
		mem[0x8000 : 0x8000 + 0x4000] = prg
		mem[0xC000 : 0xC000 + 0x4000] = prg
	else:
		base = 0x8000
		mem[base : base + prg_size] = prg

	dis = Disassembler(mpu)

	nmi_vec = mem[0xFFFA] | (mem[0xFFFB] << 8)
	reset_vec = mem[0xFFFC] | (mem[0xFFFD] << 8)
	irq_vec = mem[0xFFFE] | (mem[0xFFFF] << 8)

	labels = {"NMI": nmi_vec, "RESET": reset_vec, "IRQ": irq_vec}
	for label, addr in labels.items():
		dis._address_parser.labels[label] = addr

	last_imm = {"A": None, "X": None, "Y": None}
	tri_timer_low = None
	tri_timer_high = None
	last_tri_linear = None

	lines = []
	lines.append("; Disassembly of PRG-ROM for fadeout_and_triangle_test.nes")
	lines.append(f"; PRG ROM banks: {prg_banks} (size={prg_size} bytes)")
	lines.append(f"; CHR ROM banks: {chr_banks}")
	lines.append(f"; Mapper: {(flags6 >> 4) | (flags7 & 0xF0)}")
	lines.append(
		f"; Mirroring: {'vertical' if (flags6 & 0x01) else 'horizontal'}"
	)
	lines.append(
		f"; Vectors: NMI=${nmi_vec:04X} RESET=${reset_vec:04X} IRQ=${irq_vec:04X}"
	)
	lines.append(";")
	lines.append("; Triangle channel notes:")
	lines.append("; - $4015 bit 2 enables triangle output")
	lines.append(
		"; - $4008: bit7=control (length counter halt/linear control), "
		"bits6-0=linear counter reload"
	)
	lines.append("; - $400A/$400B: 11-bit timer; frequency = CPU/(32*(timer+1))")
	lines.append("; - $400B bits7-3 set length counter load")
	lines.append(";")

	pc = base
	end = base + prg_size

	while pc < end:
		length, text = dis.instruction_at(pc)
		raw = bytes(mem[pc : pc + length])
		bytes_str = " ".join(f"{b:02X}" for b in raw)

		imm_match = re.match(r"^(LDA|LDX|LDY) #\$([0-9A-F]{2})$", text)
		if imm_match:
			reg = imm_match.group(1)[-1]
			last_imm[reg] = int(imm_match.group(2), 16)

		comment_parts = []
		store_match = re.match(r"^(STA|STX|STY) \$([0-9A-F]{4})", text)
		if store_match:
			reg = store_match.group(1)[-1]
			target = int(store_match.group(2), 16)
			value = last_imm.get(reg)

			if target == 0x4015:
				if value is not None:
					enabled = []
					if value & 0x01:
						enabled.append("square1")
					if value & 0x02:
						enabled.append("square2")
					if value & 0x04:
						enabled.append("triangle")
					if value & 0x08:
						enabled.append("noise")
					if value & 0x10:
						enabled.append("dmc")
					comment_parts.append(
						f"APU enable: {', '.join(enabled) if enabled else 'none'}"
					)
				else:
					comment_parts.append("APU enable write")

			if target == 0x4008:
				if value is not None:
					tri_linear = value & 0x7F
					control = (value >> 7) & 1
					comment_parts.append(
						f"triangle linear reload={tri_linear} control={control}"
					)
					if last_tri_linear is not None and tri_linear != last_tri_linear:
						comment_parts.append("linear reload change (fade/gate shaping)")
					last_tri_linear = tri_linear
				else:
					comment_parts.append("triangle linear counter write")

			if target == 0x400A:
				if value is not None:
					tri_timer_low = value
					comment_parts.append(f"triangle timer low=${value:02X}")
				else:
					comment_parts.append("triangle timer low write")

			if target == 0x400B:
				if value is not None:
					tri_timer_high = value & 0x07
					length_load = value >> 3
					comment_parts.append(
						f"triangle timer high=${tri_timer_high:02X} length=${length_load}"
					)
					if tri_timer_low is not None:
						timer = (tri_timer_high << 8) | tri_timer_low
						freq = triangle_freq(timer)
						comment_parts.append(f"triangle freq≈{freq:.2f} Hz")
				else:
					comment_parts.append("triangle timer high/length write")

		for label, addr in labels.items():
			if addr == pc:
				lines.append(f"{label}:")
				break

		comment = "" if not comment_parts else " ; " + " | ".join(comment_parts)
		lines.append(f"${pc:04X}: {bytes_str:<11} {text}{comment}")

		pc += max(1, length)

	asm_path.write_text("\n".join(lines))
	print(f"Wrote {asm_path}")


if __name__ == "__main__":
	main()