diff --git a/components/box-emu/example/main/box_emu_example.cpp b/components/box-emu/example/main/box_emu_example.cpp index 1cbbcd0a..6915b425 100644 --- a/components/box-emu/example/main/box_emu_example.cpp +++ b/components/box-emu/example/main/box_emu_example.cpp @@ -154,15 +154,15 @@ static size_t load_audio() { } // these are configured in the CMakeLists.txt file - extern const char wav_start[] asm("_binary_click_wav_start"); // cppcheck-suppress syntaxError - extern const char wav_end[] asm("_binary_click_wav_end"); // cppcheck-suppress syntaxError + extern const char wav_start[] asm("_binary_click_wav_start"); + extern const char wav_end[] asm("_binary_click_wav_end"); // -1 due to the size being 1 byte too large, I think because end is the byte // immediately after the last byte in the memory but I'm not sure - cmm 2022-08-20 // // Suppression as these are linker symbols and cppcheck doesn't know how to ensure // they are the same object - // cppcheck-suppress comparePointers + // cppcheck-suppress subtractPointers size_t wav_size = (wav_end - wav_start) - 1; FILE *fp = fmemopen((void *)wav_start, wav_size, "rb"); // read the file into the audio_bytes vector diff --git a/components/box-emu/idf_component.yml b/components/box-emu/idf_component.yml index 88c818f9..df13dc4b 100644 --- a/components/box-emu/idf_component.yml +++ b/components/box-emu/idf_component.yml @@ -1,16 +1,17 @@ ## IDF Component Manager Manifest File dependencies: - idf: ">=5.5" - espressif/esp_tinyusb: ">=2.0" + idf: '>=5.5' + espressif/esp_tinyusb: '>=2.0' lvgl/lvgl: '>=9.2.2' - espp/adc: ">=1.0" - espp/aw9523: ">=1.0" - espp/button: ">=1.0" - espp/drv2605: ">=1.0" - espp/esp-box: ">=1.0" - espp/event_manager: ">=1.0" - espp/max1704x: ">=1.0" - espp/mcp23x17: ">=1.0" - espp/task: ">=1.0" - espp/timer: ">=1.0" - espp/serialization: ">=1.0" + espp/adc: '>=1.0' + espp/aw9523: '>=1.0' + espp/button: '>=1.0' + espp/drv2605: '>=1.0' + espp/esp-box: '>=1.0' + espp/event_manager: '>=1.0' + espp/max1704x: '>=1.0' + espp/mcp23x17: '>=1.0' + espp/task: '>=1.0' + espp/timer: '>=1.0' + espp/serialization: '>=1.0' + espressif/usb: '>=1.4.1' diff --git a/components/box-emu/include/box-emu.hpp b/components/box-emu/include/box-emu.hpp index 66616270..e36865f4 100644 --- a/components/box-emu/include/box-emu.hpp +++ b/components/box-emu/include/box-emu.hpp @@ -7,7 +7,7 @@ #include #include -#include +// #include #include #include diff --git a/components/box-emu/src/box-emu.cpp b/components/box-emu/src/box-emu.cpp index 58daf0b9..edad023d 100644 --- a/components/box-emu/src/box-emu.cpp +++ b/components/box-emu/src/box-emu.cpp @@ -1,5 +1,7 @@ #include "box-emu.hpp" +#include + BoxEmu::BoxEmu() : espp::BaseComponent("BoxEmu") { detect(); } @@ -498,7 +500,7 @@ void BoxEmu::palette(const uint16_t *palette, size_t size) { palette_size_ = size; } -void IRAM_ATTR BoxEmu::push_frame(const void* frame) { +void BoxEmu::push_frame(const void* frame) { if (video_queue_ == nullptr) { logger_.error("video queue is null, make sure to call initialize_video() first!"); return; @@ -655,6 +657,7 @@ bool BoxEmu::initialize_usb() { .max_files = 5, .allocation_unit_size = 2 * 1024, // sector size is 512 bytes, this should be between sector size and (128 * sector size). Larger means higher read/write performance and higher overhead for small files. .disk_status_check_enable = false, // true if you see issues or are unmounted properly; slows down I/O + .use_one_fat = true, }; tinyusb_msc_fatfs_config_t config_msc = { @@ -773,7 +776,7 @@ bool BoxEmu::video_task_callback(std::mutex &m, std::condition_variable& cv, boo int num_lines = std::min(num_lines_to_write, lcd_height()-y); // memset the buffer to 0 memset(_buf, 0, lcd_width() * num_lines * sizeof(Pixel)); - box.write_lcd_frame(0, y + _y_offset, lcd_width(), num_lines, (uint8_t*)&_buf[0]); + box.write_lcd_frame(0, y + _y_offset, lcd_width(), num_lines, reinterpret_cast(&_buf[0])); } // now return @@ -782,21 +785,38 @@ bool BoxEmu::video_task_callback(std::mutex &m, std::condition_variable& cv, boo if (is_native()) { if (has_palette()) { + const bool palette_is_power_of_two = palette_size_ && ((palette_size_ & (palette_size_ - 1)) == 0); + const size_t palette_mask = palette_size_ - 1; for (int y=0; y(num_lines_to_write, display_height_-y); const uint8_t* _frame = (const uint8_t*)_frame_ptr; for (int i=0; i(&_buf[0])); } } else { // no palette @@ -806,16 +826,11 @@ bool BoxEmu::video_task_callback(std::mutex &m, std::condition_variable& cv, boo int num_lines = std::min(num_lines_to_write, display_height_-y); const uint16_t* _frame = (const uint16_t*)_frame_ptr; for (int i=0; i(&_buf[0])); } } } else { @@ -850,7 +865,7 @@ bool BoxEmu::video_task_callback(std::mutex &m, std::condition_variable& cv, boo _buf[dst_index + 1] = _palette[_frame[src_index + 1] % palette_size_]; } } - box.write_lcd_frame(0 + _x_offset, y, max_x, i, (uint8_t*)&_buf[0]); + box.write_lcd_frame(0 + _x_offset, y, max_x, i, reinterpret_cast(&_buf[0])); } } else { // no palette @@ -876,7 +891,7 @@ bool BoxEmu::video_task_callback(std::mutex &m, std::condition_variable& cv, boo _buf[dst_index + 1] = _frame[src_index + 1]; } } - box.write_lcd_frame(0 + _x_offset, y, max_x, i, (uint8_t*)&_buf[0]); + box.write_lcd_frame(0 + _x_offset, y, max_x, i, reinterpret_cast(&_buf[0])); } } } diff --git a/components/doom/CMakeLists.txt b/components/doom/CMakeLists.txt index 30a83ca8..9f188d16 100644 --- a/components/doom/CMakeLists.txt +++ b/components/doom/CMakeLists.txt @@ -9,6 +9,10 @@ target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-misleading-indentation -Wno-format-overflow -Wno-char-subscripts + $<$:-std=gnu17> + $<$:-Wno-discarded-qualifiers> + $<$:-Wno-error=incompatible-pointer-types> + $<$:-Wno-error=compare-distinct-pointer-types> -Wno-missing-field-initializers -DHAVE_CONFIG_H -O2 diff --git a/components/doom/prboom/m_cheat.c b/components/doom/prboom/m_cheat.c index f941e626..25be7fc1 100644 --- a/components/doom/prboom/m_cheat.c +++ b/components/doom/prboom/m_cheat.c @@ -48,6 +48,10 @@ #include "p_tick.h" #define plyr (players+consoleplayer) /* the console player */ +#define CHEAT_ARGS_MAX 8 /* Maximum number of args at end of cheats */ + +static char cheat_argbuf[CHEAT_ARGS_MAX + 1]; +static int cheat_arg_value; //----------------------------------------------------------------------------- // @@ -55,37 +59,37 @@ // //----------------------------------------------------------------------------- -static void cheat_mus(); -static void cheat_choppers(); -static void cheat_god(); -static void cheat_fa(); -static void cheat_k(); -static void cheat_kfa(); -static void cheat_noclip(); -static void cheat_pw(); -static void cheat_behold(); -static void cheat_clev(); -static void cheat_mypos(); -static void cheat_rate(); -static void cheat_comp(); -static void cheat_friction(); -static void cheat_pushers(); -static void cheat_tnttran(); -static void cheat_massacre(); -static void cheat_ddt(); -static void cheat_hom(); -static void cheat_fast(); -static void cheat_tntkey(); -static void cheat_tntkeyx(); -static void cheat_tntkeyxx(); -static void cheat_tntweap(); -static void cheat_tntweapx(); -static void cheat_tntammo(); -static void cheat_tntammox(); -static void cheat_smart(); -static void cheat_pitch(); -static void cheat_megaarmour(); -static void cheat_health(); +static void cheat_mus(void); +static void cheat_choppers(void); +static void cheat_god(void); +static void cheat_fa(void); +static void cheat_k(void); +static void cheat_kfa(void); +static void cheat_noclip(void); +static void cheat_pw(void); +static void cheat_behold(void); +static void cheat_clev(void); +static void cheat_mypos(void); +static void cheat_rate(void); +static void cheat_comp(void); +static void cheat_friction(void); +static void cheat_pushers(void); +static void cheat_tnttran(void); +static void cheat_massacre(void); +static void cheat_ddt(void); +static void cheat_hom(void); +static void cheat_fast(void); +static void cheat_tntkey(void); +static void cheat_tntkeyx(void); +static void cheat_tntkeyxx(void); +static void cheat_tntweap(void); +static void cheat_tntweapx(void); +static void cheat_tntammo(void); +static void cheat_tntammox(void); +static void cheat_smart(void); +static void cheat_pitch(void); +static void cheat_megaarmour(void); +static void cheat_health(void); //----------------------------------------------------------------------------- // @@ -263,9 +267,9 @@ const size_t init_cheat_bytes = sizeof(init_cheat); //----------------------------------------------------------------------------- -static void cheat_mus(buf) -char buf[3]; +static void cheat_mus(void) { + char *buf = cheat_argbuf; int musnum; //jff 3/20/98 note: this cheat allowed in netgame/demorecord @@ -305,14 +309,14 @@ char buf[3]; } // 'choppers' invulnerability & chainsaw -static void cheat_choppers() +static void cheat_choppers(void) { plyr->weaponowned[wp_chainsaw] = true; plyr->powers[pw_invulnerability] = true; plyr->message = s_STSTR_CHOPPERS; // Ty 03/27/98 - externalized } -static void cheat_god() +static void cheat_god(void) { // 'dqd' cheat for toggleable god mode plyr->cheats ^= CF_GODMODE; if (plyr->cheats & CF_GODMODE) @@ -328,7 +332,7 @@ static void cheat_god() } // CPhipps - new health and armour cheat codes -static void cheat_health() +static void cheat_health(void) { if (!(plyr->cheats & CF_GODMODE)) { if (plyr->mo) @@ -338,14 +342,14 @@ static void cheat_health() } } -static void cheat_megaarmour() +static void cheat_megaarmour(void) { plyr->armorpoints = idfa_armor; // Ty 03/09/98 - deh plyr->armortype = idfa_armor_class; // Ty 03/09/98 - deh plyr->message = s_STSTR_BEHOLDX; // Ty 03/27/98 - externalized } -static void cheat_fa() +static void cheat_fa(void) { int i; @@ -372,7 +376,7 @@ static void cheat_fa() plyr->message = s_STSTR_FAADDED; } -static void cheat_k() +static void cheat_k(void) { int i; for (i=0;imessage = STSTR_KFAADDED; } -static void cheat_noclip() +static void cheat_noclip(void) { // Simplified, accepting both "noclip" and "idspispopd". // no clipping mode cheat @@ -400,8 +404,9 @@ static void cheat_noclip() } // 'behold?' power-up cheats (modified for infinite duration -- killough) -static void cheat_pw(int pw) +static void cheat_pw(void) { + int pw = cheat_arg_value; if (plyr->powers[pw]) plyr->powers[pw] = pw!=pw_strength && pw!=pw_allmap; // killough else @@ -414,14 +419,15 @@ static void cheat_pw(int pw) } // 'behold' power-up menu -static void cheat_behold() +static void cheat_behold(void) { plyr->message = s_STSTR_BEHOLD; // Ty 03/27/98 - externalized } // 'clev' change-level cheat -static void cheat_clev(char buf[3]) +static void cheat_clev(void) { + char *buf = cheat_argbuf; int epsd, map; if (gamemode == commercial) @@ -454,7 +460,7 @@ static void cheat_clev(char buf[3]) // 'mypos' for player position // killough 2/7/98: simplified using dprintf and made output more user-friendly -static void cheat_mypos() +static void cheat_mypos(void) { doom_printf("Position (%d,%d,%d)\tAngle %-.0f", players[consoleplayer].mo->x >> FRACBITS, @@ -464,7 +470,7 @@ static void cheat_mypos() } // cph - cheat to toggle frame rate/rendering stats display -static void cheat_rate() +static void cheat_rate(void) { rendering_stats ^= 1; plyr->message = ""; @@ -472,7 +478,7 @@ static void cheat_rate() // compatibility cheat -static void cheat_comp() +static void cheat_comp(void) { // CPhipps - modified for new compatibility system compatibility_level++; compatibility_level %= MAX_COMPATIBILITY_LEVEL; @@ -484,7 +490,7 @@ static void cheat_comp() } // variable friction cheat -static void cheat_friction() +static void cheat_friction(void) { plyr->message = // Ty 03/27/98 - *not* externalized (variable_friction = !variable_friction) ? "Variable Friction enabled" : @@ -494,14 +500,14 @@ static void cheat_friction() // Pusher cheat // phares 3/10/98 -static void cheat_pushers() +static void cheat_pushers(void) { plyr->message = // Ty 03/27/98 - *not* externalized (allow_pushers = !allow_pushers) ? "Pushers enabled" : "Pushers disabled"; } // translucency cheat -static void cheat_tnttran() +static void cheat_tnttran(void) { plyr->message = // Ty 03/27/98 - *not* externalized (general_translucency = !general_translucency) ? "Translucency enabled" : @@ -512,7 +518,7 @@ static void cheat_tnttran() R_InitTranMap(0); } -static void cheat_massacre() // jff 2/01/98 kill all monsters +static void cheat_massacre(void) // jff 2/01/98 kill all monsters { // jff 02/01/98 'em' cheat - kill all monsters // partially taken from Chi's .46 port @@ -529,7 +535,7 @@ static void cheat_massacre() // jff 2/01/98 kill all monsters P_MapStart(); do while ((currentthinker = P_NextThinker(currentthinker,th_all)) != NULL) - if (currentthinker->function == P_MobjThinker && + if (currentthinker->function == (think_t)P_MobjThinker && !(((mobj_t *) currentthinker)->flags & mask) && // killough 7/20/98 (((mobj_t *) currentthinker)->flags & MF_COUNTKILL || ((mobj_t *) currentthinker)->type == MT_SKULL)) @@ -554,7 +560,7 @@ static void cheat_massacre() // jff 2/01/98 kill all monsters // killough 2/7/98: move iddt cheat from am_map.c to here // killough 3/26/98: emulate Doom better -static void cheat_ddt() +static void cheat_ddt(void) { extern int ddt_cheating; if (automapmode & am_active) @@ -562,7 +568,7 @@ static void cheat_ddt() } // killough 2/7/98: HOM autodetection -static void cheat_hom() +static void cheat_hom(void) { extern int autodetect_hom; // Ty 03/27/98 - *not* externalized plyr->message = (autodetect_hom = !autodetect_hom) ? "HOM Detection On" : @@ -570,7 +576,7 @@ static void cheat_hom() } // killough 3/6/98: -fast parameter toggle -static void cheat_fast() +static void cheat_fast(void) { plyr->message = (fastparm = !fastparm) ? "Fast Monsters On" : "Fast Monsters Off"; // Ty 03/27/98 - *not* externalized @@ -578,33 +584,34 @@ static void cheat_fast() } // killough 2/16/98: keycard/skullkey cheat functions -static void cheat_tntkey() +static void cheat_tntkey(void) { plyr->message = "Red, Yellow, Blue"; // Ty 03/27/98 - *not* externalized } -static void cheat_tntkeyx() +static void cheat_tntkeyx(void) { plyr->message = "Card, Skull"; // Ty 03/27/98 - *not* externalized } -static void cheat_tntkeyxx(int key) +static void cheat_tntkeyxx(void) { + int key = cheat_arg_value; plyr->message = (plyr->cards[key] = !plyr->cards[key]) ? "Key Added" : "Key Removed"; // Ty 03/27/98 - *not* externalized } // killough 2/16/98: generalized weapon cheats -static void cheat_tntweap() +static void cheat_tntweap(void) { // Ty 03/27/98 - *not* externalized plyr->message = gamemode==commercial ? // killough 2/28/98 "Weapon number 1-9" : "Weapon number 1-8"; } -static void cheat_tntweapx(buf) -char buf[3]; +static void cheat_tntweapx(void) { + char *buf = cheat_argbuf; int w = *buf - '1'; if ((w==wp_supershotgun && gamemode!=commercial) || // killough 2/28/98 @@ -612,7 +619,10 @@ char buf[3]; return; if (w==wp_fist) // make '1' apply beserker strength toggle - cheat_pw(pw_strength); + { + cheat_arg_value = pw_strength; + cheat_pw(); + } else if (w >= 0 && w < NUMWEAPONS) { if ((plyr->weaponowned[w] = !plyr->weaponowned[w])) @@ -627,14 +637,14 @@ char buf[3]; } // killough 2/16/98: generalized ammo cheats -static void cheat_tntammo() +static void cheat_tntammo(void) { plyr->message = "Ammo 1-4, Backpack"; // Ty 03/27/98 - *not* externalized } -static void cheat_tntammox(buf) -char buf[1]; +static void cheat_tntammox(void) { + char *buf = cheat_argbuf; int a = *buf - '1'; if (*buf == 'b') // Ty 03/27/98 - strings *not* externalized if ((plyr->backpack = !plyr->backpack)) @@ -655,13 +665,13 @@ char buf[1]; } } -static void cheat_smart() +static void cheat_smart(void) { plyr->message = (monsters_remember = !monsters_remember) ? "Smart Monsters Enabled" : "Smart Monsters Disabled"; } -static void cheat_pitch() +static void cheat_pitch(void) { plyr->message=(pitched_sounds = !pitched_sounds) ? "Pitch Effects Enabled" : "Pitch Effects Disabled"; @@ -672,12 +682,10 @@ static void cheat_pitch() // scrambling and to use a more general table-driven approach. //----------------------------------------------------------------------------- -#define CHEAT_ARGS_MAX 8 /* Maximum number of args at end of cheats */ - boolean M_FindCheats(int key) { static uint_64_t sr; - static char argbuf[CHEAT_ARGS_MAX+1], *arg; + static char *arg; static int init, argsleft, cht; int i, ret, matchedbefore; @@ -688,7 +696,10 @@ boolean M_FindCheats(int key) { *arg++ = tolower(key); // store key in arg buffer if (!--argsleft) // if last key in arg list, - cheat[cht].func(argbuf); // process the arg buffer + { + *arg = '\0'; + cheat[cht].func(); // process the arg buffer + } return 1; // affirmative response } @@ -732,7 +743,7 @@ boolean M_FindCheats(int key) if (cheat[i].arg < 0) // if additional args are required { cht = i; // remember this cheat code - arg = argbuf; // point to start of arg buffer + arg = cheat_argbuf; // point to start of arg buffer argsleft = -cheat[i].arg; // number of args expected ret = 1; // responder has eaten key } @@ -740,8 +751,9 @@ boolean M_FindCheats(int key) if (!matchedbefore) // allow only one cheat at a time { matchedbefore = ret = 1; // responder has eaten key - cheat[i].func(cheat[i].arg); // call cheat handler - } + cheat_arg_value = cheat[i].arg; + cheat[i].func(); // call cheat handler + } } return ret; } diff --git a/components/doom/prboom/m_cheat.h b/components/doom/prboom/m_cheat.h index 8655d637..187edb43 100644 --- a/components/doom/prboom/m_cheat.h +++ b/components/doom/prboom/m_cheat.h @@ -48,7 +48,7 @@ extern struct cheat_s { not_deh = 16, not_net = not_dm | not_coop } const when; - void (*const func)(); + void (*const func)(void); const int arg; uint_64_t code, mask; } *cheat; diff --git a/components/doom/src/doom.cpp b/components/doom/src/doom.cpp index c3c4ea49..c2f3ebec 100644 --- a/components/doom/src/doom.cpp +++ b/components/doom/src/doom.cpp @@ -77,7 +77,11 @@ extern "C" { #define AUDIO_SAMPLE_RATE (22050 / 2) +// Number of stereo frames produced per game tic. The OPL music renderer and the +// SFX mixer both output stereo (interleaved L/R), and the I2S output is stereo, +// so the mix buffer must hold AUDIO_BUFFER_LENGTH * 2 int16 samples. #define AUDIO_BUFFER_LENGTH (AUDIO_SAMPLE_RATE / TICRATE + 1) +#define AUDIO_BUFFER_SAMPLES (AUDIO_BUFFER_LENGTH * 2) #define NUM_MIX_CHANNELS 8 // Expected variables by doom @@ -102,7 +106,7 @@ extern "C" { static channel_t *channels=nullptr; // [NUM_MIX_CHANNELS]; static const doom_sfx_t **sfx = nullptr; // [NUMSFX]; - static uint16_t *mixbuffer = nullptr; // [AUDIO_BUFFER_LENGTH]; + static uint16_t *mixbuffer = nullptr; // [AUDIO_BUFFER_SAMPLES] (stereo, interleaved L/R) static const music_player_t *music_player = &opl_synth_player; static bool musicPlaying = false; @@ -244,7 +248,7 @@ extern "C" { } // Main screen uses internal ram for speed - screens[0].data = (uint8_t*)framebuffer; + screens[0].data = reinterpret_cast(framebuffer); screens[0].not_on_heap = true; // statusbar @@ -340,7 +344,7 @@ extern "C" { if (haveSFX) { int16_t *audioBuffer = (int16_t *)mixbuffer; - const int16_t *audioBufferEnd = audioBuffer + AUDIO_BUFFER_LENGTH; + const int16_t *audioBufferEnd = audioBuffer + AUDIO_BUFFER_SAMPLES; while (audioBuffer < audioBufferEnd) { int totalSample = 0; int totalSources = 0; @@ -383,11 +387,11 @@ extern "C" { } if (!haveMusic && !haveSFX) { - memset(mixbuffer, 0, AUDIO_BUFFER_LENGTH * sizeof(int16_t)); + memset(mixbuffer, 0, AUDIO_BUFFER_SAMPLES * sizeof(int16_t)); } static auto& box = BoxEmu::get(); - box.play_audio((const uint8_t*)mixbuffer, AUDIO_BUFFER_LENGTH * sizeof(int16_t)); + box.play_audio(reinterpret_cast(mixbuffer), AUDIO_BUFFER_SAMPLES * sizeof(int16_t)); std::this_thread::sleep_for(std::chrono::microseconds(1'000'000 / TICRATE)); return false; } @@ -524,7 +528,7 @@ void init_doom(const std::string& wad_filename, uint8_t *wad_data, size_t wad_da // needed for doom.cpp channels = (channel_t *)shared_malloc(sizeof(channel_t) * NUM_MIX_CHANNELS); sfx = (const doom_sfx_t **)shared_malloc(sizeof(doom_sfx_t*) * NUMSFX); - mixbuffer = (uint16_t *)shared_malloc(sizeof(uint16_t) * AUDIO_BUFFER_LENGTH); + mixbuffer = (uint16_t *)shared_malloc(sizeof(uint16_t) * AUDIO_BUFFER_SAMPLES); doom_init_shared_memory(); @@ -636,11 +640,11 @@ void save_doom(std::string_view save_path, int save_slot) { std::span get_doom_video_buffer() { size_t num_pixels = SCREENWIDTH * SCREENHEIGHT; - uint8_t *span_ptr = (uint8_t*)(currentBuffer ? displayBuffer[0] : displayBuffer[1]); + uint8_t *span_ptr = reinterpret_cast(currentBuffer ? displayBuffer[0] : displayBuffer[1]); std::span frame(span_ptr, num_pixels * sizeof(uint16_t)); // use the palette to convert the framebuffer to RGB565 - const uint8_t *buf = (const uint8_t*)framebuffer; + const uint8_t *buf = reinterpret_cast(framebuffer); const uint16_t *palette = BoxEmu::get().palette(); if (palette) { for (int i = 0; i < num_pixels; i++) { diff --git a/components/gbc/CMakeLists.txt b/components/gbc/CMakeLists.txt index df73aff1..60b41c1d 100644 --- a/components/gbc/CMakeLists.txt +++ b/components/gbc/CMakeLists.txt @@ -6,5 +6,5 @@ idf_component_register( REQUIRES "box-emu" "statistics" "shared_memory" ) -target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-misleading-indentation -Wno-implicit-fallthrough -Wno-unused-function -Wno-unused-variable -Wno-discarded-qualifiers) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-misleading-indentation -Wno-implicit-fallthrough -Wno-unused-function -Wno-unused-variable $<$:-Wno-discarded-qualifiers>) target_compile_definitions(${COMPONENT_LIB} PRIVATE GNUBOY_NO_MINIZIP GNUBOY_NO_SCREENSHOT IS_LITTLE_ENDIAN) diff --git a/components/gbc/src/gameboy.cpp b/components/gbc/src/gameboy.cpp index c07b725c..ce860d96 100644 --- a/components/gbc/src/gameboy.cpp +++ b/components/gbc/src/gameboy.cpp @@ -11,7 +11,7 @@ static const size_t GAMEBOY_SCREEN_WIDTH = 160; static const size_t GAMEBOY_SCREEN_HEIGHT = 144; -static const int GAMEBOY_AUDIO_SAMPLE_RATE = 32000; +static const int GAMEBOY_AUDIO_SAMPLE_RATE = 32768; extern "C" { #include @@ -65,7 +65,7 @@ void run_to_vblank() { // swap buffers currentBuffer = currentBuffer ? 0 : 1; framebuffer = displayBuffer[currentBuffer]; - fb.ptr = (uint8_t*)framebuffer; + fb.ptr = reinterpret_cast(framebuffer); } rtc_tick(); @@ -115,7 +115,7 @@ void init_gameboy(const std::string& rom_filename, uint8_t *romdata, size_t rom_ // Use shared memory regions lcd->vbank = vram; ram.ibank = wram; - pcm.buf = (int16_t*)audio; + pcm.buf = reinterpret_cast(audio); static constexpr int GBC_AUDIO_BUFFER_SIZE = GAMEBOY_AUDIO_SAMPLE_RATE * 2 * 2 / 5; // TODO: 5 is a hack to make it work pcm.len = GBC_AUDIO_BUFFER_SIZE / sizeof(int16_t); @@ -132,7 +132,7 @@ void init_gameboy(const std::string& rom_filename, uint8_t *romdata, size_t rom_ fb.pelsize = 2; fb.pitch = fb.w * fb.pelsize; fb.indexed = 0; - fb.ptr = (uint8_t*)displayBuffer[0]; + fb.ptr = reinterpret_cast(displayBuffer[0]); fb.enabled = 1; fb.dirty = 0; framebuffer = displayBuffer[0]; @@ -193,6 +193,7 @@ void run_gameboy_rom() { void load_gameboy(std::string_view save_path) { if (save_path.size()) { auto f = fopen(save_path.data(), "rb"); + if (!f) return; loadstate(f); fclose(f); vram_dirty(); @@ -205,12 +206,13 @@ void load_gameboy(std::string_view save_path) { void save_gameboy(std::string_view save_path) { // save state auto f = fopen(save_path.data(), "wb"); + if (!f) return; savestate(f); fclose(f); } std::span get_gameboy_video_buffer() { - return std::span((uint8_t*)framebuffer, GAMEBOY_SCREEN_WIDTH * GAMEBOY_SCREEN_HEIGHT * 2); + return std::span(reinterpret_cast(framebuffer), GAMEBOY_SCREEN_WIDTH * GAMEBOY_SCREEN_HEIGHT * 2); } void deinit_gameboy() { diff --git a/components/genesis/gwenesis/src/bus/gwenesis_bus.c b/components/genesis/gwenesis/src/bus/gwenesis_bus.c index 641e2076..6cd80bca 100644 --- a/components/genesis/gwenesis/src/bus/gwenesis_bus.c +++ b/components/genesis/gwenesis/src/bus/gwenesis_bus.c @@ -31,6 +31,7 @@ __license__ = "GPLv3" #include "gwenesis_vdp.h" #include "gwenesis_sn76489.h" #include "gwenesis_savestate.h" +#include "genesis_dualcore.h" #pragma GCC optimize("Ofast") @@ -84,6 +85,10 @@ void load_cartridge(unsigned char *buffer, size_t size) // Clear all volatile memory memset(M68K_RAM, 0, MAX_RAM_SIZE); memset(ZRAM, 0, MAX_Z80_RAM_SIZE); + memset(TMSS, 0, sizeof(TMSS)); + tmss_state = 0; + tmss_count = 0; + gwenesis_io_reset(); // Set Z80 Memory as ZRAM z80_set_memory(ZRAM); @@ -368,96 +373,93 @@ unsigned int gwenesis_bus_map_address(unsigned int address) { ******************************************************************************/ static inline unsigned int gwenesis_bus_read_memory_8(unsigned int address) { bus_log(__FUNCTION__,"read8 %x", address); + const unsigned int page = address & 0xFF0000; - switch (gwenesis_bus_map_address(address)) { - - case VDP_ADDR: + if ((address & 0xE00000) == 0xC00000) return gwenesis_vdp_read_memory_8(address); - - case ROM_ADDR: + if (address < 0x800000) return FETCH8ROM(address); - - case RAM_ADDR: + if (address >= 0xFF0000) return FETCH8RAM(address); - case IO_CTRL: - return gwenesis_io_read_ctrl(address & 0x1F); - - case Z80_CTRL: + if (page == 0xA10000) { + if ((address & 0x1000) == 0) + return gwenesis_io_read_ctrl(address & 0x1F); return z80_read_ctrl(address & 0xFFFF); + } - case Z80_RAM_ADDR: - case Z80_RAM_ADDR1K: - return ZRAM[address & 0x1FFF]; - - case Z80_YM2612_ADDR: - return YM2612Read(m68k_cycles_master()); - - case Z80_SN76489_ADDR: - return 0xff; - - case Z80_BANK_ADDR: - return 0xff; - - case TMSS_CTRL: - bus_log(__FUNCTION__,"TMS"); - if (tmss_state == 0) - return TMSS[address & 0x3]; - return 0xFF; - - default: - bus_log(__FUNCTION__," default read 8 %x", address); - return 0x00; + if (page == 0xA00000) { + switch (address & 0xF000) { + case 0: + case 0x1000: + case 0x2000: + case 0x3000: + return ZRAM[address & 0x1FFF]; + case 0x4000: +#if GENESIS_DUAL_CORE + return genesis_ym2612_status_peek(); +#else + return YM2612Read(m68k_cycles_master()); +#endif + case 0x6000: + case 0x7000: + return 0xff; + default: + break; + } } + + bus_log(__FUNCTION__," default read 8 %x", address); return 0x00; } static inline unsigned int gwenesis_bus_read_memory_16(unsigned int address) { bus_log(__FUNCTION__,"read16 %x", address); - unsigned int ret_value; + const unsigned int page = address & 0xFF0000; - switch (gwenesis_bus_map_address(address)) { - - case VDP_ADDR: + if ((address & 0xE00000) == 0xC00000) return gwenesis_vdp_read_memory_16(address); - - case RAM_ADDR: + if (address >= 0xFF0000) return FETCH16RAM(address); - - case ROM_ADDR: + if (address < 0x800000) return FETCH16ROM(address); - case IO_CTRL: - return gwenesis_io_read_ctrl(address & 0x1F); - - case Z80_CTRL: - // ret_value = z80_read_ctrl(address & 0xFFFF); - // return ret_value | ret_value << 8; - address &=0xFFFF; - return (z80_read_ctrl(address) << 8) | z80_read_ctrl(address | 1); - + if (page == 0xA10000) { + if ((address & 0x1000) == 0) + return gwenesis_io_read_ctrl(address & 0x1F); - case Z80_RAM_ADDR: - case Z80_RAM_ADDR1K: - return ZRAM[address & 0X1FFF] | (ZRAM[address & 0X1FFF] << 8); - - case Z80_YM2612_ADDR: - ret_value = YM2612Read(m68k_cycles_master()); - return ret_value | ret_value << 8; - - - case Z80_SN76489_ADDR: - return 0xff; - - case Z80_BANK_ADDR: - return 0xff; + address &= 0xFFFF; + return (z80_read_ctrl(address) << 8) | z80_read_ctrl(address | 1); + } - default: - bus_log(__FUNCTION__,"read mem 16 default %x", address); - return (gwenesis_bus_read_memory_8(address) << 8) | - gwenesis_bus_read_memory_8(address + 1); + if (page == 0xA00000) { + switch (address & 0xF000) { + case 0: + case 0x1000: + case 0x2000: + case 0x3000: { + const unsigned int zram_value = ZRAM[address & 0x1FFF]; + return zram_value | (zram_value << 8); + } + case 0x4000: { +#if GENESIS_DUAL_CORE + const unsigned int ym_value = genesis_ym2612_status_peek(); +#else + const unsigned int ym_value = YM2612Read(m68k_cycles_master()); +#endif + return ym_value | (ym_value << 8); + } + case 0x6000: + case 0x7000: + return 0xff; + default: + break; + } } - return 0x00; + + bus_log(__FUNCTION__,"read mem 16 default %x", address); + return (gwenesis_bus_read_memory_8(address) << 8) | + gwenesis_bus_read_memory_8(address + 1); } /****************************************************************************** @@ -469,108 +471,119 @@ static inline unsigned int gwenesis_bus_read_memory_16(unsigned int address) { static inline void gwenesis_bus_write_memory_8(unsigned int address, unsigned int value) { bus_log(__FUNCTION__,"write8 @%x:%x", address,value); + const unsigned int page = address & 0xFF0000; - switch (gwenesis_bus_map_address(address)) { - - case VDP_ADDR: + if ((address & 0xE00000) == 0xC00000) { gwenesis_vdp_write_memory_16(address & ~1, (value << 8) | value); return; - - case RAM_ADDR: + } + if (address >= 0xFF0000) { WRITE8RAM(address, value); return; - - case IO_CTRL: - gwenesis_io_write_ctrl(address & 0x1F, value); - return; - - case Z80_CTRL: - z80_write_ctrl(address & 0x1FFF, value); - return; - - case Z80_RAM_ADDR: - case Z80_RAM_ADDR1K: - ZRAM[address & 0x1FFF] = value; - return; - - case Z80_YM2612_ADDR: - bus_log(__FUNCTION__,"CPUZ80PSG8 ,m68kclk= %d", m68k_cycles_master()); - YM2612Write(address & 0x3, value & 0Xff,m68k_cycles_master()); - return; - - case Z80_SN76489_ADDR: - bus_log(__FUNCTION__,"CPUZ80FM8 ,m68kclk= %d", m68k_cycles_master()); - gwenesis_SN76489_Write( value & 0Xff, m68k_cycles_master()); + } + if (address < 0x800000) return; - case Z80_BANK_ADDR: - //TODO + if (page == 0xA10000) { + if ((address & 0x1000) == 0) + gwenesis_io_write_ctrl(address & 0x1F, value); + else + z80_write_ctrl(address & 0x1FFF, value); return; + } - case TMSS_CTRL: - - if (tmss_state == 0) { - TMSS[address & 0x3] = value; - tmss_count++; - if (tmss_count == 4) - tmss_state = 1; + if (page == 0xA00000) { + switch (address & 0xF000) { + case 0: + case 0x1000: + case 0x2000: + case 0x3000: + ZRAM[address & 0x1FFF] = value; + return; + case 0x4000: + bus_log(__FUNCTION__,"CPUZ80PSG8 ,m68kclk= %d", m68k_cycles_master()); +#if GENESIS_DUAL_CORE + genesis_sound_queue_push(GEN_SND_YM2612, address & 0x3, value & 0xff, m68k_cycles_master()); +#else + YM2612Write(address & 0x3, value & 0Xff, m68k_cycles_master()); +#endif + return; + case 0x6000: + return; + case 0x7000: + bus_log(__FUNCTION__,"CPUZ80FM8 ,m68kclk= %d", m68k_cycles_master()); +#if GENESIS_DUAL_CORE + genesis_sound_queue_push(GEN_SND_SN76489, 0, value & 0xff, m68k_cycles_master()); +#else + gwenesis_SN76489_Write(value & 0Xff, m68k_cycles_master()); +#endif + return; + default: + return; } - return; - - - - default: - //printf("write(%x, %x)\n", address, value); - return; } + return; } static inline void gwenesis_bus_write_memory_16(unsigned int address, unsigned int value) { bus_log(__FUNCTION__,"write16 @%x:%x", address,value); + const unsigned int page = address & 0xFF0000; - switch (gwenesis_bus_map_address(address)) { - - case VDP_ADDR: + if ((address & 0xE00000) == 0xC00000) { gwenesis_vdp_write_memory_16(address, value); return; - - case RAM_ADDR: + } + if (address >= 0xFF0000) { WRITE16RAM(address, value); return; - - case Z80_RAM_ADDR: - case Z80_RAM_ADDR1K: - ZRAM[address & 0X1FFF]= value >> 8; - return; - - case IO_CTRL: - gwenesis_io_write_ctrl(address & 0x1F, value); - return; - - case Z80_CTRL: - z80_write_ctrl(address & 0xFFFF, value >> 8) ; - return; - - case Z80_YM2612_ADDR: - bus_log(__FUNCTION__,"CZYM16 ,mclk=%d", m68k_cycles_master()); - YM2612Write(address & 0x3, value >> 8,m68k_cycles_master() ); + } + if (address < 0x800000) return; - case Z80_SN76489_ADDR: - bus_log(__FUNCTION__,"CZSN16 ,mclk=%d", m68k_cycles_master()); - gwenesis_SN76489_Write(value >> 8,m68k_cycles_master() ); + if (page == 0xA10000) { + if ((address & 0x1000) == 0) + gwenesis_io_write_ctrl(address & 0x1F, value); + else + z80_write_ctrl(address & 0xFFFF, value >> 8); return; + } - default: - bus_log(__FUNCTION__,"write mem 16 default %x ", address); - gwenesis_bus_write_memory_8(address, (value >> 8) & 0xff); - gwenesis_bus_write_memory_8(address + 1, (value)&0xff); - - return; + if (page == 0xA00000) { + switch (address & 0xF000) { + case 0: + case 0x1000: + case 0x2000: + case 0x3000: + ZRAM[address & 0X1FFF]= value >> 8; + return; + case 0x4000: + bus_log(__FUNCTION__,"CZYM16 ,mclk=%d", m68k_cycles_master()); +#if GENESIS_DUAL_CORE + genesis_sound_queue_push(GEN_SND_YM2612, address & 0x3, value >> 8, m68k_cycles_master()); +#else + YM2612Write(address & 0x3, value >> 8, m68k_cycles_master()); +#endif + return; + case 0x7000: + bus_log(__FUNCTION__,"CZSN16 ,mclk=%d", m68k_cycles_master()); +#if GENESIS_DUAL_CORE + genesis_sound_queue_push(GEN_SND_SN76489, 0, value >> 8, m68k_cycles_master()); +#else + gwenesis_SN76489_Write(value >> 8, m68k_cycles_master()); +#endif + return; + case 0x6000: + return; + default: + break; + } } - return; + + bus_log(__FUNCTION__,"write mem 16 default %x ", address); + gwenesis_bus_write_memory_8(address, (value >> 8) & 0xff); + gwenesis_bus_write_memory_8(address + 1, (value)&0xff); } /****************************************************************************** @@ -581,7 +594,10 @@ static inline void gwenesis_bus_write_memory_16(unsigned int address, ******************************************************************************/ unsigned int m68k_read_memory_8(unsigned int address) { - // if ((address & 0xFF0000 ) == 0xFF0000) return FETCH8RAM(address); + if (address < 0x800000) + return FETCH8ROM(address); + if (address >= 0xFF0000) + return FETCH8RAM(address); return gwenesis_bus_read_memory_8(address); } @@ -593,7 +609,10 @@ unsigned int m68k_read_memory_8(unsigned int address) ******************************************************************************/ unsigned int m68k_read_memory_16(unsigned int address) { - // if ((address & 0xFF0000 ) == 0xFF0000) return FETCH16RAM(address); + if (address < 0x800000) + return FETCH16ROM(address); + if (address >= 0xFF0000) + return FETCH16RAM(address); return gwenesis_bus_read_memory_16(address); } @@ -605,7 +624,10 @@ unsigned int m68k_read_memory_8(unsigned int address) ******************************************************************************/ unsigned int m68k_read_memory_32(unsigned int address) { - // if ((address & 0xFF0000 ) == 0xFF0000) return FETCH32RAM(address); + if (address < 0x800000) + return FETCH32ROM(address); + if (address >= 0xFF0000) + return FETCH32RAM(address); return (gwenesis_bus_read_memory_16(address) << 16) | gwenesis_bus_read_memory_16(address + 2); } @@ -616,10 +638,12 @@ unsigned int m68k_read_memory_8(unsigned int address) * ******************************************************************************/ void m68k_write_memory_8(unsigned int address, unsigned int value) { - // if ((address & 0xFF0000) == 0xFF0000) { - // WRITE8RAM(address, value); - // return; - // } + if (address >= 0xFF0000) { + WRITE8RAM(address, value); + return; + } + if (address < 0x800000) + return; gwenesis_bus_write_memory_8(address, value); return; } @@ -631,10 +655,12 @@ void m68k_write_memory_8(unsigned int address, unsigned int value) { * ******************************************************************************/ void m68k_write_memory_16(unsigned int address, unsigned int value) { - // if ((address & 0xFF0000) == 0xFF0000) { - // WRITE16RAM(address, value); - // return; - // } + if (address >= 0xFF0000) { + WRITE16RAM(address, value); + return; + } + if (address < 0x800000) + return; gwenesis_bus_write_memory_16(address, value); return; } @@ -646,10 +672,12 @@ void m68k_write_memory_16(unsigned int address, unsigned int value) { ******************************************************************************/ void m68k_write_memory_32(unsigned int address, unsigned int value) { - // if ((address & 0xFF0000) == 0xFF0000) { - // WRITE32RAM(address, value); - // return; - // } + if (address >= 0xFF0000) { + WRITE32RAM(address, value); + return; + } + if (address < 0x800000) + return; gwenesis_bus_write_memory_16(address, (value >> 16) & 0xffff); gwenesis_bus_write_memory_16(address + 2, (value)&0xffff); diff --git a/components/genesis/gwenesis/src/cpus/M68K/m68k.h b/components/genesis/gwenesis/src/cpus/M68K/m68k.h index 56bd071e..93887865 100644 --- a/components/genesis/gwenesis/src/cpus/M68K/m68k.h +++ b/components/genesis/gwenesis/src/cpus/M68K/m68k.h @@ -153,7 +153,6 @@ extern unsigned char *ROM_DATA; extern unsigned char *M68K_RAM; - #define FETCH8ROM(A) ((ROM_DATA[((A) ^ 1)])) #define FETCH16ROM(A) ((*(unsigned short *)&ROM_DATA[(A)])) #define FETCH32ROM(A) ( (*(unsigned int *)&ROM_DATA[(A)] << 16) | (*(unsigned int *)&ROM_DATA[(A)] >> 16) ) diff --git a/components/genesis/gwenesis/src/cpus/M68K/m68kconf.h b/components/genesis/gwenesis/src/cpus/M68K/m68kconf.h index d426577f..8613cc7f 100644 --- a/components/genesis/gwenesis/src/cpus/M68K/m68kconf.h +++ b/components/genesis/gwenesis/src/cpus/M68K/m68kconf.h @@ -62,7 +62,7 @@ * access a word or longword at an odd address. * NOTE: This is only emulated properly for 68000 mode. */ -#define M68K_EMULATE_ADDRESS_ERROR OPT_ON +#define M68K_EMULATE_ADDRESS_ERROR OPT_OFF /* If ON and previous option is also ON, address error exceptions will also be checked when fetching instructions. Disabling this can help diff --git a/components/genesis/gwenesis/src/cpus/Z80/Z80.c b/components/genesis/gwenesis/src/cpus/Z80/Z80.c index 6ec53f1f..6f0325f3 100644 --- a/components/genesis/gwenesis/src/cpus/Z80/Z80.c +++ b/components/genesis/gwenesis/src/cpus/Z80/Z80.c @@ -62,8 +62,8 @@ INLINE byte OpZ80(word A) { return(RAM[A>>13][A&0x1FFF]); } #ifdef GENESIS #define FAST_RDOP -extern byte *Z80_RAM[]; -INLINE byte OpZ80(word A) { return(Z80_RAM[A>>13][A&0x1FFF]); } +extern byte *Z80_RAM; +INLINE byte OpZ80(word A) { return(A < 0x4000 ? Z80_RAM[A & 0x1FFF] : RdZ80(A)); } #endif /** FAST_RDOP ************************************************/ diff --git a/components/genesis/gwenesis/src/io/gwenesis_io.c b/components/genesis/gwenesis/src/io/gwenesis_io.c index 0735dfdb..871bac47 100644 --- a/components/genesis/gwenesis/src/io/gwenesis_io.c +++ b/components/genesis/gwenesis/src/io/gwenesis_io.c @@ -133,6 +133,31 @@ unsigned char io_reg[16] = {GWENESIS_IO_VERSION, /* 0x1 Version */ 0xff, 0, 0, /* PORT 2 */ 0xff, 0, 0}; /* PORT 3 */ +void gwenesis_io_reset(void) +{ + memset(button_state, 0xff, sizeof(button_state)); + gwenesis_io_pad_state[0] = 0x33; + gwenesis_io_pad_state[1] = 0x33; + gwenesis_io_pad_state[2] = 0x33; + + io_reg[0] = GWENESIS_IO_VERSION; + io_reg[1] = 0x7f; + io_reg[2] = 0x7f; + io_reg[3] = 0x7f; + io_reg[4] = 0x00; + io_reg[5] = 0x00; + io_reg[6] = 0x00; + io_reg[7] = 0xff; + io_reg[8] = 0x00; + io_reg[9] = 0x00; + io_reg[10] = 0xff; + io_reg[11] = 0x00; + io_reg[12] = 0x00; + io_reg[13] = 0xff; + io_reg[14] = 0x00; + io_reg[15] = 0x00; +} + void gwenesis_io_pad_release_button(int pad, int button) { button_state[pad] |= (1 << button); diff --git a/components/genesis/gwenesis/src/io/gwenesis_io.h b/components/genesis/gwenesis/src/io/gwenesis_io.h index a1a1b753..5dc091a9 100644 --- a/components/genesis/gwenesis/src/io/gwenesis_io.h +++ b/components/genesis/gwenesis/src/io/gwenesis_io.h @@ -27,6 +27,7 @@ void gwenesis_io_pad_release_button(int pad, int button); void gwenesis_io_write_ctrl(unsigned int address, unsigned int value); unsigned int gwenesis_io_read_ctrl(unsigned int address); +void gwenesis_io_reset(void); void gwenesis_io_set_reg(unsigned int reg, unsigned int value); void gwenesis_io_get_buttons(); diff --git a/components/genesis/gwenesis/src/sound/gwenesis_sn76489.c b/components/genesis/gwenesis/src/sound/gwenesis_sn76489.c index 32cd3e90..65780fa0 100644 --- a/components/genesis/gwenesis/src/sound/gwenesis_sn76489.c +++ b/components/genesis/gwenesis/src/sound/gwenesis_sn76489.c @@ -77,8 +77,10 @@ static SN76489_Context gwenesis_SN76489; void gwenesis_SN76489_Init( int PSGClockValue, int SamplingRate,int freq_divisor) { + memset(&gwenesis_SN76489, 0, sizeof(gwenesis_SN76489)); gwenesis_SN76489.dClock=(float)PSGClockValue/16/SamplingRate; gwenesis_SN76489.divisor = freq_divisor; + gwenesis_SN76489.WhiteNoiseFeedback = GWENESIS_FB_SEGAVDP; gwenesis_SN76489_Reset(); } diff --git a/components/genesis/gwenesis/src/sound/gwenesis_sn76489.h b/components/genesis/gwenesis/src/sound/gwenesis_sn76489.h index 6b904567..7f158260 100644 --- a/components/genesis/gwenesis/src/sound/gwenesis_sn76489.h +++ b/components/genesis/gwenesis/src/sound/gwenesis_sn76489.h @@ -7,6 +7,10 @@ SN76489 variants and compatible chips. */ +enum gwenesis_sn76489_feedback_patterns { + GWENESIS_FB_SEGAVDP = 0x0009, +}; + #undef uint8 #undef uint16 #undef uint32 diff --git a/components/genesis/gwenesis/src/sound/ym2612.c b/components/genesis/gwenesis/src/sound/ym2612.c index a7e9f636..09fa4af9 100644 --- a/components/genesis/gwenesis/src/sound/ym2612.c +++ b/components/genesis/gwenesis/src/sound/ym2612.c @@ -525,10 +525,94 @@ static INT32 m2,c1,c2; /* Phase Modulation input for operators 2,3,4 */ static INT32 mem; /* one sample delay memory */ static INT32 out_fm[8]; /* outputs of working channels */ static UINT32 bitmask; /* working channels output bitmasking (DAC quantization) */ +static int ssg_eg_active_slots; +static int phase_lfo_active_channels; +static UINT8 dirty_channels_mask; +static UINT8 phase_only_channels_mask; +static UINT8 eg_active_channels_mask; +static int ym2612_sample_step = 1; /* mirror of all OPN registers */ uint8_t *OPNREGS = NULL; // [512]; +static void recalculate_ssg_eg_active_slots(void) +{ + ssg_eg_active_slots = 0; + + for (int ch = 0; ch < 6; ch++) + { + for (int slot = 0; slot < 4; slot++) + { + if (ym2612->CH[ch].SLOT[slot].ssg & 0x08) + ssg_eg_active_slots++; + } + } +} + +static void recalculate_phase_lfo_active_channels(void) +{ + phase_lfo_active_channels = 0; + + for (int ch = 0; ch < 6; ch++) + { + if (ym2612->CH[ch].pms) + phase_lfo_active_channels++; + } +} + +INLINE void mark_dirty_channel_index(int index) +{ + dirty_channels_mask |= (UINT8)(1u << index); +} + +INLINE void mark_dirty_channel(FM_CH *CH) +{ + mark_dirty_channel_index((int)(CH - &ym2612->CH[0])); +} + +INLINE int channel_is_phase_only(FM_CH *CH) +{ + return CH->SLOT[SLOT1].vol_out >= ENV_QUIET && + CH->SLOT[SLOT2].vol_out >= ENV_QUIET && + CH->SLOT[SLOT3].vol_out >= ENV_QUIET && + CH->SLOT[SLOT4].vol_out >= ENV_QUIET && + CH->op1_out[0] == 0 && CH->op1_out[1] == 0 && CH->mem_value == 0; +} + +INLINE int channel_has_active_eg(FM_CH *CH) +{ + return CH->SLOT[SLOT1].state != EG_OFF || + CH->SLOT[SLOT2].state != EG_OFF || + CH->SLOT[SLOT3].state != EG_OFF || + CH->SLOT[SLOT4].state != EG_OFF; +} + +INLINE void recalculate_channel_runtime_masks(FM_CH *CH) +{ + const UINT8 channel_mask = (UINT8)(1u << (CH - &ym2612->CH[0])); + + if (channel_is_phase_only(CH)) + phase_only_channels_mask |= channel_mask; + else + phase_only_channels_mask &= (UINT8)~channel_mask; + + if (channel_has_active_eg(CH)) + eg_active_channels_mask |= channel_mask; + else + eg_active_channels_mask &= (UINT8)~channel_mask; +} + +static void recalculate_runtime_channel_masks(void) +{ + int ch; + + phase_only_channels_mask = 0; + eg_active_channels_mask = 0; + + for (ch = 0; ch < 6; ch++) + recalculate_channel_runtime_masks(&ym2612->CH[ch]); +} + INLINE void FM_KEYON(FM_CH *CH , int s ) { FM_SLOT *SLOT = &CH->SLOT[s]; @@ -562,6 +646,7 @@ INLINE void FM_KEYON(FM_CH *CH , int s ) } SLOT->key = 1; + recalculate_channel_runtime_masks(CH); } INLINE void FM_KEYOFF(FM_CH *CH , int s ) @@ -595,6 +680,7 @@ INLINE void FM_KEYOFF(FM_CH *CH , int s ) } SLOT->key = 0; + recalculate_channel_runtime_masks(CH); } INLINE void FM_KEYON_CSM(FM_CH *CH , int s ) @@ -628,6 +714,8 @@ INLINE void FM_KEYON_CSM(FM_CH *CH , int s ) else SLOT->vol_out = (UINT32)SLOT->volume + SLOT->tl; } + + recalculate_channel_runtime_masks(CH); } INLINE void FM_KEYOFF_CSM(FM_CH *CH , int s ) @@ -658,6 +746,8 @@ INLINE void FM_KEYOFF_CSM(FM_CH *CH , int s ) } } } + + recalculate_channel_runtime_masks(CH); } /* CSM Key Controll */ @@ -675,15 +765,15 @@ INLINE void INTERNAL_TIMER_A() { if (ym2612->OPN.ST.mode & 0x01) { - ym2612->OPN.ST.TAC--; - if (ym2612->OPN.ST.TAC <= 0) + ym2612->OPN.ST.TAC -= ym2612_sample_step; + while (ym2612->OPN.ST.TAC <= 0) { /* set status (if enabled) */ if (ym2612->OPN.ST.mode & 0x04) ym2612->OPN.ST.status |= 0x01; /* reload the counter */ - ym2612->OPN.ST.TAC = ym2612->OPN.ST.TAL; + ym2612->OPN.ST.TAC += ym2612->OPN.ST.TAL; /* CSM mode auto key on */ if ((ym2612->OPN.ST.mode & 0xC0) == 0x80) @@ -728,6 +818,7 @@ INLINE void set_timers(int v ) { /* phase increment need to be recalculated */ ym2612->CH[2].SLOT[SLOT1].Incr=-1; + mark_dirty_channel_index(2); /* CSM mode disabled and CSM key ON active*/ if (((v & 0xC0) != 0x80) && ym2612->OPN.SL3.key_csm) @@ -846,6 +937,7 @@ INLINE void set_det_mul(FM_CH *CH,FM_SLOT *SLOT,int v) SLOT->mul = (v&0x0f)? (v&0x0f)*2 : 1; SLOT->DT = ym2612->OPN.ST.dt_tab[(v>>4)&7]; CH->SLOT[SLOT1].Incr=-1; + mark_dirty_channel(CH); } /* set total level */ @@ -871,6 +963,7 @@ INLINE void set_ar_ksr(FM_CH *CH,FM_SLOT *SLOT,int v) if (SLOT->KSR != old_KSR) { CH->SLOT[SLOT1].Incr=-1; + mark_dirty_channel(CH); } /* Even if it seems unnecessary to do it here, it could happen that KSR and KC */ @@ -929,12 +1022,12 @@ INLINE void advance_lfo() if (ym2612->OPN.lfo_timer_overflow) /* LFO enabled ? */ { /* increment LFO timer (every samples) */ - ym2612->OPN.lfo_timer ++; + ym2612->OPN.lfo_timer += ym2612_sample_step; /* when LFO is enabled, one level will last for 108, 77, 71, 67, 62, 44, 8 or 5 samples */ - if (ym2612->OPN.lfo_timer >= ym2612->OPN.lfo_timer_overflow) + while (ym2612->OPN.lfo_timer >= ym2612->OPN.lfo_timer_overflow) { - ym2612->OPN.lfo_timer = 0; + ym2612->OPN.lfo_timer -= ym2612->OPN.lfo_timer_overflow; /* There are 128 LFO steps */ ym2612->OPN.lfo_cnt = ( ym2612->OPN.lfo_cnt + 1 ) & 127; @@ -955,13 +1048,17 @@ INLINE void advance_lfo() INLINE void advance_eg_channels(FM_CH *CH, unsigned int eg_cnt) { - unsigned int i = 6; /* six channels */ + unsigned int active_mask = eg_active_channels_mask; unsigned int j; FM_SLOT *SLOT; - do + while (active_mask) { - SLOT = &CH->SLOT[SLOT1]; + unsigned int channel_index = __builtin_ctz(active_mask); + FM_CH *channel = &CH[channel_index]; + active_mask &= (active_mask - 1); + + SLOT = &channel->SLOT[SLOT1]; j = 4; /* four operators per channel */ do { @@ -1104,9 +1201,8 @@ INLINE void advance_eg_channels(FM_CH *CH, unsigned int eg_cnt) SLOT++; } while (--j); - /* next channel */ - CH++; - } while (--i); + recalculate_channel_runtime_masks(channel); + } } /* SSG-EG update process */ @@ -1209,7 +1305,7 @@ INLINE void update_phase_lfo_slot(FM_SLOT *SLOT, INT32 pms, UINT32 block_fnum) fc = (((block_fnum << 5) >> (7 - blk)) + SLOT->DT[kc]) & DT_MASK; /* update phase */ - SLOT->phase += (fc * SLOT->mul) >> 1; + SLOT->phase += ((fc * SLOT->mul) >> 1) * ym2612_sample_step; } else /* LFO phase modulation = zero */ { @@ -1248,16 +1344,16 @@ INLINE void update_phase_lfo_channel(FM_CH *CH) /* apply DETUNE & MUL operator specific values */ finc = (fc + CH->SLOT[SLOT1].DT[kc]) & DT_MASK; - CH->SLOT[SLOT1].phase += (finc*CH->SLOT[SLOT1].mul) >> 1; + CH->SLOT[SLOT1].phase += ((finc*CH->SLOT[SLOT1].mul) >> 1) * ym2612_sample_step; finc = (fc + CH->SLOT[SLOT2].DT[kc]) & DT_MASK; - CH->SLOT[SLOT2].phase += (finc*CH->SLOT[SLOT2].mul) >> 1; + CH->SLOT[SLOT2].phase += ((finc*CH->SLOT[SLOT2].mul) >> 1) * ym2612_sample_step; finc = (fc + CH->SLOT[SLOT3].DT[kc]) & DT_MASK; - CH->SLOT[SLOT3].phase += (finc*CH->SLOT[SLOT3].mul) >> 1; + CH->SLOT[SLOT3].phase += ((finc*CH->SLOT[SLOT3].mul) >> 1) * ym2612_sample_step; finc = (fc + CH->SLOT[SLOT4].DT[kc]) & DT_MASK; - CH->SLOT[SLOT4].phase += (finc*CH->SLOT[SLOT4].mul) >> 1; + CH->SLOT[SLOT4].phase += ((finc*CH->SLOT[SLOT4].mul) >> 1) * ym2612_sample_step; } else /* LFO phase modulation = zero */ { @@ -1278,7 +1374,7 @@ INLINE void refresh_fc_eg_slot(FM_SLOT *SLOT , unsigned int fc , unsigned int kc fc &= DT_MASK; /* (frequency) phase increment counter */ - SLOT->Incr = (fc * SLOT->mul) >> 1; + SLOT->Incr = ((fc * SLOT->mul) >> 1) * ym2612_sample_step; /* ksr */ kc = kc >> SLOT->KSR; @@ -1345,12 +1441,81 @@ INLINE signed int op_calc1(UINT32 phase, unsigned int env, unsigned int pm) return tl_tab[p]; } +INLINE int channel_is_effectively_silent(unsigned int eg1, unsigned int eg2, unsigned int eg3, unsigned int eg4) +{ + return eg1 >= ENV_QUIET && eg2 >= ENV_QUIET && eg3 >= ENV_QUIET && eg4 >= ENV_QUIET; +} + INLINE void chan_calc(FM_CH *CH, int num) { + UINT8 phase_only_mask = phase_only_channels_mask; + UINT8 channel_mask = 1; + do { + if (phase_only_mask & channel_mask) + { + if(CH->pms) + { + if ((ym2612->OPN.ST.mode & 0xC0) && (CH == &ym2612->CH[2])) + { + update_phase_lfo_slot(&CH->SLOT[SLOT1], CH->pms, ym2612->OPN.SL3.block_fnum[1]); + update_phase_lfo_slot(&CH->SLOT[SLOT2], CH->pms, ym2612->OPN.SL3.block_fnum[2]); + update_phase_lfo_slot(&CH->SLOT[SLOT3], CH->pms, ym2612->OPN.SL3.block_fnum[0]); + update_phase_lfo_slot(&CH->SLOT[SLOT4], CH->pms, CH->block_fnum); + } + else + { + update_phase_lfo_channel(CH); + } + } + else + { + CH->SLOT[SLOT1].phase += CH->SLOT[SLOT1].Incr; + CH->SLOT[SLOT2].phase += CH->SLOT[SLOT2].Incr; + CH->SLOT[SLOT3].phase += CH->SLOT[SLOT3].Incr; + CH->SLOT[SLOT4].phase += CH->SLOT[SLOT4].Incr; + } + + CH++; + channel_mask <<= 1; + continue; + } + UINT32 AM = ym2612->OPN.LFO_AM >> CH->ams; - unsigned int eg_out = volume_calc(&CH->SLOT[SLOT1]); + unsigned int eg1 = volume_calc(&CH->SLOT[SLOT1]); + unsigned int eg3 = volume_calc(&CH->SLOT[SLOT3]); + unsigned int eg2 = volume_calc(&CH->SLOT[SLOT2]); + unsigned int eg4 = volume_calc(&CH->SLOT[SLOT4]); + + if (channel_is_effectively_silent(eg1, eg2, eg3, eg4)) + { + if(CH->pms) + { + if ((ym2612->OPN.ST.mode & 0xC0) && (CH == &ym2612->CH[2])) + { + update_phase_lfo_slot(&CH->SLOT[SLOT1], CH->pms, ym2612->OPN.SL3.block_fnum[1]); + update_phase_lfo_slot(&CH->SLOT[SLOT2], CH->pms, ym2612->OPN.SL3.block_fnum[2]); + update_phase_lfo_slot(&CH->SLOT[SLOT3], CH->pms, ym2612->OPN.SL3.block_fnum[0]); + update_phase_lfo_slot(&CH->SLOT[SLOT4], CH->pms, CH->block_fnum); + } + else + { + update_phase_lfo_channel(CH); + } + } + else + { + CH->SLOT[SLOT1].phase += CH->SLOT[SLOT1].Incr; + CH->SLOT[SLOT2].phase += CH->SLOT[SLOT2].Incr; + CH->SLOT[SLOT3].phase += CH->SLOT[SLOT3].Incr; + CH->SLOT[SLOT4].phase += CH->SLOT[SLOT4].Incr; + } + + CH++; + channel_mask <<= 1; + continue; + } m2 = c1 = c2 = mem = 0; @@ -1368,26 +1533,23 @@ INLINE void chan_calc(FM_CH *CH, int num) } CH->op1_out[1] = 0; - if( eg_out < ENV_QUIET ) /* SLOT 1 */ + if( eg1 < ENV_QUIET ) /* SLOT 1 */ { if (!CH->FB) out=0; - CH->op1_out[1] = op_calc1(CH->SLOT[SLOT1].phase, eg_out, (out<FB) ); + CH->op1_out[1] = op_calc1(CH->SLOT[SLOT1].phase, eg1, (out<FB) ); } } - eg_out = volume_calc(&CH->SLOT[SLOT3]); - if( eg_out < ENV_QUIET ) /* SLOT 3 */ - *CH->connect3 += op_calc(CH->SLOT[SLOT3].phase, eg_out, m2); + if( eg3 < ENV_QUIET ) /* SLOT 3 */ + *CH->connect3 += op_calc(CH->SLOT[SLOT3].phase, eg3, m2); - eg_out = volume_calc(&CH->SLOT[SLOT2]); - if( eg_out < ENV_QUIET ) /* SLOT 2 */ - *CH->connect2 += op_calc(CH->SLOT[SLOT2].phase, eg_out, c1); + if( eg2 < ENV_QUIET ) /* SLOT 2 */ + *CH->connect2 += op_calc(CH->SLOT[SLOT2].phase, eg2, c1); - eg_out = volume_calc(&CH->SLOT[SLOT4]); - if( eg_out < ENV_QUIET ) /* SLOT 4 */ - *CH->connect4 += op_calc(CH->SLOT[SLOT4].phase, eg_out, c2); + if( eg4 < ENV_QUIET ) /* SLOT 4 */ + *CH->connect4 += op_calc(CH->SLOT[SLOT4].phase, eg4, c2); /* store current MEM */ @@ -1419,6 +1581,86 @@ INLINE void chan_calc(FM_CH *CH, int num) /* next channel */ CH++; + channel_mask <<= 1; + } while (--num); +} + +INLINE void chan_calc_no_phase_lfo(FM_CH *CH, int num) +{ + UINT8 phase_only_mask = phase_only_channels_mask; + UINT8 channel_mask = 1; + + do + { + if (phase_only_mask & channel_mask) + { + CH->SLOT[SLOT1].phase += CH->SLOT[SLOT1].Incr; + CH->SLOT[SLOT2].phase += CH->SLOT[SLOT2].Incr; + CH->SLOT[SLOT3].phase += CH->SLOT[SLOT3].Incr; + CH->SLOT[SLOT4].phase += CH->SLOT[SLOT4].Incr; + CH++; + channel_mask <<= 1; + continue; + } + + UINT32 AM = ym2612->OPN.LFO_AM >> CH->ams; + unsigned int eg1 = volume_calc(&CH->SLOT[SLOT1]); + unsigned int eg3 = volume_calc(&CH->SLOT[SLOT3]); + unsigned int eg2 = volume_calc(&CH->SLOT[SLOT2]); + unsigned int eg4 = volume_calc(&CH->SLOT[SLOT4]); + + if (channel_is_effectively_silent(eg1, eg2, eg3, eg4)) + { + CH->SLOT[SLOT1].phase += CH->SLOT[SLOT1].Incr; + CH->SLOT[SLOT2].phase += CH->SLOT[SLOT2].Incr; + CH->SLOT[SLOT3].phase += CH->SLOT[SLOT3].Incr; + CH->SLOT[SLOT4].phase += CH->SLOT[SLOT4].Incr; + CH++; + channel_mask <<= 1; + continue; + } + + m2 = c1 = c2 = mem = 0; + + *CH->mem_connect = CH->mem_value; + { + INT32 out = CH->op1_out[0] + CH->op1_out[1]; + CH->op1_out[0] = CH->op1_out[1]; + + if (!CH->connect1) { + mem = c1 = c2 = CH->op1_out[0]; + } else { + *CH->connect1 += CH->op1_out[0]; + } + + CH->op1_out[1] = 0; + if (eg1 < ENV_QUIET) + { + if (!CH->FB) + out = 0; + + CH->op1_out[1] = op_calc1(CH->SLOT[SLOT1].phase, eg1, (out << CH->FB)); + } + } + + if (eg3 < ENV_QUIET) + *CH->connect3 += op_calc(CH->SLOT[SLOT3].phase, eg3, m2); + + if (eg2 < ENV_QUIET) + *CH->connect2 += op_calc(CH->SLOT[SLOT2].phase, eg2, c1); + + if (eg4 < ENV_QUIET) + *CH->connect4 += op_calc(CH->SLOT[SLOT4].phase, eg4, c2); + + CH->mem_value = mem; + + CH->SLOT[SLOT1].phase += CH->SLOT[SLOT1].Incr; + CH->SLOT[SLOT2].phase += CH->SLOT[SLOT2].Incr; + CH->SLOT[SLOT3].phase += CH->SLOT[SLOT3].Incr; + CH->SLOT[SLOT4].phase += CH->SLOT[SLOT4].Incr; + + CH++; + channel_mask <<= 1; } while (--num); } @@ -1504,6 +1746,7 @@ INLINE void OPNWriteReg(int r, int v) case 0x40: /* TL */ set_tl(SLOT,v); + recalculate_channel_runtime_masks(CH); break; case 0x50: /* KS, AR */ @@ -1521,10 +1764,14 @@ INLINE void OPNWriteReg(int r, int v) case 0x80: /* SL, RR */ set_sl_rr(SLOT,v); + recalculate_channel_runtime_masks(CH); break; case 0x90: /* SSG-EG */ - SLOT->ssg = v&0x0f; + { + const uint8_t ssg = v & 0x0f; + ssg_eg_active_slots += ((ssg & 0x08) != 0) - ((SLOT->ssg & 0x08) != 0); + SLOT->ssg = ssg; /* recalculate EG output */ if (SLOT->state > EG_REL) @@ -1608,9 +1855,9 @@ INLINE void OPNWriteReg(int r, int v) That is not necessary, but then EG will be generating Attack phase. */ - - + recalculate_channel_runtime_masks(CH); break; + } case 0xa0: switch( OPN_SLOT(r) ){ @@ -1627,6 +1874,7 @@ INLINE void OPNWriteReg(int r, int v) CH->block_fnum = (blk<<11) | fn; CH->SLOT[SLOT1].Incr=-1; + mark_dirty_channel(CH); break; } case 1: /* 0xa4-0xa6 : FNUM2,BLK */ @@ -1643,6 +1891,7 @@ INLINE void OPNWriteReg(int r, int v) ym2612->OPN.SL3.fc[c] = (fn << 6) >> (7 - blk); ym2612->OPN.SL3.block_fnum[c] = (blk<<11) | fn; ym2612->CH[2].SLOT[SLOT1].Incr=-1; + mark_dirty_channel_index(2); } break; case 3: /* 0xac-0xae : 3CH FNUM2,BLK */ @@ -1662,8 +1911,12 @@ INLINE void OPNWriteReg(int r, int v) break; } case 1: /* 0xb4-0xb6 : L , R , AMS , PMS */ + { + const int new_pms = (v & 7) * 16; + phase_lfo_active_channels += (new_pms != 0) - (CH->pms != 0); + /* b0-2 PMS */ - CH->pms = (v & 7) * 16; // 32; /* CH->pms = PM depth * 32 (index in lfo_pm_table) */ + CH->pms = new_pms; // 32; /* CH->pms = PM depth * 32 (index in lfo_pm_table) */ /* b4-5 AMS */ CH->ams = lfo_ams_depth_shift[(v>>4) & 0x03]; @@ -1672,6 +1925,7 @@ INLINE void OPNWriteReg(int r, int v) // ym2612->OPN.pan[ c*2 ] = (v & 0x80) ? bitmask : 0; // ym2612->OPN.pan[ c*2+1 ] = (v & 0x40) ? bitmask : 0; break; + } } break; } @@ -1821,13 +2075,18 @@ static void init_tables(void) /* initialize ym2612 emulator */ void YM2612Init(void) { - static unsigned init_table_done = 0; - memset(ym2612, 0, sizeof(YM2612)); - if (init_table_done == 0) { - init_tables(); - init_table_done = 1; - } + if (OPNREGS) + memset(OPNREGS, 0, 512); + m2 = c1 = c2 = mem = 0; + memset(out_fm, 0, sizeof(out_fm)); + ssg_eg_active_slots = 0; + phase_lfo_active_channels = 0; + dirty_channels_mask = 0x3f; + phase_only_channels_mask = 0; + eg_active_channels_mask = 0; + init_tables(); + recalculate_runtime_channel_masks(); } /* reset OPN registers */ @@ -1853,6 +2112,10 @@ void YM2612ResetChip(void) ym2612->dacen = 0; ym2612->dacout = 0; + if (OPNREGS) + memset(OPNREGS, 0, 512); + m2 = c1 = c2 = mem = 0; + memset(out_fm, 0, sizeof(out_fm)); set_timers(0x30); ym2612->OPN.ST.TB = 0; @@ -1861,6 +2124,11 @@ void YM2612ResetChip(void) ym2612->OPN.ST.TAL = 1024; reset_channels(&ym2612->CH[0] , 6 ); + ssg_eg_active_slots = 0; + phase_lfo_active_channels = 0; + dirty_channels_mask = 0x3f; + phase_only_channels_mask = 0; + eg_active_channels_mask = 0; for(i = 0xb6 ; i >= 0xb4 ; i-- ) { @@ -1872,6 +2140,8 @@ void YM2612ResetChip(void) OPNWriteReg(i ,0); OPNWriteReg(i|0x100,0); } + + recalculate_runtime_channel_masks(); } /* YM2612 execution */ @@ -1881,31 +2151,37 @@ static inline void YM2612Update(int16_t *buffer, int length) int i; int lt; - /* refresh PG increments and EG rates if required */ - refresh_fc_eg_chan(&ym2612->CH[0]); - refresh_fc_eg_chan(&ym2612->CH[1]); - - if (!(ym2612->OPN.ST.mode & 0xC0)) - { - refresh_fc_eg_chan(&ym2612->CH[2]); - } - else + /* refresh PG increments and EG rates only for channels touched by register changes */ + if (dirty_channels_mask) { - /* 3SLOT MODE (operator order is 0,1,3,2) */ - if(ym2612->CH[2].SLOT[SLOT1].Incr==-1) + if (dirty_channels_mask & 0x01) refresh_fc_eg_chan(&ym2612->CH[0]); + if (dirty_channels_mask & 0x02) refresh_fc_eg_chan(&ym2612->CH[1]); + + if (dirty_channels_mask & 0x04) { - refresh_fc_eg_slot(&ym2612->CH[2].SLOT[SLOT1] , ym2612->OPN.SL3.fc[1] , ym2612->OPN.SL3.kcode[1] ); - refresh_fc_eg_slot(&ym2612->CH[2].SLOT[SLOT2] , ym2612->OPN.SL3.fc[2] , ym2612->OPN.SL3.kcode[2] ); - refresh_fc_eg_slot(&ym2612->CH[2].SLOT[SLOT3] , ym2612->OPN.SL3.fc[0] , ym2612->OPN.SL3.kcode[0] ); - refresh_fc_eg_slot(&ym2612->CH[2].SLOT[SLOT4] , ym2612->CH[2].fc , ym2612->CH[2].kcode ); + if (!(ym2612->OPN.ST.mode & 0xC0)) + { + refresh_fc_eg_chan(&ym2612->CH[2]); + } + else if (ym2612->CH[2].SLOT[SLOT1].Incr==-1) + { + /* 3SLOT MODE (operator order is 0,1,3,2) */ + refresh_fc_eg_slot(&ym2612->CH[2].SLOT[SLOT1] , ym2612->OPN.SL3.fc[1] , ym2612->OPN.SL3.kcode[1] ); + refresh_fc_eg_slot(&ym2612->CH[2].SLOT[SLOT2] , ym2612->OPN.SL3.fc[2] , ym2612->OPN.SL3.kcode[2] ); + refresh_fc_eg_slot(&ym2612->CH[2].SLOT[SLOT3] , ym2612->OPN.SL3.fc[0] , ym2612->OPN.SL3.kcode[0] ); + refresh_fc_eg_slot(&ym2612->CH[2].SLOT[SLOT4] , ym2612->CH[2].fc , ym2612->CH[2].kcode ); + } } - } - refresh_fc_eg_chan(&ym2612->CH[3]); - refresh_fc_eg_chan(&ym2612->CH[4]); - refresh_fc_eg_chan(&ym2612->CH[5]); + if (dirty_channels_mask & 0x08) refresh_fc_eg_chan(&ym2612->CH[3]); + if (dirty_channels_mask & 0x10) refresh_fc_eg_chan(&ym2612->CH[4]); + if (dirty_channels_mask & 0x20) refresh_fc_eg_chan(&ym2612->CH[5]); + + dirty_channels_mask = 0; + } /* buffering */ + const int use_phase_lfo = ym2612->OPN.lfo_timer_overflow && phase_lfo_active_channels; for(i=0; i < length ; i++) { /* clear outputs */ @@ -1917,32 +2193,40 @@ static inline void YM2612Update(int16_t *buffer, int length) out_fm[5] = 0; /* update SSG-EG output */ - update_ssg_eg_channels(&ym2612->CH[0]); + if (ssg_eg_active_slots) + update_ssg_eg_channels(&ym2612->CH[0]); /* calculate FM */ if (!ym2612->dacen) { - chan_calc(&ym2612->CH[0],6); + if (use_phase_lfo) + chan_calc(&ym2612->CH[0],6); + else + chan_calc_no_phase_lfo(&ym2612->CH[0],6); } else { /* DAC Mode */ out_fm[5] = ym2612->dacout; - chan_calc(&ym2612->CH[0],5); + if (use_phase_lfo) + chan_calc(&ym2612->CH[0],5); + else + chan_calc_no_phase_lfo(&ym2612->CH[0],5); } /* advance LFO */ advance_lfo(); /* advance envelope generator */ - ym2612->OPN.eg_timer ++; + ym2612->OPN.eg_timer += ym2612_sample_step; /* EG is updated every 3 samples */ - if (ym2612->OPN.eg_timer >= 3) + while (ym2612->OPN.eg_timer >= 3) { - ym2612->OPN.eg_timer = 0; + ym2612->OPN.eg_timer -= 3; ym2612->OPN.eg_cnt++; - advance_eg_channels(&ym2612->CH[0], ym2612->OPN.eg_cnt); + if (eg_active_channels_mask) + advance_eg_channels(&ym2612->CH[0], ym2612->OPN.eg_cnt); } /* 14-bit accumulator channels outputs (range is -8192;+8191) */ if (out_fm[0] > 8191) out_fm[0] = 8191; @@ -2008,7 +2292,7 @@ static inline void YM2612Update(int16_t *buffer, int length) } /* timer B control */ - INTERNAL_TIMER_B(length); + INTERNAL_TIMER_B(length * ym2612_sample_step); } void ym2612_run( int target) { @@ -2108,7 +2392,14 @@ void YM2612Config(unsigned char dac_bits) //,unsigned int AUDIO_FREQ_DIVISOR) ym2612->OPN.pan[i] = bitmask; } } - ym2612->divisor = AUDIO_FREQ_DIVISOR; + ym2612->divisor = AUDIO_FREQ_DIVISOR * ym2612_sample_step; +} + +void YM2612SetSampleStep(int step) +{ + ym2612_sample_step = step > 0 ? step : 1; + if (ym2612) + ym2612->divisor = AUDIO_FREQ_DIVISOR * ym2612_sample_step; } void YM2612SaveRegs(uint8_t *regs) @@ -2217,4 +2508,8 @@ void gwenesis_ym2612_load_state() { saveGwenesisStateGetBuffer(state, "out_fm", out_fm, sizeof(out_fm)); bitmask = saveGwenesisStateGet(state, "bitmask"); saveGwenesisStateGetBuffer(state, "OPNREGS", OPNREGS, sizeof(OPNREGS)); + recalculate_ssg_eg_active_slots(); + recalculate_phase_lfo_active_channels(); + dirty_channels_mask = 0x3f; + recalculate_runtime_channel_masks(); } diff --git a/components/genesis/gwenesis/src/sound/ym2612.h b/components/genesis/gwenesis/src/sound/ym2612.h index 305feb02..ccc0c2bc 100644 --- a/components/genesis/gwenesis/src/sound/ym2612.h +++ b/components/genesis/gwenesis/src/sound/ym2612.h @@ -23,6 +23,7 @@ extern int ym2612_clock; extern void YM2612Init(void); extern void YM2612Config(unsigned char dac_bits); //,unsigned int AUDIO_FREQ_DIVISOR); extern void YM2612ResetChip(void); +extern void YM2612SetSampleStep(int step); //extern void YM2612Update(int16_t *buffer, int length); extern void YM2612Write(unsigned int a, unsigned int v, int target); extern void ym2612_run(int target); @@ -36,7 +37,9 @@ extern int YM2612SaveContext(unsigned char *state); #if GW_TARGET extern uint8_t *lfo_pm_table; #else -extern int32_t *lfo_pm_table; +// GW_TARGET=1 indexes 128*8*16 entries with values in 0..255; int16_t is +// sufficient and halves the table footprint / per-sample load width. +extern int16_t *lfo_pm_table; #endif /* operator unit */ diff --git a/components/genesis/gwenesis/src/sound/z80inst.c b/components/genesis/gwenesis/src/sound/z80inst.c index 5fbf1fcc..03e82b37 100644 --- a/components/genesis/gwenesis/src/sound/z80inst.c +++ b/components/genesis/gwenesis/src/sound/z80inst.c @@ -28,14 +28,25 @@ __license__ = "GPLv3" #include "gwenesis_savestate.h" #include +#include "genesis_dualcore.h" +#if GENESIS_DUAL_CORE +#include "esp_timer.h" +#endif #pragma GCC optimize("Ofast") static int bus_ack = 0; static int reset = 0; static int reset_once = 0; +#if GENESIS_DUAL_CORE +// Set by core 1 (the sound task): 1 while the Z80 is not executing (gated by +// bus_ack/reset or quiescent between frames), 0 while it is running. The 68k +// (core 0) reads this to confirm the Z80 has stopped before touching Z80 state. +static int z80_halted = 1; +#endif int zclk = 0; static int initialized = 0; +static int current_timeslice = 0; unsigned char *Z80_RAM; @@ -73,19 +84,26 @@ void z80_start() { cpu.ICount = 0; cpu.Trace = 0; cpu.Trap = 0x0009; + // Auto-clear IRequest when an interrupt is taken, so the V-int can be held + // asserted across the whole vblank (to be caught whenever the Z80 re-enables + // interrupts) without being taken more than once per frame. + cpu.IAutoReset = 1; ResetZ80(&cpu); + Z80_BANK = 0; reset=1; reset_once=0; bus_ack=0; zclk=0; + current_timeslice = 0; } void z80_pulse_reset() { ResetZ80(&cpu); + Z80_BANK = 0; + current_timeslice = 0; } -static int current_timeslice = 0; -void z80_run(int target) { +IRAM_ATTR void z80_run(int target) { // we are in advance,nothind to do current_timeslice = 0; @@ -97,24 +115,62 @@ current_timeslice = 0; current_timeslice = target - zclk; int rem = 0; +#if GENESIS_DUAL_CORE + // Core 1. Read the 68k-owned gate flags with acquire ordering, and publish + // the halt state with release ordering so the 68k's BUSREQ/RESET handshake + // sees a consistent view. + const int can_run = (__atomic_load_n(&reset_once, __ATOMIC_ACQUIRE) == 1) + && (__atomic_load_n(&bus_ack, __ATOMIC_ACQUIRE) == 0) + && (__atomic_load_n(&reset, __ATOMIC_ACQUIRE) == 0); + if (can_run) { + __atomic_store_n(&z80_halted, 0, __ATOMIC_RELEASE); + rem = ExecZ80(&cpu, current_timeslice / Z80_FREQ_DIVISOR); + __atomic_store_n(&z80_halted, 1, __ATOMIC_RELEASE); + } else { + __atomic_store_n(&z80_halted, 1, __ATOMIC_RELEASE); + } +#else if ((reset_once == 1) && (bus_ack == 0) && (reset == 0)) { // z80_log("z80_run", "%1d%1d%1d||zclk=%d,tgt=%d",reset_once, bus_ack, reset, zclk, target); rem = ExecZ80(&cpu, current_timeslice / Z80_FREQ_DIVISOR); } +#endif zclk = target - rem * Z80_FREQ_DIVISOR; } -void z80_sync(void) { +#if GENESIS_DUAL_CORE +// Called by core 1 at the end of a frame: the Z80 is quiescent until the next +// frame is dispatched, so report it halted (covers a 68k BUSREQ that arrives +// after the sound task has finished its frame and stopped calling z80_run). +void z80_mark_quiescent(void) { + __atomic_store_n(&z80_halted, 1, __ATOMIC_RELEASE); +} + +// Called by core 0: spin until core 1 acknowledges the Z80 has halted, so it is +// safe to touch Z80 state (RAM / reset). Bounded by a timeout so a stalled +// sound task can never hard-hang the 68k. +static IRAM_ATTR void z80_wait_until_halted(void) { + const int64_t deadline = esp_timer_get_time() + 2000; // 2 ms safety net + while (__atomic_load_n(&z80_halted, __ATOMIC_ACQUIRE) == 0) { + if (esp_timer_get_time() > deadline) + break; + } +} +#endif + +#if !GENESIS_DUAL_CORE +static IRAM_ATTR void z80_sync(void) { /* - get M68K cycles + get M68K cycles Execute cycles on z80 to sync with m68K */ z80_run(m68k_cycles_master()); } +#endif void z80_set_memory(unsigned char *buffer) { @@ -123,6 +179,34 @@ void z80_set_memory(unsigned char *buffer) } void z80_write_ctrl(unsigned int address, unsigned int value) { +#if GENESIS_DUAL_CORE + // The Z80 runs on core 1; we never execute it here. For operations that stop + // the Z80 so the 68k can safely touch Z80 state, assert the gate flag and + // wait for core 1 to acknowledge the halt. Releasing operations need no wait. + if (address == 0x1100) { // BUSREQ + z80_log(__FUNCTION__,"BUSREQ = %d, current=%d", value,bus_ack); + if (value) { + __atomic_store_n(&bus_ack, 1, __ATOMIC_RELEASE); + z80_wait_until_halted(); + } else { + __atomic_store_n(&bus_ack, 0, __ATOMIC_RELEASE); + } + } else if (address == 0x1200) { // RESET + z80_log(__FUNCTION__,"RESET = %d, current=%d", value,reset); + if (value == 0) { + __atomic_store_n(&reset, 1, __ATOMIC_RELEASE); + z80_wait_until_halted(); + } else { + // De-assert reset (start the Z80). Force the gate first so the Z80 is + // halted on core 1 before we mutate its state, then resume. + __atomic_store_n(&reset, 1, __ATOMIC_RELEASE); + z80_wait_until_halted(); + z80_pulse_reset(); + __atomic_store_n(&reset_once, 1, __ATOMIC_RELEASE); + __atomic_store_n(&reset, 0, __ATOMIC_RELEASE); + } + } +#else z80_sync(); if (address == 0x1100) // BUSREQ @@ -142,7 +226,7 @@ void z80_write_ctrl(unsigned int address, unsigned int value) { } else if (address == 0x1200) // RESET { z80_log(__FUNCTION__,"RESET = %d, current=%d", value,reset); - + if (value == 0) { reset = 1; } else { @@ -152,10 +236,25 @@ void z80_write_ctrl(unsigned int address, unsigned int value) { reset_once = 1; } } +#endif } unsigned int z80_read_ctrl(unsigned int address) { - +#if GENESIS_DUAL_CORE + // Report current state without running the Z80 (it runs on core 1). The + // RUNNING bit reflects the requested bus state, which the BUSREQ handshake + // has already made consistent with the actual halt. + if (address == 0x1100) { + return __atomic_load_n(&bus_ack, __ATOMIC_ACQUIRE) == 1 ? 0 : 1; + } else if (address == 0x1101) { + return 0x00; + } else if (address == 0x1200) { + return __atomic_load_n(&reset, __ATOMIC_ACQUIRE); + } else if (address == 0x1201) { + return 0x00; + } + return 0xFF; +#else z80_sync(); if (address == 0x1100) { @@ -175,6 +274,7 @@ unsigned int z80_read_ctrl(unsigned int address) { return 0x00; } return 0xFF; +#endif } void z80_irq_line(unsigned int value) @@ -353,4 +453,3 @@ void gwenesis_z80inst_load_state() { current_timeslice = saveGwenesisStateGet(state, "current_timeslice"); } - diff --git a/components/genesis/gwenesis/src/sound/z80inst.h b/components/genesis/gwenesis/src/sound/z80inst.h index b8913944..c04ed0e7 100644 --- a/components/genesis/gwenesis/src/sound/z80inst.h +++ b/components/genesis/gwenesis/src/sound/z80inst.h @@ -25,6 +25,9 @@ void z80_start(); void z80_pulse_reset(); void z80_execute(unsigned int target); void z80_run(int target); +// Dual-core: called by core 1 when the Z80 becomes quiescent at end of frame, +// so a 68k BUSREQ arriving after the sound task finishes sees it as halted. +void z80_mark_quiescent(void); extern int zclk; void gwenesis_z80inst_save_state(); diff --git a/components/genesis/gwenesis/src/vdp/gwenesis_vdp.h b/components/genesis/gwenesis/src/vdp/gwenesis_vdp.h index d659db3d..c49c1d57 100644 --- a/components/genesis/gwenesis/src/vdp/gwenesis_vdp.h +++ b/components/genesis/gwenesis/src/vdp/gwenesis_vdp.h @@ -119,6 +119,9 @@ extern uint8_t *render_buffer; // [SCREEN_WIDTH + PIX_OVERFLOW*2]; extern uint8_t *sprite_buffer; // [SCREEN_WIDTH + PIX_OVERFLOW*2]; void gwenesis_vdp_reset(); +void gwenesis_vdp_reset_render_state(void); +void gwenesis_vdp_update_sat_cache_entry(unsigned int sat_byte_offset); +void gwenesis_vdp_rebuild_sat_cache(void); void gwenesis_vdp_set_hblank(); void gwenesis_vdp_clear_hblank(); void gwenesis_vdp_set_vblank(); diff --git a/components/genesis/gwenesis/src/vdp/gwenesis_vdp_gfx.c b/components/genesis/gwenesis/src/vdp/gwenesis_vdp_gfx.c index a9e295d4..a576e9f8 100644 --- a/components/genesis/gwenesis/src/vdp/gwenesis_vdp_gfx.c +++ b/components/genesis/gwenesis/src/vdp/gwenesis_vdp_gfx.c @@ -83,9 +83,46 @@ static int PlanA_lastcol; static int Window_firstcol; static int Window_lastcol; +static uint16_t ntwidth_x2; +static uint16_t ntw_mask, nth_mask; +static int plane_b_cached_back = -1; +static uint16_t plane_b_pair_lut[8][256]; +static uint16_t plane_b_pair_lut_fliph[8][256]; +enum { + MAX_RENDER_LINES = 240, + MAX_LINE_SPRITES = 20, +}; +static uint8_t sprite_line_counts[MAX_RENDER_LINES]; +static uint8_t sprite_line_entries[MAX_RENDER_LINES][MAX_LINE_SPRITES]; +static bool sprite_line_cache_dirty = true; +static int sprite_line_cache_width = 0; +static int sprite_line_cache_height = 0; + +typedef struct { + int16_t sy; + int16_t sx; + uint16_t name; + uint8_t link; + uint8_t sw; + uint8_t sh; + uint8_t width_pixels; + uint8_t height_pixels; + uint8_t isflipv; + uint8_t isfliph; +} sat_sprite_t; + +static sat_sprite_t sat_sprite_cache[SAT_CACHE_MAX_SIZE / 8]; + // 16 bits access to VRAM // #define FETCH16VRAM(A) ({size_t addr = (A); (VRAM[addr+1]) | (VRAM[addr] << 8);}) #define FETCH16VRAM(A) ( (VRAM[(A)+1]) | (VRAM[(A)] << 8) ) + +static inline __attribute__((always_inline)) +uint16_t load_vram_name(const uint8_t *ptr) +{ + return (uint16_t)((ptr[0] << 8) | ptr[1]); +} + #define VDP_GFX_DISABLE_LOGGING 1 #if !VDP_GFX_DISABLE_LOGGING @@ -118,6 +155,119 @@ void gwenesis_vdp_set_buffer(uint8_t *ptr_screen_buffer) screen_buffer = ptr_screen_buffer; } +void gwenesis_vdp_reset_render_state(void) +{ + mode_h40 = 0; + mode_pal = 0; + screen_width = 256; + screen_height = 224; + gwenesis_H32upscaler = 0; + sprite_overflow = -1; + sprite_collision = false; + base_w = 0; + PlanA_firstcol = 0; + PlanA_lastcol = 0; + Window_firstcol = 0; + Window_lastcol = 0; + ntwidth_x2 = 0; + ntw_mask = 0; + nth_mask = 0; + plane_b_cached_back = -1; + memset(sprite_line_counts, 0, sizeof(sprite_line_counts)); + sprite_line_cache_dirty = true; + sprite_line_cache_width = 0; + sprite_line_cache_height = 0; + memset(sat_sprite_cache, 0, sizeof(sat_sprite_cache)); +} + +void gwenesis_vdp_update_sat_cache_entry(unsigned int sat_byte_offset) +{ + unsigned int entry_index = sat_byte_offset >> 3; + if (entry_index >= (SAT_CACHE_MAX_SIZE / 8)) + return; + + uint8_t *cache = SAT_CACHE + (entry_index << 3); + sat_sprite_t *sprite = &sat_sprite_cache[entry_index]; + + sprite->sy = (int16_t)((((cache[0] & 0x3) << 8) | cache[1]) - 128); + sprite->sx = (int16_t)((((cache[6] & 0x3) << 8) | cache[7]) - 128); + sprite->name = (uint16_t)((cache[4] << 8) | cache[5]); + sprite->link = (uint8_t)BITS_GEN(cache[3], 0, 7); + sprite->sh = (uint8_t)(BITS_GEN(cache[2], 0, 2) + 1); + sprite->sw = (uint8_t)(BITS_GEN(cache[2], 2, 2) + 1); + sprite->width_pixels = (uint8_t)(sprite->sw << 3); + sprite->height_pixels = (uint8_t)(sprite->sh << 3); + sprite->isflipv = (uint8_t)(cache[4] & 0x10); + sprite->isfliph = (uint8_t)(cache[4] & 0x08); + sprite_line_cache_dirty = true; +} + +void gwenesis_vdp_rebuild_sat_cache(void) +{ + for (unsigned int entry_index = 0; entry_index < (SAT_CACHE_MAX_SIZE / 8); entry_index++) { + uint8_t *cache = SAT_CACHE + (entry_index << 3); + sat_sprite_t *sprite = &sat_sprite_cache[entry_index]; + + sprite->sy = (int16_t)((((cache[0] & 0x3) << 8) | cache[1]) - 128); + sprite->sx = (int16_t)((((cache[6] & 0x3) << 8) | cache[7]) - 128); + sprite->name = (uint16_t)((cache[4] << 8) | cache[5]); + sprite->link = (uint8_t)BITS_GEN(cache[3], 0, 7); + sprite->sh = (uint8_t)(BITS_GEN(cache[2], 0, 2) + 1); + sprite->sw = (uint8_t)(BITS_GEN(cache[2], 2, 2) + 1); + sprite->width_pixels = (uint8_t)(sprite->sw << 3); + sprite->height_pixels = (uint8_t)(sprite->sh << 3); + sprite->isflipv = (uint8_t)(cache[4] & 0x10); + sprite->isfliph = (uint8_t)(cache[4] & 0x08); + } + + sprite_line_cache_dirty = true; +} + +static void rebuild_sprite_line_cache(void) +{ + const int sprite_table_size = (screen_width == 320) ? 80 : 64; + const uint8_t max_line_sprites = (screen_width == 320) ? 20 : 16; + const int visible_lines = screen_height < MAX_RENDER_LINES ? screen_height : MAX_RENDER_LINES; + int sidx = 0; + + memset(sprite_line_counts, 0, sizeof(sprite_line_counts)); + + for (int i = 0; i < sprite_table_size && sidx < sprite_table_size; ++i) { + const sat_sprite_t *sprite = &sat_sprite_cache[sidx]; + int first_line = sprite->sy; + int last_line = sprite->sy + sprite->height_pixels; + + if (last_line > 0 && first_line < visible_lines) { + if (first_line < 0) + first_line = 0; + if (last_line > visible_lines) + last_line = visible_lines; + + for (int line = first_line; line < last_line; ++line) { + uint8_t count = sprite_line_counts[line]; + if (count < max_line_sprites) { + sprite_line_entries[line][count] = (uint8_t)sidx; + sprite_line_counts[line] = (uint8_t)(count + 1); + } + } + } + + if (sprite->link == 0) + break; + sidx = sprite->link; + } + + sprite_line_cache_width = screen_width; + sprite_line_cache_height = screen_height; + sprite_line_cache_dirty = false; +} + +static inline __attribute__((always_inline)) void ensure_sprite_line_cache(void) +{ + if (sprite_line_cache_dirty) + rebuild_sprite_line_cache(); +} + /****************************************************************************** * * Draw Sprite character /8pixels in row @@ -139,7 +289,6 @@ void gwenesis_vdp_set_buffer(uint8_t *ptr_screen_buffer) #define PIX6(P) ( ((P) & 0xF0000000 ) >> 28 ) #define PIX7(P) ( ((P) & 0x0F000000 ) >> 24 ) -static inline __attribute__((always_inline)) void draw_pattern_nofliph_sprite(uint8_t *scr, uint32_t p, uint8_t attrs) { if (p == 0) return; @@ -176,7 +325,7 @@ static inline __attribute__((always_inline)) void draw_pattern_nofliph_sprite_over_planes(uint8_t *scr, uint32_t p, uint8_t attrs) { if (p == 0) return; - + /* High priority */ if (attrs & PIXATTR_HIPRI) { @@ -252,63 +401,37 @@ void draw_pattern_fliph_sprite_over_planes(uint8_t *scr, uint32_t p, uint8_t att ******************************************************************************/ -static inline __attribute__((always_inline)) void -draw_pattern_nofliph_planeB(uint8_t *scr, uint32_t p, uint8_t attrs) { - - const uint8_t back = gwenesis_vdp_regs[7]; - - if (p == 0) { - - scr[0] = back; - scr[1] = back; - scr[2] = back; - scr[3] = back; - scr[4] = back; - scr[5] = back; - scr[6] = back; - scr[7] = back; - - return; - } - - scr[0] = PIX0(p) ? attrs | (PIX0(p)) : back; - scr[1] = PIX1(p) ? attrs | (PIX1(p)) : back; - scr[2] = PIX2(p) ? attrs | (PIX2(p)) : back; - scr[3] = PIX3(p) ? attrs | (PIX3(p)) : back; - scr[4] = PIX4(p) ? attrs | (PIX4(p)) : back; - scr[5] = PIX5(p) ? attrs | (PIX5(p)) : back; - scr[6] = PIX6(p) ? attrs | (PIX6(p)) : back; - scr[7] = PIX7(p) ? attrs | (PIX7(p)) : back; +static inline __attribute__((always_inline)) +void store_pair(uint8_t *dst, uint16_t pair) +{ + dst[0] = (uint8_t)pair; + dst[1] = (uint8_t)(pair >> 8); } -static inline __attribute__((always_inline)) void -draw_pattern_fliph_planeB(uint8_t *scr, uint32_t p, uint8_t attrs) { - - const uint8_t back = gwenesis_vdp_regs[7]; - if (p == 0) { - - scr[0] = back; - scr[1] = back; - scr[2] = back; - scr[3] = back; - scr[4] = back; - scr[5] = back; - scr[6] = back; - scr[7] = back; +static inline __attribute__((always_inline)) +uint8_t plane_attrs_from_index(int attr_index) +{ + return (uint8_t)(((attr_index & 0x3) << 4) | ((attr_index & 0x4) << 5)); +} +static void update_plane_b_pair_luts(uint8_t back) +{ + if (plane_b_cached_back == back) return; - } - - scr[0] = PIX7(p) ? attrs | (PIX7(p)) : back; - scr[1] = PIX6(p) ? attrs | (PIX6(p)) : back; - scr[2] = PIX5(p) ? attrs | (PIX5(p)) : back; - scr[3] = PIX4(p) ? attrs | (PIX4(p)) : back; - scr[4] = PIX3(p) ? attrs | (PIX3(p)) : back; - scr[5] = PIX2(p) ? attrs | (PIX2(p)) : back; - scr[6] = PIX1(p) ? attrs | (PIX1(p)) : back; - scr[7] = PIX0(p) ? attrs | (PIX0(p)) : back; + for (int attr_index = 0; attr_index < 8; attr_index++) { + const uint8_t attrs = plane_attrs_from_index(attr_index); + for (int value = 0; value < 256; value++) { + const uint8_t hi = (uint8_t)(value >> 4); + const uint8_t lo = (uint8_t)(value & 0x0F); + const uint8_t out0 = hi ? (uint8_t)(attrs | hi) : back; + const uint8_t out1 = lo ? (uint8_t)(attrs | lo) : back; + plane_b_pair_lut[attr_index][value] = (uint16_t)out0 | ((uint16_t)out1 << 8); + plane_b_pair_lut_fliph[attr_index][value] = (uint16_t)out1 | ((uint16_t)out0 << 8); + } + } + plane_b_cached_back = back; } static inline __attribute__((always_inline)) void @@ -317,7 +440,7 @@ draw_pattern_nofliph_planeAoverB(uint8_t *scr, uint32_t p, uint8_t attrs) { if (p == 0) return; if (attrs & PIXATTR_HIPRI) { - + if (PIX0(p)) scr[0] = attrs | (PIX0(p)); if (PIX1(p)) scr[1] = attrs | (PIX1(p)); if (PIX2(p)) scr[2] = attrs | (PIX2(p)); @@ -347,7 +470,7 @@ draw_pattern_fliph_planeAoverB(uint8_t *scr, uint32_t p, uint8_t attrs) { if (p == 0) return; if (attrs & PIXATTR_HIPRI) { - + if (PIX7(p)) scr[0] = attrs | (PIX7(p)); if (PIX6(p)) scr[1] = attrs | (PIX6(p)); if (PIX5(p)) scr[2] = attrs | (PIX5(p)); @@ -381,31 +504,15 @@ draw_pattern_fliph_planeAoverB(uint8_t *scr, uint32_t p, uint8_t attrs) { ******************************************************************************/ static inline __attribute__((always_inline)) void draw_pattern_sprite(uint8_t *scr, uint16_t name, int paty) { - - // uint16_t pat_addr = name << 5; //name * 32; - // uint8_t pat_palette = BITS_GEN(name, 13, 2); - // //unsigned int is_pat_pri = name & 0x8000; - // uint8_t *pattern = VRAM + pat_addr; - // uint8_t attrs = (pat_palette << 4) | ((name & 0x8000) ? PIXATTR_SPRITE_HIPRI : PIXATTR_SPRITE); uint8_t attrs = ( (name & 0x6000 ) >> 9 ) + ((name & 0x8000) >> 8) + PIXATTR_SPRITE; unsigned int pattern; - // Vertical flip ? - // if (name & 0x1000) - // pattern += (7 - paty) * 4; - // else - // pattern += paty * 4; - - // unsigned int pattern; - - // Vertical flip ? if (name & 0x1000) - pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + ((7 - paty) * 4)); //) pat_addr; + pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + ((7 - paty) * 4)); else pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + (paty * 4)); - // Horizontal flip ? if (name & 0x0800) draw_pattern_fliph_sprite(scr, pattern, attrs); else @@ -414,32 +521,15 @@ void draw_pattern_sprite(uint8_t *scr, uint16_t name, int paty) { static inline __attribute__((always_inline)) void draw_pattern_sprite_over_planes(uint8_t *scr, uint16_t name, int paty) { - - // uint16_t pat_addr = name << 5 ; //* 32; - // int pat_palette = BITS_GEN(name, 13, 2); - // int is_pat_pri = name & 0x8000; - // uint8_t *pattern = VRAM + pat_addr; - // uint8_t attrs = (pat_palette << 4) | (is_pat_pri ? PIXATTR_SPRITE_HIPRI : PIXATTR_SPRITE); - - // Vertical flip ? - // if (name & 0x1000) - // pattern += (7 - paty) * 4; - // else - // pattern += paty * 4; - - //uint8_t attrs = ( (name & 0x6000 ) >> 9 ) | ((name & 0x8000) ? PIXATTR_SPRITE_HIPRI : PIXATTR_SPRITE); uint8_t attrs = ( (name & 0x6000 ) >> 9 ) + ((name & 0x8000) >> 8) + PIXATTR_SPRITE; - //uint8_t attrs = ( (name >>9) & 0x70 ) | PIXATTR_SPRITE; unsigned int pattern; - // Vertical flip ? if (name & 0x1000) - pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + ((7 - paty) * 4)); //) pat_addr; + pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + ((7 - paty) * 4)); else pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + (paty * 4)); - // Horizontal flip ? if (name & 0x0800) draw_pattern_fliph_sprite_over_planes (scr, pattern, attrs); else @@ -448,72 +538,39 @@ void draw_pattern_sprite_over_planes(uint8_t *scr, uint16_t name, int paty) { static inline __attribute__((always_inline)) void draw_pattern_planeB(uint8_t *scr, uint16_t name, int paty) { - // uint16_t pat_addr = name << 5; // * 32; - // uint8_t pat_palette = BITS_GEN(name, 13, 2); - // unsigned int is_pat_pri = name & 0x8000; - //uint8_t *pattern = VRAM + pat_addr; - - uint8_t attrs = ( (name & 0x6000 ) >> 9 ) + ((name & 0x8000) >> 8); - - unsigned int pattern; - - // Vertical flip ? - if (name & 0x1000) - pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + ((7 - paty) * 4)); //) pat_addr; - else - pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + (paty * 4)); - -// if ((*(unsigned int *)pattern) == 0 ) return; - // uint8_t *pattern = VRAM + ((name << 5) & 0xFFFF); //) pat_addr; - //uint32_t pattern = VRAM[(name & 0x07FF) << 5]; //) pat_addr; - - // Horizontal flip ? - if (name & 0x0800) - draw_pattern_fliph_planeB(scr, pattern, attrs); - - else - draw_pattern_nofliph_planeB(scr, pattern, attrs); - + const uint8_t attr_index = (uint8_t)((name >> 13) & 0x07); + const uint8_t *pattern = VRAM + ((name & 0x07FF) << 5) + ((name & 0x1000) ? ((7 - paty) << 2) : (paty << 2)); + + if (name & 0x0800) { + store_pair(scr + 0, plane_b_pair_lut_fliph[attr_index][pattern[3]]); + store_pair(scr + 2, plane_b_pair_lut_fliph[attr_index][pattern[2]]); + store_pair(scr + 4, plane_b_pair_lut_fliph[attr_index][pattern[1]]); + store_pair(scr + 6, plane_b_pair_lut_fliph[attr_index][pattern[0]]); + } else { + store_pair(scr + 0, plane_b_pair_lut[attr_index][pattern[0]]); + store_pair(scr + 2, plane_b_pair_lut[attr_index][pattern[1]]); + store_pair(scr + 4, plane_b_pair_lut[attr_index][pattern[2]]); + store_pair(scr + 6, plane_b_pair_lut[attr_index][pattern[3]]); + } } static inline __attribute__((always_inline)) void draw_pattern_planeA(uint8_t *scr, uint16_t name, int paty) { - // uint16_t pat_addr = name << 5; //* 32; - // uint8_t pat_palette = BITS_GEN(name, 13, 2); - // unsigned int is_pat_pri = name & 0x8000; - // uint8_t *pattern = VRAM + pat_addr; - - uint8_t attrs = ( (name & 0x6000 ) >> 9 ) + ((name & 0x8000) >> 8); - - - + uint8_t attrs = ( (name & 0x6000 ) >> 9 ) + ((name & 0x8000) >> 8); unsigned int pattern; - // Vertical flip ? - // if (name & 0x1000) - // pattern += (7 - paty) * 4; - // else - // pattern += paty * 4; - - // Vertical flip ? if (name & 0x1000) - pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + ((7 - paty) * 4)); //) pat_addr; + pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + ((7 - paty) * 4)); else pattern = *(unsigned int *)(VRAM + ((name & 0x07FF) << 5) + (paty * 4)); - // Horizontal flip ? if (name & 0x0800) draw_pattern_fliph_planeAoverB(scr, pattern, attrs); - else draw_pattern_nofliph_planeAoverB(scr, pattern, attrs); - } -static uint16_t ntwidth_x2; -static uint16_t ntw_mask, nth_mask; - /****************************************************************************** * * Return the Horizontal scrolling @@ -551,7 +608,7 @@ unsigned int get_hscroll_vram(int line) * Render PLANE B on screen line * ******************************************************************************/ -__attribute__((optimize("unroll-loops"))) +IRAM_ATTR __attribute__((optimize("unroll-loops"))) static inline __attribute__((always_inline)) void draw_line_b(int line) { @@ -559,9 +616,10 @@ void draw_line_b(int line) unsigned int ntaddr = REG4_NAMETABLE_B; uint16_t scrollx=FETCH16VRAM(get_hscroll_vram(line) + 2) & 0x3FF; - uint16_t *vsram = &VSRAM[1]; uint8_t *end = scr + screen_width; + update_plane_b_pair_luts(gwenesis_vdp_regs[7]); + //bool column_scrolling = BIT_GEN(gwenesis_vdp_regs[11], 2); const unsigned int column_scrolling = gwenesis_vdp_regs[11] & 0x4; @@ -571,25 +629,37 @@ void draw_line_b(int line) uint8_t col = (scrollx >> 3) & ntw_mask; uint8_t patx = scrollx & 7; - unsigned int numcell = 0; scr -= patx; - // while (scr < end) { - for (scr; scr < end; scr += 8) { - // Calculate vertical scrolling for the current line - uint16_t scrolly = *vsram + line; + if (!column_scrolling) { + const uint16_t scrolly = VSRAM[1] + line; uint8_t row = (scrolly >> 3) & nth_mask; uint8_t paty = scrolly & 7; + const uint8_t *row_base = VRAM + ntaddr + row * ntwidth_x2; + const uint8_t *row_end = row_base + ntwidth_x2; + const uint8_t *tile = row_base + col * 2; + for (; scr < end; scr += 8) { + draw_pattern_planeB(scr, load_vram_name(tile), paty); + tile += 2; + if (tile == row_end) + tile = row_base; + col = (col + 1) & ntw_mask; + } + return; + } - // unsigned int nt = ntaddr + row * (2 * ntwidth); - unsigned int nt = ntaddr + row * ntwidth_x2; - - draw_pattern_planeB(scr, FETCH16VRAM(nt + col * 2), paty); + uint16_t *vsram = &VSRAM[1]; + unsigned int numcell = 0; + for (; scr < end; scr += 8) { + uint16_t scrolly = *vsram + line; + uint8_t row = (scrolly >> 3) & nth_mask; + uint8_t paty = scrolly & 7; + const uint8_t *tile = VRAM + ntaddr + row * ntwidth_x2 + col * 2; + draw_pattern_planeB(scr, load_vram_name(tile), paty); col = (col + 1) & ntw_mask; - // scr += 8; numcell++; // If per-column scrolling is active, increment VSRAM pointer - if (column_scrolling && (numcell & 1) == 0) + if ((numcell & 1) == 0) vsram += 2; } } @@ -598,7 +668,7 @@ void draw_line_b(int line) * Render PLANE A and Window on screen line * ******************************************************************************/ -__attribute__((optimize("unroll-loops"))) +IRAM_ATTR __attribute__((optimize("unroll-loops"))) static inline __attribute__((always_inline)) void draw_line_aw(int line) { @@ -606,7 +676,6 @@ void draw_line_aw(int line) { unsigned int ntaddr = REG2_NAMETABLE_A; uint16_t scrollx=FETCH16VRAM(get_hscroll_vram(line) + 0) & 0x3FF; - uint16_t *vsram = &VSRAM[0]; // Check if we are in the window region only // if it's the case, we cancel the plane A drawing @@ -647,27 +716,37 @@ void draw_line_aw(int line) { uint8_t col = (scrollx >> 3) & ntw_mask; uint8_t patx = scrollx & 7; - unsigned int numcell = 0; pos -= patx; - for (; pos < end; pos += 8) { - // while (pos < end) { - // Calculate vertical scrolling for the current line - uint16_t scrolly = *vsram + line; + if (!column_scrolling) { + const uint16_t scrolly = VSRAM[0] + line; uint8_t row = (scrolly >> 3) & nth_mask; uint8_t paty = scrolly & 7; - - // unsigned int nt = ntaddr + row * (2 * ntwidth); - unsigned int nt = ntaddr + row * ntwidth_x2; - - draw_pattern_planeA(pos, FETCH16VRAM(nt + col * 2), paty); - - col = (col + 1) & ntw_mask; - // pos += 8; - numcell++; - - // If per-column scrolling is active, increment VSRAM pointer - if (column_scrolling && (numcell & 1) == 0) - vsram += 2; + const uint8_t *row_base = VRAM + ntaddr + row * ntwidth_x2; + const uint8_t *row_end = row_base + ntwidth_x2; + const uint8_t *tile = row_base + col * 2; + for (; pos < end; pos += 8) { + draw_pattern_planeA(pos, load_vram_name(tile), paty); + tile += 2; + if (tile == row_end) + tile = row_base; + col = (col + 1) & ntw_mask; + } + } else { + uint16_t *vsram = &VSRAM[0]; + unsigned int numcell = 0; + for (; pos < end; pos += 8) { + uint16_t scrolly = *vsram + line; + uint8_t row = (scrolly >> 3) & nth_mask; + uint8_t paty = scrolly & 7; + const uint8_t *tile = VRAM + ntaddr + row * ntwidth_x2 + col * 2; + draw_pattern_planeA(pos, load_vram_name(tile), paty); + + col = (col + 1) & ntw_mask; + numcell++; + + if ((numcell & 1) == 0) + vsram += 2; + } } // Second Draw Window Plane @@ -678,11 +757,11 @@ void draw_line_aw(int line) { int wdwidth_x2 = (screen_width == 320 ? 128 : 64); - unsigned int nt = base_w + row * wdwidth_x2 + Window_first / 4; + const uint8_t *tile = VRAM + base_w + row * wdwidth_x2 + Window_first / 4; for (int i = Window_first / 8; i < Window_last / 8; ++i) { - draw_pattern_planeA(end, FETCH16VRAM(nt), paty); - nt += 2; + draw_pattern_planeA(end, load_vram_name(tile), paty); + tile += 2; end += 8; } } @@ -693,211 +772,159 @@ void draw_line_aw(int line) { * ******************************************************************************/ -__attribute__((optimize("unroll-loops"))) +IRAM_ATTR __attribute__((optimize("unroll-loops"))) static inline __attribute__((always_inline)) void draw_sprites_over_planes(int line) { - uint8_t *scr; - - scr = &render_buffer[PIX_OVERFLOW]; - - // uint8_t mask = mode_h40 ? 0x7E : 0x7F; - // uint8_t *start_table = VRAM + ((gwenesis_vdp_regs[5] & mask) << 9); - - uint8_t *start_table = VRAM + REG5_SAT_ADDRESS; - - // This is both the size of the table as seen by the VDP - // *and* the maximum number of sprites that are processed - // (important in case of infinite loops in links). - const int SPRITE_TABLE_SIZE = (screen_width == 320) ? 80 : 64; + uint8_t *scr = &render_buffer[PIX_OVERFLOW]; + const uint8_t *line_sprites; const int MAX_SPRITES_PER_LINE = (screen_width == 320) ? 20 : 16; const int MAX_PIXELS_PER_LINE = (screen_width == 320) ? 320 : 256; + bool masking = false, one_sprite_nonzero = false; + int num_sprites = 0, num_pixels = 0; - bool masking = false, one_sprite_nonzero = false; // overdraw = false; - int sidx = 0, num_sprites = 0, num_pixels = 0; - for (int i = 0; (i < SPRITE_TABLE_SIZE) && sidx < (SPRITE_TABLE_SIZE); ++i) - { - uint8_t *table = start_table + sidx*8; - uint8_t *cache = SAT_CACHE + sidx*8; - //uint8_t *cache = start_table + sidx*8; - - - int sy = ((cache[0] & 0x3) << 8) | cache[1]; - int sx = ((table[6] & 0x3) << 8) | table[7]; - uint16_t name = (table[4] << 8) | table[5]; - - - int sh = BITS_GEN(cache[2], 0, 2) + 1; - int link = BITS_GEN(cache[3], 0, 7); + ensure_sprite_line_cache(); + line_sprites = sprite_line_entries[line]; - int isflipv = table[4] & 0x10; - int isfliph = table[4] & 0x8; - - int sw = BITS_GEN(table[2], 2, 2) + 1; - - sy -= 128; - if ((line >= sy) && (line < sy+sh*8)) + for (int i = 0; i < sprite_line_counts[line]; ++i) + { + const sat_sprite_t *sprite = &sat_sprite_cache[line_sprites[i]]; + int sy = sprite->sy; + int sx = sprite->sx; + uint16_t name = sprite->name; + int sh = sprite->sh; + int isflipv = sprite->isflipv; + int isfliph = sprite->isfliph; + int sw = sprite->sw; + int width_pixels = sprite->width_pixels; + + // Sprite masking: a sprite on column 0 masks + // any lower-priority sprite, but with the following conditions + // * it only works from the second visible sprite on each line + // * if the previous line had a sprite pixel overflow, it + // works even on the first sprite + if (sx == -128) { - // Sprite masking: a sprite on column 0 masks - // any lower-priority sprite, but with the following conditions - // * it only works from the second visible sprite on each line - // * if the previous line had a sprite pixel overflow, it - // works even on the first sprite - // Notice that we need to continue parsing the table after masking - // to see if we reach a pixel overflow (because it would affect masking - // on next line). - if (sx == 0) - { - if (one_sprite_nonzero || (sprite_overflow == line-1)) - masking = true; - } - else - one_sprite_nonzero = true; - - int row = (line - sy) >> 3; - int paty = (line - sy) & 7; - if (isflipv) - row = sh - row - 1; - - sx -= 128; - if ((sx > (-sw * 8)) && (sx < screen_width) && !masking) { - - name += row; - - if (isfliph) { - name += sh * (sw - 1); - for (int p = 0; (p < sw) && (num_pixels < MAX_PIXELS_PER_LINE); p++) { - - draw_pattern_sprite_over_planes(scr + sx + p * 8, name, paty); - name -= sh; - num_pixels += 8; - - } - } else { - for (int p = 0; (p < sw) && (num_pixels < MAX_PIXELS_PER_LINE); p++) { - - draw_pattern_sprite_over_planes(scr + sx + p * 8, name, paty); - name += sh; - num_pixels += 8; - - } - } + if (one_sprite_nonzero || (sprite_overflow == line-1)) + masking = true; + } + else + one_sprite_nonzero = true; + + int row = (line - sy) >> 3; + int paty = (line - sy) & 7; + if (isflipv) + row = sh - row - 1; + + if ((sx > (-width_pixels)) && (sx < screen_width) && !masking) { + name += row; + + if (isfliph) { + name += sh * (sw - 1); + for (int p = 0; (p < sw) && (num_pixels < MAX_PIXELS_PER_LINE); p++) { + const int tile_x = sx + p * 8; + if (tile_x > -8 && tile_x < screen_width) + draw_pattern_sprite_over_planes(scr + tile_x, name, paty); + name -= sh; + num_pixels += 8; } - else - num_pixels += sw*8; - - if (num_pixels >= MAX_PIXELS_PER_LINE) - { - sprite_overflow = line; - break; + } else { + for (int p = 0; (p < sw) && (num_pixels < MAX_PIXELS_PER_LINE); p++) { + const int tile_x = sx + p * 8; + if (tile_x > -8 && tile_x < screen_width) + draw_pattern_sprite_over_planes(scr + tile_x, name, paty); + name += sh; + num_pixels += 8; } - if (++num_sprites >= MAX_SPRITES_PER_LINE) - break; + } } + else + num_pixels += width_pixels; - if (link == 0) break; - sidx = link; + if (num_pixels >= MAX_PIXELS_PER_LINE) + { + sprite_overflow = line; + break; + } + if (++num_sprites >= MAX_SPRITES_PER_LINE) + break; } // if (overdraw) // sprite_collision = true; } -static inline __attribute__((always_inline)) +IRAM_ATTR static inline __attribute__((always_inline)) void draw_sprites(int line) { - uint8_t *scr; - - scr = &sprite_buffer[PIX_OVERFLOW]; - - // uint8_t mask = mode_h40 ? 0x7E : 0x7F; - // uint8_t *start_table = VRAM + ((gwenesis_vdp_regs[5] & mask) << 9); - - uint8_t *start_table = VRAM + REG5_SAT_ADDRESS; - - // This is both the size of the table as seen by the VDP - // *and* the maximum number of sprites that are processed - // (important in case of infinite loops in links). - const int SPRITE_TABLE_SIZE = (screen_width == 320) ? 80 : 64; + uint8_t *scr = &sprite_buffer[PIX_OVERFLOW]; + const uint8_t *line_sprites; const int MAX_SPRITES_PER_LINE = (screen_width == 320) ? 20 : 16; const int MAX_PIXELS_PER_LINE = (screen_width == 320) ? 320 : 256; - bool masking = false, one_sprite_nonzero = false; // overdraw = false; - int sidx = 0, num_sprites = 0, num_pixels = 0; - for (int i = 0; i < SPRITE_TABLE_SIZE && sidx < SPRITE_TABLE_SIZE; ++i) { - uint8_t *table = start_table + sidx * 8; - uint8_t *cache = start_table + sidx * 8; - - //uint8_t *cache = SAT_CACHE + sidx * 8; - - int sy = ((cache[0] & 0x3) << 8) | cache[1]; - int sx = ((table[6] & 0x3) << 8) | table[7]; - uint16_t name = (table[4] << 8) | table[5]; - - int sh = BITS_GEN(cache[2], 0, 2) + 1; - int link = BITS_GEN(cache[3], 0, 7); - - int isflipv = table[4] & 0x10; - int isfliph = table[4] & 0x8; - - int sw = BITS_GEN(table[2], 2, 2) + 1; - - sy -= 128; - if (line >= sy && line < sy + sh * 8) { - // Sprite masking: a sprite on column 0 masks - // any lower-priority sprite, but with the following conditions - // * it only works from the second visible sprite on each line - // * if the previous line had a sprite pixel overflow, it - // works even on the first sprite - // Notice that we need to continue parsing the table after masking - // to see if we reach a pixel overflow (because it would affect masking - // on next line). - if (sx == 0) { - if (one_sprite_nonzero || sprite_overflow == line - 1) - masking = true; - } else - one_sprite_nonzero = true; - - int row = (line - sy) >> 3; - int paty = (line - sy) & 7; - if (isflipv) - row = sh - row - 1; - - sx -= 128; - if (sx > -sw * 8 && sx < screen_width && !masking) { - - name += row; - - if (isfliph) { - name += sh * (sw - 1); - for (int p = 0; p < sw && num_pixels < MAX_PIXELS_PER_LINE; p++) { - - draw_pattern_sprite(scr + sx + p * 8, name, paty); - name -= sh; - num_pixels += 8; - } - } else { - for (int p = 0; p < sw && num_pixels < MAX_PIXELS_PER_LINE; p++) { - - draw_pattern_sprite(scr + sx + p * 8, name, paty); - name += sh; - num_pixels += 8; - } - } - } else - num_pixels += sw * 8; - - if (num_pixels >= MAX_PIXELS_PER_LINE) { - sprite_overflow = line; - break; + bool masking = false, one_sprite_nonzero = false; + int num_sprites = 0, num_pixels = 0; + + ensure_sprite_line_cache(); + line_sprites = sprite_line_entries[line]; + + for (int i = 0; i < sprite_line_counts[line]; ++i) { + const sat_sprite_t *sprite = &sat_sprite_cache[line_sprites[i]]; + int sy = sprite->sy; + int sx = sprite->sx; + uint16_t name = sprite->name; + int sh = sprite->sh; + int isflipv = sprite->isflipv; + int isfliph = sprite->isfliph; + int sw = sprite->sw; + int width_pixels = sprite->width_pixels; + + // Sprite masking: a sprite on column 0 masks + // any lower-priority sprite, but with the following conditions + // * it only works from the second visible sprite on each line + // * if the previous line had a sprite pixel overflow, it + // works even on the first sprite + if (sx == -128) { + if (one_sprite_nonzero || sprite_overflow == line - 1) + masking = true; + } else + one_sprite_nonzero = true; + + int row = (line - sy) >> 3; + int paty = (line - sy) & 7; + if (isflipv) + row = sh - row - 1; + + if (sx > -width_pixels && sx < screen_width && !masking) { + name += row; + + if (isfliph) { + name += sh * (sw - 1); + for (int p = 0; p < sw && num_pixels < MAX_PIXELS_PER_LINE; p++) { + const int tile_x = sx + p * 8; + if (tile_x > -8 && tile_x < screen_width) + draw_pattern_sprite(scr + tile_x, name, paty); + name -= sh; + num_pixels += 8; + } + } else { + for (int p = 0; p < sw && num_pixels < MAX_PIXELS_PER_LINE; p++) { + const int tile_x = sx + p * 8; + if (tile_x > -8 && tile_x < screen_width) + draw_pattern_sprite(scr + tile_x, name, paty); + name += sh; + num_pixels += 8; } - if (++num_sprites >= MAX_SPRITES_PER_LINE) - break; } + } else + num_pixels += width_pixels; - if (link == 0) - break; - sidx = link; - } + if (num_pixels >= MAX_PIXELS_PER_LINE) { + sprite_overflow = line; + break; + } + if (++num_sprites >= MAX_SPRITES_PER_LINE) + break; + } // if (overdraw) // sprite_collision = true; @@ -911,6 +938,9 @@ void draw_sprites(int line) void gwenesis_vdp_render_config() { + if (sprite_line_cache_width != screen_width || sprite_line_cache_height != screen_height) + sprite_line_cache_dirty = true; + mode_h40 = REG12_MODE_H40; mode_pal = REG1_PAL; @@ -995,7 +1025,7 @@ blit_4to5_line(uint16_t *in, uint16_t *out) { } } -__attribute__((optimize("unroll-loops"))) +IRAM_ATTR __attribute__((optimize("unroll-loops"))) void gwenesis_vdp_render_line(int line) { mode_h40 = REG12_MODE_H40; @@ -1013,6 +1043,11 @@ void gwenesis_vdp_render_line(int line) if (line >= (REG1_PAL ? 240 : 224)) return; + if (line == 0) { + sprite_overflow = -1; + sprite_collision = false; + } + screen_buffer_line = &screen_buffer[line * 320]; /* clean up line screen not refreshed when mode is !H40 */ // if (REG12_MODE_H40 == 0) memset(screen_buffer_line - (320-256)/2, 0, 320 * sizeof(screen_buffer_line[0])); diff --git a/components/genesis/gwenesis/src/vdp/gwenesis_vdp_mem.c b/components/genesis/gwenesis/src/vdp/gwenesis_vdp_mem.c index e3d11cc2..ee9ff919 100644 --- a/components/genesis/gwenesis/src/vdp/gwenesis_vdp_mem.c +++ b/components/genesis/gwenesis/src/vdp/gwenesis_vdp_mem.c @@ -27,10 +27,10 @@ __license__ = "GPLv3" #include "gwenesis_io.h" #include "gwenesis_bus.h" #include "gwenesis_sn76489.h" +#include "genesis_dualcore.h" #include "gwenesis_savestate.h" #include - #pragma GCC optimize("Ofast") #define VDP_MEM_DISABLE_LOGGING 1 @@ -177,6 +177,7 @@ void gwenesis_vdp_reset() { gwenesis_vdp_status = 0x3C00; // //line_counter_interrupt = 0; hvcounter_latched = 0; + gwenesis_vdp_reset_render_state(); // register the M68K interrupt m68k_set_int_ack_callback(m68k_irq_acked); @@ -354,8 +355,10 @@ void gwenesis_vdp_vram_write(unsigned int address, unsigned int value) // Update internal SAT Cache // used in Castlevania Bloodlines - if (address >= REG5_SAT_ADDRESS && address < REG5_SAT_ADDRESS + REG5_SAT_SIZE) + if (address >= REG5_SAT_ADDRESS && address < REG5_SAT_ADDRESS + REG5_SAT_SIZE) { SAT_CACHE[address - REG5_SAT_ADDRESS] = value; + gwenesis_vdp_update_sat_cache_entry(address - REG5_SAT_ADDRESS); + } } static inline __attribute__((always_inline)) @@ -813,7 +816,6 @@ void gwenesis_vdp_control_port_write(unsigned int value) * Write an data value to mapped memory on specified address * ******************************************************************************/ -static inline __attribute__((always_inline)) void gwenesis_vdp_write_data_port_16(unsigned int value) { vdpm_log(__FUNCTION__,"%04x",value); @@ -968,8 +970,11 @@ void gwenesis_vdp_write_memory_16(unsigned int address, unsigned int value) { } if (address < 0x18) { // PSG 8 bits write vdpm_log(__FUNCTION__,"PSG sclk=%d,mclk=%d", system_clock, m68k_cycles_master()); +#if GENESIS_DUAL_CORE + genesis_sound_queue_push(GEN_SND_SN76489, 0, value, m68k_cycles_master()); +#else gwenesis_SN76489_Write(value, m68k_cycles_master()); - +#endif return; } // UNHANDLED @@ -1016,4 +1021,5 @@ void gwenesis_vdp_mem_load_state() { hvcounter_latch = saveGwenesisStateGet(state, "hvcounter_latch"); hvcounter_latched = saveGwenesisStateGet(state, "hvcounter_latched"); hint_pending = saveGwenesisStateGet(state, "hint_pending"); + gwenesis_vdp_rebuild_sat_cache(); } diff --git a/components/genesis/include/genesis_dualcore.h b/components/genesis/include/genesis_dualcore.h new file mode 100644 index 00000000..5a5dcbf2 --- /dev/null +++ b/components/genesis/include/genesis_dualcore.h @@ -0,0 +1,50 @@ +// Dual-core support for the Genesis emulator. +// +// Core 0 runs the 68000 + VDP; core 1 runs the sound unit (Z80 + YM2612 + +// SN76489). The 68000 occasionally writes the YM2612 / SN76489 registers and +// reads the YM2612 status. Those chips are owned by core 1, so instead of +// touching their state directly the 68000 (core 0) hands writes to a lock-free +// single-producer/single-consumer queue that the core-1 sound task drains in +// FIFO order, and reads a published snapshot of the YM2612 status. +// +// GENESIS_DUAL_CORE is the single source of truth for the feature flag and is +// shared by the C emulator sources (bus, vdp_mem) and the C++ driver. +#pragma once + +#include + +// 0 = single-core (known-good path; zero overhead). 1 = dual-core. +#define GENESIS_DUAL_CORE 1 + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + GEN_SND_YM2612 = 0, + GEN_SND_SN76489 = 1, +}; + +// --- Producer side: called from the 68000 (core 0) ------------------------- +// Enqueue a sound-chip register write (applied later, in order, by core 1). +// In single-core builds these are never referenced (the bus calls the chip +// write functions directly), so they cost nothing. +void genesis_sound_queue_push(uint8_t kind, uint8_t addr, uint8_t value, int cycles); + +// Relaxed snapshot of the YM2612 status byte (busy/timer flags) published by +// core 1; read by the 68000 in place of YM2612Read(). +unsigned int genesis_ym2612_status_peek(void); + +// --- Consumer side: called from the core-1 sound task ---------------------- +// Apply all queued writes via the real chip write functions (FIFO order). +void genesis_sound_queue_drain(void); +// Publish the current YM2612 status byte for the 68000 to read. +void genesis_ym2612_status_publish(unsigned int status); + +// --- Lifecycle ------------------------------------------------------------- +void genesis_sound_queue_reset(void); // clear the queue (frame start / init) +uint32_t genesis_sound_queue_dropped(void); // count of writes dropped on overflow + +#ifdef __cplusplus +} +#endif diff --git a/components/genesis/include/genesis_shared_memory.hpp b/components/genesis/include/genesis_shared_memory.hpp index 6add4254..9153c517 100644 --- a/components/genesis/include/genesis_shared_memory.hpp +++ b/components/genesis/include/genesis_shared_memory.hpp @@ -17,7 +17,7 @@ void genesis_free_shared_memory(void); // Get pointers to shared memory regions void genesis_get_memory_regions(uint8_t** vram, uint8_t** m68k_ram, uint8_t** z80_ram, - int32_t** lfo_pm_table, int** tl_tab, int16_t** audio_buffer, + int16_t** lfo_pm_table, int** tl_tab, int16_t** audio_buffer, uint16_t** cram, uint16_t** vsram, uint8_t** sat_cache, uint8_t** fifo, uint8_t** vdp_regs); diff --git a/components/genesis/src/genesis.cpp b/components/genesis/src/genesis.cpp index c33a9525..02833d54 100644 --- a/components/genesis/src/genesis.cpp +++ b/components/genesis/src/genesis.cpp @@ -3,6 +3,18 @@ #pragma GCC optimize("Ofast") #include "genesis_shared_memory.hpp" +#include "genesis_dualcore.h" + +#include +#include +#include +#include + +#if GENESIS_DUAL_CORE +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#endif extern "C" { /* Gwenesis Emulator */ @@ -22,17 +34,21 @@ extern "C" { #include "statistics.hpp" static constexpr int AUDIO_BUFFER_LENGTH = std::max(GWENESIS_AUDIO_BUFFER_LENGTH_NTSC, GWENESIS_AUDIO_BUFFER_LENGTH_PAL); +static constexpr int AUDIO_OUTPUT_CHANNELS = 2; +static constexpr int GENESIS_AUDIO_SAMPLE_STEP = 2; static constexpr size_t GENESIS_SCREEN_WIDTH = 320; static constexpr size_t GENESIS_VISIBLE_HEIGHT = 224; static constexpr size_t PALETTE_SIZE = 256; static uint16_t *palette = nullptr; +static bool genesis_initialized = false; static int frame_counter = 0; static uint16_t muteFrameCount = 0; static int frame_buffer_index = 0; static uint8_t *frame_buffer = nullptr; +static GamepadState previous_gamepad_state = {}; /// BEGIN GWENESIS EMULATOR @@ -47,18 +63,104 @@ int16_t *gwenesis_ym2612_buffer = nullptr; int ym2612_index; int ym2612_clock; -static constexpr int full_frameskip = 3; -static constexpr int muted_frameskip = 2; +static constexpr int full_frameskip = 1; +static constexpr int muted_frameskip = 1; static int frameskip = full_frameskip; +// Dual-core split: core 0 runs the 68000 + VDP, core 1 runs the sound unit +// (Z80 + YM2612 + SN76489). GENESIS_DUAL_CORE lives in genesis_dualcore.h. Set +// it to 0 for the single-core fallback (used as the muted path and a known-good +// reference). Only meaningful with GWENESIS_AUDIO_ACCURATE==0 (line-accurate +// audio), the configured build. + +#if GENESIS_DUAL_CORE +// Lock-free SPSC queue of 68000-issued sound-chip register writes. Producer is +// core 0 (the 68k bus); consumer is the core-1 sound task. Entries are packed +// as (kind<<16)|(addr<<8)|value. In GWENESIS_AUDIO_ACCURATE==0 the write +// functions ignore their cycle target, so writes only need to be applied in +// FIFO order (per-line granularity), not at an exact sample. +static constexpr uint32_t SOUND_Q_SIZE = 1024; // power of two; 4 KB internal +static constexpr uint32_t SOUND_Q_MASK = SOUND_Q_SIZE - 1; +static uint32_t *sound_q = nullptr; +static std::atomic sound_q_head{0}; // consumer index (core 1) +static std::atomic sound_q_tail{0}; // producer index (core 0) +static std::atomic sound_q_dropped{0}; +static std::atomic ym_status_snapshot{0}; + +// Per-line lockstep leash. Core 0 publishes the index of the last scanline whose +// 68000 step has completed; core 1 must not run a scanline's sound ahead of that, +// so 68k->sound register writes (queued during m68k_run) and the 68k<->Z80 +// handoff stay aligned to the correct line instead of drifting up to a frame. +static std::atomic core0_line{-1}; + +extern "C" void genesis_sound_queue_push(uint8_t kind, uint8_t addr, uint8_t value, int cycles) { + (void)cycles; // ignored in line-accurate mode; kept for API/future use + const uint32_t tail = sound_q_tail.load(std::memory_order_relaxed); + const uint32_t head = sound_q_head.load(std::memory_order_acquire); + if (tail - head >= SOUND_Q_SIZE) { // full: drop rather than block the 68k + sound_q_dropped.fetch_add(1, std::memory_order_relaxed); + return; + } + sound_q[tail & SOUND_Q_MASK] = + ((uint32_t)kind << 16) | ((uint32_t)addr << 8) | (uint32_t)value; + sound_q_tail.store(tail + 1, std::memory_order_release); +} + +extern "C" void genesis_sound_queue_drain(void) { + uint32_t head = sound_q_head.load(std::memory_order_relaxed); + const uint32_t tail = sound_q_tail.load(std::memory_order_acquire); + for (; head != tail; ++head) { + const uint32_t e = sound_q[head & SOUND_Q_MASK]; + const uint8_t kind = (uint8_t)(e >> 16); + const uint8_t addr = (uint8_t)(e >> 8); + const uint8_t value = (uint8_t)e; + if (kind == GEN_SND_YM2612) + YM2612Write(addr, value, 0); + else + gwenesis_SN76489_Write(value, 0); + } + sound_q_head.store(head, std::memory_order_release); +} + +extern "C" void genesis_sound_queue_reset(void) { + sound_q_head.store(0, std::memory_order_relaxed); + sound_q_tail.store(0, std::memory_order_relaxed); +} + +extern "C" uint32_t genesis_sound_queue_dropped(void) { + return sound_q_dropped.load(std::memory_order_relaxed); +} + +extern "C" void genesis_ym2612_status_publish(unsigned int status) { + ym_status_snapshot.store(status, std::memory_order_relaxed); +} + +extern "C" unsigned int genesis_ym2612_status_peek(void) { + return ym_status_snapshot.load(std::memory_order_relaxed); +} +#endif // GENESIS_DUAL_CORE + static FILE *savestate_fp = NULL; static int savestate_errors = 0; -int32_t *lfo_pm_table = nullptr; // 128*8*32*sizeof(int32_t) = 131072 bytes +// GW_TARGET=1 build only indexes 128*8*16 entries and stores values in 0..255, +// so int16_t is sufficient. This is 32768 bytes vs the old 131072 bytes, which +// makes it far more likely to land in internal RAM and halves the per-sample +// LFO load width. (Previously declared int32_t[128*8*32] = 131072 bytes.) +int16_t *lfo_pm_table = nullptr; // 128*8*16*sizeof(int16_t) = 32768 bytes + +static inline int16_t clamp_audio_sample(int sample) { + return (int16_t)std::clamp(sample, -32768, 32767); +} extern unsigned char *gwenesis_vdp_regs; // [0x20]; +extern unsigned char *VRAM; // [VRAM_MAX_SIZE]; +extern unsigned short *CRAM; // [CRAM_MAX_SIZE]; +extern unsigned char *SAT_CACHE; // [SAT_CACHE_MAX_SIZE]; +extern unsigned short *fifo; // [FIFO_SIZE]; extern unsigned int gwenesis_vdp_status; extern unsigned short *CRAM565; // [256]; +extern unsigned short *VSRAM; // [VSRAM_MAX_SIZE]; extern unsigned int screen_width, screen_height; extern int hint_pending; @@ -140,26 +242,377 @@ void reset_genesis() { reset_emulation(); } +static void reset_genesis_runtime_state() { + system_clock = 0; + scan_line = 0; + frame_counter = 0; + muteFrameCount = 0; + frame_buffer_index = 0; + previous_gamepad_state = {}; + + zclk = 0; + ym2612_index = 0; + ym2612_clock = 0; + sn76489_index = 0; + sn76489_clock = 0; + + if (gwenesis_sn76489_buffer) { + std::memset(gwenesis_sn76489_buffer, 0, AUDIO_BUFFER_LENGTH * sizeof(int16_t)); + } + if (gwenesis_ym2612_buffer) { + std::memset(gwenesis_ym2612_buffer, 0, AUDIO_BUFFER_LENGTH * sizeof(int16_t)); + } + + for (int i = 0; i < 8; i++) { + gwenesis_io_pad_release_button(0, i); + } +} + +static void *allocate_hot_memory(size_t size, const char *name = "hot") { + void *ptr = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const bool internal = (ptr != nullptr); + if (!ptr) { + ptr = heap_caps_malloc(size, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM); + } + fmt::print("[genesis mem] {}: {} bytes -> {}\n", name, size, internal ? "INTERNAL" : "PSRAM"); + return ptr; +} + +static void *allocate_shared_hot_memory(size_t size, shared_mem_region_t region = SHARED_MEM_DEFAULT) { + shared_mem_request_t request = { + .size = size, + .region = region, + .storage = SHARED_MEM_INTERNAL, + }; + return shared_mem_allocate(&request); +} + +#if !GENESIS_DUAL_CORE && GWENESIS_AUDIO_ACCURATE == 0 +static IRAM_ATTR void run_genesis_frame_sound_on_no_frameskip(int screen_height, int lines_per_frame) { + static constexpr int vdp_cycles_per_line = VDP_CYCLES_PER_LINE; + int hint_counter = REG10_LINE_COUNTER; + + scan_line = 0; + for (; scan_line < screen_height; ++scan_line) { + system_clock += vdp_cycles_per_line; + + m68k_run(system_clock); + z80_run(system_clock); + gwenesis_SN76489_run(system_clock); + ym2612_run(system_clock); + gwenesis_vdp_render_line(scan_line); + + if (--hint_counter < 0) { + if (REG0_LINE_INTERRUPT != 0) { + hint_pending = 1; + if ((gwenesis_vdp_status & STATUS_VIRQPENDING) == 0) + m68k_update_irq(4); + } + hint_counter = REG10_LINE_COUNTER; + } + } + + if (REG1_VBLANK_INTERRUPT != 0) { + gwenesis_vdp_status |= STATUS_VIRQPENDING; + m68k_set_irq(6); + } + z80_irq_line(1); + + if (scan_line < lines_per_frame) { + system_clock += vdp_cycles_per_line; + + m68k_run(system_clock); + z80_run(system_clock); + gwenesis_SN76489_run(system_clock); + ym2612_run(system_clock); + + if (--hint_counter < 0) { + if (REG0_LINE_INTERRUPT != 0) { + hint_pending = 1; + if ((gwenesis_vdp_status & STATUS_VIRQPENDING) == 0) + m68k_update_irq(4); + } + // (no hint_counter reset here: it is not read again this frame) + } + + ++scan_line; + z80_irq_line(0); + } + + for (; scan_line < lines_per_frame; ++scan_line) { + system_clock += vdp_cycles_per_line; + + m68k_run(system_clock); + z80_run(system_clock); + gwenesis_SN76489_run(system_clock); + ym2612_run(system_clock); + } +} +#endif + +#if GENESIS_DUAL_CORE && GWENESIS_AUDIO_ACCURATE == 0 +// --- Dual-core frame execution (behind GENESIS_DUAL_CORE) ------------------- +// +// The single-core loop above interleaves the 68k+VDP and the sound unit per +// scanline. For the dual-core split we run them as two independent per-frame +// loops that march through the SAME deterministic per-line clock +// (line*VDP_CYCLES_PER_LINE), so neither shares a clock write with the other: +// - cpu_vdp_run_frame() on core 0: 68000 + VDP render + 68k IRQs. +// - sound_unit_run_frame() on core 1: Z80 + SN76489 + YM2612 + Z80 IRQs. +// The IRQ lines split cleanly by target CPU (m68k_set_irq/m68k_update_irq vs +// z80_irq_line). cpu_vdp owns the global scan_line (used by the VDP); the sound +// unit uses a private loop counter and clock so it never touches scan_line or +// system_clock. + +// Core 0: 68000 + VDP. Owns system_clock and scan_line. +static IRAM_ATTR void cpu_vdp_run_frame(int screen_height, int lines_per_frame) { + static constexpr int vdp_cycles_per_line = VDP_CYCLES_PER_LINE; + int hint_counter = REG10_LINE_COUNTER; + + scan_line = 0; + for (; scan_line < screen_height; ++scan_line) { + system_clock += vdp_cycles_per_line; + m68k_run(system_clock); + // Release this line to the sound core: its 68k->sound writes are now queued. + core0_line.store(scan_line, std::memory_order_release); + gwenesis_vdp_render_line(scan_line); + + if (--hint_counter < 0) { + if (REG0_LINE_INTERRUPT != 0) { + hint_pending = 1; + if ((gwenesis_vdp_status & STATUS_VIRQPENDING) == 0) + m68k_update_irq(4); + } + hint_counter = REG10_LINE_COUNTER; + } + } + + if (REG1_VBLANK_INTERRUPT != 0) { + gwenesis_vdp_status |= STATUS_VIRQPENDING; + m68k_set_irq(6); + } + + if (scan_line < lines_per_frame) { + system_clock += vdp_cycles_per_line; + m68k_run(system_clock); + core0_line.store(scan_line, std::memory_order_release); + + if (--hint_counter < 0) { + if (REG0_LINE_INTERRUPT != 0) { + hint_pending = 1; + if ((gwenesis_vdp_status & STATUS_VIRQPENDING) == 0) + m68k_update_irq(4); + } + // (no hint_counter reset here: it is not read again this frame) + } + + ++scan_line; + } + + for (; scan_line < lines_per_frame; ++scan_line) { + system_clock += vdp_cycles_per_line; + m68k_run(system_clock); + core0_line.store(scan_line, std::memory_order_release); + } +} + +// Core 1: Z80 + SN76489 + YM2612. Uses a private clock that mirrors +// system_clock's per-line progression (both start at 0 each frame). +// Run one sound-unit scanline: apply any 68k-issued register writes (in FIFO +// order) before advancing the chips, then publish the YM2612 status for the +// 68k to read. +static inline IRAM_ATTR void sound_unit_scanline(int sclk) { + genesis_sound_queue_drain(); + z80_run(sclk); + gwenesis_SN76489_run(sclk); + ym2612_run(sclk); + genesis_ym2612_status_publish(YM2612Read(0)); +} + +// Leash: do not run this scanline's sound until core 0 has finished its 68000 +// step for the same line. Cannot deadlock against a 68k BUSREQ — z80_halted is +// 1 between scanlines, so the 68k's halt-wait returns while we park here. +static inline IRAM_ATTR void sound_wait_for_line(int line) { + // The sound unit is faster per scanline than the 68k+VDP, so it waits here a + // good fraction of each frame. Yield instead of busy-spinning so the video + // task (same core, same priority) gets that time — otherwise it is starved + // and the display tears. A short spin first avoids a scheduler call when the + // wait is only a few microseconds. + while (core0_line.load(std::memory_order_acquire) < line) { + for (int i = 0; i < 64; ++i) { + if (core0_line.load(std::memory_order_acquire) >= line) + return; + } + taskYIELD(); + } +} + +static IRAM_ATTR void sound_unit_run_frame(int screen_height, int lines_per_frame) { + static constexpr int vdp_cycles_per_line = VDP_CYCLES_PER_LINE; + int sclk = 0; + int line = 0; + + for (; line < screen_height; ++line) { + sclk += vdp_cycles_per_line; + sound_wait_for_line(line); + sound_unit_scanline(sclk); + } + + // Assert the Z80 V-int at the start of vblank and HOLD it across the whole + // vblank period (all remaining scanlines), deasserting only at end of frame. + // A one-scanline pulse is unreliable here: the Z80 frequently has interrupts + // disabled during that single line, so V-int-driven sound drivers (e.g. + // Sonic 2) miss it and stall. Holding it lets the Z80 take it as soon as it + // re-enables interrupts; IAutoReset clears it on take so it fires only once. + z80_irq_line(1); + + for (; line < lines_per_frame; ++line) { + sclk += vdp_cycles_per_line; + sound_wait_for_line(line); + sound_unit_scanline(sclk); + } + + z80_irq_line(0); + + // Apply any 68k writes that arrived after the last scanline drain, then mark + // the Z80 quiescent so a 68k BUSREQ after this point sees it halted. + genesis_sound_queue_drain(); + z80_mark_quiescent(); +} + +// --- Core-1 sound task + per-frame barrier --------------------------------- +// The sound task is pinned to core 1 at a priority above the video task so it +// runs promptly when a frame is dispatched (and so the BUSREQ handshake is +// acknowledged quickly), then blocks between frames, leaving core 1 to the +// video task. Coordination is two binary semaphores: core 0 gives "start", +// runs the 68k+VDP, then takes "done"; the sound task takes "start", runs the +// sound frame, then gives "done". +static TaskHandle_t sound_task_handle = nullptr; +static SemaphoreHandle_t sound_start_sem = nullptr; +static SemaphoreHandle_t sound_done_sem = nullptr; +static volatile int sound_frame_screen_height = 0; +static volatile int sound_frame_lines = 0; +static volatile bool sound_task_quit = false; + +static void sound_task_fn(void *arg) { + (void)arg; + for (;;) { + xSemaphoreTake(sound_start_sem, portMAX_DELAY); + if (sound_task_quit) + break; + sound_unit_run_frame(sound_frame_screen_height, sound_frame_lines); + xSemaphoreGive(sound_done_sem); + } + // Unblock any waiter and self-delete. + xSemaphoreGive(sound_done_sem); + sound_task_handle = nullptr; + vTaskDelete(nullptr); +} + +// Core 0: hand the frame to core 1. +static inline void sound_frame_start(int screen_height, int lines_per_frame) { + sound_frame_screen_height = screen_height; + sound_frame_lines = lines_per_frame; + xSemaphoreGive(sound_start_sem); +} + +// Core 0: barrier — wait for the sound task to finish the frame. +static inline void sound_frame_wait(void) { + xSemaphoreTake(sound_done_sem, portMAX_DELAY); +} + +static bool genesis_sound_task_create(void) { + sound_task_quit = false; + sound_start_sem = xSemaphoreCreateBinary(); + sound_done_sem = xSemaphoreCreateBinary(); + if (!sound_start_sem || !sound_done_sem) + return false; + // Priority 20 (same as the video task) and pinned to core 1, so that when the + // sound unit yields during its leash wait the video task gets core 1 instead + // of being starved. The BUSREQ handshake stays correct because z80_halted is + // already 1 between scanlines (when the sound task would be preempted). + BaseType_t ok = xTaskCreatePinnedToCore(sound_task_fn, "genesis_snd", + 4 * 1024, nullptr, 20, + &sound_task_handle, 1); + return ok == pdPASS; +} + +static void genesis_sound_task_destroy(void) { + if (sound_task_handle) { + sound_task_quit = true; + xSemaphoreGive(sound_start_sem); // wake the task so it observes quit + // Wait for it to acknowledge (it gives done before deleting itself). + if (sound_done_sem) + xSemaphoreTake(sound_done_sem, pdMS_TO_TICKS(100)); + sound_task_handle = nullptr; + } + if (sound_start_sem) { vSemaphoreDelete(sound_start_sem); sound_start_sem = nullptr; } + if (sound_done_sem) { vSemaphoreDelete(sound_done_sem); sound_done_sem = nullptr; } +} +#endif // GENESIS_DUAL_CORE + static void init(uint8_t *romdata, size_t rom_data_size) { + genesis_initialized = false; + + // M68K work RAM is the hottest, most random-access structure in the emulator: + // it is touched by nearly every 68000 instruction, so PSRAM latency here + // dominates the frame time. Internal RAM is too fragmented to hold both this + // (64 KB) and VRAM (64 KB), so allocate M68K_RAM FIRST to win the scarce large + // internal block; VRAM is downgraded to prefer-internal (PSRAM fallback) in + // genesis_init_shared_memory() and is accessed more sequentially during render. + M68K_RAM = (uint8_t*)allocate_hot_memory(MAX_RAM_SIZE, "M68K_RAM"); + genesis_init_shared_memory(); // local shared memory (used in this file): - palette = (uint16_t*)shared_malloc(sizeof(uint16_t) * PALETTE_SIZE); - gwenesis_sn76489_buffer = (int16_t*)shared_malloc(AUDIO_BUFFER_LENGTH * sizeof(int16_t)); - gwenesis_ym2612_buffer = (int16_t*)shared_malloc(AUDIO_BUFFER_LENGTH * sizeof(int16_t)); - - // PSRAM (too big for shared mem): - M68K_RAM = (uint8_t*)heap_caps_malloc(MAX_RAM_SIZE, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM); // 0x10000 (64kB) for M68K RAM - lfo_pm_table = (int32_t*)heap_caps_malloc(128*8*32 * sizeof(int32_t), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM); + palette = (uint16_t*)allocate_shared_hot_memory(sizeof(uint16_t) * PALETTE_SIZE); + gwenesis_sn76489_buffer = (int16_t*)allocate_shared_hot_memory(AUDIO_BUFFER_LENGTH * sizeof(int16_t), SHARED_MEM_CACHE_LINE); + gwenesis_ym2612_buffer = (int16_t*)allocate_shared_hot_memory(AUDIO_BUFFER_LENGTH * sizeof(int16_t), SHARED_MEM_CACHE_LINE); + + lfo_pm_table = (int16_t*)allocate_hot_memory(128*8*16 * sizeof(int16_t), "lfo_pm_table"); + +#if GENESIS_DUAL_CORE + sound_q = (uint32_t*)allocate_hot_memory(SOUND_Q_SIZE * sizeof(uint32_t), "sound_q"); + genesis_sound_queue_reset(); + if (!sound_q) { + fmt::print("Failed to allocate Genesis sound queue\n"); + deinit_genesis(); + return; + } + if (!genesis_sound_task_create()) { + fmt::print("Failed to create Genesis sound task\n"); + deinit_genesis(); + return; + } +#endif + + if (!palette || !gwenesis_sn76489_buffer || !gwenesis_ym2612_buffer || !M68K_RAM || !lfo_pm_table || + !VRAM || !ZRAM || !ym2612 || !OPNREGS || !sin_tab || !render_buffer || !sprite_buffer || + !CRAM || !SAT_CACHE || !gwenesis_vdp_regs || !fifo || !CRAM565 || !VSRAM || !tl_tab) { + fmt::print("Failed to allocate Genesis runtime memory\n"); + deinit_genesis(); + return; + } + YM2612SetSampleStep(GENESIS_AUDIO_SAMPLE_STEP); load_cartridge(romdata, rom_data_size); power_on(); reset_genesis(); - frame_counter = 0; - muteFrameCount = 0; + if (REG1_PAL) { + gwenesis_SN76489_Init(3546895, + (GWENESIS_AUDIO_BUFFER_LENGTH_PAL / GENESIS_AUDIO_SAMPLE_STEP) * GWENESIS_REFRESH_RATE_PAL, + AUDIO_FREQ_DIVISOR * GENESIS_AUDIO_SAMPLE_STEP); + } else { + gwenesis_SN76489_Init(3579545, + (GWENESIS_AUDIO_BUFFER_LENGTH_NTSC / GENESIS_AUDIO_SAMPLE_STEP) * GWENESIS_REFRESH_RATE_NTSC, + AUDIO_FREQ_DIVISOR * GENESIS_AUDIO_SAMPLE_STEP); + } + + reset_genesis_runtime_state(); BoxEmu::get().audio_sample_rate(REG1_PAL ? GWENESIS_AUDIO_FREQ_PAL/2 : GWENESIS_AUDIO_FREQ_NTSC/2); + BoxEmu::get().palette(palette, PALETTE_SIZE); frame_buffer = frame_buffer_index ? BoxEmu::get().frame_buffer1() @@ -169,6 +622,7 @@ static void init(uint8_t *romdata, size_t rom_data_size) { fmt::print("Num bytes allocated: {}\n", shared_num_bytes_allocated()); reset_frame_time(); + genesis_initialized = true; } void init_genesis(uint8_t *romdata, size_t rom_data_size) { @@ -177,16 +631,24 @@ void init_genesis(uint8_t *romdata, size_t rom_data_size) { } void IRAM_ATTR run_genesis_rom() { + if (!genesis_initialized) { + return; + } auto start = esp_timer_get_time(); // handle input here (see system.h and use input.pad and input.system) - static GamepadState previous_state = {}; auto state = BoxEmu::get().gamepad_state(); bool sound_enabled = !BoxEmu::get().is_muted(); frameskip = sound_enabled ? full_frameskip : muted_frameskip; - - if (previous_state != state) { + const bool fast_sound_path = +#if GWENESIS_AUDIO_ACCURATE == 0 + sound_enabled && full_frameskip == 1; +#else + false; +#endif + + if (previous_gamepad_state != state) { // button mapping: // up, down, left, right, c, b, a, start // from gwenesis/src/io/gwenesis_io.c @@ -210,9 +672,10 @@ void IRAM_ATTR run_genesis_rom() { } } - previous_state = state; + previous_gamepad_state = state; - bool drawFrame = (frame_counter++ % frameskip) == 0; + const int current_frame = frame_counter++; + bool drawFrame = fast_sound_path ? true : ((current_frame % frameskip) == 0); int lines_per_frame = REG1_PAL ? LINES_PER_FRAME_PAL : LINES_PER_FRAME_NTSC; int hint_counter = gwenesis_vdp_regs[10]; @@ -236,59 +699,70 @@ void IRAM_ATTR run_genesis_rom() { static constexpr int _vdp_cycles_per_line = VDP_CYCLES_PER_LINE; - while (scan_line < lines_per_frame) { - system_clock += _vdp_cycles_per_line; - - m68k_run(system_clock); - z80_run(system_clock); - - /* Audio */ - /* GWENESIS_AUDIO_ACCURATE: - * =1 : cycle accurate mode. audio is refreshed when CPUs are performing a R/W access - * =0 : line accurate mode. audio is refreshed every lines. - */ - if (GWENESIS_AUDIO_ACCURATE == 0 && sound_enabled) { - gwenesis_SN76489_run(system_clock); - ym2612_run(system_clock); - } + if (fast_sound_path) { +#if GENESIS_DUAL_CORE + // Dual-core: the audio clocks/indices (reset just above) and the write + // queue are now consistent and the sound task is idle, so it is safe to + // hand the frame to core 1, run the 68k+VDP here on core 0, then barrier. + genesis_sound_queue_reset(); + core0_line.store(-1, std::memory_order_relaxed); // sem give below publishes it + sound_frame_start(screen_height, lines_per_frame); + cpu_vdp_run_frame(screen_height, lines_per_frame); + sound_frame_wait(); +#elif GWENESIS_AUDIO_ACCURATE == 0 + run_genesis_frame_sound_on_no_frameskip(screen_height, lines_per_frame); +#endif + } else { + while (scan_line < lines_per_frame) { + system_clock += _vdp_cycles_per_line; + + m68k_run(system_clock); + z80_run(system_clock); + + /* Audio */ + /* GWENESIS_AUDIO_ACCURATE: + * =1 : cycle accurate mode. audio is refreshed when CPUs are performing a R/W access + * =0 : line accurate mode. audio is refreshed every lines. + */ + if (GWENESIS_AUDIO_ACCURATE == 0 && sound_enabled) { + gwenesis_SN76489_run(system_clock); + ym2612_run(system_clock); + } - /* Video */ - if (drawFrame && scan_line < screen_height) - gwenesis_vdp_render_line(scan_line); /* render scan_line */ + /* Video */ + if (drawFrame && scan_line < screen_height) + gwenesis_vdp_render_line(scan_line); /* render scan_line */ - // On these lines, the line counter interrupt is reloaded - if ((scan_line == 0) || (scan_line > screen_height)) { - // if (REG0_LINE_INTERRUPT != 0) - // printf("HINTERRUPT counter reloaded: (scan_line: %d, new - // counter: %d)\n", scan_line, REG10_LINE_COUNTER); - hint_counter = REG10_LINE_COUNTER; - } + // On these lines, the line counter interrupt is reloaded + if ((scan_line == 0) || (scan_line > screen_height)) { + hint_counter = REG10_LINE_COUNTER; + } - // interrupt line counter - if (--hint_counter < 0) { - if ((REG0_LINE_INTERRUPT != 0) && (scan_line <= screen_height)) { - hint_pending = 1; - // printf("Line int pending %d\n",scan_line); - if ((gwenesis_vdp_status & STATUS_VIRQPENDING) == 0) - m68k_update_irq(4); + // interrupt line counter + if (--hint_counter < 0) { + if ((REG0_LINE_INTERRUPT != 0) && (scan_line <= screen_height)) { + hint_pending = 1; + if ((gwenesis_vdp_status & STATUS_VIRQPENDING) == 0) + m68k_update_irq(4); + } + hint_counter = REG10_LINE_COUNTER; } - hint_counter = REG10_LINE_COUNTER; - } - scan_line++; + scan_line++; - // vblank begin at the end of last rendered line - if (scan_line == screen_height) { - if (REG1_VBLANK_INTERRUPT != 0) { - gwenesis_vdp_status |= STATUS_VIRQPENDING; - m68k_set_irq(6); + // vblank begin at the end of last rendered line + if (scan_line == screen_height) { + if (REG1_VBLANK_INTERRUPT != 0) { + gwenesis_vdp_status |= STATUS_VIRQPENDING; + m68k_set_irq(6); + } + z80_irq_line(1); } - z80_irq_line(1); - } - if (scan_line == (screen_height + 1)) { - z80_irq_line(0); - } - } // end of scanline loop + if (scan_line == (screen_height + 1)) { + z80_irq_line(0); + } + } // end of scanline loop + } /* Audio * synchronize YM2612 and SN76489 to system_clock @@ -305,8 +779,6 @@ void IRAM_ATTR run_genesis_rom() { if (drawFrame) { // copy the palette memcpy(palette, CRAM565, PALETTE_SIZE * sizeof(uint16_t)); - // set the palette - BoxEmu::get().palette(palette, PALETTE_SIZE); // push the frame buffer to the display task BoxEmu::get().push_frame(frame_buffer); // ping pong the frame buffer @@ -318,28 +790,56 @@ void IRAM_ATTR run_genesis_rom() { } if (sound_enabled) { - // push the audio buffer to the audio task - int audio_len = std::max(sn76489_index, ym2612_index); - // Mix gwenesis_sn76489_buffer and gwenesis_ym2612_buffer together + const int max_audio_frames = AUDIO_BUFFER_LENGTH / AUDIO_OUTPUT_CHANNELS; + int audio_len = std::min(max_audio_frames, std::max(sn76489_index, ym2612_index)); const int16_t* sn76489_buffer = gwenesis_sn76489_buffer; - const int16_t* ym2612_buffer = gwenesis_ym2612_buffer; - for (int i = 0; i < audio_len; i++) { - int16_t sample = 0; - if (sn76489_index < audio_len) { - sample += sn76489_buffer[sn76489_index]; + int16_t* ym2612_buffer = gwenesis_ym2612_buffer; + const int shared_audio_len = std::min(sn76489_index, ym2612_index); + int i = audio_len - 1; + + if (ym2612_index > sn76489_index) { + for (; i >= shared_audio_len; i--) { + const int16_t sample = ym2612_buffer[i]; + ym2612_buffer[i * AUDIO_OUTPUT_CHANNELS + 0] = sample; + ym2612_buffer[i * AUDIO_OUTPUT_CHANNELS + 1] = sample; } - if (ym2612_index < audio_len) { - sample += ym2612_buffer[ym2612_index]; + } else { + for (; i >= shared_audio_len; i--) { + const int16_t sample = sn76489_buffer[i]; + ym2612_buffer[i * AUDIO_OUTPUT_CHANNELS + 0] = sample; + ym2612_buffer[i * AUDIO_OUTPUT_CHANNELS + 1] = sample; } - gwenesis_sn76489_buffer[i] = sample; } - BoxEmu::get().play_audio((uint8_t*)gwenesis_ym2612_buffer, audio_len * sizeof(int16_t)); + + for (; i >= 3; i -= 4) { + const int16_t sample3 = clamp_audio_sample((int)ym2612_buffer[i - 0] + (int)sn76489_buffer[i - 0]); + const int16_t sample2 = clamp_audio_sample((int)ym2612_buffer[i - 1] + (int)sn76489_buffer[i - 1]); + const int16_t sample1 = clamp_audio_sample((int)ym2612_buffer[i - 2] + (int)sn76489_buffer[i - 2]); + const int16_t sample0 = clamp_audio_sample((int)ym2612_buffer[i - 3] + (int)sn76489_buffer[i - 3]); + + ym2612_buffer[(i - 0) * AUDIO_OUTPUT_CHANNELS + 0] = sample3; + ym2612_buffer[(i - 0) * AUDIO_OUTPUT_CHANNELS + 1] = sample3; + ym2612_buffer[(i - 1) * AUDIO_OUTPUT_CHANNELS + 0] = sample2; + ym2612_buffer[(i - 1) * AUDIO_OUTPUT_CHANNELS + 1] = sample2; + ym2612_buffer[(i - 2) * AUDIO_OUTPUT_CHANNELS + 0] = sample1; + ym2612_buffer[(i - 2) * AUDIO_OUTPUT_CHANNELS + 1] = sample1; + ym2612_buffer[(i - 3) * AUDIO_OUTPUT_CHANNELS + 0] = sample0; + ym2612_buffer[(i - 3) * AUDIO_OUTPUT_CHANNELS + 1] = sample0; + } + + for (; i >= 0; i--) { + const int16_t sample = clamp_audio_sample((int)ym2612_buffer[i] + (int)sn76489_buffer[i]); + ym2612_buffer[i * AUDIO_OUTPUT_CHANNELS + 0] = sample; + ym2612_buffer[i * AUDIO_OUTPUT_CHANNELS + 1] = sample; + } + BoxEmu::get().play_audio(reinterpret_cast(ym2612_buffer), audio_len * AUDIO_OUTPUT_CHANNELS * sizeof(int16_t)); } // manage statistics auto end = esp_timer_get_time(); uint64_t elapsed = end - start; update_frame_time(elapsed); + static constexpr uint64_t max_frame_time = 1000000 / 60; if (elapsed < max_frame_time) { auto sleep_time = (max_frame_time - elapsed) / 1e3; @@ -350,6 +850,9 @@ void IRAM_ATTR run_genesis_rom() { } void load_genesis(std::string_view save_path) { + if (!genesis_initialized) { + return; + } if (save_path.size()) { savestate_fp = fopen(save_path.data(), "rb"); gwenesis_load_state(); @@ -358,6 +861,9 @@ void load_genesis(std::string_view save_path) { } void save_genesis(std::string_view save_path) { + if (!genesis_initialized) { + return; + } // open the save path as a file descriptor savestate_fp = fopen(save_path.data(), "wb"); gwenesis_save_state(); @@ -365,6 +871,9 @@ void save_genesis(std::string_view save_path) { } std::span get_genesis_video_buffer() { + if (!genesis_initialized || !frame_buffer || !palette) { + return {}; + } static constexpr int height = GENESIS_VISIBLE_HEIGHT; static constexpr int width = GENESIS_SCREEN_WIDTH; @@ -379,7 +888,7 @@ std::span get_genesis_video_buffer() { // the frame data for genesis is stored in the frame buffer as 8 bit palette // indexes, so we need to convert it to 16 bit color const uint8_t *buffer = (const uint8_t*)frame_buffer; - uint16_t *frame_ptr = (uint16_t*)frame.data(); + uint16_t *frame_ptr = reinterpret_cast(frame.data()); for (int i = 0; i < (height*width); i++) { uint8_t index = buffer[i]; frame_ptr[i] = palette[index % PALETTE_SIZE]; @@ -388,8 +897,25 @@ std::span get_genesis_video_buffer() { } void deinit_genesis() { + genesis_initialized = false; +#if GENESIS_DUAL_CORE + // Stop the core-1 sound task before tearing down the state it uses. + genesis_sound_task_destroy(); +#endif + reset_genesis_runtime_state(); BoxEmu::get().audio_sample_rate(48000); shared_mem_clear(); free(M68K_RAM); free(lfo_pm_table); +#if GENESIS_DUAL_CORE + free(sound_q); + sound_q = nullptr; + genesis_sound_queue_reset(); +#endif + M68K_RAM = nullptr; + lfo_pm_table = nullptr; + palette = nullptr; + gwenesis_sn76489_buffer = nullptr; + gwenesis_ym2612_buffer = nullptr; + frame_buffer = nullptr; } diff --git a/components/genesis/src/genesis_shared_memory.cpp b/components/genesis/src/genesis_shared_memory.cpp index 03a80b2f..c29ff9cd 100644 --- a/components/genesis/src/genesis_shared_memory.cpp +++ b/components/genesis/src/genesis_shared_memory.cpp @@ -23,28 +23,52 @@ uint8_t *M68K_RAM = nullptr; // MAX_RAM_SIZE uint8_t *ZRAM = nullptr; // MAX_Z80_RAM_SIZE signed int *tl_tab = nullptr; // 13*2*TL_RES_LEN (13*2*256 * sizeof(signed int)) = 26624 bytes +static constexpr const char *TAG = "genesis_shared_memory"; + +static void *allocate_shared(size_t size, shared_mem_storage_t storage, shared_mem_region_t region = SHARED_MEM_DEFAULT) { + shared_mem_request_t request = { + .size = size, + .region = region, + .storage = storage, + }; + return shared_mem_allocate(&request); +} + +static void *allocate_shared_prefer_internal(size_t size, const char *name, shared_mem_region_t region = SHARED_MEM_DEFAULT) { + void *ptr = allocate_shared(size, SHARED_MEM_INTERNAL, region); + if (ptr != nullptr) { + return ptr; + } + + ESP_LOGW(TAG, "Allocating %s in PSRAM fallback", name); + return allocate_shared(size, SHARED_MEM_PSRAM, region); +} + void genesis_init_shared_memory(void) { // allocate m68k cpu state in shared memory - m68k = (m68ki_cpu_core*)shared_malloc(sizeof(m68ki_cpu_core)); + m68k = (m68ki_cpu_core*)allocate_shared(sizeof(m68ki_cpu_core), SHARED_MEM_INTERNAL); - VRAM = (uint8_t*)shared_malloc(VRAM_MAX_SIZE); // 0x10000 (64kB) for VRAM - ZRAM = (uint8_t*)shared_malloc(MAX_Z80_RAM_SIZE); // 0x2000 (8kB) for Z80 RAM + // VRAM is large (64kB) and competes with M68K_RAM for the single big internal + // block. M68K_RAM is allocated first (in genesis.cpp init()) and is hotter, so + // VRAM prefers internal but falls back to PSRAM. See note in genesis.cpp. + VRAM = (uint8_t*)allocate_shared_prefer_internal(VRAM_MAX_SIZE, "VRAM", SHARED_MEM_CACHE_LINE); // 0x10000 (64kB) for VRAM + ZRAM = (uint8_t*)allocate_shared_prefer_internal(MAX_Z80_RAM_SIZE, "ZRAM"); // 0x2000 (8kB) for Z80 RAM - ym2612 = (YM2612*)shared_malloc(sizeof(YM2612)); - OPNREGS = (uint8_t*)shared_malloc(512); - sin_tab = (unsigned int*)shared_malloc(SIN_LEN * sizeof(unsigned int)); + ym2612 = (YM2612*)allocate_shared(sizeof(YM2612), SHARED_MEM_INTERNAL); + OPNREGS = (uint8_t*)allocate_shared(512, SHARED_MEM_INTERNAL); + sin_tab = (unsigned int*)allocate_shared_prefer_internal(SIN_LEN * sizeof(unsigned int), "sin_tab"); - render_buffer = (uint8_t*)shared_malloc(SCREEN_WIDTH + PIX_OVERFLOW*2); - sprite_buffer = (uint8_t*)shared_malloc(SCREEN_WIDTH + PIX_OVERFLOW*2); + render_buffer = (uint8_t*)allocate_shared(SCREEN_WIDTH + PIX_OVERFLOW*2, SHARED_MEM_INTERNAL, SHARED_MEM_CACHE_LINE); + sprite_buffer = (uint8_t*)allocate_shared(SCREEN_WIDTH + PIX_OVERFLOW*2, SHARED_MEM_INTERNAL, SHARED_MEM_CACHE_LINE); - CRAM = (uint16_t*)shared_malloc(CRAM_MAX_SIZE * sizeof(uint16_t)); - SAT_CACHE = (uint8_t*)shared_malloc(SAT_CACHE_MAX_SIZE); - gwenesis_vdp_regs = (uint8_t*)shared_malloc(REG_SIZE); - fifo = (uint16_t*)shared_malloc(FIFO_SIZE * sizeof(uint16_t)); - CRAM565 = (uint16_t*)shared_malloc(CRAM_MAX_SIZE * 4 * sizeof(uint16_t)); - VSRAM = (uint16_t*)shared_malloc(VSRAM_MAX_SIZE * sizeof(uint16_t)); + CRAM = (uint16_t*)allocate_shared(CRAM_MAX_SIZE * sizeof(uint16_t), SHARED_MEM_INTERNAL); + SAT_CACHE = (uint8_t*)allocate_shared(SAT_CACHE_MAX_SIZE, SHARED_MEM_INTERNAL, SHARED_MEM_CACHE_LINE); + gwenesis_vdp_regs = (uint8_t*)allocate_shared(REG_SIZE, SHARED_MEM_INTERNAL); + fifo = (uint16_t*)allocate_shared(FIFO_SIZE * sizeof(uint16_t), SHARED_MEM_INTERNAL); + CRAM565 = (uint16_t*)allocate_shared(CRAM_MAX_SIZE * 4 * sizeof(uint16_t), SHARED_MEM_INTERNAL); + VSRAM = (uint16_t*)allocate_shared(VSRAM_MAX_SIZE * sizeof(uint16_t), SHARED_MEM_INTERNAL); - tl_tab = (signed int*)shared_malloc(13*2*256 * sizeof(signed int)); + tl_tab = (signed int*)allocate_shared_prefer_internal(13*2*256 * sizeof(signed int), "tl_tab"); } void genesis_free_shared_memory(void) { diff --git a/components/genesis/src/z80_shared_memory.cpp b/components/genesis/src/z80_shared_memory.cpp index e3550cad..a115a2f0 100644 --- a/components/genesis/src/z80_shared_memory.cpp +++ b/components/genesis/src/z80_shared_memory.cpp @@ -28,7 +28,8 @@ void z80_init_shared_memory() { // Allocate memory for each table shared_mem_request_t cycles_request = { .size = Z80_CYCLES_SIZE, - .region = SHARED_MEM_DEFAULT + .region = SHARED_MEM_DEFAULT, + .storage = SHARED_MEM_PSRAM, }; cycles = static_cast(shared_mem_allocate(&cycles_request)); if (!cycles) { @@ -38,7 +39,8 @@ void z80_init_shared_memory() { shared_mem_request_t cycles_cb_request = { .size = Z80_CYCLES_CB_SIZE, - .region = SHARED_MEM_DEFAULT + .region = SHARED_MEM_DEFAULT, + .storage = SHARED_MEM_PSRAM, }; cycles_cb = static_cast(shared_mem_allocate(&cycles_cb_request)); if (!cycles_cb) { @@ -48,7 +50,8 @@ void z80_init_shared_memory() { shared_mem_request_t cycles_ed_request = { .size = Z80_CYCLES_ED_SIZE, - .region = SHARED_MEM_DEFAULT + .region = SHARED_MEM_DEFAULT, + .storage = SHARED_MEM_PSRAM, }; cycles_ed = static_cast(shared_mem_allocate(&cycles_ed_request)); if (!cycles_ed) { @@ -58,7 +61,8 @@ void z80_init_shared_memory() { shared_mem_request_t cycles_xx_request = { .size = Z80_CYCLES_XX_SIZE, - .region = SHARED_MEM_DEFAULT + .region = SHARED_MEM_DEFAULT, + .storage = SHARED_MEM_PSRAM, }; cycles_xx = static_cast(shared_mem_allocate(&cycles_xx_request)); if (!cycles_xx) { @@ -68,7 +72,8 @@ void z80_init_shared_memory() { shared_mem_request_t cycles_xxcb_request = { .size = Z80_CYCLES_XXCB_SIZE, - .region = SHARED_MEM_DEFAULT + .region = SHARED_MEM_DEFAULT, + .storage = SHARED_MEM_PSRAM, }; cycles_xxcb = static_cast(shared_mem_allocate(&cycles_xxcb_request)); if (!cycles_xxcb) { @@ -78,7 +83,8 @@ void z80_init_shared_memory() { shared_mem_request_t zs_table_request = { .size = Z80_ZS_TABLE_SIZE, - .region = SHARED_MEM_DEFAULT + .region = SHARED_MEM_DEFAULT, + .storage = SHARED_MEM_PSRAM, }; zs_table = static_cast(shared_mem_allocate(&zs_table_request)); if (!zs_table) { @@ -88,7 +94,8 @@ void z80_init_shared_memory() { shared_mem_request_t pzs_table_request = { .size = Z80_PZS_TABLE_SIZE, - .region = SHARED_MEM_DEFAULT + .region = SHARED_MEM_DEFAULT, + .storage = SHARED_MEM_PSRAM, }; pzs_table = static_cast(shared_mem_allocate(&pzs_table_request)); if (!pzs_table) { @@ -98,7 +105,8 @@ void z80_init_shared_memory() { shared_mem_request_t daa_table_request = { .size = Z80_DAA_TABLE_SIZE * sizeof(uint16_t), - .region = SHARED_MEM_DEFAULT + .region = SHARED_MEM_DEFAULT, + .storage = SHARED_MEM_PSRAM, }; daa_table = static_cast(shared_mem_allocate(&daa_table_request)); if (!daa_table) { diff --git a/components/jpeg/include/jpeg.hpp b/components/jpeg/include/jpeg.hpp index 45239e3e..1c781ba5 100644 --- a/components/jpeg/include/jpeg.hpp +++ b/components/jpeg/include/jpeg.hpp @@ -103,7 +103,7 @@ class Jpeg { auto height = pDraw->iHeight; auto xs = pDraw->x; auto ys = pDraw->y; - uint16_t *dst_buffer = (uint16_t*)decoded_data_; + uint16_t *dst_buffer = reinterpret_cast(decoded_data_); const uint16_t *src_buffer = (const uint16_t*)pDraw->pPixels; // two bytes per pixel for RGB565 uint16_t num_bytes_per_row = width * 2; diff --git a/components/msx/CMakeLists.txt b/components/msx/CMakeLists.txt index d8d0022b..c0f6b7e5 100644 --- a/components/msx/CMakeLists.txt +++ b/components/msx/CMakeLists.txt @@ -13,7 +13,7 @@ target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-stringop-truncation -Wno-unused-but-set-variable -Wno-unused-variable - -Wno-register + $<$:-Wno-register> -DBPS16 -DUNIX -DLSB_FIRST -DNARROW -O2 ) file(GLOB_RECURSE c_sources "fmsx/src/*.c") diff --git a/components/msx/src/msx.cpp b/components/msx/src/msx.cpp index d6f7d7e1..11700e13 100644 --- a/components/msx/src/msx.cpp +++ b/components/msx/src/msx.cpp @@ -241,6 +241,9 @@ int InitMachine(void) .H = HEIGHT, .L = WIDTH, .D = 16, + .Cropped = 0, + .XImg = nullptr, + .Attrs = 0, }; XBuf = NormScreen->Data; @@ -399,7 +402,7 @@ unsigned int WriteAudio(sample *Data, unsigned int Length) { bool sound_enabled = !box.is_muted(); if (sound_enabled) { if (audio_buffer_offset + Length > AUDIO_BUFFER_LENGTH) { - box.play_audio((uint8_t*)audio_buffer, audio_buffer_offset * sizeof(int16_t)); + box.play_audio(reinterpret_cast(audio_buffer), audio_buffer_offset * sizeof(int16_t)); audio_buffer_offset = 0; currentAudioBuffer = currentAudioBuffer ? 0 : 1; audio_buffer = audio_buffers[currentAudioBuffer]; @@ -535,7 +538,7 @@ void save_msx(std::string_view save_path) { } std::span get_msx_video_buffer() { - return std::span((uint8_t*)framebuffer, MSX_SCREEN_WIDTH * MSX_SCREEN_HEIGHT * 2); + return std::span(reinterpret_cast(framebuffer), MSX_SCREEN_WIDTH * MSX_SCREEN_HEIGHT * 2); } void deinit_msx() { diff --git a/components/nes/CMakeLists.txt b/components/nes/CMakeLists.txt index 839a7e21..eb38204b 100644 --- a/components/nes/CMakeLists.txt +++ b/components/nes/CMakeLists.txt @@ -4,4 +4,4 @@ idf_component_register( PRIV_INCLUDE_DIRS "nofrendo/cpu" "nofrendo/libsnss" "nofrendo/nes" "nofrendo/sndhrdw" "nofrendo" REQUIRES "box-emu" "statistics" "shared_memory" ) -target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-char-subscripts -Wno-attributes -Wno-implicit-fallthrough -Wno-unused-function -Wno-unused-variable -Wno-discarded-qualifiers) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-char-subscripts -Wno-attributes -Wno-implicit-fallthrough -Wno-unused-function -Wno-unused-variable $<$:-std=gnu17> $<$:-Wno-discarded-qualifiers>) diff --git a/components/nes/nofrendo/noftypes.h b/components/nes/nofrendo/noftypes.h index 10dc12a8..c7e6c290 100644 --- a/components/nes/nofrendo/noftypes.h +++ b/components/nes/nofrendo/noftypes.h @@ -28,11 +28,7 @@ #ifndef __cplusplus -typedef enum -{ - false = 0, - true = 1 -} bool; +#include #ifndef NULL #define NULL ((void *) 0) diff --git a/components/nes/src/nes.cpp b/components/nes/src/nes.cpp index 878dd141..64dc2223 100644 --- a/components/nes/src/nes.cpp +++ b/components/nes/src/nes.cpp @@ -37,7 +37,7 @@ void init_nes(const std::string& rom_filename, uint8_t *romdata, size_t rom_data BoxEmu::get().native_size(NES_SCREEN_WIDTH, NES_VISIBLE_HEIGHT); BoxEmu::get().palette(get_nes_palette()); - BoxEmu::get().audio_sample_rate(44100 / 2); + BoxEmu::get().audio_sample_rate(22050); nes_insertcart(rom_filename.c_str(), console_nes); vid_setmode(NES_SCREEN_WIDTH, NES_VISIBLE_HEIGHT); diff --git a/components/nes/src/video_audio.cpp b/components/nes/src/video_audio.cpp index 692598c6..88cc7df6 100644 --- a/components/nes/src/video_audio.cpp +++ b/components/nes/src/video_audio.cpp @@ -47,7 +47,7 @@ static int16_t *audio_frame = nullptr; extern "C" void do_audio_frame() { if (audio_callback == NULL) return; audio_callback(audio_frame, num_samples); - BoxEmu::get().play_audio((uint8_t*)audio_frame, num_bytes); + BoxEmu::get().play_audio(reinterpret_cast(audio_frame), num_bytes); } extern "C" void osd_setsound(void (*playfunc)(void *buffer, int length)) @@ -157,7 +157,7 @@ static void set_palette(rgb_t *pal) } uint16_t* get_nes_palette() { - return (uint16_t*)myPalette; + return reinterpret_cast(myPalette); } /* clear all frames to a particular color */ diff --git a/components/pool_allocator/src/pool_allocator.c b/components/pool_allocator/src/pool_allocator.c index c6c67ebf..900b33a5 100644 --- a/components/pool_allocator/src/pool_allocator.c +++ b/components/pool_allocator/src/pool_allocator.c @@ -29,7 +29,8 @@ void pool_create(void* mem, size_t size) { } int pool_contains(const void* ptr) { - return (ptr >= memory_pool && ptr < memory_pool + memory_pool_size); + return (ptr >= (const void*)memory_pool && + ptr < (const void*)(memory_pool + memory_pool_size)); } void* pool_alloc(size_t size) { diff --git a/components/shared_memory/include/shared_memory.h b/components/shared_memory/include/shared_memory.h index 45d02bc7..68b6ec91 100644 --- a/components/shared_memory/include/shared_memory.h +++ b/components/shared_memory/include/shared_memory.h @@ -14,10 +14,19 @@ typedef enum { SHARED_MEM_CACHE_LINE // 32-byte aligned for cache line optimization } shared_mem_region_t; +// Backing store for shared allocations. Default to PSRAM so emulators can opt +// specific hot-path data back into internal RAM without paying that cost for the +// whole shared working set. +typedef enum { + SHARED_MEM_PSRAM = 0, + SHARED_MEM_INTERNAL = 1, +} shared_mem_storage_t; + // Memory allocation request typedef struct { size_t size; shared_mem_region_t region; + shared_mem_storage_t storage; } shared_mem_request_t; // Memory usage statistics diff --git a/components/shared_memory/src/shared_memory.c b/components/shared_memory/src/shared_memory.c index 94a1b3b5..ee61a876 100644 --- a/components/shared_memory/src/shared_memory.c +++ b/components/shared_memory/src/shared_memory.c @@ -1,20 +1,19 @@ #include "shared_memory.h" +#include "esp_heap_caps.h" #include #include -// Total shared memory size - can be tuned based on needs -// 130KB total shared memory needed. -// -// SMS/GG: 130756 -// Genesis: 121032 -// MSX: 77612 -// Doom: 69724 -// GB/C: 59372 -// NES: 17536 -#define TOTAL_MEMORY_SIZE (130 * 1024) - -// Aligned memory pool -static uint8_t memory_pool_[TOTAL_MEMORY_SIZE] __attribute__((aligned(32))); +#define MAX_SHARED_ALLOCATIONS 256 + +typedef struct { + void *raw_ptr; + void *aligned_ptr; + size_t size; + shared_mem_storage_t storage; +} shared_mem_allocation_t; + +static shared_mem_allocation_t allocations_[MAX_SHARED_ALLOCATIONS]; +static size_t allocation_count_ = 0; static size_t current_offset_ = 0; // Calculate alignment offset for a region @@ -29,14 +28,34 @@ static size_t get_alignment_offset(shared_mem_region_t region) { } } +static int get_storage_caps(shared_mem_storage_t storage) { + switch (storage) { + case SHARED_MEM_INTERNAL: + return MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; + case SHARED_MEM_PSRAM: + default: + return MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + } +} + +static void *align_pointer(const void *ptr, size_t alignment) { + uintptr_t raw_address = (uintptr_t)ptr; + uintptr_t aligned_address = (raw_address + (alignment - 1)) & ~((uintptr_t)alignment - 1); + return (void *)aligned_address; +} + void* shared_mem_get_instance(void) { - return memory_pool_; // Return base address of memory pool + if (allocation_count_ == 0) { + return NULL; + } + return allocations_[0].aligned_ptr; } void* shared_malloc(size_t size) { shared_mem_request_t request = { .size = size, - .region = SHARED_MEM_DEFAULT + .region = SHARED_MEM_DEFAULT, + .storage = SHARED_MEM_PSRAM, }; return shared_mem_allocate(&request); } @@ -50,35 +69,53 @@ void* shared_mem_allocate(const shared_mem_request_t* request) { return NULL; } + if (allocation_count_ >= MAX_SHARED_ALLOCATIONS) { + return NULL; + } + // Calculate alignment offset size_t alignment = get_alignment_offset(request->region); - size_t offset = (alignment - (current_offset_ % alignment)) % alignment; - - // Check if we have enough space - if (current_offset_ + offset + request->size > TOTAL_MEMORY_SIZE) { + size_t allocation_size = request->size + alignment - 1; + void *raw_ptr = heap_caps_malloc(allocation_size, get_storage_caps(request->storage)); + if (raw_ptr == NULL) { return NULL; } - // Calculate aligned address - void* ptr = memory_pool_ + current_offset_ + offset; - - // Update current offset - current_offset_ += offset + request->size; - - return ptr; + void *aligned_ptr = align_pointer(raw_ptr, alignment); + memset(aligned_ptr, 0, request->size); + + allocations_[allocation_count_++] = (shared_mem_allocation_t){ + .raw_ptr = raw_ptr, + .aligned_ptr = aligned_ptr, + .size = request->size, + .storage = request->storage, + }; + current_offset_ += request->size; + + return aligned_ptr; } void shared_mem_clear(void) { printf("Num bytes allocated: %d\n", (int)current_offset_); - // TODO: Use SIMD-accelerated memset from ESP32s3 vector instructions - memset(memory_pool_, 0, TOTAL_MEMORY_SIZE); + + for (size_t i = 0; i < allocation_count_; i++) { + if (allocations_[i].raw_ptr != NULL) { + heap_caps_free(allocations_[i].raw_ptr); + } + allocations_[i].raw_ptr = NULL; + allocations_[i].aligned_ptr = NULL; + allocations_[i].size = 0; + allocations_[i].storage = SHARED_MEM_PSRAM; + } + allocation_count_ = 0; current_offset_ = 0; } shared_mem_stats_t shared_mem_get_stats(void) { shared_mem_stats_t stats = { .total_allocated = current_offset_, - .total_free = TOTAL_MEMORY_SIZE - current_offset_ + .total_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) + + heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) }; return stats; } diff --git a/components/sms/CMakeLists.txt b/components/sms/CMakeLists.txt index 28dbe6c4..6bccdbab 100644 --- a/components/sms/CMakeLists.txt +++ b/components/sms/CMakeLists.txt @@ -5,5 +5,5 @@ idf_component_register( REQUIRES box-emu statistics shared_memory ) # target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-char-subscripts -Wno-attributes -Wno-implicit-fallthrough -Wno-unused-function -Wno-unused-variable -Wno-discarded-qualifiers) -target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-unused-const-variable) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-unused-const-variable $<$:-std=gnu17> $<$:-Wno-discarded-qualifiers> $<$:-Wno-error=address-of-packed-member>) target_compile_definitions(${COMPONENT_LIB} PRIVATE LSB_FIRST=1) diff --git a/components/sms/src/sms.cpp b/components/sms/src/sms.cpp index 09560d6c..de497c45 100644 --- a/components/sms/src/sms.cpp +++ b/components/sms/src/sms.cpp @@ -200,7 +200,7 @@ void run_sms_rom() { auto sms_audio_buffer_len = sms_snd->sample_count - 1; // push the audio buffer to the audio task - BoxEmu::get().play_audio((uint8_t*)sms_audio_buffer, sms_audio_buffer_len * 2 * 2); // 2 channels, 2 bytes per sample + BoxEmu::get().play_audio(reinterpret_cast(sms_audio_buffer), sms_audio_buffer_len * 2 * 2); // 2 channels, 2 bytes per sample // update unlock based on x button static bool last_x = false; @@ -223,6 +223,7 @@ void run_sms_rom() { void load_sms(std::string_view save_path) { if (save_path.size()) { auto f = fopen(save_path.data(), "rb"); + if (!f) return; system_load_state(f); fclose(f); } @@ -231,6 +232,7 @@ void load_sms(std::string_view save_path) { void save_sms(std::string_view save_path) { // open the save path as a file descriptor auto f = fopen(save_path.data(), "wb"); + if (!f) return; system_save_state(f); fclose(f); } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 0c978aae..c1f796c0 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -31,6 +31,12 @@ CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y CONFIG_ESP_MAIN_TASK_STACK_SIZE=30000 CONFIG_ESP_TIMER_TASK_STACK_SIZE=10240 +# Instruction cache: 32KB (default 16KB). The m68k/Z80 interpreters and VDP code +# are large and thrash a 16KB I-cache; with code/rodata mapped through PSRAM +# (SPIRAM_FETCH_INSTRUCTIONS/RODATA below) every miss is a PSRAM round-trip. +# Doubling the I-cache helps all subsystems. Costs ~16KB of internal SRAM. +CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y + # SPIRAM Configuration CONFIG_SPIRAM=y CONFIG_SPIRAM_USE_MALLOC=y diff --git a/suppressions.txt b/suppressions.txt index a0709c22..5430bc3f 100644 --- a/suppressions.txt +++ b/suppressions.txt @@ -6,6 +6,17 @@ unusedStructMember functionStatic cstyleCast +// A few findings that are intentional and can't be cleanly fixed in code: +// - jpeg on_data_decode() must match JPEGDEC's callback typedef, so its pDraw +// parameter cannot be made const. +constParameterCallback:components/jpeg/include/jpeg.hpp +// - the genesis save-state shims hand back a non-null sentinel handle, and the +// compile-time frameskip knobs (full_frameskip/muted_frameskip) are both 1, +// which cppcheck constant-folds to "always true"/"duplicate". +intToPointerCast:components/genesis/src/genesis.cpp +knownConditionTrueFalse:components/genesis/src/genesis.cpp +duplicateExpressionTernary:components/genesis/src/genesis.cpp + // Specific suppressions of the form: // [error id]:[filename]:[line] *:lib/*