Skip to content

Commit

Permalink
Add support to trace.py for executing code in 128K snapshots
Browse files Browse the repository at this point in the history
  • Loading branch information
skoolkid committed Jul 11, 2023
1 parent c306599 commit ce20db4
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 95 deletions.
4 changes: 2 additions & 2 deletions skoolkit/pagingtracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

from skoolkit.snapshot import BANKS_128K

class PagingTracer: # pragma: no cover
class PagingTracer:
def write_port(self, registers, port, value):
if port % 2 == 0:
if port % 2 == 0: # pragma: no cover
self.border = value % 8
elif port & 0x8002 == 0:
mem = self.simulator.memory
Expand Down
60 changes: 28 additions & 32 deletions skoolkit/snapinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def _parse_z80(z80file):
reg.pc = get_word(header, 6)
else:
reg.pc = get_word(header, 32)
reg.out7ffd = header[35]
reg.sp = get_word(header, 8)
reg.i = header[10]
reg.r = 128 * (header[12] & 1) + (header[11] & 127)
Expand Down Expand Up @@ -202,7 +203,7 @@ def _analyse_z80(z80file, header, reg, ram_blocks):
print(f'T-states: {reg.tstates}')
print('Border: {}'.format((header[12] // 2) & 7))
if bank is not None:
print('Port $7FFD: {} - bank {} (block {}) paged into 49152-65535 C000-FFFF'.format(header[35], bank, bank + 3))
print('Port $7FFD: {} - bank {} (block {}) paged into 49152-65535 C000-FFFF'.format(reg.out7ffd, bank, bank + 3))

# Print register contents
print('Registers:')
Expand Down Expand Up @@ -288,50 +289,48 @@ def _parse_szx(szxfile):
reg.tstates = get_dword(block, 29)
elif block_id == 'SPCR':
reg.border = block[0]
reg.out7ffd = block[1]

return header, reg, blocks

def _get_block_id(data, index):
return ''.join([chr(b) for b in data[index:index+ 4]])

def _print_keyb(block, variables):
def _print_keyb(block, reg):
issue2 = get_dword(block, 0) & 1
return ['Issue 2 emulation: {}abled'.format('en' if issue2 else 'dis')]

def _print_ramp(block, variables):
lines = []
def _print_ramp(block, reg):
flags = get_word(block, 0)
compressed = 'compressed' if flags & 1 else 'uncompressed'
page = block[2]
ram = block[3:]
lines.append("Page: {}".format(page))
machine_id = variables['chMachineId']
addresses = ''
if page == 5:
addresses = '16384-32767 4000-7FFF'
elif page == 2:
addresses = '32768-49151 8000-BFFF'
elif machine_id > 1 and page == variables['ch7ffd'] & 7:
elif reg.machine_id > 1 and page == reg.out7ffd & 7:
addresses = '49152-65535 C000-FFFF'
if addresses:
addresses += ': '
lines.append("RAM: {}{} bytes, {}".format(addresses, len(ram), compressed))
return lines

def _print_spcr(block, variables):
lines = []
lines.append('Border: {}'.format(block[0]))
ch7ffd = block[1]
variables['ch7ffd'] = ch7ffd
bank = ch7ffd & 7
lines.append('Port $7FFD: {} (bank {} paged into 49152-65535 C000-FFFF)'.format(ch7ffd, bank))
return lines

def _print_z80r(reg):
lines = []
lines.append('Interrupts: {}abled'.format('en' if reg.iff2 else 'dis'))
lines.append('Interrupt mode: {}'.format(reg.im))
lines.append(f'T-states: {reg.tstates}')
return (
f'Page: {page}',
f'RAM: {addresses}{len(ram)} bytes, {compressed}'
)

def _print_spcr(block, reg):
return (
f'Border: {reg.border}',
f'Port $7FFD: {reg.out7ffd} (bank {reg.out7ffd & 7} paged into 49152-65535 C000-FFFF)'
)

def _print_z80r(block, reg):
lines = [
'Interrupts: {}abled'.format('en' if reg.iff2 else 'dis'),
f'Interrupt mode: {reg.im}',
f'T-states: {reg.tstates}'
]
return lines + reg.get_lines()

SZX_BLOCK_PRINTERS = {
Expand All @@ -343,19 +342,14 @@ def _print_z80r(reg):

def _analyse_szx(header, reg, blocks):
print('Version: {}.{}'.format(header[4], header[5]))
machine_id = header[6]
print('Machine: {}'.format(SZX_MACHINES.get(machine_id, 'Unknown')))
variables = {'chMachineId': machine_id}
reg.machine_id = header[6]
print('Machine: {}'.format(SZX_MACHINES.get(reg.machine_id, 'Unknown')))

for block_id, block in blocks:
print('{}: {} bytes'.format(block_id, len(block)))
printer = SZX_BLOCK_PRINTERS.get(block_id)
if printer:
if block_id == 'Z80R':
lines = printer(reg)
else:
lines = printer(block, variables)
for line in lines:
for line in printer(block, reg):
print(" " + line)

###############################################################################
Expand Down Expand Up @@ -385,8 +379,10 @@ def _parse_sna(snafile):
reg.sp = get_word(sna, 23)
if len(sna) > 49179:
reg.pc = get_word(sna, 49179)
reg.out7ffd = sna[49181]
else:
reg.pc = get_word(sna, reg.sp - 16357)
reg.out7ffd = 0
reg.border = sna[26]
reg.iff2 = (sna[19] & 4) // 4
reg.im = sna[25]
Expand Down
75 changes: 44 additions & 31 deletions skoolkit/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
import argparse
import time

from skoolkit import ROM48, VERSION, SkoolKitError, get_int_param, integer, read_bin_file
from skoolkit.snapshot import make_snapshot, poke, print_reg_help, write_z80v3
from skoolkit import ROM48, ROM128, VERSION, SkoolKitError, get_int_param, integer, read_bin_file
from skoolkit.pagingtracer import PagingTracer
from skoolkit.snapshot import BANKS_128K, make_snapshot, poke, print_reg_help, write_z80v3
from skoolkit.simulator import (Simulator, A, F, B, C, D, E, H, L, IXh, IXl, IYh, IYl,
SP, I, R, xA, xF, xB, xC, xD, xE, xH, xL, PC, T)
from skoolkit.snapinfo import parse_snapshot
Expand All @@ -35,14 +36,17 @@
A'={^A:<3} F'={^F:08b} BC'={BC':<5} DE'={DE':<5} HL'={HL':<5} SP={SP:<5}
""".strip()

class Tracer:
def __init__(self, border):
self.operations = 0
class Tracer(PagingTracer):
def __init__(self, simulator, border, out7ffd):
self.simulator = simulator
self.border = border
self.out7ffd = out7ffd
self.operations = 0
self.spkr = None
self.out_times = []

def run(self, simulator, start, stop, verbose, max_operations, max_tstates, decimal, interrupts):
def run(self, start, stop, verbose, max_operations, max_tstates, decimal, interrupts):
simulator = self.simulator
opcodes = simulator.opcodes
memory = simulator.memory
registers = simulator.registers
Expand Down Expand Up @@ -130,6 +134,8 @@ def write_port(self, registers, port, value):
if self.spkr != value & 0x10:
self.spkr = value & 0x10
self.out_times.append(registers[T])
elif port & 0x8002 == 0:
super().write_port(registers, port, value)

def get_registers(sna_reg, specs):
if sna_reg:
Expand Down Expand Up @@ -197,15 +203,26 @@ def simplify(delays, depth):
return ', '.join(s0)

def run(snafile, options):
if snafile == '.':
memory = [0] * 65536
if snafile in ('48', '128'):
if snafile == '48':
memory = [0] * 0x10000
else:
memory = [0] * 0x28000
reg = None
org = 0
else:
memory, org = make_snapshot(snafile, options.org)[0:2]
memory, org = make_snapshot(snafile, options.org, page=-1)[:2]
reg = parse_snapshot(snafile)[1]
if snafile.lower()[-4:] == '.sna':
reg.sp = (reg.sp + 2) % 65536
if reg:
state = {'im': reg.im, 'iff': reg.iff2, 'tstates': reg.tstates}
border = reg.border
out7ffd = reg.out7ffd
else:
state = {'im': 1, 'iff': 1, 'tstates': 0}
border = 7
out7ffd = 0
start = options.start
if start is None:
if reg:
Expand All @@ -216,34 +233,30 @@ def run(snafile, options):
start = org
if options.rom:
rom = read_bin_file(options.rom)
memory[:len(rom)] = rom
elif len(memory) == 65536:
memory[:0x4000] = read_bin_file(ROM48)
else:
rom = read_bin_file(ROM48)
memory[:len(rom)] = rom
rom = (out7ffd & 16) // 16
memory[:0x4000] = read_bin_file(ROM128[rom])
memory[0x24000:] = read_bin_file(ROM128[1 - rom])
page = out7ffd & 7
if page:
n = BANKS_128K[page]
memory[n:n + 0x4000], memory[0xC000:0x10000] = memory[0xC000:0x10000], memory[n:n + 0x4000]
for spec in options.pokes:
poke(memory, spec)
if reg:
im = reg.im
iff = reg.iff2
border = reg.border
tstates = reg.tstates
else:
im = 1
iff = 1
border = 7
tstates = 0
state = {'im': im, 'iff': iff, 'tstates': tstates}
fast = options.verbose == 0 and not options.interrupts
config = {'fast_djnz': fast, 'fast_ldir': fast}
simulator = Simulator(memory, get_registers(reg, options.reg), state, config)
tracer = Tracer(border)
tracer = Tracer(simulator, border, out7ffd)
simulator.set_tracer(tracer)
begin = time.time()
tracer.run(simulator, start, options.stop, options.verbose,
options.max_operations, options.max_tstates, options.decimal,
options.interrupts)
tracer.run(start, options.stop, options.verbose, options.max_operations,
options.max_tstates, options.decimal, options.interrupts)
rt = time.time() - begin
if options.stats:
z80t = simulator.registers[T] - tstates
z80t = simulator.registers[T] - state['tstates']
z80s = z80t / 3500000
speed = z80s / (rt or 0.001) # Avoid division by zero
print(f'Z80 execution time: {z80t} T-states ({z80s:.03f}s)')
Expand All @@ -257,7 +270,7 @@ def run(snafile, options):
print('Sound duration: {} T-states ({:.03f}s)'.format(duration, duration / 3500000))
print('Delays: {}'.format(simplify(delays, options.depth)))
if options.dump:
ram = simulator.memory[16384:]
ram = simulator.memory[0x4000:0x24000]
r = simulator.registers
registers = (
f'a={r[A]}',
Expand All @@ -279,6 +292,7 @@ def run(snafile, options):
)
state = (
f'border={tracer.border}',
f'7ffd={tracer.out7ffd}',
f'iff={simulator.iff}',
f'im={simulator.imode}',
f'tstates={r[T]}'
Expand All @@ -290,7 +304,7 @@ def main(args):
parser = argparse.ArgumentParser(
usage='trace.py [options] FILE [file.z80]',
description="Trace Z80 machine code execution. "
"FILE may be a binary (raw memory) file, a SNA, SZX or Z80 snapshot, or '.' for no snapshot. "
"FILE may be a binary (raw memory) file, a SNA, SZX or Z80 snapshot, or '48' or '128' for no snapshot. "
"If 'file.z80' is given, a Z80 snapshot is written after execution has completed.",
add_help=False
)
Expand Down Expand Up @@ -319,8 +333,7 @@ def main(args):
help="Set the value of a register. Do '--reg help' for more information. "
"This option may be used multiple times.")
group.add_argument('--rom', metavar='FILE',
help='Patch in a ROM at address 0 from this file. '
'By default the 48K ZX Spectrum ROM is used.')
help='Patch in a ROM at address 0 from this file.')
group.add_argument('-s', '--start', metavar='ADDR', type=integer,
help='Start execution at this address.')
group.add_argument('-S', '--stop', metavar='ADDR', type=integer,
Expand Down
1 change: 1 addition & 0 deletions sphinx/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Changelog
* Added the ``load`` simulated LOAD configuration parameter to
:ref:`tap2sna.py <tap2sna-sim-load>` (to specify an alternative command line
to use to load the tape)
* Added support to :ref:`trace.py` for executing machine code in 128K snapshots
* Added support to :ref:`control directive loops <ctlLoops>` for avoiding
repetition of an ``N`` directive at the start of a loop

Expand Down
13 changes: 7 additions & 6 deletions sphinx/source/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1699,8 +1699,8 @@ To list the options supported by `tapinfo.py`, run it with no arguments::

trace.py
--------
`trace.py` simulates the execution of machine code in a 48K memory snapshot.
For example::
`trace.py` simulates the execution of machine code in a 48K or 128K memory
snapshot. For example::

$ trace.py --start 32768 --stop 49152 game.z80

Expand All @@ -1709,8 +1709,8 @@ To list the options supported by `trace.py`, run it with no arguments::
usage: trace.py [options] FILE [file.z80]

Trace Z80 machine code execution. FILE may be a binary (raw memory) file, a
SNA, SZX or Z80 snapshot, or '.' for no snapshot. If 'file.z80' is given, a
Z80 snapshot is written after execution has completed.
SNA, SZX or Z80 snapshot, or '48' or '128' for no snapshot. If 'file.z80' is
given, a Z80 snapshot is written after execution has completed.

Options:
--audio Show audio delays.
Expand All @@ -1729,8 +1729,7 @@ To list the options supported by `trace.py`, run it with no arguments::
-r name=value, --reg name=value
Set the value of a register. Do '--reg help' for more
information. This option may be used multiple times.
--rom FILE Patch in a ROM at address 0 from this file. By default
the 48K ZX Spectrum ROM is used.
--rom FILE Patch in a ROM at address 0 from this file.
-s ADDR, --start ADDR
Start execution at this address.
-S ADDR, --stop ADDR Stop execution at this address.
Expand All @@ -1756,6 +1755,8 @@ running on a real ZX Spectrum.
+---------+-------------------------------------------------------------------+
| Version | Changes |
+=========+===================================================================+
| 9.0 | Added support for 128K snapshots |
+---------+-------------------------------------------------------------------+
| 8.9 | Added the ``--interrupts`` option; reads and writes the T-states |
| | counter in Z80 snapshots and reads the T-states counter in SZX |
| | snapshots |
Expand Down
2 changes: 1 addition & 1 deletion sphinx/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@
('man/tapinfo.py', 'tapinfo.py',
'show the blocks in a TAP or TZX file', _authors, 1),
('man/trace.py', 'trace.py',
'simulate code execution in a 48K memory snapshot', _authors, 1)
'simulate code execution in a 48K or 128K memory snapshot', _authors, 1)
]

# If true, show URL addresses after external links.
Expand Down
11 changes: 5 additions & 6 deletions sphinx/source/man/trace.py.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ SYNOPSIS

DESCRIPTION
===========
``trace.py`` simulates the execution of machine code in a 48K binary (raw
memory) file or a SNA, SZX or Z80 snapshot. If FILE is '.', no snapshot is
loaded, and the RAM is left blank (all zeroes). If 'file.z80' is given, a Z80
snapshot is written after execution has completed.
``trace.py`` simulates the execution of machine code in a 48K or 128K binary
(raw memory) file or a SNA, SZX or Z80 snapshot. If FILE is '48' or '128', no
snapshot is loaded, and the RAM is left blank (all zeroes). If 'file.z80' is
given, a Z80 snapshot is written after execution has completed.

OPTIONS
=======
Expand Down Expand Up @@ -58,8 +58,7 @@ OPTIONS
be used multiple times.

--rom `FILE`
Patch in a ROM at address 0 from this file. By default the 48K ZX Spectrum
ROM is used.
Patch in a ROM at address 0 from this file.

-s, --start `ADDR`
Start execution at this address. `ADDR` must be a decimal number, or a
Expand Down
4 changes: 4 additions & 0 deletions sphinx/source/migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ Z80 snapshot file. In SkoolKit 9, this option has been removed; instead the
output filename may be specified after the input filename. For example::

$ trace.py --stop 32768 in.z80 out.z80

In SkoolKit 8, if the input filename was '.', a blank 48K snapshot was
substituted. In SkoolKit 9, this no longer works; instead, use '48' for a
blank 48K snapshot, or '128' for a blank 128K snapshot.
6 changes: 4 additions & 2 deletions tests/skoolkittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,10 @@ def _set_z80_registers(self, header, version, registers):
header[32] = pc % 256
header[33] = pc // 256
if version == 3:
t = 69887 - (registers.get('tstates', 0) % 69888)
t1, t2 = t % 17472, t // 17472
frame_duration = 69888 if header[34] < 4 else 70908
qframe_duration = frame_duration // 4
t = frame_duration - 1 - (registers.get('tstates', 0) % frame_duration)
t1, t2 = t % qframe_duration, t // qframe_duration
header[55:58] = (t1 % 256, t1 // 256, (2 - t2) % 4)
sp = registers.get('SP', 0)
header[8] = sp % 256
Expand Down
Loading

0 comments on commit ce20db4

Please sign in to comment.