exl100

Émulateur Exelvision EXL 100 (1984) dans le navigateur : dsp-cpp en WebAssembly, cartouches et cassettes BASIC
git clone https://git.schurger.org/exl100.git
Log | Files | Refs | README

commit 603d9cba2e900619a4483b204eac4184d582c8e1
parent 7207ae1c02403917ef9066ba08756055cc8f4d69
Author: Jean Schurger <jean@schurger.org>
Date:   Wed,  4 Nov 2026 16:49:57 -0500

Couleurs TMS3556 : fond de zone par délimiteurs (d'après MAME)

Le port dsp-cpp du VDP TMS3556 utilisait un fond global au lieu du fond de zone
porté par des cellules délimiteur (code $20), et testait le mode alphanumérique
sur le mauvais registre. En vidéo inverse le premier plan doit devenir le fond de
zone, pas le fond global — d'où le noir sur noir des menus (Exeldrums) et les
couleurs fausses (Imagix). draw_line_text_common réécrite d'après MAME
tms3556.cpp (délimiteurs, zone_bg, zones masquées). Corrige Exeldrums (surbrillance
verte) et Imagix (titre correct) ; Capitaine Menkar, Tennis, Guppy, ExelBASIC
inchangés. Correctifs source regroupés dans patches/exl100.patch (appliqué par
git apply au lieu des sed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyJB9yqUAYm3wRQi4uytj9

Diffstat:
MMakefile | 15+++++----------
Apatches/exl100.patch | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 112 insertions(+), 10 deletions(-)

diff --git a/Makefile b/Makefile @@ -39,16 +39,11 @@ build: upstream cartridges web/index.html web/index.html: $(wildcard roms/*) roms/cartridges/titles.json web/shell.html.in Makefile cp web/shell.html.in upstream/web/shell.html.in - # Correctif de portabilité Emscripten : mpeg_audio.cpp (musique Sega Model 3, hors sujet ici) - # appelle abort() sans inclure <cstdlib>, que la libc++ d'Emscripten ne fournit pas en transitif. - grep -q '<cstdlib>' upstream/src/sound/mpeg_audio.cpp || \ - sed -i 's|#include <cstring>|#include <cstring>\n#include <cstdlib>|' upstream/src/sound/mpeg_audio.cpp - # Correctif son (parole trop rapide) : dans on_main_cycles la puce TMS5220 était avancée deux - # fois — par speech_.tick(cycles CPU) ET par speech_.update() (qui l'avance déjà au bon rythme - # 640 kHz). Le tick redondant la faisait tourner ~4,8x trop vite. On le retire ; io_ready reste - # décompté par les ticks-horloge de update() (16 horloges-puce ≈ 25 µs, le vrai timing). - grep -q 'exl100_patch_speech' upstream/src/drivers/computers/exelv.cpp || \ - sed -i 's|^ speech_.tick(cycles);| /* exl100_patch_speech: pilotée par update(), ce tick(cycles CPU) la faisait tourner ~4,8x trop vite */|' upstream/src/drivers/computers/exelv.cpp + # Correctifs source appliqués au clone amont (patches/exl100.patch) : portabilité Emscripten de + # mpeg_audio.cpp (abort sans <cstdlib>), son de la parole TMS5220 (double avance ~4,8x trop vite), + # et couleurs du TMS3556 (fond de zone par délimiteurs, d'après MAME : corrige le noir sur noir + # des menus comme Exeldrums et les attributs couleur). Voir patches/exl100.patch. + grep -q 'zone_bg' upstream/src/video/tms3556.cpp || git -C upstream apply $(CURDIR)/patches/exl100.patch $(DOCKER) emcmake cmake -S /work/upstream -B /work/build-web $(CMFLAGS) $(DOCKER) cmake --build /work/build-web -j --target dsp mkdir -p web diff --git a/patches/exl100.patch b/patches/exl100.patch @@ -0,0 +1,107 @@ +diff --git a/src/drivers/computers/exelv.cpp b/src/drivers/computers/exelv.cpp +index debcb94..69b25ef 100644 +--- a/src/drivers/computers/exelv.cpp ++++ b/src/drivers/computers/exelv.cpp +@@ -519,7 +519,7 @@ void Exelv::on_main_cycles(int cycles) { + main_cycles_ += uint64_t(cycles); + if (tape_fast_ && tape_playing_ && tape_.loaded()) fast_load_hook(); + if (tape_playing_ && tape_.loaded() && in_tape_read()) tape_.advance(cycles); +- speech_.tick(cycles); ++ /* exl100_patch_speech: pilotée par update(), ce tick(cycles CPU) la faisait tourner ~4,8x trop vite */ + const uint32_t cpu_clock = maincpu_.cpu_clock(); + audio_accumulator_ += int64_t(cycles) * kSampleRate; + while (audio_accumulator_ >= cpu_clock) { +diff --git a/src/sound/mpeg_audio.cpp b/src/sound/mpeg_audio.cpp +index ab4d563..7f2f930 100644 +--- a/src/sound/mpeg_audio.cpp ++++ b/src/sound/mpeg_audio.cpp +@@ -11,6 +11,7 @@ + #include <cmath> + #include <cassert> + #include <cstring> ++#include <cstdlib> + + + +diff --git a/src/video/tms3556.cpp b/src/video/tms3556.cpp +index 288aa7e..49487a3 100644 +--- a/src/video/tms3556.cpp ++++ b/src/video/tms3556.cpp +@@ -142,7 +142,15 @@ void Tms3556::draw_line_text_common(uint32_t* ln) { + uint16_t patterntbl_base[4]; + for (int i = 0; i < 4; i++) patterntbl_base[i] = address_regs_[i + 3]; + +- const uint32_t border = rgb3(uint8_t(bg_color_)); ++ // Attributs « série » du TMS3556 (d'après MAME tms3556.cpp) : la couleur de ++ // fond est portée par des cellules délimiteur (code caractère $20), pas par un ++ // fond global. En vidéo inverse le premier plan devient ce fond de zone ; c'est ++ // ce qui manquait (d'où le noir sur noir des menus, p. ex. Exeldrums). ++ const uint8_t margin_color = uint8_t((control_regs_[7] >> 5) & 0x7); // VDP_CM4 ++ uint8_t zone_bg = margin_color; ++ bool zone_masked = (control_regs_[7] & 0x08) != 0; ++ ++ const uint32_t border = rgb3(margin_color); + int xx = 0; + for (; xx < kLeftBorder; xx++) *ln++ = border; + +@@ -151,32 +159,39 @@ void Tms3556::draw_line_text_common(uint32_t* ln) { + for (int x = 0; x < 40; x++) { + const int name_hi = read_byte(uint16_t(nametbl_base + name_offset)); + const int name_lo = read_byte(uint16_t(nametbl_base + name_offset + 1)); +- const int pattern_ix = ((name_hi >> 2) & 2) | ((name_hi >> 4) & 1); +- const bool alphanumeric = (pattern_ix < 2) || ((pattern_ix == 3) && !(control_regs_[7] & 0x08)); +- uint8_t fg = uint8_t((name_hi >> 5) & 0x7); ++ uint8_t fg; + uint8_t bg; + int dbl_w = 0; + int dbl_h = 0; ++ uint8_t pattern; ++ if ((name_lo & 0x7f) == 0x20) { ++ // Délimiteur : fixe le fond de la zone qui suit, affiché dans sa couleur. ++ fg = uint8_t((name_hi >> 5) & 0x7); ++ if (zone_masked && (control_regs_[5] & 0x20)) fg = zone_bg; ++ zone_bg = uint8_t(name_hi & 0x7); ++ zone_masked = (name_hi & 0x08) != 0; ++ bg = zone_bg; ++ pattern = 0xff; ++ if (char_line_counter_ == 0) dbl_h_phase_[x] = 0; ++ } else { ++ const int pattern_ix = ((name_hi >> 2) & 2) | ((name_hi >> 4) & 1); ++ const bool alphanumeric = (pattern_ix < 2) || ((pattern_ix == 3) && !(control_regs_[5] & 0x08)); ++ fg = uint8_t((name_hi >> 5) & 0x7); + if (alphanumeric) { + if (name_hi & 4) { + bg = fg; +- fg = uint8_t(bg_color_); ++ fg = zone_bg; + } else { +- bg = uint8_t(bg_color_); ++ bg = zone_bg; + } +- // Software never pairs inverse video with double width: EXELTEL +- // highlights menu entries with $1E and Capitaine Menkar draws +- // $1F cells, both inverse with bit 1 set and no duplicated +- // characters, which double width (left half / right half of +- // consecutive cells) needs. Only honour bit 1 on normal video. +- dbl_w = (name_hi & 0x4) ? 0 : (name_hi & 0x2); ++ dbl_w = name_hi & 0x2; + dbl_h = name_hi & 0x1; + } else { + bg = uint8_t(name_hi & 0x7); ++ zone_bg = bg; // le fond mosaïque est un attribut série + } + if ((name_lo & 0x80) && blink_) fg = bg; + +- uint8_t pattern; + if (!dbl_h) { + pattern = read_byte(uint16_t(patterntbl_base[pattern_ix] + (name_lo & 0x7f) + + 128 * char_line_counter_)); +@@ -190,6 +205,8 @@ void Tms3556::draw_line_text_common(uint32_t* ln) { + 128 * (char_line_counter_ >> 1))); + if (char_line_counter_ == 0) dbl_h_phase_[x] = !dbl_h_phase_[x]; + } ++ if (zone_masked && (control_regs_[5] & 0x20)) pattern = 0; // zone masquée : espaces ++ } + + if (!dbl_w) { + for (int bit = 0; bit < 8; bit++) {