diff --git a/src/outfmt/__init__.py b/src/outfmt/__init__.py index 03501b5ab..f35c15ddd 100644 --- a/src/outfmt/__init__.py +++ b/src/outfmt/__init__.py @@ -5,11 +5,13 @@ from .sna import SnaEmitter from .tap import TAP from .tzx import TZX +from .z80 import Z80Emitter __all__ = ( "BinaryEmitter", "CodeEmitter", - "TZX", - "TAP", "SnaEmitter", + "TAP", + "TZX", + "Z80Emitter", ) diff --git a/src/outfmt/gensnapshot.py b/src/outfmt/gensnapshot.py new file mode 100644 index 000000000..918e4a238 --- /dev/null +++ b/src/outfmt/gensnapshot.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 + +# 48K Snapshot generation module +# +# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors. +# See the file AUTHORS for copyright details. +# +# This file is part of Boriel BASIC Compiler. +# +# Boriel BASIC Compiler is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# Boriel BASIC Compiler is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +# for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Boriel BASIC Compiler. If not, see . + + +class GenSnapshot: + """Generate 48K snapshots with the given BASIC and MC code + + If no BASIC is given, it will insert its own BASIC program, consisting of + a single line that reads: + + 10 IF USR THEN + """ + + # Utility functions + @staticmethod + def word(x: int): + """Convert an integer to an iterable of 2 little-endian bytes""" + return bytes((x % 256, x >> 8)) + + def patchAddr(self, addr: int, data: bytes): + """Patch the snapshot's memory image at the given address""" + self.mem[addr - 16384 : addr - 16384 + len(data)] = data + assert len(self.mem) == 49152 + + def __init__( + self, + loader_bytes, + clear_addr, + mc_addr, + mc_bytes, + ): + """ + Creates a snapshot object ready to run a BASIC program as if RUN was just executed. + + Input: + + loader_bytes: ZX Spectrum BASIC code to inject as the BASIC program. + If None, a program consisting of this single line will be generated: + 10 IF USR THEN + clear_addr: Address of CLEAR + mc_addr: Address where the bytes need to be stored + mc_bytes: Bytes to store starting at mc_addr + + Output: An object with the following fields: + + mem: A bytearray object with the memory dump + A: the Z80 A register + B, C, D, E, H, L, F, A2, B2, C2, D2, E2, H2, L2, F2, I, R: same + W: high byte of the WZ internal register + Z: low byte of the WZ internal register + SPH: High byte of SP + SPL: Low byte of SP + PCH: High byte of PC + PCL: Low byte of PC + IXH: High byte of IX + IXL: Low byte of IX + IYH: High byte of IY + IYL: Low byte of IY + IFF1: IFF1 flag of Z80 (0 to 1, as int) + IFF2: IFF2 flag of Z80 (0 to 1, as int) + outFE: Last byte output to port 0FEh + IM: Interrupt mode (0 to 2, as int) + cycles: Cycles after the last interrupt + halted: Whether we're in a halted state + eilast: Whether the last instruction prevents an interrupt + """ + + self.A = self.A2 = self.B = self.B2 = self.C = self.C2 = self.D = self.D2 = self.E = self.E2 = self.H = ( + self.H2 + ) = self.L = self.L2 = self.F = self.F2 = self.R = self.IXL = self.IXH = 0 + + self.IYH = 0x5C + self.IYL = 0x3A # 0x5C3A is the normal value of IY for ROM use + self.I = 0x3F + self.W = 0 + self.Z = 0 + self.IFF1 = 1 + self.IFF2 = 1 + self.PCH = 0x1B + self.PCL = 0x9E # Entry point: 1B9E, LINE_NEW + SP = clear_addr - 3 + self.SPH = (SP >> 8) & 0xFF + self.SPL = SP & 0xFF + self.IM = 1 + self.outFE = 0x0F # Border 7, input enabled, speaker disabled + self.cycles = 35000 # Half a screen, roughly + self.halted = False + self.eilast = False + + # Build a valid memory image from scratch + # Start with an array of 49152 zeros, then patch the different areas + self.mem = bytearray(b"\0" * 49152) + + # Screen Attributes + self.patchAddr(0x5800, b"\x38" * 768) # all Paper 7 / Ink 0 + + # System Variables + # The author knows very little about KSTATE, so just in case, the + # eight state bytes have been copied from an actual snapshot. + self.patchAddr( + 0x5C00, + b"\xff\0\0\0" + b"\x0d\2\x20\x0d" # KSTATE + b"\x0d" # LAST_K + b"\x23" # REPDEL + b"\x05" # REPPER + b"\0\0\0\0\0" # DEFADD, K_DATA, TVDATA + b"\1\0\6\0\x0b\0\1\0\1\0\6\0\x10\0" + b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + b"\0\0\0\0\0\0\0\0" # STRMS (38 bytes total) + b"\x00\x3c" # CHARS + b"\x40\x00" # RASP, PIP + b"\xff" # ERR_NR + b"\xcc" # FLAGS + b"\x00" # TV_FLAG + b"\0\0" # ERR_SP (to be patched with clear addr - 3) + b"\0\0" # LIST_SP (overwritten by ROM) + b"\0" # MODE + b"\0\0\0" # NEWPPC, NSPPC (at start of BASIC) + b"\xfe\xff\1" # PPC, SUBPPC (at line -2, edit line) + b"\x38" # BORDCR + b"\0\0" # E_PPC + b"\0\0" # VARS (patched later, depends on prog length) + b"\0\0" # DEST + b"\xb6\x5c" # CHANS + b"\xb6\x5c" # CURCHL + b"\xcb\x5c" # PROG + b"\0\0" # NXTLIN (overwritten by ROM) + b"\xca\x5c" # DATADD + b"\0\0" # E_LINE (patched later) + b"\0\0" # K_CUR (overwritten by ROM) + b"\0\0" # CH_ADD (overwritten by ROM) + b"\0\0" # X_PTR + b"\0\0" # WORKSP (patched later) + b"\0\0" # STKBOT (patched later) + b"\0\0" # STKEND (patched later) + b"\0" # BREG + b"\x92\x5c" # MEM + b"\x10" # FLAGS2 + b"\2" # DF_SZ + b"\0\0\0\0\0" # S_TOP, OLDPPC, OSPPC + b"\0\0\0" # FLAGX, STRLEN + b"\0\0" # T_ADDR (overwritten by ROM) + b"\0\0" # SEED + b"\0\0\0" # FRAMES + b"\x58\xff" # UDG + b"\0\0" # COORDS + b"\x21" # P_POSN + b"\0\x5b" # PR_CC + b"\x21\x17" # ECHO_E + b"\0\x40" # DF_CC + b"\xe0\x50" # DFCCL + b"\x21\x18" # S_POSN + b"\x21\x17" # SPOSNL + b"\1" # SCR_CT + b"\x38\x00\x38\x00" # ATTR_P, MASK_P, ATTR_T, MASK_T + b"\0" # P_FLAG + b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + b"\0\0\0\0\0\0\0\0" # MEMBOT (30 bytes) + b"\0\0" # NMIADD + b"\0\0" # RAMTOP (patched later) + b"\xff\xff", # P_RAMT + ) + + ChansData = ( + # CHANS data (routine pointers, channel name) + b"\xf4\x09\xa8\x10" + b"K" # PRINT_OUT, KEY_INPUT, "K" + b"\xf4\x09\xc4\x15" + b"S" # PRINT_OUT, REPORT_J, "S" + b"\x81\x0f\xc4\x15" + b"R" # ADD_CHAR, REPORT_J, "R" + b"\xf4\x09\xc4\x15" + b"P" # PRINT_OUT, REPORT_J, "P" + b"\x80" # Terminator + ) + + # CHANS data starts at 23734 and could vary in length depending on + # the presence of an Interface 1. + self.patchAddr(23734, ChansData) + + # BASIC start (usually 23755 in absence of Interface 1) + self.BasicStart = 23734 + len(ChansData) + + if loader_bytes is None: + # Create a single line with these contents: 10 IF USR THEN + loader_bytes = bytearray( + b"\0\x0a" # BASIC big endian line num + b"\0\0" # BASIC little endian line length (patched below) + b"\xfa\xc0" # BASIC IF USR + ) + loader_bytes.extend(b"%05d\x0e\0\0\0\0\0" % mc_addr) + loader_bytes[-3:-1] = self.word(mc_addr) + loader_bytes.extend(b"\xcb\x0d") # THEN + final newline + loader_bytes[2:4] = self.word(len(loader_bytes) - 4) # line length + + BasicLength = len(loader_bytes) + + # Address to array index conversion offset; 0x1B is the header size + BasicEnd = self.BasicStart + BasicLength + + # Clear everything from the channel variables to the UDG start + self.patchAddr(self.BasicStart, b"\x00" * (65368 - self.BasicStart)) + + # Patch ERR_SP + self.patchAddr(23613, self.word(clear_addr - 3)) + # Patch VARS + self.patchAddr(23627, self.word(BasicEnd)) + # Patch E_LINE + self.patchAddr(23641, self.word(BasicEnd + 1)) + # Patch WORKSP + self.patchAddr(23649, self.word(BasicEnd + 4)) + # Patch STKBOT + self.patchAddr(23651, self.word(BasicEnd + 4)) + # Patch STKEND + self.patchAddr(23653, self.word(BasicEnd + 4)) + # Patch RAMTOP + self.patchAddr(23730, self.word(clear_addr)) + + # Patch BASIC program + self.patchAddr(self.BasicStart, loader_bytes) + + # Patch variables area, edit line and calculator stack (edit line + # contains a RUN command, 0xF7; calculator stack is empty) + self.patchAddr(BasicEnd, b"\x80\xf7\x0d\x80") + + # Patch stack + self.patchAddr( + clear_addr - 3, + b"\x03\x13" # Error resume routine (ERR_SP points here): MAIN_4 + b"\x00\x3e", # GOSUB stack end marker + ) + + # UDG area (might be overwritten by compiled code) + self.patchAddr( + 65368, + b"\x00\x3c\x42\x42\x7e\x42\x42\x00\x00\x7c\x42\x7c\x42\x42\x7c\x00" + b"\x00\x3c\x42\x40\x40\x42\x3c\x00\x00\x78\x44\x42\x42\x44\x78\x00" + b"\x00\x7e\x40\x7c\x40\x40\x7e\x00\x00\x7e\x40\x7c\x40\x40\x40\x00" + b"\x00\x3c\x42\x40\x4e\x42\x3c\x00\x00\x42\x42\x7e\x42\x42\x42\x00" + b"\x00\x3e\x08\x08\x08\x08\x3e\x00\x00\x02\x02\x02\x42\x42\x3c\x00" + b"\x00\x44\x48\x70\x48\x44\x42\x00\x00\x40\x40\x40\x40\x40\x7e\x00" + b"\x00\x42\x66\x5a\x42\x42\x42\x00\x00\x42\x62\x52\x4a\x46\x42\x00" + b"\x00\x3c\x42\x42\x42\x42\x3c\x00\x00\x7c\x42\x42\x7c\x40\x40\x00" + b"\x00\x3c\x42\x42\x52\x4a\x3c\x00\x00\x7c\x42\x42\x7c\x44\x42\x00" + b"\x00\x3c\x40\x3c\x02\x42\x3c\x00\x00\xfe\x10\x10\x10\x10\x10\x00" + b"\x00\x42\x42\x42\x42\x42\x3c\x00", + ) + + # Finally, patch compiled code in + assert mc_addr + len(mc_bytes) <= 65536 + self.patchAddr(mc_addr, mc_bytes) diff --git a/src/outfmt/sna.py b/src/outfmt/sna.py index 218c8037c..58fd55e2c 100644 --- a/src/outfmt/sna.py +++ b/src/outfmt/sna.py @@ -8,45 +8,35 @@ # This file is part of Boriel BASIC Compiler. # # Boriel BASIC Compiler is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. # # Boriel BASIC Compiler is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +# for more details. # -# You should have received a copy of the GNU General Public License along with -# Boriel BASIC Compiler. If not, see . +# You should have received a copy of the GNU Affero General Public License +# along with Boriel BASIC Compiler. If not, see . from .codeemitter import CodeEmitter +from .gensnapshot import GenSnapshot class SnaEmitter(CodeEmitter): - """Class to emit 48K .SNA snapshots""" - - # Utility functions - @staticmethod - def word(x: int): - """Convert an integer to an iterable of 2 little-endian bytes""" - return bytes((x % 256, x >> 8)) - - def patchAddr(self, addr: int, data: bytes): - """Patch the snapshot at the given address""" - self.patchIdx(addr + (0x1B - 16384), data) - - def patchIdx(self, idx: int, data: bytes): - """Patch the snapshot at the given array index""" - self.output[idx : idx + len(data)] = data - assert len(self.output) == 49179 - - def __init__(self): - """Initializes the base .SNA bytes""" + """Class to write 48K .SNA snapshots""" + def generate( + self, + loader_bytes, + clear_addr, + entry_point, + program_bytes, + ): """ - Format of SNA file: + Format of .SNA file: $00 I $01 HL' @@ -65,116 +55,54 @@ def __init__(self): $19 Interrupt mode: 0, 1 or 2 $1A Border colour + A raw memory dump (49152 bytes) follows. + PC is on the stack. A `retn` instruction should be executed after load. """ - # Start with an array of 49179 zeros, then patch the different areas - self.output = bytearray(b"\0" * 49179) - - # Registers in header - self.patchIdx( - 0, - b"\x3f" # I - b"\0\0\0\0\0\0\0\0" # HL', DE', BC', AF' - b"\0\0\0\0\0\0" # HL, DE, BC - b"\x3a\x5c" # IY - b"\0\0" # IX - b"\x04" # Interrupts enabled - b"\0" # R - b"\0\0" # AF - b"\0\0" # SP (to be patched with clear addr - 5) - b"\1\7", # IM1, Border 7 + snapshot = GenSnapshot(loader_bytes, clear_addr, entry_point, program_bytes) + + # SNA stores the start address in the stack, so SP should be adjusted + SP = ((snapshot.SPH << 8) | snapshot.SPL) - 2 + + sna_data = bytearray( + ( + snapshot.I, + snapshot.L2, + snapshot.H2, + snapshot.E2, + snapshot.D2, + snapshot.C2, + snapshot.B2, + snapshot.F2, + snapshot.A2, + snapshot.L, + snapshot.H, + snapshot.E, + snapshot.D, + snapshot.C, + snapshot.B, + snapshot.IYL, + snapshot.IYH, + snapshot.IXL, + snapshot.IXH, + 4 if snapshot.IFF1 else 0, + snapshot.R, + snapshot.F, + snapshot.A, + SP & 0xFF, + SP >> 8, + snapshot.IM, + snapshot.outFE & 7, + ) ) + snaHeaderLen = len(sna_data) - # Screen Attributes - self.patchAddr(0x5800, b"\x38" * 768) - - # System Variables - # The author knows very little about KSTATE, so just in case, the - # eight state bytes have been copied from an actual snapshot. - self.patchAddr( - 0x5C00, - b"\xff\0\0\0\x0d\2\x20\x0d" # KSTATE - b"\x0d" # LAST_K - b"\x23" # REPDEL - b"\x05" # REPPER - b"\0\0\0\0\0" # DEFADD, K_DATA, TVDATA - b"\1\0\6\0\x0b\0\1\0\1\0\6\0\x10\0" - b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" - b"\0\0\0\0\0\0\0\0" # STRMS (38 bytes total) - b"\x00\x3c" # CHARS - b"\x40\x00" # RASP, PIP - b"\xff" # ERR_NR - b"\xcc" # FLAGS - b"\x00" # TV_FLAG - b"\0\0" # ERR_SP (to be patched with clear addr - 3) - b"\0\0" # LIST_SP (overwritten by ROM) - b"\0" # MODE - b"\0\0\0" # NEWPPC, NSPPC (at start of BASIC) - b"\xfe\xff\1" # PPC, SUBPPC (at line -2, edit line) - b"\x38" # BORDCR - b"\0\0" # E_PPC - b"\0\0" # VARS (patched later, depends on prog length) - b"\0\0" # DEST - b"\xb6\x5c" # CHANS - b"\xb6\x5c" # CURCHL - b"\xcb\x5c" # PROG - b"\0\0" # NXTLIN (overwritten by ROM) - b"\xca\x5c" # DATADD - b"\0\0" # E_LINE (patched later) - b"\0\0" # K_CUR (overwritten by ROM) - b"\0\0" # CH_ADD (overwritten by ROM) - b"\0\0" # X_PTR - b"\0\0" # WORKSP (patched later) - b"\0\0" # STKBOT (patched later) - b"\0\0" # STKEND (patched later) - b"\0" # BREG - b"\x92\x5c" # MEM - b"\x10" # FLAGS2 - b"\2" # DF_SZ - b"\0\0\0\0\0" # S_TOP, OLDPPC, OSPPC - b"\0\0\0" # FLAGX, STRLEN - b"\0\0" # T_ADDR (overwritten by ROM) - b"\0\0" # SEED - b"\0\0\0" # FRAMES - b"\x58\xff" # UDG - b"\0\0" # COORDS - b"\x21" # P_POSN - b"\0\x5b" # PR_CC - b"\x21\x17" # ECHO_E - b"\0\x40" # DF_CC - b"\xe0\x50" # DFCCL - b"\x21\x18" # S_POSN - b"\x21\x17" # SPOSNL - b"\1" # SCR_CT - b"\x38\x00\x38\x00" # ATTR_P, MASK_P, ATTR_T, MASK_T - b"\0" # P_FLAG - b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" - b"\0\0\0\0\0\0\0\0" # MEMBOT (30 bytes) - b"\0\0" # NMIADD - b"\0\0" # RAMTOP (patched later) - b"\xff\xff", # P_RAMT - ) - - ChansData = ( - # CHANS data (routine pointers, channel name) - b"\xf4\x09\xa8\x10" - b"K" # PRINT_OUT, KEY_INPUT, "K" - b"\xf4\x09\xc4\x15" - b"S" # PRINT_OUT, REPORT_J, "S" - b"\x81\x0f\xc4\x15" - b"R" # ADD_CHAR, REPORT_J, "R" - b"\xf4\x09\xc4\x15" - b"P" # PRINT_OUT, REPORT_J, "P" - b"\x80" # Terminator - ) + sna_data.extend(snapshot.mem) + sna_data[SP - 16384 + 0 + snaHeaderLen] = snapshot.PCL + sna_data[SP - 16384 + 1 + snaHeaderLen] = snapshot.PCH - # CHANS data starts at 23734 and could vary in length depending on - # the presence of an Interface 1. - self.patchAddr(23734, ChansData) - - # BASIC start (usually 23755 in absence of Interface 1) - self.BasicStart = 23734 + len(ChansData) + return sna_data def emit( self, @@ -188,79 +116,8 @@ def emit( ): """Emit a .SNA file with the compiled bytes; ignores loader_bytes""" - # Clear address could be different from entry_point-1 in future. - clear_addr = entry_point - 1 - - # Ignore loader_bytes and use our own BASIC program - loader_bytes = bytearray( - b"\0\x0a" # BASIC big endian line num - b"\x0f\0" # BASIC little endian line length - b"\xfa\xc0" # BASIC IF USR - ) - loader_bytes.extend(b"%05d\x0e\0\0\0\0\0" % entry_point) - loader_bytes[-3:-1] = self.word(entry_point) - loader_bytes.extend(b"\xcb\x0d") # THEN + final newline - BasicLength = len(loader_bytes) - - # Address to array index conversion offset; 0x1B is the header size - BasicEnd = self.BasicStart + BasicLength - - # Clear everything from the channel variables to the UDG start - self.patchAddr(self.BasicStart, b"\x00" * (65368 - self.BasicStart)) - - # Patch SP register in header - self.patchIdx(0x17, self.word(clear_addr - 5)) - - # Patch ERR_SP - self.patchAddr(23613, self.word(clear_addr - 3)) - # Patch VARS - self.patchAddr(23627, self.word(BasicEnd)) - # Patch E_LINE - self.patchAddr(23641, self.word(BasicEnd + 1)) - # Patch WORKSP - self.patchAddr(23649, self.word(BasicEnd + 4)) - # Patch STKBOT - self.patchAddr(23651, self.word(BasicEnd + 4)) - # Patch STKEND - self.patchAddr(23653, self.word(BasicEnd + 4)) - # Patch RAMTOP - self.patchAddr(23730, self.word(clear_addr)) - - # Patch BASIC program - self.patchAddr(self.BasicStart, loader_bytes) - - # Patch variables, edit line and calculator stack (edit line contains - # a RUN command, 0xF7; calculator stack is empty) - self.patchAddr(BasicEnd, b"\x80\xf7\x0d\x80") - - # Patch stack - self.patchAddr( - clear_addr - 5, - b"\x9e\x1b" # Entry address: LINE_NEW - b"\x03\x13" # Error resume routine (ERR_SP points here): MAIN_4 - b"\x00\x3e", # GOSUB stack end marker - ) - - # UDG area (might be overwritten by compiled code) - self.patchAddr( - 65368, - b"\x00\x3c\x42\x42\x7e\x42\x42\x00\x00\x7c\x42\x7c\x42\x42\x7c\x00" - b"\x00\x3c\x42\x40\x40\x42\x3c\x00\x00\x78\x44\x42\x42\x44\x78\x00" - b"\x00\x7e\x40\x7c\x40\x40\x7e\x00\x00\x7e\x40\x7c\x40\x40\x40\x00" - b"\x00\x3c\x42\x40\x4e\x42\x3c\x00\x00\x42\x42\x7e\x42\x42\x42\x00" - b"\x00\x3e\x08\x08\x08\x08\x3e\x00\x00\x02\x02\x02\x42\x42\x3c\x00" - b"\x00\x44\x48\x70\x48\x44\x42\x00\x00\x40\x40\x40\x40\x40\x7e\x00" - b"\x00\x42\x66\x5a\x42\x42\x42\x00\x00\x42\x62\x52\x4a\x46\x42\x00" - b"\x00\x3c\x42\x42\x42\x42\x3c\x00\x00\x7c\x42\x42\x7c\x40\x40\x00" - b"\x00\x3c\x42\x42\x52\x4a\x3c\x00\x00\x7c\x42\x42\x7c\x44\x42\x00" - b"\x00\x3c\x40\x3c\x02\x42\x3c\x00\x00\xfe\x10\x10\x10\x10\x10\x00" - b"\x00\x42\x42\x42\x42\x42\x3c\x00", - ) - - # Patch compiled code in - assert entry_point + len(program_bytes) <= 65536 - self.patchAddr(entry_point, program_bytes) + sna_data = self.generate(None, entry_point - 1, entry_point, program_bytes) # Write output file with open(output_filename, "wb") as f: - f.write(self.output) + f.write(sna_data) diff --git a/src/outfmt/z80.py b/src/outfmt/z80.py new file mode 100644 index 000000000..7298dc40a --- /dev/null +++ b/src/outfmt/z80.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 + +# 48K .Z80 format output module +# +# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors. +# See the file AUTHORS for copyright details. +# +# This file is part of Boriel BASIC Compiler. +# +# Boriel BASIC Compiler is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# Boriel BASIC Compiler is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +# for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Boriel BASIC Compiler. If not, see . + + +import io + +from .codeemitter import CodeEmitter +from .gensnapshot import GenSnapshot + + +class Z80Emitter(CodeEmitter): + """Class to emit 48K .Z80 snapshots""" + + def __init__(self): + """Initializes the .Z80 generation""" + + super().__init__() + + def generate( + self, + loader_bytes, + clear_addr, + entry_point, + program_bytes, + ): + """Generate the bytes of a .Z80 format snapshot""" + + """ + Format of .Z80 version 1 (the only one we generate here): + + Offset Length Description + --------------------------- + 0 1 A register + 1 1 F register + 2 2 BC register pair (LSB, i.e. C, first) + 4 2 HL register pair + 6 2 Program counter + 8 2 Stack pointer + 10 1 Interrupt register + 11 1 Refresh register (Bit 7 is not significant!) + 12 1 Bit 0 : Bit 7 of the R-register + Bit 1-3: Border colour + Bit 4 : 1=Basic SamRom switched in + Bit 5 : 1=Block of data is compressed + Bit 6-7: No meaning + 13 2 DE register pair + 15 2 BC' register pair + 17 2 DE' register pair + 19 2 HL' register pair + 21 1 A' register + 22 1 F' register + 23 2 IY register (Again LSB first) + 25 2 IX register + 27 1 Interrupt flipflop, 0=DI, otherwise EI + 28 1 IFF2 (not particularly important...) + 29 1 Bit 0-1: Interrupt mode (0, 1 or 2) + Bit 2 : 1=Issue 2 emulation + Bit 3 : 1=Double interrupt frequency + Bit 4-5: 1=High video synchronisation + 3=Low video synchronisation + 0,2=Normal + Bit 6-7: 0=Cursor/Protek/AGF joystick + 1=Kempston joystick + 2=Sinclair 2 Left joystick (or user + defined, for version 3 .z80 files) + 3=Sinclair 2 Right joystick + + After that, a run-length compressed stream follows, containing the + 48 KB of the memory. Each block of 5 to 255 bytes that are all equal + (or 2 to 255 if the repeated bytes are ED) is replaced with ED ED xx yy, + where xx is the repeat count and yy the byte to repeat. The byte + following a single ED is not considered eligible as a block. + + After the memory dump, an end marker comes: 00 ED ED 00 + + Examples: + 01 01 01 01 01 01 01 -> ED ED 07 01 + ED ED 22 22 -> ED ED 02 ED 22 22 + (the first two ED's must be run-length encoded, otherwise ambiguity + would arise). + ED 00 00 00 00 00 00 -> ED 00 ED ED 05 00 + (only the bytes starting after the second 00 are considered for + compression; the first ED 00 sequence is encoded verbatim). + + """ + + snapshot = GenSnapshot(loader_bytes, clear_addr, entry_point, program_bytes) + + z80 = io.BytesIO() + + z80.write( + bytes( + ( + snapshot.A, + snapshot.F, + snapshot.C, + snapshot.B, + snapshot.L, + snapshot.H, + snapshot.PCL, + snapshot.PCH, + snapshot.SPL, + snapshot.SPH, + snapshot.I, + snapshot.R & 0x7F, + ((snapshot.R & 0x80) >> 7) | ((snapshot.outFE & 7) << 1) | 0x20, + snapshot.E, + snapshot.D, + snapshot.C2, + snapshot.B2, + snapshot.E2, + snapshot.D2, + snapshot.L2, + snapshot.H2, + snapshot.A2, + snapshot.F2, + snapshot.IYL, + snapshot.IYH, + snapshot.IXL, + snapshot.IXH, + 1 if snapshot.IFF1 else 0, # IFF1 + 1 if snapshot.IFF2 else 0, # IFF2 + snapshot.IM & 3, + ) + ) + ) + + idx: int = 0 + runlength: int = 0 + b: int = -1 + + while True: + if idx == 49152: + break + + b = snapshot.mem[idx] + idx += 1 + if idx != 49152 and b == snapshot.mem[idx]: + # Repetition found + runlength = 1 + + # Find the end of this run + while idx != 49152 and runlength != 255: + if b != snapshot.mem[idx]: + break + idx += 1 + runlength += 1 + + if runlength < 5 and b != 0xED: + # Doesn't qualify for compression + z80.write(bytes((b,)) * runlength) + else: + # Must compress + z80.write(bytes((0xED, 0xED, runlength, b))) + + else: + z80.write(bytes((b,))) + # Store byte after ED and don't consider it for run length + if b == 0xED and idx != 49152: + z80.write(bytes((snapshot.mem[idx],))) + idx += 1 + + z80.write(b"\0\xed\xed\0") + return z80.getvalue() + + def emit( + self, + output_filename, + program_name, + loader_bytes, + entry_point, + program_bytes, + aux_bin_blocks, + aux_headless_bin_blocks, + ): + """Save a .Z80 file with the compiled bytes; ignores loader_bytes""" + + output = self.generate(None, entry_point - 1, entry_point, program_bytes) + + # Write output file + with open(output_filename, "wb") as f: + f.write(output) diff --git a/src/zxbasm/asmparse.py b/src/zxbasm/asmparse.py index 08115b628..3c4e9b92e 100755 --- a/src/zxbasm/asmparse.py +++ b/src/zxbasm/asmparse.py @@ -1016,7 +1016,7 @@ def assemble(input_): def generate_binary(outputfname, format_, progname="", binary_files=None, headless_binary_files=None, emitter=None): """Outputs the memory binary to the output filename using one of the given - formats: tap, tzx, sna or bin + formats: tap, tzx, sna, z80 or bin """ global AUTORUN_ADDR @@ -1066,6 +1066,8 @@ def generate_binary(outputfname, format_, progname="", binary_files=None, headle emitter = {"tap": outfmt.TAP, "tzx": outfmt.TZX}[format_]() elif format_ == "sna": emitter = outfmt.SnaEmitter() + elif format_ == "z80": + emitter = outfmt.Z80Emitter() else: emitter = outfmt.BinaryEmitter() diff --git a/src/zxbc/args_config.py b/src/zxbc/args_config.py index 58bb423fb..c3ae2e59c 100644 --- a/src/zxbc/args_config.py +++ b/src/zxbc/args_config.py @@ -120,11 +120,15 @@ def parse_options(args: list[str] | None = None) -> Namespace: FileType.TAP, FileType.TZX, FileType.SNA, + FileType.Z80, }: - parser.error("Options --BASIC and --autorun require one of tzx, tap, or sna output format") + parser.error("Options --BASIC and --autorun require one of sna, tzx, tap or z80 output format") - if not (options.basic and options.autorun) and OPTIONS.output_file_type == FileType.SNA: - parser.error("Options --BASIC and --autorun are both required for sna format") + if not (options.basic and options.autorun) and OPTIONS.output_file_type in { + FileType.SNA, + FileType.Z80, + }: + parser.error("Options --BASIC and --autorun are both required for snapshot formats") if options.append_binary and OPTIONS.output_file_type not in {FileType.TAP, FileType.TZX}: parser.error("Option --append-binary needs either tap or tzx output format") diff --git a/src/zxbc/args_parser.py b/src/zxbc/args_parser.py index 57358667b..a28e32d82 100644 --- a/src/zxbc/args_parser.py +++ b/src/zxbc/args_parser.py @@ -17,6 +17,7 @@ class FileType(StrEnum): SNA = "sna" TAP = "tap" TZX = "tzx" + Z80 = "z80" def parse_warning_option(code: str) -> str: diff --git a/tests/functional/Makefile b/tests/functional/Makefile index 6fc35ceda..55a0ed282 100644 --- a/tests/functional/Makefile +++ b/tests/functional/Makefile @@ -27,7 +27,7 @@ basic_tests: ./test.py '**/[0-9]*.bas' bin: - ./test.py '**/tzx_*.bas' '**/tap_*.bas' '**/sna_*.bas' + ./test.py '**/tzx_*.bas' '**/tap_*.bas' '**/sna_*.bas' '**/z80_*.bas' # Measures coverage using only basic tests .PHONY: basic_coverage diff --git a/tests/functional/arch/zx48k/z80_00.bas b/tests/functional/arch/zx48k/z80_00.bas new file mode 100644 index 000000000..06ceceac4 --- /dev/null +++ b/tests/functional/arch/zx48k/z80_00.bas @@ -0,0 +1 @@ +PRINT "HELLO WORLD" diff --git a/tests/functional/arch/zx48k/z80_00.z80 b/tests/functional/arch/zx48k/z80_00.z80 new file mode 100644 index 000000000..a614187a4 Binary files /dev/null and b/tests/functional/arch/zx48k/z80_00.z80 differ diff --git a/tests/functional/cmdline/test_cmdline.txt b/tests/functional/cmdline/test_cmdline.txt index ac3246a65..8be8c00db 100644 --- a/tests/functional/cmdline/test_cmdline.txt +++ b/tests/functional/cmdline/test_cmdline.txt @@ -4,8 +4,8 @@ >>> process_file('arch/zx48k/arrbase1.bas', ['-q', '-S', '-O --mmap arrbase1.map']) usage: zxbc.py [-h] [-d] [-O OPTIMIZE] [-o OUTPUT_FILE] [-T] [-t] [-A] [-E] - [--parse-only] [-f {asm,bin,IR,sna,tap,tzx}] [-B] [-a] [-S ORG] - [-e STDERR] [--array-base ARRAY_BASE] + [--parse-only] [-f {asm,bin,IR,sna,tap,tzx,z80}] [-B] [-a] + [-S ORG] [-e STDERR] [--array-base ARRAY_BASE] [--string-base STRING_BASE] [-Z] [-H HEAP_SIZE] [--heap-address HEAP_ADDRESS] [--debug-memory] [--debug-array] [--strict-bool] [--enable-break] [--explicit] [-D DEFINES] diff --git a/tests/functional/test.py b/tests/functional/test.py index d28bceedf..6d5f4eefa 100755 --- a/tests/functional/test.py +++ b/tests/functional/test.py @@ -11,12 +11,13 @@ import sys import tempfile from collections.abc import Callable, Iterable -from typing import Final +from typing import Final, cast +from src.api.utils import open_file from src.zxbc.args_parser import FileType reOPT = re.compile(r"^opt([0-9]+)_") # To detect -On tests -reBIN = re.compile(r"^(?:.*/)?(tzx|tap|sna)_.*") # To detect tzx / tap / sna test +reBIN = re.compile(r"^(?:.*/)?(tzx|tap|sna|z80)_.*") # To detect tzx / tap / snapshot test reIC = re.compile(r"^.*_IC$") # To detect intermediate code tests EXIT_CODE = 0 @@ -124,10 +125,8 @@ def get_file_lines( """Opens source file and load its lines, discarding those not important for comparison. """ - from src.api.utils import open_file - with open_file(filename, "rt", "utf-8") as f: - lines = [x.rstrip("\r\n") for x in f] + lines = [cast(str, x.rstrip("\r\n")) for x in f] if ignore_regexp is not None: r = re.compile(ignore_regexp) @@ -240,7 +239,7 @@ def _get_testbas_options(fname: str) -> tuple[list[str], str, str]: match_bin = reBIN.match(getName(fname)) match_ic = reIC.match(getName(fname)) - if match_bin and match_bin.groups()[0].lower() in (FileType.SNA, FileType.TAP, FileType.TZX): + if match_bin and match_bin.groups()[0].lower() in {FileType.SNA, FileType.TAP, FileType.TZX, FileType.Z80}: ext = match_bin.groups()[0].lower() tfname = os.path.join(TEMP_DIR, getName(fname) + os.extsep + ext) options.extend([f"--output-format={ext}", fname, "-o", tfname, "-a", "-B"] + prep) diff --git a/tests/runtime/test_case b/tests/runtime/test_case index cdfac9e9b..5ca2603f9 100755 --- a/tests/runtime/test_case +++ b/tests/runtime/test_case @@ -8,13 +8,10 @@ TIMEOUT=180 TIMEKILL=$((TIMEOUT+30)) echo -n "Testing $(basename $1): " -SNA=$(basename -s .bas $1).sna RUN=$(basename -s .bas $1).z80 EXPECTED=$(basename -s .bas $1).tzx.scr -rm -f "$RUN" 2>/dev/null -../../zxbc.py --sna -aB -o "$SNA" $1 $(grep "PARAMS:" $1 |cut -d':' -f2-) --debug-memory 2>/dev/null -snapconv "$SNA" "$RUN" 2>/dev/null 1>/dev/null +../../zxbc.py -f z80 -aB -o "$RUN" $1 $(grep "PARAMS:" $1 |cut -d':' -f2-) --debug-memory 2>/dev/null timeout -k $TIMEKILL $TIMEOUT ./check_test.py "$RUN" "./expected/${EXPECTED}" RETVAL=$? -rm -f "$RUN" "$SNA" 2>/dev/null +rm -f "$RUN" 2>/dev/null exit $RETVAL diff --git a/tests/runtime/update_test.sh b/tests/runtime/update_test.sh index bb4af8396..8a67c152b 100755 --- a/tests/runtime/update_test.sh +++ b/tests/runtime/update_test.sh @@ -2,9 +2,6 @@ # ./run $1 NAME=$(basename -s .bas $1).z80 -RUN=$(basename -s .bas $1) -rm -f "$NAME" -../../zxbc.py --sna -aB "$@" --debug-memory || exit 1 -snapconv "$RUN.sna" "$NAME" +../../zxbc.py -f z80 -aB "$@" --debug-memory || exit 1 ./update_test.py $NAME # mv $NAME.scr expected