/* 3dsspec - Nintendo 3DS Spectrum emulator Copyright (C) 2021 Ian Cowburn This program 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. This program 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. You should have received a copy of the GNU General Public License along with this program; if not, see ------------------------------------------------------------------------- Provides the emulation for the Spectrum */ #include #include #include #include #include <3ds.h> #include "spec.h" #include "gui.h" #include "framebuffer.h" #include "stream.h" #include "config.h" #include "spec48_bin.h" #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif /* ---------------------------------------- STATICS */ #define ROMLEN 0x4000 #define ROM_SAVE 0x2fc #define ROM_LOAD 0x347 #define ED_SAVE 0xf0 #define ED_LOAD 0xf1 #define SLOW_TSTATES 16000 #define FAST_TSTATES 64000 #define E_LINE 16404 #define LASTK1 16421 #define LASTK2 16422 #define MARGIN 16424 #define FRAMES 16436 #define CDFLAG 16443 #define NMI_PERIOD 208 /* The SPEC screen and memory */ static int fast_mode=FALSE; static int nmigen=FALSE; static int hsync=FALSE; static int drawing_screen=FALSE; static int ula_line_counter; static u16 black; static u16 white; #define SCR_W 256 #define SCR_H 192 #define TXT_W 32 #define TXT_H 24 static Z80Byte mem[0x10000]; static u16 screen[SCR_W*SCR_H]; static int ula_ptr; static Z80Word RAMBOT=0; static Z80Word RAMTOP=0; #define DFILE 0x400c #define WORD(a) (mem[a] | (Z80Word)mem[a+1]<<8) /* Tape */ static int enable_filesystem; static int allow_save; static const Z80Byte *tape_image; static int tape_len; static char last_dir[FILENAME_MAX] = "/"; /* The keyboard */ static Z80Byte matrix[8]; static struct { int row; int bit; } key_matrix[]= { {3,0x01}, {3,0x02}, {3,0x04}, {3,0x08}, {3,0x10}, /* 1 - 5 */ {4,0x10}, {4,0x08}, {4,0x04}, {4,0x02}, {4,0x01}, /* 6 - 0 */ {2,0x01}, {2,0x02}, {2,0x04}, {2,0x08}, {2,0x10}, /* Q - T */ {5,0x10}, {5,0x08}, {5,0x04}, {5,0x02}, {5,0x01}, /* Y - P */ {1,0x01}, {1,0x02}, {1,0x04}, {1,0x08}, {1,0x10}, /* A - G */ {6,0x10}, {6,0x08}, {6,0x04}, {6,0x02}, {6,0x01}, /* H - NL */ {0,0x01}, {0,0x02}, {0,0x04}, {0,0x08}, {0,0x10}, /* CAPS - V */ {7,0x10}, {7,0x08}, {7,0x04}, {7,0x02}, {7,0x01} /* B - SPACE */ }; /* ---------------------------------------- PRIVATE FUNCTIONS */ #define PEEKW(addr) (mem[addr] | (Z80Word)mem[addr+1]<<8) #define POKEW(addr,val) do \ { \ Z80Word wa=addr; \ Z80Word wv=val; \ mem[wa]=wv; \ mem[wa+1]=wv>>8; \ } while(0) static void RomPatch(void) { static const Z80Byte save[]= { 0xed, ED_SAVE, /* (SAVE) */ 0xc3, 0x07, 0x02, /* JP $0207 */ 0xff /* End of patch */ }; static const Z80Byte load[]= { 0xed, ED_LOAD, /* (LOAD) */ 0xc3, 0x07, 0x02, /* JP $0207 */ 0xff /* End of patch */ }; int f; for(f=0;save[f]!=0xff;f++) { mem[ROM_SAVE+f]=save[f]; } for(f=0;load[f]!=0xff;f++) { mem[ROM_LOAD+f]=load[f]; } } /* Open a tape file the passed address */ static FILE *OpenTapeFile(Z80Word addr, int *cancelled, const char *mode) { static const char zx_chars[] = "\"#$:?()><=+-*/;,." "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; FILE *fp; char full_fn[FILENAME_MAX] = DEFAULT_SNAPDIR; char fn[FILENAME_MAX]; int f; int done; fp = NULL; f = 0; done = FALSE; *cancelled = FALSE; while(f<(FILENAME_MAX-3) && !done) { int ch; ch = mem[addr++]; if (ch&0x80) { done = TRUE; ch &= 0x7f; } if (ch>=11 && ch<=63) { fn[f++] = zx_chars[ch-11]; } } if (fn[0] == '*') { if (GUI_FileSelect(last_dir,fn,".P")) { fp = fopen(fn, mode); } else { *cancelled = TRUE; } SK_DisplayKeyboard(); } else { fn[f++] = '.'; fn[f++] = 'P'; fn[f] = 0; strcat(full_fn,fn); if (!(fp = fopen(full_fn, mode))) { fp = fopen(fn, mode); } } return fp; } static void LoadInternalTape(Z80 *z80) { memcpy(mem+0x4009,tape_image,tape_len); } static void LoadExternalTape(FILE *tape, Z80 *z80) { int c; Z80Byte *a; a=mem+0x4009; while((c=getc(tape))!=EOF) { *a++=c; } } static void SaveExternalTape(FILE *tape, Z80 *z80) { int f; int end; f = 0x4009; end = WORD(E_LINE); while(f <= end) { fputc(mem[f++], tape); } } static int CheckTimers(Z80 *z80, Z80Val val) { static Z80Word last_PC; static Z80Byte last_R; int ret = TRUE; /* See if we're started or stopping executing the display file */ if (z80->PC > 0x7fff) { if (last_PC < 0x8000) { drawing_screen = TRUE; ula_ptr = 0; } } if (z80->PC < 0x8000) { if (last_PC > 0x7fff && ula_ptr == SCR_W * SCR_H) { drawing_screen = FALSE; ula_ptr = 0; Z80ResetCycles(z80, val - SLOW_TSTATES); ret = FALSE; } } last_PC = z80->PC; /* Check HSYNC generation */ if (hsync && !nmigen) { if (last_R & 0x40 && !(z80->R & 0x40)) { Z80Interrupt(z80, 0xff); ula_line_counter = (ula_line_counter + 1) % 8; } } last_R = z80->R; /* Check NMI generator */ if (nmigen && val > NMI_PERIOD) { Z80NMI(z80); Z80ResetCycles(z80, val - NMI_PERIOD); } /* We should only be able to execute FAST_TSTATES if we're not drawing the screen. */ if (val >= FAST_TSTATES) { Z80ResetCycles(z80,val-FAST_TSTATES); if (mem[CDFLAG] & 0x80) { fast_mode = FALSE; } else { fast_mode = TRUE; } ret = FALSE; } return ret; } static int EDCallback(Z80 *z80, Z80Val data) { switch((Z80Byte)data) { case ED_SAVE: if (allow_save && z80->DE.w<0x8000) { FILE *fp; int cancel; if ((fp=OpenTapeFile(z80->HL.w, &cancel, "wb"))) { SaveExternalTape(fp,z80); fclose(fp); } } break; case ED_LOAD: /* Try and load the external file if a name given. Otherwise, we try the internal one. Some of this is slightly dodgy -- it was never intended for the emulator to be doing any GUI related nonsense (like the alerts) but simply emulating. */ if (enable_filesystem && z80->DE.w<0x8000) { FILE *fp; int cancel; if ((fp=OpenTapeFile(z80->DE.w, &cancel, "rb"))) { LoadExternalTape(fp,z80); fclose(fp); } else { if (!cancel) { GUI_Alert(FALSE,"Couldn't open tape"); SK_DisplayKeyboard(); } } } else { if (tape_image) { LoadInternalTape(z80); } else { GUI_Alert(FALSE,"No tape image selected"); SK_DisplayKeyboard(); } } mem[CDFLAG]=0xc0; break; default: break; } return TRUE; } /* ---------------------------------------- EXPORTED INTERFACES */ void SPECInit(Z80 *z80) { Z80Word f; black = FB_GetColour(COL_BLACK); white = FB_GetColour(COL_WHITE); /* Load the ROM */ memcpy(mem,spec48_bin,ROMLEN); /* Patch the ROM */ RomPatch(); Z80LodgeCallback(z80,eZ80_EDHook,EDCallback); Z80LodgeCallback(z80,eZ80_Instruction,CheckTimers); /* Mirror the ROM */ memcpy(mem+ROMLEN,mem,ROMLEN); /* Memory size (16K) */ RAMBOT=0x4000; RAMTOP=RAMBOT+0x4000; for(f = RAMBOT; f <= RAMTOP; f++) { mem[f] = 0; } for(f = 0; f < 8; f++) { matrix[f] = 0x1f; } } void SPECRenderDisplay(Framebuffer *fb, Z80 *z80) { if (fast_mode) { u16 x; u16 y; for(x = 0; x < fb->width; x++) { for(y = 0; y < fb->height; y++) { FB_ADDR(fb, x, y) = ((x+y) % 2) ? black : white; } } } else { int x; int y; FB_Clear(fb, COL_WHITE); for(y = 0; y < SCR_H; y++) { for(x = 0; x < SCR_W; x++) { FB_ADDR(fb, 72 + x, fb->height - 24 - y) = screen[x + y * SCR_W]; } } } } void SPECHandleKey(SoftKey key, int is_pressed) { if (keyI<<8)|(b<<3)|ula_line_counter; b = mem[base]; for(f = 0; f < 8; f++) { if (b & 0x80) { screen[ula_ptr++] = rand(); // inv ? white : black; } else { screen[ula_ptr++] = rand(); // inv ? black : white; } b = b << 1; } b = 0; } return b; } else { return mem[addr]; } } void SPECWriteMem(Z80 *z80, Z80Word addr, Z80Byte val) { if (addr>=RAMBOT && addr<=RAMTOP) { mem[addr]=val; } } Z80Byte SPECReadPort(Z80 *z80, Z80Word port) { Z80Byte b=0; switch(port&0xff) { case 0xfe: /* ULA */ /* Key matrix */ switch(port&0xff00) { case 0xfe00: b=matrix[0]; break; case 0xfd00: b=matrix[1]; break; case 0xfb00: b=matrix[2]; break; case 0xf700: b=matrix[3]; break; case 0xef00: b=matrix[4]; break; case 0xdf00: b=matrix[5]; break; case 0xbf00: b=matrix[6]; break; case 0x7f00: b=matrix[7]; break; } /* Some code expects some of the top bits set... Of course, whether or not this may be worse as other code doesn't expect the bits, we shall find out! */ b |= 0x60; /* Reset ULA line counter */ ula_line_counter = 0; break; default: b = 0xff; /* Idle bus */ break; } return b; } void SPECWritePort(Z80 *z80, Z80Word port, Z80Byte val) { switch(port&0xff) { case 0xfd: /* NMI generator OFF */ nmigen = FALSE; break; case 0xfe: /* NMI generator ON */ nmigen = TRUE; Z80ResetCycles(z80, 0); break; case 0xff: /* HSYNC generator ON */ hsync = TRUE; break; } } void SPECReset(Z80 *z80) { int f; for(f=0;f<8;f++) matrix[f]=0x1f; Z80Reset(z80); Z80ResetCycles(z80,0); nmigen = FALSE; hsync = FALSE; } void SPECEnableFileSystem(int enable) { enable_filesystem=enable; } void SPECSetTape(const Z80Byte *image, int len) { tape_image=image; tape_len=len; } void SPECReconfigure(void) { allow_save = enable_filesystem && DSSPEC_Config[DSSPEC_ALLOW_TAPE_SAVE]; } void SPECSaveSnapshot(FILE *fp) { int f; for(f=0; f