diff --git a/c/csimulator.c b/c/csimulator.c index 7199702f..97dcd624 100644 --- a/c/csimulator.c +++ b/c/csimulator.c @@ -5356,18 +5356,19 @@ static PyObject* CSimulator_accept_interrupt(CSimulatorObject* self, PyObject* a } static PyObject* CSimulator_trace(CSimulatorObject* self, PyObject* args, PyObject* kwds) { - static char* kwlist[] = {"", "", "", "", "", "", "", "", "", NULL}; + static char* kwlist[] = {"", "", "", "", "", "", "", "", "", "", NULL}; PyObject* start_obj; PyObject* stop_obj; unsigned long long max_operations; unsigned long long max_time; int interrupts; PyObject* draw; + PyObject* exec_map; PyObject* keyboard; PyObject* disassemble; PyObject* trace; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOKKiOOOO", kwlist, &start_obj, &stop_obj, &max_operations, &max_time, &interrupts, &draw, &keyboard, &disassemble, &trace)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOKKiOOOOO", kwlist, &start_obj, &stop_obj, &max_operations, &max_time, &interrupts, &draw, &exec_map, &keyboard, &disassemble, &trace)) { return NULL; } @@ -5431,6 +5432,15 @@ static PyObject* CSimulator_trace(CSimulatorObject* self, PyObject* args, PyObje return NULL; } + if (exec_map != Py_None) { + PyObject* addr = PyLong_FromLong(pc); + int rv = PySet_Add(exec_map, addr); + Py_XDECREF(addr); + if (rv == -1) { + return NULL; + } + } + if (trace == Py_None) { CHECK_SIGNALS; } else { diff --git a/skoolkit/trace.py b/skoolkit/trace.py index 2c92d74d..4cf48f44 100644 --- a/skoolkit/trace.py +++ b/skoolkit/trace.py @@ -46,7 +46,7 @@ def __init__(self, simulator, border, out7ffd, outfffd, ay, outfe): self.out_times = [] self.keyboard = None - def run(self, start, stop, max_operations, max_tstates, interrupts, draw, trace_line, prefix, byte_fmt, word_fmt): + def run(self, start, stop, max_operations, max_tstates, interrupts, draw, exec_map, trace_line, prefix, byte_fmt, word_fmt): simulator = self.simulator memory = simulator.memory registers = simulator.registers @@ -67,7 +67,7 @@ def run(self, start, stop, max_operations, max_tstates, interrupts, draw, trace_ tf = lambda pc, i, t0: print(trace_line.format(pc=pc, i=i, r=r, t=t0, m=memory)) else: df = tf = None - stop_cond, operations = simulator.trace(start, stop, max_operations, max_time, interrupts, draw, keyboard, df, tf) + stop_cond, operations = simulator.trace(start, stop, max_operations, max_time, interrupts, draw, exec_map, keyboard, df, tf) else: opcodes = simulator.opcodes frame_duration = simulator.frame_duration @@ -86,6 +86,9 @@ def run(self, start, stop, max_operations, max_tstates, interrupts, draw, trace_ opcodes[memory[pc]]() tstates = registers[25] + if exec_map is not None: + exec_map.add(pc) + if interrupts and registers[26] and tstates % frame_duration < int_active: simulator.accept_interrupt(registers, memory, pc) tstates = registers[25] @@ -285,8 +288,12 @@ def run(snafile, options, config): draw = screen.draw else: draw = None + if options.map: + exec_map = set() + else: + exec_map = None tracer.run(start, options.stop, options.max_operations, options.max_tstates, - options.interrupts, draw, trace_line, prefix, byte_fmt, word_fmt) + options.interrupts, draw, exec_map, trace_line, prefix, byte_fmt, word_fmt) rt = time.time() - begin if len(simulator.memory) == 65536: cpu_freq = 3500000 @@ -306,6 +313,11 @@ def run(snafile, options, config): print(f'Sound duration: {z80t} T-states ({z80s:.3f}s)') lines = textwrap.wrap(simplify(delays, options.depth), 78) print('Delays:\n {}'.format('\n '.join(lines))) + if options.map: + with open(options.map, 'w') as f: + for addr in sorted(exec_map): + f.write(f'${addr:04X}\n') + print(f'Wrote {options.map}') for fname in options.dump: ext = fname.lower()[-4:] if ext == '.wav': @@ -347,6 +359,8 @@ def main(args): help='Simplify audio delays to this depth (default: 2).') group.add_argument('-I', '--ini', dest='params', metavar='p=v', action='append', default=[], help="Set the value of the configuration parameter 'p' to 'v'. This option may be used multiple times.") + group.add_argument('--map', metavar='FILE', + help="Log addresses of executed instructions to a file.") group.add_argument('-m', '--max-operations', metavar='MAX', type=int, default=0, help='Maximum number of instructions to execute.') group.add_argument('-M', '--max-tstates', metavar='MAX', type=int, default=0, diff --git a/sphinx/source/changelog.rst b/sphinx/source/changelog.rst index 23951d77..2eac2961 100644 --- a/sphinx/source/changelog.rst +++ b/sphinx/source/changelog.rst @@ -12,6 +12,8 @@ Changelog enable instruction comment generation) * :ref:`trace.py` now responds to keypresses while running with screen contents displayed +* Added the ``--map`` option to :ref:`trace.py` (for writing a code execution + map file) * Added the ``UserAgent`` configuration parameter for :ref:`tap2sna.py ` (to specify the User-Agent header in HTTP/HTTPS requests) diff --git a/sphinx/source/commands.rst b/sphinx/source/commands.rst index 7fbc5433..c10c5477 100644 --- a/sphinx/source/commands.rst +++ b/sphinx/source/commands.rst @@ -946,7 +946,7 @@ to be a binary file. The ``-m`` option may be used to specify a code execution map to use when generating a control file. The supported file formats are: -* Files created by the ``--map`` option of :ref:`rzxplay.py` +* Files created by the ``--map`` option of :ref:`rzxplay.py` or :ref:`trace.py` * Profiles created by the Fuse emulator * Code execution logs created by the SpecEmu, Spud and Zero emulators * Map files created by the SpecEmu and Z80 emulators @@ -2031,6 +2031,7 @@ To list the options supported by `trace.py`, run it with no arguments:: -D, --decimal Show decimal values in verbose mode. -I p=v, --ini p=v Set the value of the configuration parameter 'p' to 'v'. This option may be used multiple times. + --map FILE Log addresses of executed instructions to a file. -m MAX, --max-operations MAX Maximum number of instructions to execute. -M MAX, --max-tstates MAX @@ -2150,7 +2151,8 @@ Configuration parameters may also be set on the command line by using the +---------+-------------------------------------------------------------------+ | Version | Changes | +=========+===================================================================+ -| 9.5 | Responds to keypresses while the screen is displayed | +| 9.5 | Responds to keypresses while the screen is displayed; added the | +| | ``--map`` option | +---------+-------------------------------------------------------------------+ | 9.4 | Added the ``--screen`` option; added support for writing a PNG | | | file after execution has completed; added the ``PNGScale``, | diff --git a/sphinx/source/man/sna2ctl.py.rst b/sphinx/source/man/sna2ctl.py.rst index 447b4c1b..f1713e92 100644 --- a/sphinx/source/man/sna2ctl.py.rst +++ b/sphinx/source/man/sna2ctl.py.rst @@ -36,8 +36,8 @@ OPTIONS -m, --map `FILE` Specify a code execution map to use. Code execution maps produced by - ``rzxplay.py`` and by the Fuse, SpecEmu, Spud, Zero and Z80 Spectrum - emulators are supported. + ``rzxplay.py`` and ``trace.py``, and by the Fuse, SpecEmu, Spud, Zero and Z80 + Spectrum emulators are supported. -o, --org `ADDR` Specify the origin address of a binary file. The default origin address is diff --git a/sphinx/source/man/trace.py.rst b/sphinx/source/man/trace.py.rst index df857fb9..020c89ec 100644 --- a/sphinx/source/man/trace.py.rst +++ b/sphinx/source/man/trace.py.rst @@ -39,6 +39,9 @@ OPTIONS overriding any value found in ``skoolkit.ini``. This option may be used multiple times. +--map FILE + Log addresses of executed instructions to a file. + -m, --max-operations `MAX` Maximum number of instructions to execute. Overrides the `STOP` address (if given). diff --git a/tests/test_trace.py b/tests/test_trace.py index e92aa38a..b6b09baa 100644 --- a/tests/test_trace.py +++ b/tests/test_trace.py @@ -129,6 +129,7 @@ def test_default_option_values(self): self.assertEqual(options.depth, 2) self.assertEqual([], options.dump) self.assertTrue(options.interrupts) + self.assertIsNone(options.map) self.assertEqual(options.max_operations, 0) self.assertEqual(options.max_tstates, 0) self.assertIsNone(options.org) @@ -1323,6 +1324,45 @@ def test_option_I_overrides_config_read_from_file(self): self.assertEqual(['TraceLine=Goodbye'], options.params) self.assertEqual(config['TraceLine'], 'Goodbye') + def test_option_map(self): + data = ( + 0xAF, # $8000 XOR A + 0x3C, # $8001 INC A + 0x18, 0x01, # $8002 JR $8005 + 0x00, # $8004 NOP + 0x3D, # $8005 DEC A + 0xCD, 0x0C, 0x80, # $8006 CALL $800C + 0xC3, 0x0E, 0x80, # $8009 JP $800E + 0xC9, # $800C RET + 0x00, # $800D NOP + 0x3C, # $800E INC A + ) + mapfile = 'exec.map' + binfile = self.write_bin_file(data, suffix='.bin') + start = 32768 + stop = start + len(data) + output, error = self.run_trace(f'-n -o {start} -S {stop} --map {mapfile} {binfile}') + self.assertEqual(error, '') + exp_output = f""" + Stopped at ${stop:04X} + Wrote {mapfile} + """ + self.assertEqual(dedent(exp_output).strip(), output.rstrip()) + self.assertTrue(os.path.isfile(mapfile)) + exp_map = """ + $8000 + $8001 + $8002 + $8005 + $8006 + $8009 + $800C + $800E + """ + with open(mapfile) as f: + map_contents = f.read() + self.assertEqual(dedent(exp_map).lstrip(), map_contents) + def test_option_max_operations(self): data = [ 0xAF, # XOR A