exl100.patch (46101B)
1 diff --git a/CMakeLists.txt b/CMakeLists.txt 2 index a1ed784..676588b 100644 3 --- a/CMakeLists.txt 4 +++ b/CMakeLists.txt 5 @@ -297,6 +297,7 @@ add_library(dsp_lib STATIC 6 src/drivers/computers/apple2gs.cpp 7 src/drivers/computers/exelv.cpp 8 src/drivers/computers/exelv_tape.cpp 9 + src/drivers/computers/exelv_disk.cpp 10 src/drivers/computers/ql.cpp 11 src/drivers/computers/atari_st.cpp 12 src/drivers/computers/amiga.cpp 13 @@ -442,7 +443,7 @@ function(dsp_add_frontend target) 14 "-sTOTAL_STACK=8388608" 15 "-sEXIT_RUNTIME=0" 16 "-sFORCE_FILESYSTEM=1" 17 - "-sEXPORTED_FUNCTIONS=['_dsp_web_start','_dsp_web_list','_dsp_web_has_embedded']" 18 + "-sEXPORTED_FUNCTIONS=['_dsp_web_start','_dsp_web_list','_dsp_web_has_embedded','_dsp_web_cram_present','_dsp_web_cram_dirty','_dsp_web_cram_save','_dsp_web_cram_load','_dsp_web_disk_present','_dsp_web_disk_dirty','_dsp_web_disk_save','_dsp_web_disk_load','_dsp_web_tape_load']" 19 "-sEXPORTED_RUNTIME_METHODS=['ccall','UTF8ToString','FS']" 20 "-sMINIFY_HTML=0" 21 "--shell-file=${CMAKE_CURRENT_BINARY_DIR}/shell.html") 22 diff --git a/src/drivers/computers/exelv.cpp b/src/drivers/computers/exelv.cpp 23 index debcb94..c1fad73 100644 24 --- a/src/drivers/computers/exelv.cpp 25 +++ b/src/drivers/computers/exelv.cpp 26 @@ -2,6 +2,7 @@ 27 28 #include <algorithm> 29 #include <cctype> 30 +#include <cstdio> 31 #include <cstring> 32 #include "core/fs_compat.h" 33 #include <fstream> 34 @@ -94,6 +95,19 @@ Exelv::Exelv(Model model) 35 maincpu_.set_port_in(Tms7000::kPortA, [this]() { return tms7020_porta_r(); }); 36 maincpu_.set_port_out(Tms7000::kPortB, [this](uint8_t v) { tms7020_portb_w(v); }); 37 maincpu_.set_cycle_handler([this](int c) { on_main_cycles(c); }); 38 +#ifdef CRAM_DBG 39 + // Trace le dispatcher de commandes du 7042 ($F1C8) et l'entrée des handlers de bloc. 40 + subcpu_.set_cycle_handler([this](int) { 41 + const uint16_t pc = subcpu_.pc(); 42 + if (pc == 0xf1c8) std::fprintf(stderr, "7042 dispatch cmd=%02x cont=%02x%02x\n", unsigned(subcpu_.b()), unsigned(subcpu_.ram_at(45)), unsigned(subcpu_.ram_at(46))); 43 + else if (pc == 0xf1e4) std::fprintf(stderr, "7042 RESET par cmd %02x\n", unsigned(subcpu_.b())); 44 + else if (pc == 0xf006) std::fprintf(stderr, "7042 entre en RESET ($F006)\n"); 45 + else if (pc == 0xf2d8) std::fprintf(stderr, "7042 handler $0B (bloc)\n"); 46 + else if (pc == 0xf1a1) std::fprintf(stderr, "7042 poste %02x\n", unsigned(subcpu_.b())); 47 + else if (pc == 0xf4ac) std::fprintf(stderr, "7041 IR code brut %02x\n", unsigned(subcpu_.ram_at(7))); 48 + else if (pc == 0xf4b7) std::fprintf(stderr, "7041 IR touche R43=%02x R9=%02x R52=%02x\n", unsigned(subcpu_.ram_at(43)), unsigned(subcpu_.ram_at(9)), unsigned(subcpu_.ram_at(52))); 49 + }); 50 +#endif 51 52 subcpu_.set_memory_handlers([this](uint16_t a) { return read_sub(a); }, 53 [this](uint16_t a, uint8_t v) { write_sub(a, v); }); 54 @@ -178,6 +192,30 @@ bool Exelv::load_bios(const std::string& rom_path, std::string* error) { 55 if (loader.open(rom_path, &ignored) && loader.load(kExeltelSys, sys, &ignored)) { 56 system_rom_ = std::move(sys); 57 } 58 + // Exelmémoire (chantier) : si l'OS CROS64 est présent dans /roms, le 59 + // mapper en page 1. ROM 8 Ko à $D000-$EFFF, RAM 16 Ko à $8000-$BFFF. 60 + std::vector<uint8_t> os; 61 + if (read_plain_file("/roms/cros64.rom", os) && os.size() == 0x2000) { 62 + cram_os_ = std::move(os); 63 + cram_ram_.assign(0x4000, 0); // CRAM vierge : $BFE1 != $AA -> présence OK 64 + cram_pram_.assign(0x0e00, 0); // RAM $0200-$0FFF : pile logicielle, variables, volume A 65 + cram_present_ = true; 66 + } 67 + } 68 + 69 + if (model_ == Model::Exl100) { 70 + // Exelmémoire 16K de l'EXL 100 : même carte (OS $D000-$EFFF, CRAM $8000-$BFFF), pas de RAM 71 + // basse. Le BIOS ($FF03-$FF17) lit $8000 : $AA -> lance $8001. C'est CROS64 qui écrit cette 72 + // amorce au Format ($DEDA-$DEE5, $DF38 : AA, BR @$E62A) ; on la sème sur CRAM vierge, comme 73 + // la cartouche livrée initialisée, sinon rien ne permet d'entrer dans CROS64 pour formater. 74 + std::vector<uint8_t> os; 75 + if (read_plain_file("/roms/cros64_exl100.rom", os) && os.size() == 0x2000) { 76 + cram_os_ = std::move(os); 77 + cram_ram_.assign(0x4000, 0); 78 + const uint8_t stub[4] = {0xaa, 0x8c, 0xe6, 0x2a}; 79 + std::copy(stub, stub + 4, cram_ram_.begin()); 80 + cram_present_ = true; 81 + } 82 } 83 84 if (main_rom.size() < (model_ == Model::Exl100 ? 0x800u : 0x1000u)) return false; 85 @@ -249,6 +287,17 @@ bool Exelv::load_media(const std::string& path, std::string* error) { 86 namespace fs = dsp::fsx; 87 std::error_code ec; 88 const std::string ext = lower_copy(fs::path(path).extension().string()); 89 + if (ext == ".fd") { 90 + if (!read_plain_file(path, data)) { 91 + if (error) *error = "cannot read disk image " + path; 92 + return false; 93 + } 94 + if (!disk_.mount(std::move(data))) { 95 + if (error) *error = "bad .fd image (expected 655360 bytes) " + path; 96 + return false; 97 + } 98 + return true; 99 + } 100 if (ext == ".k7" || ext == ".wav") { 101 if (!read_plain_file(path, data)) { 102 if (error) *error = "cannot read cassette " + path; 103 @@ -286,6 +335,18 @@ bool Exelv::load_media(const std::string& path, std::string* error) { 104 } 105 106 void Exelv::reset() { 107 + // Exeldisk (EXL 100) : branché si son ROM d'amorçage est dans le MEMFS (la page le retire 108 + // quand aucune disquette n'est choisie, sinon le BIOS amorcerait sur le lecteur vide). 109 + if (model_ == Model::Exl100 && disk_rom_.empty()) { 110 + std::vector<uint8_t> rom; 111 + if (read_plain_file("/roms/exeldisk.rom", rom) && rom.size() == 0x2000) { 112 + disk_rom_ = std::move(rom); 113 + disk_present_ = true; 114 + disk_ram_.assign(0x7e00, 0); 115 + dos_ram_.assign(0x2000, 0); 116 + } 117 + } 118 + disk_.reset(); 119 ram_.fill(0); 120 tms7020_portb_ = 0; 121 tms7041_portb_ = 0; 122 @@ -366,6 +427,16 @@ uint8_t Exelv::read_main(uint16_t address) { 123 if (address == 0x0125) return vdp_.reg_r(); 124 if (address == 0x0128) return vdp_.initptr_r(); 125 if (address == 0x0130) return mailbox_wx319_r(); 126 + if (disk_active()) { 127 + if (address == 0x0140) return disk_.read_status_port(); 128 + if (address == 0x0142) return disk_.start_read(); 129 + if (address == 0x0143) return disk_.finish_read(); 130 + if (address >= 0x0200 && address <= 0x7fff && !disk_.cart_at_0200()) return disk_ram_[address - 0x0200]; 131 + if (address >= 0x8000 && address <= 0x9fff && disk_.boot_rom_at_8000()) return disk_rom_[address - 0x8000]; 132 + if (address >= 0xa000 && address <= 0xbfff && disk_.boot_rom_at_8000()) return 0xff; 133 + if (address >= 0xc800 && address <= 0xcfff) return disk_rom_[address - 0xc000]; 134 + if (address >= 0xd000 && address <= 0xefff && !disk_.cos_at_d000()) return dos_ram_[address - 0xd000]; 135 + } 136 if (model_ == Model::Exeltel) { 137 if (address == 0x0138) { 138 page_bit1_ = true; 139 @@ -377,7 +448,55 @@ uint8_t Exelv::read_main(uint16_t address) { 140 } 141 if (address == 0x0140) return p64_; 142 } 143 + // Exelmémoire : RAM $0200-$0FFF en pages 0/1 (là où la fenêtre cartouche est vide). 144 + // CROS64 y met sa pile logicielle (R16 = $02D7), ses variables ($0260-$02D8) et le 145 + // volume A ($02DA+). Sans elle, les STA y sont perdus et les LDA relisent $FF : la 146 + // restauration de contexte ($D411) remettait BRTIME ($C004) à $FFFF, et le handler 147 + // INT2 faisait CALL $FFFF -> TRAP 0 -> reset -> écran rouge après Format. 148 + if (model_ == Model::Exeltel && cram_present_ && address >= 0x0200 && address <= 0x0fff) { 149 + const int pg = exeltel_page(); 150 + if (pg != 2 && pg != 3 && pg != 6) return cram_pram_[address - 0x0200]; 151 + } 152 +#ifdef CRAM_DBG 153 + // Où lit le système (hors CROS64) dans la fenêtre cartouche des pages 0/1 ? (Exelquad, Autostart) 154 + if (cram_present_ && address >= 0x0200 && address <= 0x7fff && maincpu_.pc() < 0xd000) { 155 + static int n = 0; 156 + const int pg = exeltel_page(); 157 + if (pg != 2 && pg != 3 && pg != 6 && n++ < 300) 158 + std::fprintf(stderr, "LOW %04x page=%d pc=%04x\n", unsigned(address), pg, unsigned(maincpu_.pc())); 159 + } 160 +#endif 161 if (address >= 0x0200 && address <= 0x7fff) return cart_r(uint16_t(address - 0x0200)); 162 + // Exelmémoire : fenêtre haute fixe (non paginée). La présence est vue par 163 + // la vérif système $423A (CMPA @$D002=$55 / @$D003=$AA) qui lit $D000-$EFFF 164 + // directement en code page 2. RAM CRAM à $8000-$BFFF. Pas de $A5 à $C800 165 + // (sinon la détection boot auto-lance $8001 = RAM vide -> plantage). 166 + if (cram_present_) { 167 +#ifdef CRAM_AUTOSTART 168 + // Expérience : octet d'autostart vu par la détection d'amorçage du 7040 (page 1, $C800 == $A5 -> 169 + // BRLANC $8001) pour lancer un programme machine stocké dans la CRAM. 170 + if (model_ == Model::Exeltel && address == 0xc800) return 0xa5; 171 +#endif 172 + if (address >= 0x8000 && address <= 0xbfff) { 173 +#ifdef CRAM_DBG_READS 174 + std::fprintf(stderr, "MR %04x pc=%04x\n", unsigned(address), unsigned(maincpu_.pc())); 175 +#endif 176 + return cram_ram_[address - 0x8000]; 177 + } 178 + if (address >= 0xd000 && address <= 0xefff) { 179 +#ifdef CRAM_DBG 180 + // 1re exécution/lecture par adresse OS : trace le chemin CROS64. 181 + static bool seen[0x2000] = {false}; 182 + if (!seen[address - 0xd000]) { seen[address - 0xd000] = true; 183 + std::fprintf(stderr, "OS %04x pc=%04x\n", unsigned(address), unsigned(maincpu_.pc())); } 184 +#endif 185 + return cram_os_[address - 0xd000]; 186 + } 187 + // $0131 = statut de sortie (imprimante/souris, bus série P49). L'OS 188 + // attend bit4=0 et ($1D)==$09 ; HLE « prêt » pour ne pas bloquer. 189 + if (address == 0x0131 && maincpu_.pc() >= 0xd000 && maincpu_.pc() <= 0xefff) 190 + return 0x09; // HLE imprimante « prêt » seulement pour CROS64, pas le moniteur 191 + } 192 if (address >= 0xc000 && address <= 0xc7ff) return ram_[address - 0xc000]; 193 return 0xff; 194 } 195 @@ -395,6 +514,20 @@ void Exelv::write_main(uint16_t address, uint8_t value) { 196 mailbox_wx318_w(value); 197 return; 198 } 199 + if (disk_active()) { 200 + if (address == 0x0140) { disk_.write_control(value); return; } 201 + if (address == 0x0141) { disk_.write_register(value); return; } 202 + if (address == 0x0142 || address == 0x0143) return; // choix de densité 203 + if (address >= 0x0200 && address <= 0x7fff) { 204 + if (!disk_.cart_at_0200()) disk_ram_[address - 0x0200] = value; 205 + return; 206 + } 207 + if (address >= 0x8000 && address <= 0xbfff && disk_.boot_rom_at_8000()) return; 208 + if (address >= 0xd000 && address <= 0xefff) { 209 + if (!disk_.cos_at_d000()) dos_ram_[address - 0xd000] = value; 210 + return; 211 + } 212 + } 213 if (model_ == Model::Exeltel) { 214 if (address == 0x0138) { 215 page_bit1_ = false; 216 @@ -409,6 +542,43 @@ void Exelv::write_main(uint16_t address, uint8_t value) { 217 return; 218 } 219 } 220 + // Exelmémoire : RAM $0200-$0FFF (pages 0/1), voir read_main. 221 + if (model_ == Model::Exeltel && cram_present_ && address >= 0x0200 && address <= 0x0fff) { 222 + const int pg = exeltel_page(); 223 + if (pg != 2 && pg != 3 && pg != 6) { 224 + cram_pram_[address - 0x0200] = value; 225 + return; 226 + } 227 + } 228 +#ifdef CRAM_DBG 229 + // Écritures encore perdues (mémoire non mappée) : reste-t-il une zone à mapper ? 230 + if (model_ == Model::Exeltel && cram_present_ && 231 + ((address >= 0x0200 && address <= 0x7fff) || (address >= 0xc800 && address <= 0xefff))) 232 + std::fprintf(stderr, "DROP %04x=%02x pc=%04x\n", unsigned(address), unsigned(value), 233 + unsigned(maincpu_.pc())); 234 +#endif 235 +#ifdef CRAM_DBG 236 + // Trace port limitée aux écritures venant de CROS64 (pas la souris du moniteur $F0xx). 237 + if (model_ == Model::Exeltel && cram_present_ && (address == 0x0131 || address == 0x0132) && 238 + maincpu_.pc() < 0xf000) 239 + std::fprintf(stderr, "PW %04x=%02x pc=%04x\n", unsigned(address), unsigned(value), 240 + unsigned(maincpu_.pc())); 241 +#endif 242 + if (cram_present_ && address >= 0x8000 && address <= 0xbfff) { 243 +#ifdef CRAM_DBG 244 + std::fprintf(stderr, "MW %04x=%02x pc=%04x\n", unsigned(address), unsigned(value), 245 + unsigned(maincpu_.pc())); 246 +#endif 247 + cram_ram_[address - 0x8000] = value; 248 + cram_dirty_ = true; 249 + return; 250 + } 251 +#ifdef CRAM_DBG 252 + // Qui écrit le vecteur BRTIME ($C004:$C005) que le handler INT2 appelle ? 253 + if (address == 0xc004 || address == 0xc005) 254 + std::fprintf(stderr, "VEC %04x=%02x pc=%04x\n", unsigned(address), unsigned(value), 255 + unsigned(maincpu_.pc())); 256 +#endif 257 if (address >= 0xc000 && address <= 0xc7ff) ram_[address - 0xc000] = value; 258 } 259 260 @@ -416,10 +586,25 @@ uint8_t Exelv::read_sub(uint16_t) { return 0xff; } 261 void Exelv::write_sub(uint16_t, uint8_t) {} 262 263 uint8_t Exelv::mailbox_wx319_r() { return wx319_; } 264 -void Exelv::mailbox_wx318_w(uint8_t data) { wx318_ = data; } 265 +void Exelv::mailbox_wx318_w(uint8_t data) { 266 +#ifdef CRAM_DBG 267 + { 268 + const uint8_t sp = maincpu_.sp(); 269 + const unsigned r1 = (unsigned(maincpu_.ram_at(uint8_t(sp - 1))) << 8) | maincpu_.ram_at(sp); 270 + const unsigned r2 = (unsigned(maincpu_.ram_at(uint8_t(sp - 3))) << 8) | maincpu_.ram_at(uint8_t(sp - 2)); 271 + std::fprintf(stderr, "MBX main->sub %02x pc=%04x ret=%04x,%04x\n", unsigned(data), unsigned(maincpu_.pc()), r1, r2); 272 + } 273 +#endif 274 + wx318_ = data; 275 +} 276 277 uint8_t Exelv::tms7020_porta_r() { 278 uint8_t data = (tms7041_portb_ & 0x80) ? 0x01 : 0x00; 279 + // Exeldisk : PA.1 = DRQ du 1770, PA.3 = état ROM/RAM de $0200. 280 + if (disk_active()) { 281 + if (disk_.drq()) data |= 0x02; 282 + if (disk_.cart_at_0200()) data |= 0x08; 283 + } 284 // PA.4: cassette input, high when idle. 285 if (!tape_.loaded() || tape_.level()) data |= 0x10; 286 return data; 287 @@ -447,10 +632,16 @@ uint8_t Exelv::tms7041_porta_r() { 288 void Exelv::tms7041_portb_w(uint8_t data) { 289 speech_.strobe_ws_rs(data & 0x03); 290 if ((tms7041_portb_ & 0x04) && !(data & 0x04)) { 291 +#ifdef CRAM_DBG 292 + std::fprintf(stderr, "MBX sub->main INT1 (wx319=%02x, sub pc=%04x)\n", unsigned(wx319_), unsigned(subcpu_.pc())); 293 +#endif 294 maincpu_.set_input_line(Tms7000::kInt1, IrqLine::Hold); 295 } 296 if (!(tms7041_portb_ & 0x40) && (data & 0x40)) { 297 wx319_ = tms7041_portc_; 298 +#ifdef CRAM_DBG 299 + std::fprintf(stderr, "MBX sub poste %02x\n", unsigned(wx319_)); 300 +#endif 301 // Function $01 (key/joystick 0) is followed by the key code; $04 302 // there reports the release. 303 if (last_sent_ == 0x01 && wx319_ != 0x04) last_key_ = wx319_; 304 @@ -460,6 +651,10 @@ void Exelv::tms7041_portb_w(uint8_t data) { 305 } 306 307 uint8_t Exelv::tms7041_portc_r() { 308 +#ifdef CRAM_DBG 309 + if (!(tms7041_portb_ & 0x20)) 310 + std::fprintf(stderr, "MBX sub lit %02x (sub pc=%04x)\n", unsigned(wx318_), unsigned(subcpu_.pc())); 311 +#endif 312 if (!(tms7041_portb_ & 0x20)) return wx318_; 313 return 0xff; 314 } 315 @@ -503,9 +698,20 @@ void Exelv::fast_load_hook() { 316 const uint16_t read_byte = exl ? 0xfe82 : 0xfcbd; 317 const uint16_t pc = maincpu_.pc(); 318 if (pc == sync_start) { 319 - if (tape_.seek_sync()) maincpu_.set_pc(after_sync); 320 + const bool ok = tape_.seek_sync(); 321 +#ifdef CRAM_DBG 322 + std::fprintf(stderr, "TAPE sync %s\n", ok ? "ok" : "FAIL"); 323 +#endif 324 + if (ok) maincpu_.set_pc(after_sync); 325 } else if (pc == read_byte) { 326 const int value = tape_.next_byte(); 327 +#ifdef CRAM_DBG 328 + { 329 + static int n = 0; 330 + if (value < 0) std::fprintf(stderr, "TAPE fin de bande apres %d octets\n", n); 331 + else if (n++ < 48) std::fprintf(stderr, "TAPE byte %02x\n", unsigned(value)); 332 + } 333 +#endif 334 if (value < 0) return; // end of tape: let the BIOS time out 335 maincpu_.set_a(uint8_t(value)); 336 // RETS: the low byte of the return address is on top of the stack. 337 @@ -517,9 +723,28 @@ void Exelv::fast_load_hook() { 338 339 void Exelv::on_main_cycles(int cycles) { 340 main_cycles_ += uint64_t(cycles); 341 +#ifdef CRAM_DBG 342 + // Anneau des derniers PC : vidé quand on retombe sur l'entrée RESET/TRAP 0 343 + // ($FCDB) après que CROS64 a tourné, pour trouver l'instruction fautive. 344 + { 345 + static uint16_t ring[64]; 346 + static unsigned ri = 0; 347 + static bool armed = false; 348 + const uint16_t pc = maincpu_.pc(); 349 + ring[ri++ & 63] = pc; 350 + if (!armed && pc >= 0xd000 && pc <= 0xefff) { armed = true; std::fprintf(stderr, "ARMED pc=%04x\n", unsigned(pc)); } 351 + if (armed && pc >= 0xfcdb && pc <= 0xfd9f) { 352 + std::fprintf(stderr, "TRAP0/RESET atteint, 64 derniers PC :\n"); 353 + for (unsigned k = 0; k < 64; k++) 354 + std::fprintf(stderr, " %04x", unsigned(ring[(ri + k) & 63])); 355 + std::fprintf(stderr, "\n"); 356 + armed = false; 357 + } 358 + } 359 +#endif 360 if (tape_fast_ && tape_playing_ && tape_.loaded()) fast_load_hook(); 361 if (tape_playing_ && tape_.loaded() && in_tape_read()) tape_.advance(cycles); 362 - speech_.tick(cycles); 363 + /* exl100_patch_speech: pilotée par update(), ce tick(cycles CPU) la faisait tourner ~4,8x trop vite */ 364 const uint32_t cpu_clock = maincpu_.cpu_clock(); 365 audio_accumulator_ += int64_t(cycles) * kSampleRate; 366 while (audio_accumulator_ >= cpu_clock) { 367 @@ -601,6 +826,9 @@ void Exelv::tick_keyboard(int cpu_cycles) { 368 if (k_ch_byte_ < 2) { 369 k_channels_[0] = scan_key_channel(); 370 if (k_channels_[0] != 0xff && k_ch_byte_ == 0) { 371 +#ifdef CRAM_DBG 372 + std::fprintf(stderr, "KBD trame ch0=%02x ch1=%02x\n", unsigned(k_channels_[0]), unsigned(k_channels_[1])); 373 +#endif 374 if (k_channels_[1] == 0xff) k_channels_[1] = k_channels_[0]; 375 k_ch_bit_ = 0; 376 k_bit_num_ = false; 377 @@ -677,6 +905,22 @@ void Exelv::tick_keyboard(int cpu_cycles) { 378 } 379 380 void Exelv::run_frame() { 381 +#ifdef CRAM_AUTONAV 382 + { 383 + // Auto-navigation déterministe (test) : après l'init, Bas ×3 384 + // (-> Périphériques) puis Entrée ×2 (-> Exelmémoire -> CROS64). 385 + static int nf = 0; 386 + nf++; 387 + struct Act { int at; Key k; }; 388 + static const Act seq[] = { 389 + {560, Key::Down}, {610, Key::Down}, {660, Key::Down}, 390 + {730, Key::Enter}, {800, Key::Enter}, {870, Key::Enter}, 391 + {940, Key::Enter}, {1010, Key::Enter}, 392 + }; 393 + for (const Act& a : seq) 394 + if (nf >= a.at && nf < a.at + 3) inputs_.keys[size_t(a.k)] = true; 395 + } 396 +#endif 397 const uint32_t cpu_clock = maincpu_.cpu_clock(); 398 const int cycles_per_line = int(double(cpu_clock) / (kFramesPerSecond * kScanlines) + 0.5); 399 for (int line = 0; line < kScanlines; line++) { 400 @@ -696,6 +940,28 @@ void Exelv::run_frame() { 401 tick_keyboard(slice); 402 remain -= slice; 403 } 404 +#ifdef CRAM_DBG 405 + { 406 + // Anneau du PC par scanline (~10 instructions) : vidé la première fois que le PC 407 + // entre dans la zone reset/TRAP 0 ($FCDB-$FD9F) après que CROS64 a tourné. 408 + static uint16_t lring[512]; 409 + static unsigned lri = 0; 410 + static bool larmed = false, ldone = false; 411 + const uint16_t lpc = maincpu_.pc(); 412 + lring[lri++ & 511] = lpc; 413 + if (!larmed && lpc >= 0xd000 && lpc <= 0xefff) { 414 + larmed = true; 415 + std::fprintf(stderr, "LARMED pc=%04x\n", unsigned(lpc)); 416 + } 417 + if (larmed && !ldone && lpc >= 0xfcdb && lpc <= 0xfd9f) { 418 + ldone = true; 419 + std::fprintf(stderr, "LTRAP0: 512 derniers PC (par scanline) :\n"); 420 + for (unsigned k = 0; k < 512; k++) 421 + std::fprintf(stderr, " %04x", unsigned(lring[(lri + k) & 511])); 422 + std::fprintf(stderr, "\n"); 423 + } 424 + } 425 +#endif 426 // Without an I/O CPU ROM, post the "I/O CPU initialized" byte ($08) 427 // the main CPU waits for. The INT1 handler reads it with PA.0 low, 428 // raises PB.1 and waits for PA.0 to follow (tms7020_portb_w). 429 @@ -709,6 +975,12 @@ void Exelv::run_frame() { 430 } 431 } 432 } 433 +#ifdef CRAM_PC 434 + if (cram_present_) { 435 + static int fr = 0; 436 + std::fprintf(stderr, "FR%04d PC=%04x SUB=%04x\n", fr++, unsigned(maincpu_.pc()), unsigned(subcpu_.pc())); 437 + } 438 +#endif 439 // Write a SAVE out once the BIOS has left the tape routine for a second. 440 if (tape_.recording()) { 441 if (in_tape_read()) { 442 diff --git a/src/drivers/computers/exelv.h b/src/drivers/computers/exelv.h 443 index b3857e8..89202ab 100644 444 --- a/src/drivers/computers/exelv.h 445 +++ b/src/drivers/computers/exelv.h 446 @@ -6,6 +6,7 @@ 447 #include <vector> 448 449 #include "core/machine.h" 450 +#include "drivers/computers/exelv_disk.h" 451 #include "drivers/computers/exelv_tape.h" 452 #include "cpu/tms7000.h" 453 #include "sound/tms5220.h" 454 @@ -72,6 +73,32 @@ public: 455 } 456 457 bool bios_loaded() const { return bios_loaded_; } 458 + // Exelmémoire : contenu de la CRAM (16 Ko) pour la persistance côté page 459 + // (localStorage), comme le fichier .cram de DCExel. 460 + bool cram_present() const { return cram_present_; } 461 + const std::vector<uint8_t>& cram_data() const { return cram_ram_; } 462 + bool cram_load(const std::vector<uint8_t>& data) { 463 + if (!cram_present_ || data.size() != cram_ram_.size()) return false; 464 + cram_ram_ = data; 465 + cram_dirty_ = false; 466 + return true; 467 + } 468 + // Vrai si la CRAM a changé depuis le dernier appel (et remet le drapeau à zéro). 469 + bool cram_take_dirty() { 470 + const bool dirty = cram_dirty_; 471 + cram_dirty_ = false; 472 + return dirty; 473 + } 474 + // Exeldisk : présent si /roms/exeldisk.rom est là au démarrage ; image .fd montée par 475 + // load_media (--tape x.fd) ou disk_mount (page web), persistance comme la CRAM. 476 + bool disk_present() const { return disk_present_; } 477 + // L'Exeldisk n'est vu par le BIOS que si une disquette est montée : sans elle, l'EXL 100 478 + // démarre sur son logo (le ROM d'amorçage demanderait « Insert system disk »). 479 + bool disk_active() const { return disk_present_ && disk_.mounted(); } 480 + bool disk_mounted() const { return disk_.mounted(); } 481 + const std::vector<uint8_t>& disk_image() const { return disk_.image(); } 482 + bool disk_mount(std::vector<uint8_t> data) { return disk_.mount(std::move(data)); } 483 + bool disk_take_dirty() { return disk_.take_dirty(); } 484 bool sub_present() const { return sub_present_; } 485 uint16_t debug_pc() const { return maincpu_.pc(); } 486 uint8_t debug_a() const { return maincpu_.a(); } 487 @@ -125,6 +152,24 @@ private: 488 std::array<uint8_t, 0x800> ram_{}; 489 std::vector<uint8_t> cart_; 490 std::vector<uint8_t> system_rom_; 491 + // Exelmémoire (CRAM) — chantier. Page 1 : OS « CROS64 » à $D000-$EFFF 492 + // (ROM), RAM statique sauvegardée à $8000-$BFFF, octet de présence $A5 à 493 + // $C800 (détection boot $FDC6, emplacement B=0 : page 1, sig $A5, lance 494 + // $8001). Non déployé tant que non vérifié. 495 + std::vector<uint8_t> cram_os_; 496 + std::vector<uint8_t> cram_ram_; 497 + bool cram_present_ = false; 498 + // RAM $0200-$0FFF visible en pages 0/1 (fenêtre cartouche vide) : CROS64 y met sa pile 499 + // logicielle (R16 = $02D7), ses variables ($0260-$02D8) et le volume A ($02DA+). 500 + std::vector<uint8_t> cram_pram_; 501 + bool cram_dirty_ = false; // CRAM modifiée depuis la dernière sauvegarde de la page 502 + // Exeldisk : ROM d'amorçage 8 Ko ($8000-$9FFF au démarrage, fenêtre $C800-$CFFF = ROM+$0800), 503 + // RAM 31,5 Ko à $0200-$7FFF (à la place de la cartouche), RAM DOS 8 Ko à $D000-$EFFF. 504 + ExelDisk disk_; 505 + std::vector<uint8_t> disk_rom_; 506 + std::vector<uint8_t> disk_ram_; 507 + std::vector<uint8_t> dos_ram_; 508 + bool disk_present_ = false; 509 510 uint8_t tms7020_portb_ = 0; 511 uint8_t tms7041_portb_ = 0; 512 diff --git a/src/drivers/computers/exelv_disk.cpp b/src/drivers/computers/exelv_disk.cpp 513 new file mode 100644 514 index 0000000..088411c 515 --- /dev/null 516 +++ b/src/drivers/computers/exelv_disk.cpp 517 @@ -0,0 +1,235 @@ 518 +#include "drivers/computers/exelv_disk.h" 519 + 520 +#include <algorithm> 521 + 522 +namespace dsp { 523 + 524 +namespace { 525 +constexpr uint8_t kBusy = 0x01; 526 +constexpr uint8_t kDrq = 0x02; // type II/III ; INDEX en type I 527 +constexpr uint8_t kLost = 0x04; // type II/III ; TRK0 en type I 528 +constexpr uint8_t kCrc = 0x08; 529 +constexpr uint8_t kRnf = 0x10; 530 +constexpr uint8_t kSpinUp = 0x20; // type I ; RECORD TYPE en type II 531 +constexpr uint8_t kWriteProt = 0x40; 532 +constexpr uint8_t kMotor = 0x80; 533 +} // namespace 534 + 535 +void ExelDisk::reset() { 536 + control_ = 0; 537 + latch_ = 0xff; 538 + status_ = 0; 539 + track_ = 0; 540 + sector_ = 0; 541 + data_ = 0; 542 + drq_ = intrq_ = false; 543 + type1_ = true; 544 + op_ = Op::None; 545 + buffer_.clear(); 546 + pos_ = 0; 547 +} 548 + 549 +bool ExelDisk::mount(std::vector<uint8_t> image) { 550 + if (image.size() != kImageSize) return false; 551 + image_ = std::move(image); 552 + dirty_ = false; 553 + return true; 554 +} 555 + 556 +uint8_t ExelDisk::read_status_port() const { 557 + // bit0 : disquette changée / absente (le ROM répond « drive not ready » si elle est à 1) ; 558 + // bit1 : lecteur sélectionné ; bit2 : prêt ; bit3 : INTRQ du 1770. 559 + uint8_t v = 0; 560 + if (!mounted()) v |= 0x01; 561 + if (control_ & 0x0c) v |= 0x02; 562 + if (mounted() && (control_ & 0x0c)) v |= 0x04; 563 + if (intrq_) v |= 0x08; 564 + return v; 565 +} 566 + 567 +size_t ExelDisk::sector_offset(uint8_t track, uint8_t side, uint8_t sector) const { 568 + if (track >= kTracks || side >= kSides || sector >= kSectors) return kImageSize; 569 + return ((size_t(track) * kSides + side) * kSectors + sector) * kSectorSize; 570 +} 571 + 572 +void ExelDisk::finish(uint8_t status_bits) { 573 + status_ = uint8_t((status_ & ~kBusy) | status_bits); 574 + if (mounted()) status_ |= kMotor; 575 + drq_ = false; 576 + intrq_ = true; 577 + op_ = Op::None; 578 +} 579 + 580 +uint8_t ExelDisk::start_read() { 581 + latch_ = read_register(); 582 + return latch_; 583 +} 584 + 585 +uint8_t ExelDisk::read_register() { 586 + switch (control_ & 0x03) { 587 + case 0: { 588 + // Lire l'état acquitte INTRQ. En type I, bit1 = index (on le fait battre), bit2 = piste 0. 589 + intrq_ = false; 590 + uint8_t s = status_; 591 + if (type1_) { 592 + s = uint8_t(s & ~(kDrq | kLost)); 593 + if (track_ == 0) s |= kLost; 594 + if (((index_++) & 0x0f) == 0) s |= kDrq; 595 + } else if (drq_) { 596 + s |= kDrq; 597 + } 598 + return s; 599 + } 600 + case 1: return track_; 601 + case 2: return sector_; 602 + default: 603 + if (op_ == Op::ReadSector || op_ == Op::ReadAddress) { 604 + if (pos_ < buffer_.size()) data_ = buffer_[pos_++]; 605 + if (pos_ >= buffer_.size()) finish(0); 606 + } 607 + return data_; 608 + } 609 +} 610 + 611 +void ExelDisk::write_register(uint8_t v) { 612 + switch (control_ & 0x03) { 613 + case 0: command(v); break; 614 + case 1: track_ = v; break; 615 + case 2: sector_ = v; break; 616 + default: 617 + data_ = v; 618 + if (op_ == Op::WriteSector) { 619 + if (pos_ < buffer_.size()) buffer_[pos_++] = v; 620 + if (pos_ >= buffer_.size()) { 621 + if (target_ + kSectorSize <= image_.size()) { 622 + std::copy(buffer_.begin(), buffer_.end(), image_.begin() + long(target_)); 623 + dirty_ = true; 624 + } 625 + finish(0); 626 + } 627 + } else if (op_ == Op::WriteTrack) { 628 + write_track_byte(v); 629 + } 630 + break; 631 + } 632 +} 633 + 634 +void ExelDisk::command(uint8_t cmd) { 635 + intrq_ = false; 636 + const uint8_t side = (control_ & 0x80) ? 1 : 0; 637 + // Type IV : interruption forcée (fin de l'ordre en cours). 638 + if ((cmd & 0xf0) == 0xd0) { 639 + op_ = Op::None; 640 + drq_ = false; 641 + status_ = uint8_t(status_ & ~(kBusy | kDrq)); 642 + if (cmd & 0x0f) intrq_ = true; 643 + return; 644 + } 645 + if (!mounted()) { 646 + type1_ = (cmd & 0x80) == 0; 647 + status_ = kBusy; 648 + finish(kRnf); 649 + return; 650 + } 651 + if (!(cmd & 0x80)) { 652 + // Type I : restore / seek / step. Tout se fait d'un coup. 653 + type1_ = true; 654 + status_ = kBusy; 655 + switch (cmd >> 5) { 656 + case 0: 657 + if (cmd & 0x10) track_ = data_; // seek 658 + else track_ = 0; // restore 659 + break; 660 + case 1: break; // step (direction inconnue : rien) 661 + case 2: if (cmd & 0x10) track_ = uint8_t(track_ + 1); break; // step in 662 + case 3: if (cmd & 0x10 && track_) track_--; break; // step out 663 + } 664 + if (track_ >= kTracks) track_ = uint8_t(kTracks - 1); 665 + finish(kSpinUp); 666 + return; 667 + } 668 + type1_ = false; 669 + status_ = kBusy; 670 + switch (cmd >> 4) { 671 + case 0x8: case 0x9: { // read sector 672 + const size_t off = sector_offset(track_, side, sector_); 673 + if (off >= image_.size()) { finish(kRnf); return; } 674 + buffer_.assign(image_.begin() + long(off), image_.begin() + long(off + kSectorSize)); 675 + pos_ = 0; 676 + op_ = Op::ReadSector; 677 + drq_ = true; 678 + return; 679 + } 680 + case 0xa: case 0xb: { // write sector 681 + target_ = sector_offset(track_, side, sector_); 682 + if (target_ >= image_.size()) { finish(kRnf); return; } 683 + buffer_.assign(kSectorSize, 0); 684 + pos_ = 0; 685 + op_ = Op::WriteSector; 686 + drq_ = true; 687 + return; 688 + } 689 + case 0xc: { // read address : piste, face, secteur, taille (1 = 256), CRC 690 + buffer_ = {track_, side, sector_, 1, 0, 0}; 691 + pos_ = 0; 692 + op_ = Op::ReadAddress; 693 + drq_ = true; 694 + return; 695 + } 696 + case 0xe: { // read track : pas utilisé par le ROM ; on rend la piste brute des secteurs 697 + buffer_.clear(); 698 + for (size_t s = 0; s < kSectors; s++) { 699 + const size_t off = sector_offset(track_, side, uint8_t(s)); 700 + buffer_.insert(buffer_.end(), image_.begin() + long(off), image_.begin() + long(off + kSectorSize)); 701 + } 702 + pos_ = 0; 703 + op_ = Op::ReadSector; 704 + drq_ = true; 705 + return; 706 + } 707 + case 0xf: // write track (formatage) : on décode les champs ID/données du flux 708 + op_ = Op::WriteTrack; 709 + wt_count_ = 0; 710 + wt_state_ = 0; 711 + wt_idx_ = 0; 712 + drq_ = true; 713 + return; 714 + default: 715 + finish(kRnf); 716 + return; 717 + } 718 +} 719 + 720 +void ExelDisk::write_track_byte(uint8_t v) { 721 + // Flux de formatage du ROM : ... $F5 $F5 $F5 $FE piste face secteur $01 $F7 ... $F5 $F5 $F5 $FB 722 + // données x256 $F7 ... ; $F5 = $A1 (sync), $F7 = CRC généré. 723 + switch (wt_state_) { 724 + case 0: // en attente d'un marqueur 725 + if (v == 0xfe) { wt_state_ = 1; wt_idx_ = 0; } 726 + else if (v == 0xfb) { 727 + const size_t off = sector_offset(wt_id_[0], wt_id_[1] & 1, wt_id_[2]); 728 + target_ = off; 729 + buffer_.assign(kSectorSize, 0); 730 + pos_ = 0; 731 + wt_state_ = 2; 732 + } 733 + break; 734 + case 1: // champ ID : piste, face, secteur, taille 735 + wt_id_[wt_idx_++] = v; 736 + if (wt_idx_ >= 4) wt_state_ = 0; 737 + break; 738 + case 2: // champ de données 739 + if (pos_ < buffer_.size()) buffer_[pos_++] = v; 740 + if (pos_ >= buffer_.size()) { 741 + if (target_ + kSectorSize <= image_.size()) { 742 + std::copy(buffer_.begin(), buffer_.end(), image_.begin() + long(target_)); 743 + dirty_ = true; 744 + } 745 + wt_state_ = 0; 746 + } 747 + break; 748 + } 749 + if (++wt_count_ >= kTrackBytes) finish(0); // impulsion d'index : fin de piste 750 +} 751 + 752 +} // namespace dsp 753 diff --git a/src/drivers/computers/exelv_disk.h b/src/drivers/computers/exelv_disk.h 754 new file mode 100644 755 index 0000000..94dc47c 756 --- /dev/null 757 +++ b/src/drivers/computers/exelv_disk.h 758 @@ -0,0 +1,91 @@ 759 +#pragma once 760 + 761 +#include <cstdint> 762 +#include <vector> 763 + 764 +namespace dsp { 765 + 766 +// Exeldisk : lecteur de disquettes 3"1/2 de l'EXL 100 (et de l'EXELTEL), piloté par un 767 +// WD1770 derrière quatre registres du fichier périphérique (d'après EXELDOS.equ du 768 +// devkit Jester et la ROM d'amorçage exeldisk.rom) : 769 +// P64 $0140 lecture : état du lecteur (bit0 disquette changée / absente, bit1 lecteur 770 +// sélectionné, bit2 prêt, bit3 INTRQ du 1770) ; écriture : verrou DSKCTL 771 +// (bits 0-1 registre du 1770, bit2/3 lecteur 1/2, bit4 $8000 = CRAM(1)/ROM 772 +// d'amorçage(0), bit5 $D000 = COS(1)/RAM DOS(0), bit6 $0200 = cartouche(1)/ 773 +// RAM(0), bit7 face). 774 +// P65 $0141 écriture du registre du 1770 choisi par DSKCTL. 775 +// P66 $0142 lecture : lance la lecture du registre (verrouillée) ; écriture : double densité. 776 +// P67 $0143 lecture : rend l'octet verrouillé ; écriture : simple densité. 777 +// DRQ du 1770 arrive sur le bit 1 du port A du 7020. 778 +// 779 +// Image .fd (DCExel / webmo) : 80 pistes x 2 faces x 16 secteurs x 256 octets = 655 360 780 +// octets, à plat (piste, face, secteur), secteurs numérotés 0-15, double densité MFM. 781 +// Le ROM d'amorçage lit les secteurs 4-15 et 32-47 (le DOS) en $D000-$EFFF. 782 +class ExelDisk { 783 +public: 784 + static constexpr size_t kTracks = 80; 785 + static constexpr size_t kSides = 2; 786 + static constexpr size_t kSectors = 16; 787 + static constexpr size_t kSectorSize = 256; 788 + static constexpr size_t kImageSize = kTracks * kSides * kSectors * kSectorSize; 789 + // Longueur d'une piste en double densité (300 tr/min, 250 kbit/s) : fin de WRITE TRACK. 790 + static constexpr int kTrackBytes = 6250; 791 + 792 + void reset(); 793 + bool mount(std::vector<uint8_t> image); 794 + bool mounted() const { return !image_.empty(); } 795 + const std::vector<uint8_t>& image() const { return image_; } 796 + bool take_dirty() { 797 + const bool d = dirty_; 798 + dirty_ = false; 799 + return d; 800 + } 801 + 802 + // Verrou DSKCTL et ses lignes de sélection mémoire. 803 + uint8_t control() const { return control_; } 804 + void write_control(uint8_t v) { control_ = v; } 805 + bool boot_rom_at_8000() const { return !(control_ & 0x10); } 806 + bool cos_at_d000() const { return (control_ & 0x20) != 0; } 807 + bool cart_at_0200() const { return (control_ & 0x40) != 0; } 808 + 809 + uint8_t read_status_port() const; // P64 810 + void write_register(uint8_t v); // P65 811 + uint8_t start_read(); // P66 (verrouille le registre) 812 + uint8_t finish_read() const { return latch_; } // P67 813 + bool drq() const { return drq_; } 814 + bool intrq() const { return intrq_; } 815 + 816 +private: 817 + uint8_t read_register(); 818 + void command(uint8_t cmd); 819 + size_t sector_offset(uint8_t track, uint8_t side, uint8_t sector) const; 820 + void finish(uint8_t status_bits); 821 + void write_track_byte(uint8_t v); 822 + 823 + std::vector<uint8_t> image_; 824 + bool dirty_ = false; 825 + uint8_t control_ = 0; 826 + uint8_t latch_ = 0xff; 827 + // Registres du 1770. 828 + uint8_t status_ = 0; 829 + uint8_t track_ = 0; 830 + uint8_t sector_ = 0; 831 + uint8_t data_ = 0; 832 + bool drq_ = false; 833 + bool intrq_ = false; 834 + bool type1_ = true; // dernier ordre de type I : bits d'état TRK0/INDEX 835 + int index_ = 0; // impulsion d'index simulée (bit 1 des ordres de type I) 836 + // Transfert en cours. 837 + enum class Op { None, ReadSector, WriteSector, ReadAddress, WriteTrack }; 838 + Op op_ = Op::None; 839 + std::vector<uint8_t> buffer_; 840 + size_t pos_ = 0; 841 + size_t target_ = 0; // offset dans l'image du secteur en écriture 842 + // WRITE TRACK : analyse du flux (champs ID $FE et données $FB). 843 + int wt_count_ = 0; 844 + int wt_state_ = 0; 845 + uint8_t wt_id_[4] = {0, 0, 0, 0}; 846 + int wt_idx_ = 0; 847 +}; 848 + 849 +} // namespace dsp 850 diff --git a/src/main.cpp b/src/main.cpp 851 index 6525a1c..ed678ff 100644 852 --- a/src/main.cpp 853 +++ b/src/main.cpp 854 @@ -15,6 +15,8 @@ 855 #include "core/machine.h" 856 857 #include "frontend/machine_registry.h" 858 +#include "drivers/computers/exelv.h" 859 +#include <cstring> 860 861 #include "core/embedded_media.h" 862 #include "frontend/sdl_app.h" 863 @@ -358,6 +360,101 @@ int main(int argc, char** argv) { return run_dsp(argc, argv); } 864 // The page starts the embedded media when its URL names no game. 865 extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_has_embedded() { return dsp_embedded_file_count > 0 ? 1 : 0; } 866 867 +// Exelmémoire (driver exelv) : la page sauve et restaure la CRAM (16 Ko) en 868 +// localStorage, en base64, comme le fichier .cram de DCExel. Base64 plutôt 869 +// qu'un pointeur dans le tas : seuls ccall/UTF8ToString sont exportés. 870 +static const char kCramB64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 871 + 872 +static std::string cram_b64_encode(const std::vector<uint8_t>& in) { 873 + std::string out; 874 + out.reserve((in.size() + 2) / 3 * 4); 875 + for (size_t i = 0; i < in.size(); i += 3) { 876 + const uint32_t n = (uint32_t(in[i]) << 16) | 877 + (i + 1 < in.size() ? uint32_t(in[i + 1]) << 8 : 0u) | 878 + (i + 2 < in.size() ? uint32_t(in[i + 2]) : 0u); 879 + out += kCramB64[(n >> 18) & 63]; 880 + out += kCramB64[(n >> 12) & 63]; 881 + out += i + 1 < in.size() ? kCramB64[(n >> 6) & 63] : '='; 882 + out += i + 2 < in.size() ? kCramB64[n & 63] : '='; 883 + } 884 + return out; 885 +} 886 + 887 +static bool cram_b64_decode(const char* s, std::vector<uint8_t>& out) { 888 + out.clear(); 889 + uint32_t acc = 0; 890 + int bits = 0; 891 + for (; *s && *s != '='; ++s) { 892 + const char* p = std::strchr(kCramB64, *s); 893 + if (!p) return false; 894 + acc = (acc << 6) | uint32_t(p - kCramB64); 895 + bits += 6; 896 + if (bits >= 8) { 897 + bits -= 8; 898 + out.push_back(uint8_t((acc >> bits) & 0xff)); 899 + } 900 + } 901 + return true; 902 +} 903 + 904 +static dsp::Exelv* web_exelv() { return dynamic_cast<dsp::Exelv*>(g_web_machine.get()); } 905 + 906 +extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_cram_present() { 907 + dsp::Exelv* m = web_exelv(); 908 + return m && m->cram_present() ? 1 : 0; 909 +} 910 + 911 +// Vrai si la CRAM a changé depuis le dernier appel. 912 +extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_cram_dirty() { 913 + dsp::Exelv* m = web_exelv(); 914 + return m && m->cram_take_dirty() ? 1 : 0; 915 +} 916 + 917 +extern "C" EMSCRIPTEN_KEEPALIVE const char* dsp_web_cram_save() { 918 + static std::string b64; 919 + dsp::Exelv* m = web_exelv(); 920 + b64 = m && m->cram_present() ? cram_b64_encode(m->cram_data()) : std::string(); 921 + return b64.c_str(); 922 +} 923 + 924 +extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_cram_load(const char* b64) { 925 + dsp::Exelv* m = web_exelv(); 926 + std::vector<uint8_t> data; 927 + return m && b64 && cram_b64_decode(b64, data) && m->cram_load(data) ? 1 : 0; 928 +} 929 + 930 +// Exeldisk : même mécanique que la CRAM (image .fd de 640 Ko en base64 côté page). 931 +extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_disk_present() { 932 + dsp::Exelv* m = web_exelv(); 933 + return m && m->disk_present() ? (m->disk_mounted() ? 2 : 1) : 0; 934 +} 935 + 936 +extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_disk_dirty() { 937 + dsp::Exelv* m = web_exelv(); 938 + return m && m->disk_take_dirty() ? 1 : 0; 939 +} 940 + 941 +extern "C" EMSCRIPTEN_KEEPALIVE const char* dsp_web_disk_save() { 942 + static std::string b64; 943 + dsp::Exelv* m = web_exelv(); 944 + b64 = m && m->disk_mounted() ? cram_b64_encode(m->disk_image()) : std::string(); 945 + return b64.c_str(); 946 +} 947 + 948 +extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_disk_load(const char* b64) { 949 + dsp::Exelv* m = web_exelv(); 950 + std::vector<uint8_t> data; 951 + return m && b64 && cram_b64_decode(b64, data) && m->disk_mount(std::move(data)) ? 1 : 0; 952 +} 953 + 954 +// Cassette changée à chaud (face B, cassette 2…) : la machine continue, le BIOS lira la nouvelle bande 955 +// au prochain chargement. 956 +extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_tape_load(const char* path) { 957 + dsp::Exelv* m = web_exelv(); 958 + std::string error; 959 + return m && path && m->load_media(path, &error) ? 1 : 0; 960 +} 961 + 962 extern "C" EMSCRIPTEN_KEEPALIVE int dsp_web_start(const char* lines) { 963 static std::vector<std::string> args; 964 static std::vector<char*> argv; 965 diff --git a/src/sound/mpeg_audio.cpp b/src/sound/mpeg_audio.cpp 966 index ab4d563..7f2f930 100644 967 --- a/src/sound/mpeg_audio.cpp 968 +++ b/src/sound/mpeg_audio.cpp 969 @@ -11,6 +11,7 @@ 970 #include <cmath> 971 #include <cassert> 972 #include <cstring> 973 +#include <cstdlib> 974 975 976 977 diff --git a/src/sound/tms5220.cpp b/src/sound/tms5220.cpp 978 index e85a3d3..8ee3d1a 100644 979 --- a/src/sound/tms5220.cpp 980 +++ b/src/sound/tms5220.cpp 981 @@ -1,3 +1,4 @@ 982 +#include <cstdio> 983 #include "sound/tms5220.h" 984 985 #include <algorithm> 986 @@ -123,6 +124,9 @@ void Tms5220::chip_reset() { 987 void Tms5220::set_interrupt_state(bool state) { 988 if (state == irq_pin_) return; 989 irq_pin_ = state; 990 +#ifdef CRAM_DBG 991 + std::fprintf(stderr, "T5220 int=%d\n", state ? 1 : 0); 992 +#endif 993 if (irq_cb_) irq_cb_(state); 994 } 995 996 @@ -172,6 +176,9 @@ void Tms5220::data_write(uint8_t value) { 997 } 998 999 void Tms5220::process_command(uint8_t cmd) { 1000 +#ifdef CRAM_DBG 1001 + std::fprintf(stderr, "T5220 cmd=%02x\n", unsigned(cmd)); 1002 +#endif 1003 switch (cmd & 0x70) { 1004 case 0x00: 1005 case 0x20: // set rate on the TMS5220C, NOP otherwise 1006 @@ -320,6 +327,9 @@ int Tms5220::read_bits(int count) { 1007 1008 if (!ddis_) { 1009 // No VSM attached: the bits float high, which decodes as a stop frame. 1010 +#ifdef CRAM_DBG 1011 + { static int n = 0; if (n++ < 8) std::fprintf(stderr, "T5220 read_bits(%d) sans VSM\n", count); } 1012 +#endif 1013 return (1 << count) - 1; 1014 } 1015 1016 diff --git a/src/video/tms3556.cpp b/src/video/tms3556.cpp 1017 index 288aa7e..49487a3 100644 1018 --- a/src/video/tms3556.cpp 1019 +++ b/src/video/tms3556.cpp 1020 @@ -142,7 +142,15 @@ void Tms3556::draw_line_text_common(uint32_t* ln) { 1021 uint16_t patterntbl_base[4]; 1022 for (int i = 0; i < 4; i++) patterntbl_base[i] = address_regs_[i + 3]; 1023 1024 - const uint32_t border = rgb3(uint8_t(bg_color_)); 1025 + // Attributs « série » du TMS3556 (d'après MAME tms3556.cpp) : la couleur de 1026 + // fond est portée par des cellules délimiteur (code caractère $20), pas par un 1027 + // fond global. En vidéo inverse le premier plan devient ce fond de zone ; c'est 1028 + // ce qui manquait (d'où le noir sur noir des menus, p. ex. Exeldrums). 1029 + const uint8_t margin_color = uint8_t((control_regs_[7] >> 5) & 0x7); // VDP_CM4 1030 + uint8_t zone_bg = margin_color; 1031 + bool zone_masked = (control_regs_[7] & 0x08) != 0; 1032 + 1033 + const uint32_t border = rgb3(margin_color); 1034 int xx = 0; 1035 for (; xx < kLeftBorder; xx++) *ln++ = border; 1036 1037 @@ -151,32 +159,39 @@ void Tms3556::draw_line_text_common(uint32_t* ln) { 1038 for (int x = 0; x < 40; x++) { 1039 const int name_hi = read_byte(uint16_t(nametbl_base + name_offset)); 1040 const int name_lo = read_byte(uint16_t(nametbl_base + name_offset + 1)); 1041 - const int pattern_ix = ((name_hi >> 2) & 2) | ((name_hi >> 4) & 1); 1042 - const bool alphanumeric = (pattern_ix < 2) || ((pattern_ix == 3) && !(control_regs_[7] & 0x08)); 1043 - uint8_t fg = uint8_t((name_hi >> 5) & 0x7); 1044 + uint8_t fg; 1045 uint8_t bg; 1046 int dbl_w = 0; 1047 int dbl_h = 0; 1048 + uint8_t pattern; 1049 + if ((name_lo & 0x7f) == 0x20) { 1050 + // Délimiteur : fixe le fond de la zone qui suit, affiché dans sa couleur. 1051 + fg = uint8_t((name_hi >> 5) & 0x7); 1052 + if (zone_masked && (control_regs_[5] & 0x20)) fg = zone_bg; 1053 + zone_bg = uint8_t(name_hi & 0x7); 1054 + zone_masked = (name_hi & 0x08) != 0; 1055 + bg = zone_bg; 1056 + pattern = 0xff; 1057 + if (char_line_counter_ == 0) dbl_h_phase_[x] = 0; 1058 + } else { 1059 + const int pattern_ix = ((name_hi >> 2) & 2) | ((name_hi >> 4) & 1); 1060 + const bool alphanumeric = (pattern_ix < 2) || ((pattern_ix == 3) && !(control_regs_[5] & 0x08)); 1061 + fg = uint8_t((name_hi >> 5) & 0x7); 1062 if (alphanumeric) { 1063 if (name_hi & 4) { 1064 bg = fg; 1065 - fg = uint8_t(bg_color_); 1066 + fg = zone_bg; 1067 } else { 1068 - bg = uint8_t(bg_color_); 1069 + bg = zone_bg; 1070 } 1071 - // Software never pairs inverse video with double width: EXELTEL 1072 - // highlights menu entries with $1E and Capitaine Menkar draws 1073 - // $1F cells, both inverse with bit 1 set and no duplicated 1074 - // characters, which double width (left half / right half of 1075 - // consecutive cells) needs. Only honour bit 1 on normal video. 1076 - dbl_w = (name_hi & 0x4) ? 0 : (name_hi & 0x2); 1077 + dbl_w = name_hi & 0x2; 1078 dbl_h = name_hi & 0x1; 1079 } else { 1080 bg = uint8_t(name_hi & 0x7); 1081 + zone_bg = bg; // le fond mosaïque est un attribut série 1082 } 1083 if ((name_lo & 0x80) && blink_) fg = bg; 1084 1085 - uint8_t pattern; 1086 if (!dbl_h) { 1087 pattern = read_byte(uint16_t(patterntbl_base[pattern_ix] + (name_lo & 0x7f) + 1088 128 * char_line_counter_)); 1089 @@ -190,6 +205,8 @@ void Tms3556::draw_line_text_common(uint32_t* ln) { 1090 128 * (char_line_counter_ >> 1))); 1091 if (char_line_counter_ == 0) dbl_h_phase_[x] = !dbl_h_phase_[x]; 1092 } 1093 + if (zone_masked && (control_regs_[5] & 0x20)) pattern = 0; // zone masquée : espaces 1094 + } 1095 1096 if (!dbl_w) { 1097 for (int bit = 0; bit < 8; bit++) {